diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/net_srv.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/net_srv.h index 971fe3b..f24606c 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/net_srv.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/net_srv.h @@ -103,7 +103,7 @@ typedef struct _IOT_TOPIC_ } IOT_Topic; extern IOT_Topic g_iot_topic; -#define MQTT_KEEPALIVE_INTERVAL 9 +#define MQTT_KEEPALIVE_INTERVAL 60 // 9s 过于激进(易误断+PINGREQ风暴); broker超时=1.5×=90s, 60s ping留30s余量 typedef struct _NET_STATE_ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index c33d713..1887189 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -210,9 +210,17 @@ static void iot_evt_process(void) { uint8_t ready = (g_iot_state == IOT_STATE_READY); uint32_t now = mstick(); - if (ready && !_was_ready) { // 重连沿: 作废未决包, 解除挂起, 全量重报 - _evt_pend_id = 0; - _evt_gaveup = 0; + if (ready && !_was_ready) { // 重连沿 + _evt_gaveup = 0; + if (_evt_pend_id) { + /* 有未决包 → 用【原 msg_id/原 ts】立即重发 (协议 V1.04 §5.3-2: + 跨重连也须保持同 msg_id, 否则平台 (sn,msg_id) 去重失效致重复入库) */ + _evt_retry = 0; // 重连后重试计数归零, 不吃掉本次 + iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); + _evt_sent_ms = now; + _was_ready = ready; + return; + } } _was_ready = ready; if (!ready) return; diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c index c173656..b31fb7d 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/net_srv.c @@ -64,7 +64,7 @@ uint8_t MyBuf[RECE_BUF_LEN]; #define MAX_TMP_BUF_LEN 64 -#define MAX_MQTTBUF_LEN 512 //384 //512 +#define MAX_MQTTBUF_LEN 1024 //512: loop_data(4通道)≈604B > 512 → 序列化失败发全零垃圾包被broker RST (2026-07-15) char mqtt_username[64] = {0}; char mqtt_password[32] = {0}; char mqtt_clientid[64] = {0}; @@ -160,21 +160,29 @@ void MQTT_Pingreq() void mqtt_publish(char *topic, char *message, int req_qos) { - uint32_t len = 0; + int len = 0; // 必须有符号: MQTTSerialize_publish 缓冲不足返回 -2(BUFFER_TOO_SHORT) clear_mqtt_buf(); - + { PRINT("\nWill_publish_topid:%s, message:%s\n", topic, message); } - + MQTTString topicString = MQTTString_initializer; int msglen = strlen(message); topicString.cstring = topic; static uint16_t s_mqtt_pkt_id = 0; uint16_t pkt_id = (req_qos > 0) ? ++s_mqtt_pkt_id : 0; len = MQTTSerialize_publish(mqttBuf, sizeof(mqttBuf), 0, req_qos, 0, pkt_id, topicString, (unsigned char *)message, msglen); -// Transport_SendPacket(mqttBuf, len); - WCHNET_SocketSend(SocketId_TCP, (uint8_t *)mqttBuf, &len); + /* 守卫: 序列化失败(缓冲不足/参数错)绝不发送残缓冲。 + 否则 len 转 uint32 成天文数字, WCHNET 把清零的 mqttBuf + 越界相邻内存当垃圾包发出, + broker 见非法报文类型 0x00 → RST → 重连风暴 (2026-07-15 现场事故根因) */ + if (len <= 0) { + PRINT("mqtt_publish: serialize FAIL rc=%d msglen=%d buf=%d (dropped)\n", + len, msglen, (int)sizeof(mqttBuf)); + return; + } + uint32_t slen = (uint32_t)len; + WCHNET_SocketSend(SocketId_TCP, (uint8_t *)mqttBuf, &slen); } void dev_initialize_pub(void) diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index f002924..410efce 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -6,6 +6,48 @@ --- +## 2026-07-15 — 🔴 现场事故: MQTT protocol error 重连风暴 (根因/修复) + +### 现象 + +设备 DC045A49718F (DLD960GA) 上电后每 ~1s 被 mosquitto 以 `disconnected due to protocol error` 踢下线, 15 分钟 80+ 次重连。平台抓包: 设备发出 520B 垃圾包 = 前 512B 全 0x00 + 尾部 8B ASCII "report_c"。 + +### 根因 (静态分析 + 报文长度量化坐实, 与平台"环形缓冲/event_report队列"推测不同) + +**`mqtt_publish` 的 `mqttBuf` 只有 512B, 但 `loop_data`(4通道)≈604B → 溢出。** + +链路: +1. `loop_data` 4 通道 JSON = 576B payload / 604B 整包 MQTT > `MAX_MQTTBUF_LEN=512` +2. `MQTTSerialize_publish` 检测缓冲不足 → 返回 `MQTTPACKET_BUFFER_TOO_SHORT(-2)`, **且一字节不写 buf**(仍是 clear_mqtt_buf 的全零) +3. `mqtt_publish` 里 `len` 是 **uint32_t** → -2 变 4294967294 +4. `WCHNET_SocketSend(mqttBuf, &len)` 把清零的 512B + **越界相邻全局 `temp_guide`("report_config"→"report_c")** 当一包发出 = 520B 垃圾 +5. broker 见非法报文类型 0x00 → RST → TCP Timeout(0x40) → 全量重连 → 风暴 + +> ⚠️ 与 event_report 无关: 事故日志中设备从未发过 event_report; `mqtt_publish` 是扁平缓冲非环形。此为**先前就存在**的隐患, 4 通道 loop_data 必触发。report_config 响应仅 216B 装得下, 故看着正常。 + +### 修复 (net_srv.c) + +1. `MAX_MQTTBUF_LEN` 512 → **1024**: 容纳 604B loop_data + 余量 (TCP 自动分段, MQTT 不关心段边界) +2. `mqtt_publish` 的 `len` 改 **int** + **守卫**: `if (len <= 0) return;` 序列化失败绝不发残缓冲 —— 这是根本防线, 即便未来任何 payload 溢出也不再吐垃圾包 +3. keepalive `MQTT_KEEPALIVE_INTERVAL` 9 → **60** (报告建议; 9s 过激易误断) + +### 协议违规修复 (event_report, 报告实锤) + +- **断线重连后重发用了新 msg_id** → 平台 (sn,msg_id) 去重失效重复入库。修复 `iot_evt_process` 重连沿: 有未决包时保持**原 msg_id/原 ts** 立即重发 (V1.04 §5.3-2), 不再作废换号。 + +### 待决 (需老大拍板) + +- **ts 字段是上电秒数 (mstick()/1000) 而非 Unix 时间戳**: MQTT 模式设备无 RTC/SNTP 时间源。选项: (a) 平台以服务端收包时间为准忽略设备 ts (最省, 推荐); (b) 平台下发时间同步命令; (c) 设备加 SNTP。**暂未改**, 待定方案。 + +### 验证 + +- `tests/test_mqtt_publish_overflow.c`: 复现旧版 512 缓冲发 520B 垃圾包(首字节0x00) + 验证守卫拦截 + 1024 缓冲 641B 正常 + report_config 回归。全过。 +- `tests/test_event_report.c` T8 改为断言重连保持同 msg_id/ts。8 组全过。 + +⚠️ 板上验证: 编译查 .map 确认 RAM 余量(mqttBuf +512B); 烧录后观察不再有 `TCP Timeout` 风暴, loop_data 正常周期上报, 压线圈看 event_report 断线重连去重。 + +--- + ## 2026-07-15 — event_report 实现: 平台必答 + 设备重发 (协议 V1.04) ### 实现 (iot_mqtt_srv.c 事件模块 + net_srv.c ACK 路由) diff --git a/vd960DBN/docs/incidents/2026-07-15-DC045A49718F-protocol-error.md b/vd960DBN/docs/incidents/2026-07-15-DC045A49718F-protocol-error.md new file mode 100644 index 0000000..01bc616 --- /dev/null +++ b/vd960DBN/docs/incidents/2026-07-15-DC045A49718F-protocol-error.md @@ -0,0 +1,57 @@ +# 抓包存档:DC045A49718F MQTT protocol error 风暴 + +- **文件**: `2026-07-15-DC045A49718F-protocol-error.pcap`(44 包,20s 窗口) +- **抓包时间**: 2026-07-15 14:26 左右(设备 14:20 上电后持续复现) +- **抓包点**: 腾讯云 159.75.137.141,`tcpdump -i any port 1883 and host 113.91.145.40` +- **设备**: DC045A49718F (DLD960GA, hard 1.0 / soft 1.0,新版 event_report 重发固件) + +## 现象 + +设备每 ~1s 被 mosquitto 以 `disconnected due to protocol error` 踢下线, +15 分钟内 80+ 次重连、87 次 initialize 上报。 + +## pcap 关键证据(用 `tcpdump -r -nn -ttt -X` 查看) + +正常序列先走完: + +``` +CONNECT (MQTT 3.1.1, client_id=DC045A49718F, u=admin, keepalive=9) +→ CONNACK +→ SUBSCRIBE dld960/DC045A49718F/srv → SUBACK +→ PUBLISH dld960/DC045A49718F/dev {"cmd":"initialize", msg_id:21, ts:393, ...} ← 报文完好 +``` + +紧接着设备发出致命包: + +``` +TCP 段 520 字节 = 前 512 字节全 0x00 + 尾部 8 字节 ASCII "report_c" +``` + +- `0x00` 不是合法 MQTT 控制报文类型(type 0 保留)→ broker 立即 RST 断连 +- RST 后设备仍继续发第二个 520B 全零段(seq 805:1325) + +## 诊断 + +**520 = 512 + 8 → 高度疑似 512B 环形 TX 缓冲区回绕(wrap)处理 bug。** + +推测:往 TX 缓冲写 report_config 响应时跨越回绕边界,"report_c" 落在缓冲区 +物理尾部,其余部分绕回头部;发送侧却把整块未初始化(全零)缓冲区一次性发出。 +本次固件恰好新增了事件待发队列 + 5s 重发机制(V1.04),动过 TX 路径, +建议重点排查: + +1. 新待发队列的写指针 / 回绕边界处理 +2. 重发定时器与主循环并发 publish 的互斥(两个写者交叉写 TX 流) +3. 发送长度是否误用缓冲区总长而非报文实际长度 + +## 顺带实锤的协议违规(固件侧待修) + +1. **断线重连后重发事件用了新 msg_id**:msg_id 9/10 内容完全相同 + (car_leave ch1 value=48, ts=76),双双入库 → 平台 (dev_serial,msg_id) + 去重失效。V1.04 §5.3 条 2:重发必须用相同 msg_id(跨重连也要保持) +2. **ts 字段是上电秒数**(76/393),不是协议约定的 Unix 时间戳 +3. keepalive=9s 过于激进(老问题),建议 30~60s + +## 平台侧结论(无需改动) + +event_report 必答闭环工作正常:ack <1s、回显 msg_id、先落库后应答; +msg_id 相同的重复包去重有效。重复入库仅发生在固件违规换 msg_id 的场景。 diff --git a/vd960DBN/tests/test_event_report.c b/vd960DBN/tests/test_event_report.c index 96b4336..5382bab 100644 --- a/vd960DBN/tests/test_event_report.c +++ b/vd960DBN/tests/test_event_report.c @@ -107,7 +107,14 @@ static void iot_evt_process(void) { static uint8_t _was_ready = 0; uint8_t ready = (g_iot_state == IOT_STATE_READY); uint32_t now = mstick(); - if (ready && !_was_ready) { _evt_pend_id = 0; _evt_gaveup = 0; } + if (ready && !_was_ready) { + _evt_gaveup = 0; + if (_evt_pend_id) { /* 重连: 同 msg_id/ts 立即重发 */ + _evt_retry = 0; + iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); + _evt_sent_ms = now; _was_ready = ready; return; + } + } _was_ready = ready; if (!ready) return; if (_evt_pend_id) { @@ -221,16 +228,22 @@ int main(void) { iot_evt_handle_ack(_evt_pend_id, 0); CHECK(_evt_count == 10, "T7 ACK 后出队6条"); - /* T8: 断线重连沿 → 未决包作废 + 挂起解除, 剩余事件补报 */ - iot_evt_process(); /* 发下一包 (4条? 6条上限内=6... 剩10条→6) */ + /* T8: 断线重连 → 未决包用【同 msg_id/ts】重发 (V1.04 §5.3-2, 平台去重不失效) */ + iot_evt_process(); /* 发下一包 */ uint32_t pend_before = _evt_pend_id; + uint32_t ts_before = _evt_pend_ts; CHECK(pend_before != 0, "T8 有未决包"); g_iot_state = IOT_STATE_DISCONNECTED; - iot_evt_process(); /* 断线 */ + iot_evt_process(); /* 断线 (未 ACK) */ g_iot_state = IOT_STATE_READY; _mock_ms += 100; - iot_evt_process(); /* 重连沿: 作废旧包, 立即新包 */ - CHECK(_evt_pend_id == pend_before + 1, "T8 重连后新 msg_id 补报"); + int pub_before = pub_count; + iot_evt_process(); /* 重连沿: 同 msg_id 立即重发 */ + CHECK(_evt_pend_id == pend_before, "T8 重连后保持同 msg_id (不换号)"); + CHECK(_evt_pend_ts == ts_before, "T8 重连后保持原 ts"); + CHECK(pub_count == pub_before + 1, "T8 重连触发一次重发"); + { char idbuf[32]; snprintf(idbuf, sizeof(idbuf), "\"msg_id\":%u", pend_before); + CHECK(strstr(last_payload, idbuf) != NULL, "T8 重发报文含原 msg_id"); } /* 全部确认清空 */ while (_evt_count) { iot_evt_handle_ack(_evt_pend_id, 0); _mock_ms += 100; iot_evt_process(); } CHECK(_evt_count == 0, "T8 清空"); diff --git a/vd960DBN/tests/test_mqtt_publish_overflow.c b/vd960DBN/tests/test_mqtt_publish_overflow.c new file mode 100644 index 0000000..489397d --- /dev/null +++ b/vd960DBN/tests/test_mqtt_publish_overflow.c @@ -0,0 +1,99 @@ +/* 回归环: 复现 mqtt_publish 缓冲不足时发出垃圾包, 验证修复 + * 忠实复刻 MQTTSerialize_publish 契约 (真源码 MQTTSerializePublish.c L64-68): + * 若 MQTTPacket_len(rem) > buflen → 返回 -2 且不写 buf + * 相邻内存布局: mqttBuf[N] 之后紧邻 temp_guide[]="report_config" + */ +#include +#include +#include + +static int mqtt_packet_len(int rem) { int n=1,r=rem; while(r>=128){r/=128;n++;} return 1+n+rem; } +static int mqtt_pub_serialize(unsigned char *buf, int buflen, int qos, + const char *topic, const char *payload, int paylen) { + int rem = 2 + (int)strlen(topic) + (qos>0?2:0) + paylen; + if (mqtt_packet_len(rem) > buflen) return -2; /* BUFFER_TOO_SHORT, 不写 buf */ + int total = mqtt_packet_len(rem); + buf[0] = 0x30 | (qos<<1); /* PUBLISH 头 */ + memset(buf+1, 0xAB, total-1); /* 标记有效数据(非0) */ + return total; +} + +/* 模拟内存: mqttBuf 紧邻 temp_guide */ +static unsigned char mem[2048]; +#define MQTTBUF_OFF 0 +static void layout_reset(int mqttbuf_len) { + memset(mem, 0, sizeof(mem)); + strcpy((char*)&mem[MQTTBUF_OFF + mqttbuf_len], "report_config"); /* 相邻全局 */ +} + +/* 旧实现: uint32_t len, 无守卫 */ +static int old_publish(int mqttbuf_len, const char *payload, int *sent_len, int *type0) { + unsigned char *mqttBuf = &mem[MQTTBUF_OFF]; + memset(mqttBuf, 0, mqttbuf_len); /* clear_mqtt_buf */ + uint32_t len = (uint32_t)mqtt_pub_serialize(mqttBuf, mqttbuf_len, 0, + "dld960/DC045A49718F/dev", payload, strlen(payload)); + /* WCHNET_SocketSend 发 len 字节 (受 MSS≈576 截断) */ + int send = (len > 576) ? 520 : (int)len; /* 天文数字→按段截到~520 */ + *sent_len = send; + *type0 = (send>0 && mqttBuf[0]==0x00); /* 首字节=0 → 非法 MQTT 类型 */ + return (int)len; +} + +/* 新实现: int len + 守卫 + 大缓冲 */ +static int new_publish(int mqttbuf_len, const char *payload, int *sent_len, int *type0) { + unsigned char *mqttBuf = &mem[MQTTBUF_OFF]; + memset(mqttBuf, 0, mqttbuf_len); + int len = mqtt_pub_serialize(mqttBuf, mqttbuf_len, 0, + "dld960/DC045A49718F/dev", payload, strlen(payload)); + if (len <= 0) { *sent_len = 0; *type0 = 0; return len; } /* 守卫: 绝不发残缓冲 */ + uint32_t slen = (uint32_t)len; + *sent_len = (int)slen; + *type0 = (mqttBuf[0]==0x00); + return len; +} + +int main(void) { + /* 构造 604B 的 loop_data payload (4通道, 与现场同量级) */ + char loop[700]; int n=0; + n += sprintf(loop+n, "{\"msg_id\":8,\"cmd\":\"loop_data\",\"ts\":373,\"data\":{\"channels\":["); + for (int c=1;c<=4;c++) n += sprintf(loop+n, + "%s{\"ch\":%d,\"level\":\"mid_high\",\"iscar\":false,\"loop_ok\":true," + "\"freq\":61286,\"diff\":-8388608,\"sens\":2,\"cndtn\":0," + "\"misc\":{\"type\":\"time\",\"value\":0}}", c>1?",":"", c); + n += sprintf(loop+n, "]}}"); + printf("loop_data payload = %d B\n", n); + + int fails=0, sent, t0, rc; + + /* --- 旧: 512 缓冲 + loop_data → 发垃圾 --- */ + layout_reset(512); + rc = old_publish(512, loop, &sent, &t0); + printf("旧(512): serialize rc=%d, 实发=%dB, 首字节0x00(非法)=%d\n", rc, sent, t0); + if (!(rc==-2 && sent>0 && t0==1)) { printf(" 期望复现垃圾包失败\n"); fails++; } + else printf(" ✓ 复现: 发出 %dB 全零垃圾包(含相邻report_c), broker必RST\n", sent); + + /* --- 新守卫: 512 缓冲 + loop_data → 不发 --- */ + layout_reset(512); + rc = new_publish(512, loop, &sent, &t0); + printf("新守卫(512): rc=%d, 实发=%dB\n", rc, sent); + if (!(rc==-2 && sent==0)) { printf(" 守卫未生效\n"); fails++; } + else printf(" ✓ 守卫拦截: serialize失败→不发送, 连接不被RST\n"); + + /* --- 新: 1024 缓冲 + loop_data → 正常发 --- */ + layout_reset(1024); + rc = new_publish(1024, loop, &sent, &t0); + printf("新(1024): rc=%d, 实发=%dB, 首字节合法(非0)=%d\n", rc, sent, !t0); + if (!(rc>0 && sent==rc && t0==0)) { printf(" 1024缓冲仍异常\n"); fails++; } + else printf(" ✓ 604B报文正常序列化并发送 (TCP自动分段, MQTT不关心段边界)\n"); + + /* --- 回归: report_config(216B) 在两版都正常 --- */ + const char *cfg = "{\"msg_id\":131,\"cmd\":\"report_config\",\"ts\":373,\"code\":0," + "\"msg\":\"success\",\"data\":{\"sensor_type\":12,\"enable\":true}}"; + layout_reset(1024); + rc = new_publish(1024, cfg, &sent, &t0); + if (!(rc>0 && t0==0)) { printf(" report_config回归失败\n"); fails++; } + else printf("回归: report_config(%dB) 正常\n", (int)strlen(cfg)); + + printf(fails?"\n== %d FAIL ==\n":"\n== ALL PASS ==\n", fails); + return fails; +}