ML307N 4G模组通信链路完整修复报告
概述
本文档记录了 ESP32-S3 AI语音客户端在 ML307N 4G模组模式下,从完全无法通信到实现完整对话链路(OTA + MQTT + UDP音频)的全部修复过程。
**设备**: custom-esp32s3-voice 开发板 + ML307N 4G SIM卡模组
**目标**: 通过4G网络实现 OTA检查、MQTT控制、UDP音频通话的完整链路
**最终状态**: ✅ 全部功能正常
修复历程
阶段一:AT命令全部超时
**现象**: 设备启动后,AT+COPS?、AT+CSQ、AT+ICCID、AT+CEREG?、AT+MIPSTATE 全部超时失败
**根因**: ML307N模组在空闲时自动进入休眠模式,而硬件设计未连接DTR引脚(GPIO_NUM_NC),无法通过DTR唤醒
**修复**:
-
文件: `main/boards/common/ml307_board.cc`
-
在 modem 网络就绪后立即调用 `SetSleepMode(false)` 发送 `AT+MLPMCFG="sleepmode",0,0` 禁用自动休眠
```cpp
modem_->SetSleepMode(false);
```
阶段二:MIPSTATE命令偶发超时
**现象**: 禁用休眠后大部分AT命令正常,但 `AT+MIPSTATE=0` 仍偶尔超时
**根因**: modem空闲短暂期间响应较慢,默认1s超时不够
**修复**:
-
文件: `managed_components/78__esp-ml307/src/ml307/ml307_tcp.cc`
-
MIPSTATE命令超时延长至3s,失败后发送AT唤醒再重试一次
```cpp
std::string command = "AT+MIPSTATE=" + std::to_string(tcp_id_);
bool mipstate_ok = at_uart_->SendCommand(command, 3000);
if (!mipstate_ok) {
at_uart_->SendCommand("AT", 1000);
mipstate_ok = at_uart_->SendCommand(command, 3000);
}
```
阶段三:lwIP UDP Socket创建崩溃
**现象**:
```
assert failed: tcpip_send_msg_wait_sem (Invalid mbox)
```
**根因**: WiFi模式下 `UdpAudio_Init()` 使用ESP32原生lwIP socket创建UDP连接。ML307模式无WiFi物理接口,lwIP TCP/IP任务未启动,`socket()` 调用触发断言崩溃
**修复**:
-
文件: `main/protocols/mqtt_protocol.cc` (StartMqttClient)
-
ML307模式下跳过原生lwIP UDP初始化,改用modem内部的UDP(Ml307Udp)
```cpp
if (board_type != "ml307") {
// WiFi: 使用原生lwIP UDP
if (UdpAudio_Init(ip.c_str(), port)) {
use_simple_udp_ = true;
}
} else {
// ML307: 使用modem UDP
use_simple_udp_ = true;
use_modem_udp_ = true;
udp_server_ = ip;
udp_port_ = port;
}
```
阶段四:运营商HTTP劫持(302重定向)
**现象**: OTA检查时HTTP请求被302重定向到 `touch.10086.cn`
**根因**: 中国移动对HTTP请求实施透明劫持(SIM卡无流量或套餐受限时)
**修复**:
-
充值SIM卡流量后恢复正常
-
HTTP客户端已实现302重定向跟随(最多5次)
阶段五:ML307 Modem UDP音频完整实现
**现象**: `"UDP is not specified in server hello"` - 跳过了UDP初始化后服务器无法发送音频
**根因**: 之前仅跳过了原生UDP,但没有实现modem UDP的替代方案
**修复** --- 完整实现 ML307 modem UDP 音频收发:
5.1 新增 `use_modem_udp_` 标志
- 文件: `main/protocols/mqtt_protocol.h`
```cpp
bool use_modem_udp_ = false;
```
5.2 OpenAudioChannel - 创建modem UDP连接
- 文件: `main/protocols/mqtt_protocol.cc`
```cpp
if (use_modem_udp_) {
auto network = Board::GetInstance().GetNetwork();
udp_ = network->CreateUdp(2);
udp_->Connect(udp_server_, udp_port_);
}
```
5.3 SendAudio - 构建UDP包通过modem发送
```cpp
if (use_modem_udp_ && udp_ && udp_->connected()) {
// 包格式: device_id_len(1B) + device_id(N) + UdpAudioHeader(16B) + opus
// 与WiFi原生UDP完全一致
std::string pkt;
pkt.push_back((char)device_id_len);
pkt.append(device_mac_);
pkt.append((const char*)&header, sizeof(header));
pkt.append(opus_data, opus_len);
udp_->Send(pkt);
}
```
5.4 OnMessage回调 - 解析服务器返回的音频包
```cpp
udp_->OnMessage(this(const std::string& data) {
// 解析: device_id_len + device_id + header + opus
// 创建AudioStreamPacket → on_incoming_audio_回调
});
```
5.5 IsAudioChannelOpened - 支持modem UDP检查
```cpp
if (use_modem_udp_) {
return udp_ != nullptr && udp_->connected();
}
```
阶段六:网络中断(+MATREADY)容错
**现象**: 唤醒词检测后modem PDP上下文突然断开(`+MATREADY` URC),MQTT连接丢失
**修复**:
-
文件: `main/boards/common/ml307_board.cc`
-
网络断开时,connecting/listening/speaking 状态均回到 idle
```cpp
if (device_state == kDeviceStateListening ||
device_state == kDeviceStateSpeaking ||
device_state == kDeviceStateConnecting) {
application.Schedule(\&application() {
application.SetDeviceState(kDeviceStateIdle);
});
}
```
-
文件: `main/protocols/mqtt_protocol.cc`
-
server hello等待循环中添加MQTT连接检查,网络断开时快速失败(不等满30秒)
```cpp
if (mqtt_ == nullptr || !mqtt_->IsConnected()) {
ESP_LOGE(TAG, "MQTT disconnected while waiting for server hello");
return false;
}
```
修改文件清单
| 文件 | 修改内容 |
|------|---------|
| `main/boards/common/ml307_board.cc` | 禁用modem自动休眠;网络断开容错处理 |
| `managed_components/78__esp-ml307/src/ml307/ml307_tcp.cc` | MIPSTATE命令重试逻辑 |
| `main/protocols/mqtt_protocol.h` | 新增 `use_modem_udp_` 标志 |
| `main/protocols/mqtt_protocol.cc` | modem UDP全流程:连接/发送/接收/容错 |
关键技术要点
ML307 Modem UDP vs WiFi原生UDP
| 特性 | WiFi (UdpAudio) | ML307 Modem UDP |
|------|----------------|-----------------|
| 底层实现 | lwIP socket | AT+MIPOPEN="UDP" |
| 数据发送 | `sendto()` | AT+MIPSEND + 原始二进制 |
| 数据接收 | `recvfrom()` 任务 | +MIPURC "rudp" URC回调 |
| 网络接口 | WiFi STA | 运营商4G PDP |
| NAT穿透 | 路由器NAT(友好) | 运营商NAT(可能受限) |
| 包格式 | 完全相同 | 完全相同 |
UDP包格式(WiFi和Modem一致)
```
| device_id_len (1B) | device_id (17B, MAC地址) | UdpAudioHeader (16B) | opus_data (变长) |
```
UdpAudioHeader 字段(大端序):
-
type (1B): 固定为1
-
reserved (1B)
-
payload_length (2B): opus数据长度
-
sequence (4B): 递增序号
-
timestamp (4B): 时间戳
-
opus_length (4B): opus数据长度
AT命令encoding配置
-
`AT+MIPCFG="encoding",id,0,1` → 发送原始二进制,接收HEX编码
-
收发均正常工作,与TCP配置一致
经验教训
-
**蜂窝模组休眠是AT超时的首要嫌疑**: 无DTR引脚时必须禁用自动休眠
-
**lwIP不能假设始终可用**: 无WiFi物理接口时lwIP TCP/IP任务不启动
-
**modem UDP与原生UDP可以共享包格式**: 只要包格式一致,服务器无需区分客户端类型
-
**运营商NAT可能阻止UDP回传**: 4G carrier NAT行为不可控,但实测中国移动可以正常双向UDP
-
**网络中断需快速降级**: +MATREADY等URC表示modem协议栈重启,必须立即降级状态而非傻等超时