Files
vd_960/vd960DBN/docs/devlog.md
T

32 KiB
Raw Blame History

vd960DBN 开发日志

MCU: CH32V208 (RISC-V, WCH) | 通信: BLE + ETH (WCHNET) + UART2→Loop MCU | TCP JSON 端口: 5960

项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接


2026-07-15 — 🔴 loop_data 陈旧快照: 事件与缓存不同源 (帧竞争)

现象 (现场测试: 长车过双线圈)

车离开线圈1后, event_report(car_leave ch1 val=97) 正确上报并 ACK; 但随后 loop_data 连续 34 秒 100+ 条 ch1/ch2 iscar:true, 直到 msg_id 141 才恢复正常。

根因: 两条帧消费路径的数据源分裂

0xC0 帧有两条竞争消费路径 (谁先看到 g_pkg_uart_2.flag 谁消费):

  • uart_srv → lup_process_frame → lup 回调: 只喂事件检测 iot_evt_feed, 不更新 _cached_sr
  • iot_mqtt_publish_sensor Step1 直读: 事件 + 缓存都更新

主循环里 uart_srv 先于 publish_sensor 执行, Step1 只能吃到"帧恰好在两调用之间完成"的窄窗口 — 实测命中率 ~1/56 ≈ 2%。结果:

  1. 事件路径 (回调) 每帧必达 → car_leave 正确
  2. _cached_sr 冻结在车压线圈时的旧帧 (freq=61518/diff=517/relay_count, msg 28~140 逐字节一致)
  3. 陈旧数据自激: 冻结的 diff=517 > 阈值 → fast_mode 锁死 → 以 300ms 最高频率刷屏错误快照
  4. 34s 后 Step1 偶然赢一次 → 缓存刷新 → car_edge(陈旧true→新false) 立即档发出 msg 141 恢复

证据: 同期真实 LUP 帧 eval 字节 car_state=0、variation=±个位数、misc_type 0→3 轮转, 与冻结快照完全对不上。

修复

  • 新增 iot_sensor_ingest(): 事件沿检测 + 刷新 _cached_sr, 单一数据源; lup 回调与 Step1 直读两路全部汇入 → 帧竞争从此无关紧要
  • Step1 直读路径补 lup_verify_checksum (此前只有 uart_srv 路径过校验, 直读裸解析)

教训 (通用)

同一物理量的多个消费者必须同源。事件说"车走了"、快照说"车还在", 这种自相矛盾一定是数据源分裂 — 排查时先问: 这两个字段是从同一帧解析的吗?

板上验证

长车复测同场景: car_leave 事件后下一条 loop_data 的 iscar 必须已翻 false (两者同帧同源); 车走后 diff 回落 → 300ms 快速档应在数秒内退回空闲档, 不再刷屏。


2026-07-15 — loop_data 上报调度升级三档: car_state 沿立即上报

背景

原双速率 (空闲 interval / 变化态 300ms) 下, 进/出车沿最坏也要等 300ms 门控。车辆进出是最关键时刻 (道闸联动/计数), 王工要求: car_state 翻转沿立即上报, 不等间隔

实现 (iot_mqtt_publish_sensor Step2/3)

三档调度:

档位 触发 间隔
立即档 任一通道 car_state 翻转沿 0 (直通门控)
快速档 任一通道 |variation| > 阈值(10) 300ms
空闲档 平稳 配置 interval (默认 60s, 最小 1s)
  • car_edge 优先级最高, 命中即定档; variation 记录但不 break (后续通道可能有沿)
  • 门控: if (!car_edge && 未到间隔) return; — 沿直通, 其余照旧
  • 快照仍在门控通过后刷新 → 同一个沿只触发一次立即发布, 下一轮回归常规档

防洪泛分析

立即档天然有界: Loop MCU 事件帧最快 150ms 一帧, car_state 是 Loop 侧防抖后的状态 (进入确认 3 次 + 离开检测), 不存在毛刺沿; 且发布后快照即同步, 同沿不重复。最坏情况 = 帧率上限 150ms/次, 可接受。

验证

tests/test_report_cadence.c 8 组全过: 空闲60s到点 / 进车沿距上次50ms立即发 / 同沿不重复 / 快速档仍受300ms门控 / 出车沿20ms立即 / 回落空闲 / 双通道同帧沿单包覆盖。


2026-07-15 — 设备时钟同步 (方案B): 平台经 report_config 下发 Unix ts

背景

原上行 ts = mstick()/1000 (上电秒数), 非 Unix 时间戳 (现场事故报告实锤)。设备无 RTC/SNTP。王工定方案B: initialize 上线 → 平台经 report_config 下发真 Unix ts → 设备校准

实现

  • net_srv.c 新增时钟模块: dev_time_sync(unix_ts) (合法性门槛 ≥1600000000 挡掉上电秒数/0) + dev_time_now() (已校准→真 Unix; 未校准→退回上电秒数)。基准用 base_unix + (mstick()-base_tick)/1000, 无符号相减天然处理 49.7 天回绕。
  • manage_mqtt_recv_message 解析 msg_id 后, 从任意下行命令信封 ts 校准 (report_config 为主同步点)。
  • 全部上行 tsmstick()/1000dev_time_now(): net_srv.c 14 处命令响应 + initialize; iot_mqtt_srv.c loop_data + event_report(首发时刻, 重发不刷新) + 遗留响应。heartbeat 的 uptime 字段保留 mstick()/1000 (那本就是运行时长, 非时间戳)。

关键点

  • 校准前 (含首个 initialize) ts=上电秒数, 平台按量级识别未校准。
  • 设备重启无掉电保持 → 每次 initialize 后平台都须重下发 report_config 带 ts。
  • 门槛 1600000000 (2020-09) 挡掉上电秒数(几百)/0/异常, 防污染基准。

验证

tests/test_dev_time_sync.c 6 组全过: 未同步退回上电秒数 / 非法ts被拒 / 校准瞬间对齐 / 校准后随时钟递增 / 门槛边界 / mstick 49.7天回绕无符号相减正确。

⚠️ 平台侧须配合: 收到 initialize 后, 下发 report_config 时信封 ts 填当前 Unix 时间 (协议 V1.05 §2.3)。


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_publishmqttBuf 只有 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_publishlenuint32_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_publishlenint + 守卫: 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 路由)

组件 说明
沿检测 iot_evt_feed 每帧比对 car_state/loop_state 快照。双路汇聚: Step1 消费路径直接喂 + lup_set_sensor_callback(iot_evt_sensor_cb) 覆盖 uart_srv 消费路径(两路都存在帧竞争, 单独任一路都会漏帧漏沿)
事件队列 16 深环形, 溢出丢最旧; 事件仅在 ACK(code=0) 后出队
发送状态机 iot_evt_process 每轮主循环调用(挂在 iot_mqtt_publish_sensor 内, 置于 READY/enable 检查之前)。5s 超时重发, 同 msg_id/原始 ts, 最多 3 次; 耗尽→挂起, 新事件或重连沿解除并以新 msg_id 合并重报
ACK 入口 iot_evt_handle_ack net_srv.c manage_mqtt_recv_message 收到 cmd=event_report 的回显帧时调用, 必须 return 不回 unsupported(否则与平台互打乒乓)
msg_id 事件独立 uint32 计数器(g_iot_msg_id 是 uint8 且与 MQTT packet id 混用, 255 回绕会破坏平台去重窗口)

关键决策

  1. 帧消费提前到 READY/enable 之前: 断网/未使能期间事件照样检测入队, 重连后补报。loop_data 周期上报仍受 READY+enable 门控。
  2. event_report 不受 report_config.enable 门控(王工 2026-07-15 拍板): 事件为关键不可再生数据, 上电即检测上报, 与 loop_data 周期上报的开关解耦。report_config.enable 只管 loop_data。
  3. 线圈断开期间屏蔽 car 沿: 断开时 car_state 不可信, 仅前后两帧 loop 均正常才判进出车; loop_restore 的 value = DBN 本地计时(mstick 差)/50。
  4. car_leave 的 value 直接取离开帧 misc(时间量) = 通过时间(50ms 单位)。
  5. 首帧只建快照: 上电线圈上已有车不算进入沿。

已知边界

  • 队列溢出且恰有未决包时: 作废未决包重发新 msg_id → 平台 (sn,msg_id) 去重失效, 可能重复入库一包。触发条件: broker 不应答的 20s 窗口内涌入 >16 条事件, 现场概率极低, 平台可按事件内容+ts 二次去重兜底。
  • BSS 增量 ~700B(队列128B + 发送缓冲 512B + 状态), 编译后查 .map 确认 RAM 余量

验证

gcc 隔离单测 (/tmp/test_event_report.c) 8 组全过: 首帧快照/进出车沿/5s×3 重发同 id 同 ts/挂起与新事件解除/错 msg_id 及 code≠0 不出队/断开恢复时长+断开期屏蔽 car 沿/单包 6 条上限(实测 317B<500B)/重连沿补报。

⚠️ 平台端注意: 收到 event_report 先落库后应答, 按 (dev_serial,msg_id) 10 分钟窗口去重, 重复包直接答 code=0。


2026-07-15 — MQTT 快速上报增加 car_state 翻转沿触发

背景

原 fast_mode 仅由 |variation| >= 10 触发。灵敏度设高档时小车信号弱,variation 可能不过阈值,但 Loop MCU 已判定 car_state 翻转——进/出车事件仍按空闲间隔(≥1s)慢发,后台看到的过车时刻误差大。故增加:任一通道 car_state 翻转沿也触发 300ms 快速上报

实现要点(两个坑,首版都踩了)

  1. if (fast_mode = 0) 单等号 → 赋值恒假,沿检测死代码。修正为直接判断(循环内走到该行 fast_mode 必为 0,无需再判)。
  2. 快照刷新必须在间隔门控之后。若在门控前无条件刷新 _last_car_state,沿被门控吞掉(如距上次发布 <300ms)时快照已同步,下一轮检测不到翻转 → 事件退化为空闲间隔上报。修正:return(未到间隔)时不刷快照,沿保持"待发"持续顶住 fast_mode,直到真正发布才同步。
/* Step 2 循环内: variation 阈值 与 car_state 沿, 任一命中即 fast */
if (av >= IOT_MQTT_VARIATION_THRESHOLD) { fast_mode = 1; break; }
if (_last_car_state[i] != coils[i].car_state) { fast_mode = 1; break; }

/* Step 3 门控通过后才刷新快照 */
for (i = 0; i < coil_count; i++) _last_car_state[i] = coils[i].car_state;

验证

本地 gcc 隔离单测(buggy vs fixed 对照):进车沿后 buggy 延迟 950ms(退化空闲间隔),fixed 250ms(≤300ms 快速档);出车沿、负向 variation 回归均通过。

⚠️ 上电首帧若已有车(_last_car_state 初值 0 vs car_state=1),会触发一次 fast_mode——属良性,首帧本就该尽快发。


2026-07-14 — Loop 上报 variation 解析升级 2B→3B 有符号 (协议 V1.05)

背景

Loop MCU 上报的 SENS_MULTI_LOOP_DYNAMIC (0xC0/0x0C) 中变化量 variation 由 2B 无符号扩展为 3B 有符号补码(详见 Loop 侧 devlog 与协议 V1.05)。DBN 作为接收/转发端,解析逻辑须同步升级,否则每通道单元 12B 会被按旧的 11B 步长错位解析,4 通道数据全错

改动 (4 处)

1. loop_uart_proto.h — 结构体字段类型

// uint16_t variation;  ->  int32_t variation;  // 3B有符号, = Origin-CAPVD

2. loop_uart_proto.c — 解析函数 lup_parse_sensor_report

  • 每通道单元步长 i*11i*12
  • 通道数计算 data_len/11data_len/12
  • 频率仍 3B 无符号;变化量 3B 解码 + bit23 符号扩展
int32_t v = c[5] | ((int32_t)c[6] << 8) | ((int32_t)c[7] << 16);
if (v & 0x800000) v |= (int32_t)0xFF000000;   // 符号扩展, 漏了负值会变~1600万大正数
cs->variation = v;
  • 杂项偏移 c[7..10]c[8..11](整体右移 1B

3. iot_mqtt_srv.c — fast_mode 阈值判断(隐藏坑)

// variation 变有符号后, 车进入(正)/反向漂移(负) 都应触发加速上报
int32_t av = (v >= 0) ? v : -v;
if (av >= IOT_MQTT_VARIATION_THRESHOLD) fast_mode = 1;   // 原 variation>=10 会漏掉负向

4. JSON 输出(iot_mqtt_srv.c / tcp_json_srv.c

  • "diff":%d / "variation":%d 格式符不变——CH32V208 上 int32_t == int%d 天然正确输出负号,无需改动。

缓冲核查

整帧 52B→56BDBN 侧 g_pkg_uart_2.pkg[BUFF_STACK_SIZE=512]LUP_MAX_PKG_LEN=70 均远大于 56B,接收无截断;转发 MSS=576 不分片。

⚠️ 必须与 Loop 固件同版本发布(步长死绑定)。


2026-06-26 — TCP JSON 协议框架搭建

1. TCP JSON Server 基础实现

  • WCHNET TCP listen socket 创建(端口 5960PROTO_TYPE_TCP
  • 参考 WCH 官方 EVT/EXAM/ETH/TCPServer 例程
  • g_net_state.flag 状态机: ETH_LibInit→1, WCHNET_CreateUdpSocket→2, WCHNET_CreateTcpSocket→3
  • SSC 禁用时 (NET_SSC_ENABLE=0) 手动设 flag=2 跳过 UDP 初始化

2. 鉴权 + 15 条命令

  • pwd_verify 鉴权,3次错误 → 60s 锁定
  • 命令表驱动分发: dev_info_query, ssc_net_set/query, iot_net_set/query, iot_topic_set/query, pwd_set, factory_reset, device_reset, loop_param_set/query, loop_version_query, loop_factory_init, loop_sens_read/write
  • Deferred 响应模式: Loop MCU 命令异步 → TcpJsonPending 挂起 → json_check_pending() 轮询

3. simple_json 解析器修复

  • 6 个 bug 修复: NULL 解引用崩溃(plain values)、缺少 null terminator、buffer unsafe clear、数组支持缺失
  • 字符串值提取时不带引号(调用方无需手动 strip quotes

4. WCHNET TCP 踩坑

问题 修复
listen socket 与数据 socket 混用 → 收不到数据 CONNECT 发到 N, RECV/DISCONNECT 发到 N+1
WCHNET_SocketSend 在 listen socket 静默失败 改用 g_json_socket_listen + 1
接收缓冲区与帧缓冲区重叠 → 数据损坏 分离 WCHNET 内部 buf + 帧累加 buf
#if NET_SSC_ENABLE 误包共享函数 移出 mStopIfError/GetMacAddr/get_ipstr_to_array

2026-06-30 — Loop MCU 串口协议 (0x7F) + 传感器上报

1. USART2 Loop MCU 通信

  • 波特率 192000(文档写 115200,实际硬件 192000
  • 0x7F 协议帧解析器: lup_feed_byte() 逐字节状态机 + LEN-based 帧边界
  • 校验字节站位: total_len = 5 + LENCMD 已在 LEN 中,勿重复计)
  • 命令: lup_cmd_send() 发送 → lup_cmd_check_timeout() 轮询超时

2. 0xC0 传感器数据上报

  • Loop MCU 主动推送 0xC0 帧 → lup_process_frame() 校验 → 回调 json_sensor_callback
  • TCP JSON 输出格式: {"sens_type":"multi_coil","coils":[...]}
  • g_report_active 开关控制上报启停
  • 0xC0 帧通过回调直接驱动网络上报,不经 uart_srv 阻塞

3. 0xC0 帧时间量字段

  • misc_type=0passtime_ms(通过时间 / 车间距,根据 car_state 区分)
  • misc_type=1cut_amount(线圈断开次数)
  • misc_type=2flow_amount(车流量)
  • misc_type=3relay_count(继电器动作次数)

4. 协议文档

  • docs/vd960_loop_protocol_v1.0x.md — 0x7F 帧格式、校验算法、命令参考
  • 波特率修正: 115200 → 192000

2026-07-01 — passtime_ms5 字段改名

vd960Loop 侧时间戳从 5ms 改 50ms 后,DBN 侧同步:

  • passtime_ms5passtime_ms(字段名去 5 后缀)
  • loop_uart_proto.h/c 结构体 + 解析 + tcp_json_srv.c JSON 字段同步

2026-07-02~03 — TCP Server 超时自动重启机制

1. 三项超时触发条件

条件 时间 处理
无任何连接 5min do_restart → 计数重启
无数据交互 5min do_restart → 计数重启
Auth 密码错 3 次 立即 tcp_json_restart() 直接重启

2. 重启计数保护

  • 最多连续重启 3 次,超过进入 10 分钟冷却期
  • CONNECT 成功时 restart_count 清零(正常连接重置计数,不算异常)
  • Auth timeout 不消耗计数器(正常运维行为)

3. WCHNET 限制 - listen socket 不可关闭重建

根因: WCHNET_SocketClose(listen) 后端口 5960 仍标记"已占用"SocketCreat 返回 ERR_ISCONN(0x1D)

最终方案: tcp_json_restart() 只关数据 socket(N+1) 断开客户端,listen socket 保持不动,仅重置应用层状态。WCHNET 自动处理下一个 CONNECT。

4. Auth timeout 死循环修复

原 Auth timeout 手工 WCHNET_SocketClose 后未设 g_json_socket_listen = 0xFF,下次 poll 条件仍满足 → 无限打印。改用统一的 do_restart 路径后修复。

5. Auth 3次失败 → 直接重启

去掉 60s Auth timeout 倒计时。改为连接后 3 次鉴权失败(密码错/格式错)即 tcp_json_restart()。连接后无交互由 5min 空闲检查覆盖。


2026-07-06 — MQTT IoT 基础打通

背景

在 CH32V208RISC-V, 栈仅 2KB)上实现 MQTT IoT 协议栈,对标 DBN101GA 参考项目。WCHNET TCP MSS=576, Publish 须 ≤500B。

1. MQTT 重构对标 DBN101GA

  • iot_mqtt_srv.c 对标 DBN101GA 参考实现重写
  • MQTT socket 复用 SocketId_TCP(而非独立 socket
  • iot_connect_brokerSourPort 参数
  • 出厂默认 topic 使用真实设备序列号

2. 栈溢出系列修复

问题 修复
MQTT buffer 在栈上 → 溢出 改为 static 全局分配
iot_mqtt_publish_sensor: data_json[2KB] + payload[2KB] = 4KB 远超 2KB 栈 减小缓冲区 + static 分配
缓冲区 2048 → 1024 单次交互不超 1KB

3. MQTT CONNECT 延迟发送

根因: MQTT_connect() 在中断上下文调用 WCHNET_SocketSend → 崩溃。

修复: CONNECT 包延到 poll 轮询内发送,避免中断内调用网络 API。

4. MQTTPacket 初始化修复

MQTTPacket_connectData_initializer 是复合字面量,不能用于赋值。改用临时变量中转。

5. IoT socket 重复创建 + broker IP 解析修复

  • IoT socket 重复创建 → 连接失败,改为复用已有 socket
  • broker IP 字符串解析修正

2026-07-07 — MQTT 协议 V1.01 + 命令分发

1. MQTT 主题压缩:多主题 → 双主题

协议从 V1.00 的多 topicdld960/{sn}/loop_data, /event_report, /heartbeat, /cmd, /cfg…)压缩为 V1.01 双主题:

方向 V1.00 V1.01
设备→平台 5+ topics dld960/{sn}/dev
平台→设备 5+ topics dld960/{sn}/srv

2. MQTT 命令分发

manage_mqtt_recv_message() 实现 V1.01 协议命令分发,支持 cmd / Method 双格式:pwd_verify, dev_info_query, loop_param_set/query, loop_version_query, loop_factory_init, device_reset, factory_reset 等。

3. report_config 完整 7 参数

MQTT/TCP report_config 支持完整 7 参数配置:period, loop_data, event_report, heartbeat, passtime, cut_amount, flow_amount。与 TCP JSON 协议统一。

4. MQTT 网络配置

ssc_net_set / iot_net_set / iot_topic_set 三条命令实现,支持通过 MQTT 远程配置设备网络参数和主题。


2026-07-08 — MQTT 稳定性修复系列

1. g_iot_socket 未初始化 → 硬故障重启

g_iot_socket 声明为全局变量但未初始化,值为 0xFFWCHNET_SocketSend(0xFF) 直接触发硬件故障重启。

修复: 初始化为 INVALID_SOCKET

2. loop_data JSON 超 MSS 导致 SocketSend 溢出

WCHNET TCP MSS=576,原 loop_data JSON 四通道全字段 ~800B → WCHNET_SocketSend 溢出崩溃重启。

修复: 精简 JSON 字段至 ~400B(单包达标),再分批发送2通道/包 × 2包)。

3. iot_mqtt_send() hex dump 循环 → 栈溢出

调试用的 hex dump 循环(65次 PRINT)在仅 2KB 栈的 CH32V208 上直接栈溢出重启。

修复: 移除 hex dump 循环。

4. MQTT 主动上报无数据 + PINGREQ 缺失

poll_mqtt() 未正确触发 MQTT_Yield()MQTT_Live(),导致主动上报无数据、PINGREQ 缺失断连。

修复: 改用 net_srv.c 统一的 mqtt_publish() + 修正 poll_mqtt() 调度逻辑。

5. 传感数据 topic 修正

topic 修正为 V1.01 双主题协议 dld960/{sn}/dev(此前遗留旧 topic 格式)。

6. 恢复 loop_data 完整字段

精简 JSON 后缺失部分字段(freq, passtime_ms 等),恢复完整字段并在分批框架内发送。


2026-07-10 — 双主题发布统一 + initialize 对齐 + packet_id 断连修复

1. 双主题发布统一 (dld960/{sn}/dev)

此前 heartbeat 和传感器上报仍使用旧 topic 路径(dev/statusg_iot_topic.topic_pub),与 V1.01 双主题模型不一致。

修复: 所有 MQTT 发布统一为 dld960/{sn}/dev

  • iot_mqtt_srv.c heartbeat: iot_make_topic(..., "dev", "status", ...)snprintf(..., "dld960/%s/dev", ...)
  • iot_mqtt_srv.c iot_mqtt_publish_sensor: g_iot_topic.topic_pubdld960/{sn}/dev
  • net_srv.c dev_initialize_pub: g_iot_topic.topic_pubdld960/{sn}/dev

2. initialize 消息格式对齐 V1.03

dev_initialize_pub() 原先缺少顶层 model/hard_ver/soft_ver 字段,且在 extra_info 中有已废弃的 version 字段。

修复: 对齐协议文档 §5.1

  • 补充 model(PRODUCT_MODEL)、hard_ver(HARDWARE_VER)、soft_ver(FIRMWARE_VER) 到 data 顶层
  • 移除 extra_info.version
  • 缓冲区扩容 256→512 字节

3. mqtt_publish() packet_id=0 导致 broker 断连 🔴

现象: 设备收到查询指令后正确发布响应 JSON,10ms 后 broker 主动断开 TCP 连接(SockInt stat=0x10)。

根因: net_srv.cmqtt_publish() 调用 MQTTSerialize_publish()packet_id 硬编码为 0。当 req_qos=1(命令响应使用 QoS 1)时,MQTT 规范要求 packet_id ≠ 00 属于协议违规,broker 检测后直接踢掉连接。

修复: 新增 static uint16_t s_mqtt_pkt_id 计数器,QoS>0 时自增作为 packet_idQoS=0 保持 0。

static uint16_t s_mqtt_pkt_id = 0;
uint16_t pkt_id = (req_qos > 0) ? ++s_mqtt_pkt_id : 0;

4. DBNMQTTool 同步更新

  • 协议版本注释 V1.01→V1.03
  • 协议 Topic 树 + 模拟页增加 initialize 支持
  • device_manager.mark_online() 扩展 model/hard_ver/soft_ver 参数,设备列表正确显示型号
  • 增加 dld960/+/dev/# 通配订阅兼容旧固件多级 topic


2026-07-23 — loop_data JSON 构建优化: 消灭 data_json 中间缓冲 + coil_count 硬限

背景 / 问题

现场 DC045A49718F 出现 MQTT 上报异常: 平台收到 _raw 消息, hex 解码后发现 loop_data JSON 中间嵌入了完整的 MQTT PUBLISH 二进制帧 (event_report 的封包)。

根因: iot_mqtt_publish_sensor()static char data_json[1024]static char payload[1400] 合计 2424B BSS, 叠加同文件其他大缓冲 (~9KB total), 在 CH32V208 48KB RAM 中可能导致链接器将 data_json/payloadmqttBuf[1024] 分配到重叠地址。

污染路径:

  1. iot_evt_process()mqtt_publish()MQTTSerialize_publish(mqttBuf,...) 写入 MQTT 二进制到 mqttBuf
  2. 因 BSS 重叠, data_json 也被写入相同内容
  3. snprintf(data_json, ...) 覆写前 ~427B 为通道 JSON, 尾部残留 MQTT 二进制
  4. snprintf(payload, ..., "%s", data_json)%s 将残留二进制也拷进 payload
  5. mqtt_publish(topic, payload, 0) → 发出污染后的 payload

修复

改动 说明

2026-07-23 — MQTT 栈重构 + 硬件看门狗

问题背景

vd960DBN 现场出现三类 MQTT 上报异常:

  1. _raw 异常: loop_data JSON 中间嵌入 MQTT PUBLISH 二进制帧
  2. 频繁 initialize: 每 4 秒重连, 旧 MQTT 栈与 IoT 栈抢 socket
  3. SocketSend hard fault: MQTT CONNECT 发送后设备重启

根因链

问题 根因 修复 commit
_raw data_json[1024]mqttBuf[1024] BSS 重叠 → 消灭 data_json e11a80c
_raw 仍出现 mqtt_publish() 用全局 mqttBuf, event_report 二进制污染 loop_data 086dbc6
→ 改用 iot_mqtt_publish(), buf[1024] 与 mqttBuf 物理隔离
频繁 initialize WCHNET_HandleSockInt 调旧 mqtt_connect() + IoT 栈也发 CONNECT → 双 CONNECT 3d017cc
→ socket 事件全委托给 iot_mqtt_handle_sock_int, 旧栈切断
SocketSend 重启 iot_mqtt_send()uint16_t slenuint32_t* → 栈破坏 hard fault a577537
SocketSend 还重启 WCHNET_ModifyRecvBuf(_iot_wchnet_buf) 覆盖原生 SocketRecvBuf → DMA 非法 9521a5e
tmp[1152] 栈变量在中断上下文撑爆 2KB 栈 → 改 static
SocketSend 再重启 ⑦ WCHNET SocketSend 在中断外调用 hard fault → 改回中断内发 CONNECT 32f0edb
_raw 再次出现 WCHNET_SocketSend 部分发送 520/607B → 残留拼到下一帧 9e83063
iot_mqtt_send 改为 while 循环重试
命令处理缺失 report_config/event_report ACK 无人处理 → code=4 67976ec
msg_id 跳号 ⑩ 心跳 ++g_iot_msg_id + iot_mqtt_publish 双递增 a9c36e4

三层保活

┌─ MQTT 层: PINGREQ/PINGRESP (60s) → broker 不回 → TCP 超时
├─ TCP 层:  KeepAlive → WCHNET SINT_STAT_DISCONNECT/TIMEOUT
└─ 硬件层: IWDG (LSI≈40kHz, Prescaler=256, Reload=625 → ~4s)
             主循环 iot_mqtt_poll() 每轮喂狗

heartbeat 是单向设备上报, 平台不回复; MQTT PINGREQ/PINGRESP 才是双向保活机制。

最终架构

WCHNET_HandleSockInt (net_srv.c)
  └─ iot_enable? → SocketRecvBuf(原生) + KeepLive + iot_mqtt_handle_sock_int()
      ├─ CONNECT        → iot_mqtt_send_connect() [中断内] → MQTT_CONNECTING
      ├─ RECV           → _iot_recv_buf → iot_process_recv()
      │    ├─ CONNACK   → iot_mqtt_send_subscribe() → SUBACK → READY
      │    ├─ PUBLISH   → iot_handle_publish(report_config/event_report ACK/...)
      │    └─ PINGRESP  → (MQTT 库自动处理)
      └─ DISCONNECT     → g_iot_state=DISCONNECTED → 重连退避

Main_Circulation:
  iot_mqtt_publish_sensor()   ← 0xC0→loop_data + event_report
  iot_mqtt_poll()             ← IWDG喂狗 + 状态机 + PINGREQ(60s)

发送缓冲隔离 (v3)

event_report → mqtt_publish()       → mqttBuf[1024]      (net_srv.o BSS)
loop_data    → iot_mqtt_publish()   → buf[1024]           (iot_mqtt_srv.o BSS)
heartbeat    → iot_mqtt_publish()   → buf[1024]
initialize   → iot_mqtt_publish()   → buf[1024]

msg_id 统一

所有发布点用 g_iot_msg_id + 1 预读, iot_mqtt_publish 内部 ++g_iot_msg_id (MQTT pktid for QoS>0, 对QoS=0 也递增以保持一致)。


2026-07-23 — 保活精简 + SocketSend 故障恢复

保活机制梳理

heartbeat JSON (60s 单向设备上报) 与 MQTT PINGREQ/PINGRESP 功能重叠:

  • heartbeat 是设备→平台单向, 平台不回复, 不能做存活检测
  • MQTT PINGREQ/PINGRESP 才是双向保活, broker 不回 → TCP 超时

决策: 删除 heartbeat JSON, MQTT PINGREQ 独立保活; loop_data 按 g_report_cfg.interval 上报即可。

三层保活链路

层级 机制 超时 触发
MQTT PINGREQ/PINGRESP (60s) broker无响应→TCP超时 SINT_STAT_TIM_OUT
TCP KeepAlive WCHNET 检测 SINT_STAT_DISCONNECT
硬件 IWDG (4s, LSI) 主循环卡死 硬件复位

SocketSend 故障快速恢复

问题: WCHNET_SocketSend 突然返回 ret=0x11 sent=0, TCP 超时要等 ~2 分钟才触发 SINT_STAT_TIM_OUT, 期间所有 loop_data/event_report 全丢。

修复 v1 — 连续失败检测 (7c0de6c):

  • _iot_send_fail_cnt 追踪连续失败次数
  • 3 次连续失败 → 主动 WCHNET_SocketClose + DISCONNECTED → 重连
  • 任一次成功清零, 新连接也清零

修复 v2 — 重连时重建 socket (98d9b99):

  • iot_connect_broker() 原只设 g_iot_socket = SocketId_TCP 但 close 后 socket 已销毁 → TCP connect 一直超时
  • 改为调 WCHNET_CreateTcpMqttSocket() 真正 create + connect
  • 断连后加 3s 延迟再重连, 给 WCHNET 清理时间

BSS 变动

变量 改动 ΔBSS
data_json[1024] 删除 (v1) -1024B
iot_send_heartbeat::payload[512] 删除 (函数移除) -512B
_iot_send_fail_cnt 新增 +4B
净省 -1532B

---| | 消灭 data_json[1024] | 直接在 payload[1400] 构建完整 JSON, 省 1024B BSS | | 消除 %s 拷贝 | 不再从中间缓冲格式化到 payload, 杜绝尾部二进制残留路径 | | coil_count 硬限 | if (coil_n > 4) coil_n = 4 — 防 0xC0 坏帧致 snprintf 循环溢出 | | JSON 构建顺序调整 | 先写包装头 {"msg_id":...,"data":{"channels":[, 再追加通道, 最后 ]}} |

BSS 节省: 1024 bytes 协议兼容: JSON 输出格式不变 ({"msg_id":N,"cmd":"loop_data","ts":T,"data":{"channels":[...]}})

修订记录

| 版本 | 时间 | 说明 | | V3.5 | 2026-07-23 | 保活精简(去heartbeat+IWDG) + SocketSend故障恢复(3次失败重连+重建socket) | | V3.4 | 2026-07-23 | MQTT 栈重构: 发送缓冲隔离/命令处理/msg_id统一/看门狗, 共10项修复 |

V3.3 2026-07-23 loop_data: 消灭 data_json[1024] BSS 缓冲 + coil_count 硬限, 防 mqttBuf 重叠致 _raw 异常
V3.2 2026-07-10 双主题发布统一 + initialize 数据格式对齐 V1.03 + mqtt_publish packet_id=0 断连修复
V3.1 2026-07-09 V1.03: 订阅后发 initialize 上线消息; iot_mqtt_srv 订阅改双主题
V3.0 2026-07-08 MQTT 稳定性修复: socket初始化/buffer溢出/分批发送/hex dump/PINGREQ
V2.9 2026-07-07 MQTT V1.01 双主题协议 + 命令分发 + report_config 7参数
V2.8 2026-07-07 MQTT 网络配置: ssc_net_set / iot_net_set / iot_topic_set
V2.7 2026-07-06 MQTT IoT 基础打通: 对标DBN101GA + 栈溢出修复 + CONNECT延迟发送
V2.6 2026-07-06 IoT 模式配置 bug 修复
V2.5 2026-07-03 tcp_json_restart 只关数据socket, listen保持不动 (WCHNET限制)
V2.4 2026-07-03 去掉 Auth 60s timeout, 改为3次失败即重启
V2.3 2026-07-03 Auth timeout 不计入重启限额, 修复冷却期阻断连接
V2.2 2026-07-03 Auth timeout 死循环打印修复 → goto do_restart
V2.1 2026-07-03 TCP Server 自动重启机制 (3条件 + 3次限额 + 10min冷却)
V2.0 2026-07-01 passtime_ms5 → passtime_ms 字段改名
V1.5 2026-06-30 0xC0 传感器上报 + 时间量字段完善 + 协议文档
V1.4 2026-06-30 USART2 Loop MCU 0x7F 协议实现
V1.3 2026-06-30 simple_json 6 bug 修复
V1.2 2026-06-30 WCHNET buffer 分离 + SocketSend listen→data 修正
V1.1 2026-06-30 NET_SSC_ENABLE 隔离 + #if guard 共享函数修复
V1.0 2026-06-26 TCP JSON 协议框架 (鉴权 + 15条命令)