Files
vd_960/vd960DBN/docs/devlog.md
T
wangfq 92887a3d92 feat(vd960DBN): 传感快照区落地 snapshot.c + BLE SNAP_STAT/QUERY/CLEAR
- snapshot.h/c: 64B 定长 SnapRec (头部16B + 4x12B 0xC0线圈数据原样)
  环形分区独立于事件日志, 快照区 = 总容量 - 固定区576KB - 事件区
- 线程模型: USART2 ISR 只打包+RAM暂存(8深满丢新), 主循环 snap_flush 落盘
  (iot_sensor_ingest 在中断上下文, SPI 45ms 擦除严禁进中断)
- dbn_ble_srv: SNAP_STAT(0x28)/SNAP_QUERY(0x29)/SNAP_CLEAR(0x2A)
  QUERY 上限 2 条 (64x2+2=130B, ODR 教训防御)
- iot_mqtt_srv: ingest 挂 snap_enqueue, publish_sensor 挂 snap_flush
  (放在 READY 检查前, 断网照常落盘)
- peripheral_main: offlog_init 后加 snap_init
- 清空审计: 写事件流 log_clear payload[0]=2
- 测试: test_snapshot.c 8 例全过, offlog/ble_offlog 回归全过
- 文档: DLD960_BLE协议 V1.02 + ROADMAP P1.2 状态 + devlog
2026-08-12 17:40:14 +08:00

1048 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# vd960DBN 开发日志
> MCU: CH32V208 (RISC-V, WCH) | 通信: BLE + ETH (WCHNET) + UART2→Loop MCU | TCP JSON 端口: 5960
>
> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接
---
## 2026-08-12 — 传感快照区落地(snapshot.cBLE 新增 0x28/0x29/0x2A
### 背景
ROADMAP P1.2 分区规划(参数区→OTA→事件日志区→传感快照区)中,事件区已落地(offlog),快照区此前为预留。本次实现快照流:0xC0 传感帧按上报节奏落盘,断网期间波形照常记录,可离线回放。
### 关键设计决策
1. **64B 定长记录 SnapRec**:头部 16Bmagic=0xA6/len/flags/seq/ts_ms/boot_seq+ 4×12B 线圈数据(与 0xC0 线上格式逐字节一致,可直接对照抓包)→ W25Q32 下 48064 条。
2. **线程安全(踩坑预警)**`iot_sensor_ingest()`**USART2 ISR 上下文**`loop_uart_proto.h:209` 明确标注)被调用!SPI 擦除 ~45ms 绝不能在中断里做 → 中断只打包+入 RAM 暂存(8 深,满丢新),主循环 `iot_mqtt_publish_sensor()` 每轮 `snap_flush()` 落盘。单生产者(中断)单消费者(主循环)count 计数环形,volatile 字节天然原子。
3. **分区表同一事实来源**:快照区 = 总容量 − 固定区 576KB − 事件区(`g_offlog_part.area_size`),杜绝两模块对事件区大小理解不一致。
4. **审计**:快照清除写事件流 `log_clear`payload[0]=2=快照流)。
5. **BLE 指令对齐 OFFLOG 模式**SNAP_STAT(0x28)/SNAP_QUERY(0x29)/SNAP_CLEAR(0x2A)QUERY 上限 2 条(64×2+2=130B ≤ 单包缓冲,ODR 教训防御)。
### 测试
`test_snapshot.c` 8 例全过(中断安全/打包格式/环形回绕/掉电恢复/扇区切换/暂存满丢新/清空审计/分区表);`test_offlog` 8 例、`test_ble_offlog` 7 例回归全过。
> ⚠ 测试踩坑:不要在同文件 include offlog.c + snapshot.c —— 两个 .c 的 static 变量(如 `_wr_off`)在同一翻译单元**符号冲突**,快照 clear 审计会污染事件写指针。测试改为 mock `g_offlog_part` + `offlog_evt`。
### 待板上验证
- 快照区 0x110000 起、W25Q32 48064 条 capacity 打印
- 中断风暴下暂存满丢新行为(观察点:`g_snap_drop`
- 掉电恢复跨扇区扫描
---
## 2026-08-12 — BLE 分包粒度动态化修复(Too large noti 丢包)
### 背景
`CMD_DBN_OFFLOG_QUERY` 拉取 4 条日志时响应 dat=130B,`set_response_buf`**写死的 `MAX_BLE_DAT_RESPONSE_LEN=96`** 分包:第一包 = 帧头4 + dat96 + ckb2 = **102B**。而小程序把 MTU 协商到 96 后,ATT 通知 payload 上限 = 96-3 = **93B**`peripheralChar4Notify` 判定 `102 > 93`**"Too large noti" 直接 return,第一包永久丢失**,小程序重组永远不完整(只收到第二包 34B)。
现场日志证据:
```
BLE: offlog_query start_seq=1 req=4 fetched=4
Too large noti, len:102, peripheralMTU:96
```
### 根因链
| # | 环节 | 问题 |
|---|------|------|
| 1 | `BLE_BUFF_MAX_LEN=100` (config.h) | `MAX_BLE_DAT_RESPONSE_LEN = 100-4 = 96` 分包块大小**写死** |
| 2 | 分包粒度不随协商 MTU 变化 | MTU 协商到 96 后,每包 96B dat 必然超 93B 上限 |
| 3 | `BLE_Notify_Buf.buf[100]` | `set_response_to_notify` 写 102B → **越界 2B**UB,可能踩坏相邻 `g_flag_notify_temp`/`g_buf_ble_response` |
| 4 | `peripheralChar4Notify` 超限 return | 丢包无重传,小程序重组永久不完整 |
> 2026-08-10 日志中"响应超 96B 自动分包(既有机制)"的假设不成立:分包机制存在但**粒度没跟随协商 MTU**。
### 修复:`ble_notify_chunk_max()` 动态分包
```c
/* 整包 = 帧头4(magic/header/len/cmd) + dat + ckb2
须满足 整包 <= peripheralMTU - 3 (ATT opcode+handle)
且 整包 <= MAX_BLE_Notify_Buf_LEN (本地缓冲防越界)
MTU=23 -> 14, MTU=96 -> 87, MTU>=103 -> 94 */
static uint16_t ble_notify_chunk_max(void)
{
uint16_t _mtu = peripheral_get_mtu();
if (_mtu < 23) _mtu = ATT_MTU_SIZE; /* 未协商兜底 */
uint16_t _limit = _mtu - 9; /* 整包上限-帧头4-ckb2 */
if (_limit > (MAX_BLE_Notify_Buf_LEN - 6))
_limit = MAX_BLE_Notify_Buf_LEN - 6;
return _limit;
}
```
改动文件(GBK+CRLF,全部 Python 二进制替换):
| 文件 | 改动 |
|------|------|
| `peripheral.c` | 新增 `peripheral_get_mtu()` getterperipheralMTU 是 static |
| `dbn_ble_srv.c` | 新增 `ble_notify_chunk_max()``set_response_buf` / `set_response_to_notify` / `set_response_iot_net` / `set_response_iot_topic` 四处 `MAX_BLE_DAT_RESPONSE_LEN` 全部改用动态 chunk(组包包数与实际切包必须同一粒度,否则 pkg_amount 错乱) |
### 验证
- 隔离 C 测试(真实函数体 + mock `peripheral_get_mtu`):MTU=23/96/185/517 四组,每组验证 chunk 上限、单包 ≤ MTU-3、单包 ≤ 100B 缓冲、帧结构一致、seq 递增、**重组 130B 完整且逐字节一致**,全部 PASS
- MTU=96 时:2 包(93B + 49B)✓;MTU=185 时:2 包(100B + 42B)✓;MTU=23 未协商时降级 10 包(每包 20Bpkg_amount=10 ≤ header 高4位上限15
- 顺带消除 `g_notify_buftemp.buf[100]` 越界 2B 隐患
- 待板级验证:MRS 真编译 + 真机小程序 QUERY 拉 4 条日志(本地无 RISC-V 工具链)
### 小程序端(可选优化,非必须)
- Android 可在连接后 `wx.setBLEMTU({mtu: 185})`,分包粒度升到 94B/包,减少包数;iOS 系统自动协商,固件已自适应,不调也能正常拉取
---
## 2026-08-12 — 续:第二分包丢失 — performPeriodicTask 主动拉包
### 现场复测
小程序发 QUERY 后,第一包 93B 正常收到(`value: ArrayBuffer(93)`header `0x21` = amount=2/seq=1,含 2 条完整记录 + 第 3 条 21B 残片),**第二包 49B 永远收不到**。设备侧无 `Too large noti`(MTU 检查已过),两包间隔 50ms。
### 根因:分包续传依赖"下一次收包事件"
`poll_dbn_ble` 尾部 `set_response_to_notify` 虽在主循环每轮执行,但分包第二包的填充/发送链路脆弱:
1. 第一包由 `performPeriodicTask`50ms TMOS)发出并 `clear_ble_notify_buf`
2. 第二包的填充依赖主循环下一轮 `poll_dbn_ble` 执行 `set_response_to_notify` —— 与 TMOS 周期事件交错,任何主循环阻塞(WCHNET/uart/网络轮询)都会让时序错位
3. `peripheralChar4Notify` 发送失败(`simpleProfile_Notify != SUCCESS`)时**静默丢弃**,无任何日志
### 修复
```c
static void performPeriodicTask(void)
{
if(g_flag_notify_temp){
g_flag_notify_temp = 0;
peripheralChar4Notify(g_notify_buftemp.buf, g_notify_buftemp.len);
clear_ble_notify_buf(&g_notify_buftemp);
}
/* 分包续传: notify 空闲且响应队列还有分片时, 50ms 周期主动拉下一包,
不依赖 poll_dbn_ble 收包事件 */
if(g_notify_buftemp.flag == 0)
{
g_flag_notify_temp = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
}
}
```
- `peripheral.c``performPeriodicTask` 发完当前包后主动 `set_response_to_notify` 拉下一分片;`peripheralChar4Notify` 增加发送结果打印(`BLE notify OK/FAIL, len, MTU`),定位发送失败不再静默
- 时序:cycle1 填充 pkt1 → cycle2 发 pkt1 + 填充 pkt2 → cycle3 发 pkt2**两包间隔 50ms,与收包事件解耦**
### 验证
- 隔离 C 测试:无任何收包事件、仅靠 `performPeriodicTask` 驱动,MTU=96 → 3 周期发出 93B+49B 两包,重组 130B 逐字节一致;MTU=185 → 100B+42B,全 PASS
- 板上烧录后设备日志应出现:`BLE notify OK, len:93``BLE notify OK, len:49`;若出现 `FAIL` 则问题在 GATT 层(连接/通知使能)
- 待真机确认:若固件打印两包都 OK 但小程序仍只收 1 包 → 检查小程序 `onBLECharacteristicValueChange` 订阅连续性/连接参数(连接间隔大 + 从机延迟会拉长第二包到达时间)
---
## 2026-08-12 — 事件日志区动态分区(ROADMAP 落地:JEDEC ID 选档)
### 背景
ROADMAP P1.2 更新:分区顺序调整为 **参数区 → OTA 镜像暂存区 → 事件日志区 → 传感快照区**;事件日志区容量随存储芯片动态(W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB),上电读 JEDEC ID 选档。移除 W25Q80 支持。
### 代码改动
| 文件 | 改动 |
|------|------|
| `offlog.h` | 新增 `OfflogPart` 运行时分区表 + `g_offlog_part``OFFLOG_AREA_BASE` 改 0x090000(参数64KB+OTA512KB);`OFFLOG_AREA_SIZE/DATA_SECTOR_CNT/MAX_RECORDS/DATA_SIZE` 宏改为展开运行时值(主体零改动);新增 `OFFLOG_CHIP_W25Q32/64/128/256` JEDEC ID 宏 |
| `offlog.c` | 定义 `g_offlog_part`(默认 W25Q32 512KB/16256条);新增 `offlog_part_detect()`(读 JEDEC ID → 填表,未知按 W25Q32 兜底);`offlog_init` 开头调用 |
| `storage.c` | 新增 `SPI_Flash_ReadJEDEC_ID()`0x9F 读 3 字节,校验 EF 40,返回 device id |
| `storage.h` | 声明 |
> 注:offlog.h/c 是 **UTF-8**2026-08-04 新建),storage.c 是 **GBK**——改造按各自编码精确替换,未破坏。
### 容量对照
| 芯片 | JEDEC | 事件区 | 数据扇区 | max_records | capacity LE |
|------|-------|--------|---------|-------------|-------------|
| W25Q32(默认) | 0x16 | 512KB | 127 | 16256 | `80 3F 00 00` |
| W25Q64 | 0x17 | 1MB | 255 | 32640 | `80 7F 00 00` |
| W25Q128 | 0x18 | 2MB | 511 | 65408 | `80 FF 00 00` |
| W25Q256 | 0x19 | 4MB | 1023 | 130944 | `80 FF 01 00` |
### 兼容性
- 旧固件事件区在 0x010000256KB),新固件在 0x090000——**旧日志数据不会误读**(新地址头扇区无 magic → 视为全新区自动重建),属预期行为
- `offlog_init``storage_init` 之后调用(peripheral_main.c:305-306),SPI 已就绪
### 文档同步
- `DLD960_BLE协议.md` / `DLD960_IoT_MQTT协议.md` / `DLD960_TCP_JSON协议.md`capacity 字段 8064 → 动态(含四芯片对照)
- `DLD960_BLE日志查询交互.md`STAT 示例 capacity 更新
- `ROADMAP.md`P1.2 分区规划(前序提交)
### 验证
- `tests/test_offlog.c`mock JEDEC=0x168 例全过(环形回绕自动适配 16256)
- `tests/test_ble_offlog.c`mock `g_offlog_part`capacity 断言 162567 例全过
- 地址范围模拟:四芯片事件区 0x090000 起均不超芯片容量
---
## 2026-08-12 — 续2:第二分包丢失 — 根因确认(响应缓冲越界踩踏 g_notify_buftemp
### 破案过程(三份现场日志 + .map 对照)
| 证据 | 结论 |
|------|------|
| `CLEAR busy resp: amt=2 seq=2 off=130 ret=00008122` | ret 对 .map = `set_response_to_notify` 内部(0x7FEC+0x136)→ 第二包**填充成功**(正常清空),队列清空不是问题 |
| `BLE ntf flag 1->0 (len=0 temp=0)``BLE send` | 第二包(49B)从通知缓冲**物理消失**,非发送路径 |
| `CLR_NTF: buf=20007bdc flag=1 len=93 ret=0000d604` | 第一包发送后正常清空(ret=Peripheral_ProcessEvent 内联 performPeriodicTask |
| **`.map` RAM 布局** | `g_buf_ble_response=0x20007B58 +0x6B(107B)``g_notify_buftemp=0x20007BDC`**107B = 7 + dat[100] = MAX_BLE_DAT_BUF_LEN 编译时是 100(旧宏)** |
### 根因链(ODR 违规)
```
用户 MRS 工程 dbn_ble_srv.h 宏未同步: MAX_BLE_TMP_BUF_LEN/MAX_BLE_DAT_BUF_LEN=100 (repo 为 132)
→ Buf_DBN_BLE 实际 107B (dat[100]), 链接器把 g_notify_buftemp 排在 0x20007BDC
→ QUERY 响应组包 130B: tmp_ble_buf[100..129] 越界 + set_response_buf 写 dat[0..129] 越界 30B
→ 越界写踩中 g_notify_buftemp.flag/len (物理地址重叠)
→ 第二分包填充后 flag/len 被清零 → 下一 TMOS 周期不发送 → 永久丢失
```
> 同一时间戳 `BLE resp flag 1->0 (pull=0)` 是越界后的**连锁反应**notify flag 被清 0 → poll_dbn_ble 走 idle 分支 → `g_flag_notify_temp=0` → 发送周期被跳过。
### 修复(防御性根治,兼容新旧宏)
1. **QUERY 组包动态限制记录数**`_max_rec = (MAX_BLE_TMP_BUF_LEN - 2) / sizeof(OfflogEvt)` → 宏=100 时最多 3 条(98B),宏=132 时最多 4 条(130B),**永不越界**
2. **`set_response_buf` 截断防御**`dat_len > MAX_BLE_DAT_BUF_LEN` 时截断
3. 验证:旧宏 100 下 3 条 = 98B → 分包 [93, 17] 全合规;新宏 132 下 4 条 = 130B → [93, 49] 全合规
### ⚠ 必做:确认 MRS 工程头文件
**`.map` 铁证:用户编译的固件里 `MAX_BLE_DAT_BUF_LEN=100`(旧宏),repo 已是 132。** 即使加防御,也请确认 MRS 工程 `APP/include/dbn_ble_srv.h``MAX_BLE_TMP_BUF_LEN`/`MAX_BLE_DAT_BUF_LEN` 均为 **132**(与 repo 一致),否则结构体大小不一致(ODR 违规)的隐患仍在。
---
## 2026-08-10 — BLE 读取脱机日志接口 (OFFLOG_STAT/QUERY/CLEAR)
### 背景
offlog 日志已落地 MQTT V1.06 / TCP JSON V1.02 分发(2026-08-05),但 BLE 侧(小程序/APP 现场离线取证)无读取通道。现场常见场景:设备无网/断网,需蓝牙直连拉日志判断"重复上线"是平台问题还是设备复位。
### 方案:3 条 BLE 命令,复用 offlog 底层 API
| 命令码 | 名称 | 语义(对齐 MQTT log_* |
|--------|------|--------------------------|
| `0x25` | `CMD_DBN_OFFLOG_STAT` | 日志统计:status + boot_seq(2) + count(4) + capacity(4) + seq_first(4) + seq_last(4),全 LE19B |
| `0x26` | `CMD_DBN_OFFLOG_QUERY` | 分页拉取:请求 `start_seq(LE32) + count(1)`;响应 `status(1) + count(1) + N×32B OfflogEvt 原始结构`N≤4(对齐 OFFLOG_MAX_QUERY_RECORDS |
| `0x27` | `CMD_DBN_OFFLOG_CLEAR` | 清空(审计留痕),阻塞 ~2.8s,主循环上下文可接受 |
- QUERY 定位复用 MQTT 同款公式 `idx = start_seq - seq_first``offlog_read_idx()`,不新增 offlog API
- 记录直接传 32B 二进制 OfflogEvtBLE 是二进制协议,无需 JSON 化;文档给出结构偏移 + 事件类型表)
- 响应超 96B 自动分包(`set_response_buf` 既有机制):QUERY 最大 130B → 2 包
### 缓冲扩容(RAM +64B
| 宏 | 旧值 | 新值 | 原因 |
|----|------|------|------|
| `MAX_BLE_TMP_BUF_LEN` | `BLE_BUFF_MAX_LEN`(100) | **132** | QUERY 组包 1+4×32=129B |
| `Buf_DBN_BLE.dat` | `MAX_BLE_BUF_LEN`(100) | **`MAX_BLE_DAT_BUF_LEN`(132)** | set_response_buf 拷贝 129B |
新增 `MAX_BLE_DAT_BUF_LEN=132``clear_buf_dbn_ble_all` 的 memset 同步改用新宏。`tmp_ble_buf`/`Buf_DBN_BLE` 仅 dbn_ble_srv.c/h 内使用,影响面局部。
### 关键决策与坑
1. **GBK+CRLF 文件二进制编辑**dbn_ble_srv.c/h 是 GBK 编码(file 报 ISO-8859),`patch` 工具会静默转码成 UTF-8 致中文注释乱码。全部用 Python rb/wb 精确替换,每处 count==1 校验;改后 file 复核仍 ISO-8859。新增注释用 ASCII 规避编码问题。
2. **case 内变量名冲突**`manage_dbn_ble_default` 顶部已有 `uint8_t i = 0, k = 0`case 内再声明 `i` 会 redefinition。全部改用 `_i`/`_j`
3. **QUERY 短帧防御**`len < 11`magic+header+len+cmd+5data+2ckb)返回 status=0x02,防 pkg[4..8] 越界读。
4. **LOG_CLEAR 阻塞告警**:BLE 连接期间 2.8s 擦除阻塞,若 MQTT 在线会导致 WCHNET 短时失服务(60s keepalive 可吸收),文档已标注。
5. **编码验证**check_c_balance.py 自身 docstring 有转义 bug`\\x` 在普通字符串触发 unicodeescape),已改 raw string 修复。
### 验证
- 新增 `tests/test_ble_offlog.c`**隔离测试框架嵌入 dbn_ble_srv.c 提取的 3 case 真实文本**`extract_offlog_cases.py` 提取,生成物 gitignore),mock offlog_* + set_response_buf7 例全过:
- STAT 正常(19B 字段逐字节验证)/ disabled
- QUERY 正常(4×32B 记录 + seq/type/payload 偏移验证)/ 越界空 / 短帧 / disabled
- CLEAR 正常
- offlog 回归 8 例 ALL PASS(既有 7 例 + export_json 未破坏)
- 新增 `docs/DLD960_BLE协议.md` V1.00(帧格式 + 分包 + 3 命令 + OfflogEvt 32B 结构 + 事件类型表)
- README 协议文档表新增 BLE 协议行
- 本地无 RISC-V 工具链,MRS 真编译待板上联调(语法级 check_c_balance 通过)
---
## 2026-08-05 — offlog 协议导出命令分发落地 (MQTT V1.06 + TCP JSON V1.02)
### 背景
协议文档先行(MQTT V1.06 / TCP JSON V1.02commit 08893d5)新增 `log_stat` / `log_query` / `log_clear` 三命令规范,固件命令分发未接。本次补齐 P1.3:offlog 底层 API 已就绪(count/read_idx/clear),差协议层接线。
### offlog 导出 API(两侧共用,放 offlog 模块避免两协议层重复)
| API | 作用 |
|------|------|
| `offlog_enabled()` | 日志功能是否启用(Flash 初始化成功) |
| `offlog_seq_last()` | 最新一条记录全局序号(`_wr_seq - 1`0=空) |
| `offlog_type_str(type)` | 事件类型数字 → 协议字符串名(10 类 + unknown |
| `offlog_evt_to_json()` | 单条记录 → JSON 对象(data 按事件类型组装,无参数事件 data=null) |
| `OFFLOG_MAX_QUERY_RECORDS` | 分页上限 4MQTT ≤500B 发布限制) |
payload 大端解析(`offlog_payload_u32`)与写入侧 `offlog_boot/offlog_coil` 完全一致;coil sub 映射 1=car_enter 2=car_leave 3=loop_cut 4=loop_restore。
### 命令分发
- **MQTT**`iot_handle_publish` 三分支):`log_stat`enabled/boot_seq/count/capacity/seq_first/seq_last)、`log_query`(按全局序号分页,`idx = start_seq - seq_first` 映射 offlog_read_idx,越界返回空 records,响应用 `static resp[IOT_MQTT_SEND_BUF_LEN]` 防大栈)、`log_clear`
- **TCP JSON**(三 handler + cmd 表 3 条目,全部需鉴权):同上,`data_json[TCP_JSON_DATA_BUF_LEN]` 组装后 `json_send_ok`
### 关键决策与坑
1. **`log_clear` 阻塞 ~2.8s**63 扇区擦除 × ~45ms):SPI 擦除只能在主循环上下文(中断写 SPI 会炸栈),三命令 handler 均在主循环 poll 链上调用,可接受;MQTT 侧 60s keepalive、TCP 有状态重传均能吸收。注释已标注。
2. **`seq_first = seq_last - count + 1`**(count=0 时置 0):环形覆盖后 count < capacity 是正确语义,分页定位按全局序号不按时间(未同步段时间不可靠)。
3. **CRLF 二进制编辑**:四个 C 源文件 UTF-8+CRLF,用 Python rb/wb 精确替换,每处校验 count==1;gcc 全文件括号/引号平衡检查通过。
### 验证
- `test_offlog.c` 新增测试8 `test_export_json`enabled/seq_last/type_str/evt_to_jsonboot rst 大端、coil sub/ch/value、evt_retry msg_id+retry、无参数 data:null、seq_first 公式)
- gcc 隔离单测 **8 例全过**(含既有 7 例回归)
- CRLF/UTF-8 编码零漂移(file 复核)
---
## 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 为主同步点)。
- 全部上行 `ts``mstick()/1000``dev_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_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 路由)
| 组件 | 说明 |
|------|------|
| 沿检测 `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,直到真正发布才同步。
```c
/* 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` — 结构体字段类型**
```c
// uint16_t variation; -> int32_t variation; // 3B有符号, = Origin-CAPVD
```
**2. `loop_uart_proto.c` — 解析函数 `lup_parse_sensor_report`**
- 每通道单元步长 `i*11``i*12`
- 通道数计算 `data_len/11``data_len/12`
- 频率仍 3B 无符号;**变化量 3B 解码 + bit23 符号扩展**
```c
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 阈值判断(隐藏坑)**
```c
// 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 + LEN`(CMD 已在 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=0``passtime_ms`(通过时间 / 车间距,根据 `car_state` 区分)
- `misc_type=1``cut_amount`(线圈断开次数)
- `misc_type=2``flow_amount`(车流量)
- `misc_type=3``relay_count`(继电器动作次数)
### 4. 协议文档
- `docs/vd960_loop_protocol_v1.0x.md` — 0x7F 帧格式、校验算法、命令参考
- 波特率修正: 115200 → 192000
---
## 2026-07-01 — passtime_ms5 字段改名
`vd960Loop` 侧时间戳从 5ms 改 50ms 后,DBN 侧同步:
- `passtime_ms5``passtime_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_broker``SourPort` 参数
- 出厂默认 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 的多 topic`dld960/{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` 声明为全局变量但未初始化,值为 `0xFF``WCHNET_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/status``g_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_pub``dld960/{sn}/dev`
- `net_srv.c` dev_initialize_pub: `g_iot_topic.topic_pub``dld960/{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.c``mqtt_publish()` 调用 `MQTTSerialize_publish()``packet_id` 硬编码为 `0`。当 `req_qos=1`(命令响应使用 QoS 1)时,MQTT 规范要求 `packet_id ≠ 0``0` 属于协议违规,broker 检测后直接踢掉连接。
**修复**: 新增 `static uint16_t s_mqtt_pkt_id` 计数器,QoS>0 时自增作为 packet_idQoS=0 保持 0。
```c
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`/`payload``mqttBuf[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 slen``uint32_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":[...]}}`)
## 2026-08-04 — 脱机事件日志系统 (W25Q32 环形) — 解决"重复上线"取证
### 背景
设备挂平台测试出现**重复上线**(现场未断电)。平台每次收到 `initialize` 都视为上线,但无本地日志无法区分两种可能:
| 可能 | 特征 | 诊断 |
|------|------|------|
| 设备真复位 | 有 BOOT 事件 + boot_seq 递增 | 设备侧问题(电源/看门狗/软件) |
| MQTT 断连重连 | 无 BOOT 事件,只有 IOT_* 事件,boot_seq 不变 | 网络问题(链路/ broker) |
> 注:`iot_handle_suback()` 每次 MQTT 连上 READY 都会发 `iot_send_initialize()`——断连重连就会触发平台"重复上线",这是最大嫌疑点。
### 方案 (P1.2 落地: 事件流, 快照流后置)
**分区 (W25Q32 4MB)**:参数区 64KB(0x000000) | **事件日志区 256KB(0x010000)** | OTA 区 512KB(0x050000) | 快照区 ~3.25MB(0x0D0000, 预留)
**环形实现**:头扇区(扇区0, 32B 元数据) + 63 数据扇区 × 128 条 × 32B = 8064 条;顺序写、满扇擦下一扇区(天然磨损均衡);头在扇区切换时刷新,上电从头部写位置向后扫描恢复(掉电不丢)
**记录格式 (32B 定长)**`magic type len flags | seq(4) ts_ms(4) unix_ts(4) boot_seq(2) rsvd(2) | payload[12]`
**事件类型 (V1)**
| type | 事件 | 说明 |
|------|------|------|
| 0x01 | BOOT | 复位原因寄存器 RCC_RSTSCKR 全量入日志 (IWDG/POR/SFT 区分) |
| 0x10/0x11 | IOT_CONNECT / IOT_READY | READY 即发 initialize——**重复上线直接证据** |
| 0x12 | IOT_DISCONN | 细分原因: 1=断开 2=超时 3=CONNACK拒绝 4=连接超时 |
| 0x13 | IOT_RECONN | 退避时长 ms |
| 0x30/0x31 | EVT_RETRY / EVT_GIVEUP | event_report ACK 超时重发/耗尽 (网络质量) |
| 0x40 | COIL | 进/出/断/恢复 (sub+ch+value) |
| 0x50 | TIME_ANCHOR | 时钟同步锚点 (boot_seq↔unix 回算绝对时间) |
| 0x70 | LOG_CLEAR | 清日志审计 |
**插桩点**(全部主循环上下文,**socket 中断内不写 SPI**):
| 文件 | 位置 | 事件 |
|------|------|------|
| peripheral_main.c | main() storage_init 后 | offlog_init + 复位原因读+清标志 |
| iot_mqtt_srv.c | iot_mqtt_poll() 状态沿检测 | CONNECT/READY/DISCONN |
| iot_mqtt_srv.c | 重连退避 / TCP超时 / CONNACK拒绝 | RECONN / DISCONN(4) / DISCONN(3) |
| iot_mqtt_srv.c | iot_evt_process | EVT_RETRY / EVT_GIVEUP |
| iot_mqtt_srv.c | iot_evt_enqueue | COIL |
| net_srv.c | dev_time_sync 成功 | TIME_ANCHOR |
### 三个关键坑 (单测抓出来的)
1. **中断上下文不能写 SPI**`iot_mqtt_handle_sock_int` 是 WCHNET 中断,SPI 擦除 ~45ms 阻塞会炸;MQTT 事件改在 `iot_mqtt_poll()` 状态沿检测(毫秒级滞后,可接受)
2. **`sizeof(OfflogEvt)=36` 而非 32**payload[14] 后结构体对齐补 2B padding → 每扇区实际 113 条,8064 条撑爆 63 扇区提前回绕、写指针错乱。修复:字段重排(uint8×4→uint32×3→uint16×2→payload[12]+ **编译期断言** `typedef char size_must_be_32[...]`
3. **扇区级覆盖粒度 vs 记录级 count**:回绕擦整扇区会丢 128 条,但 count 仍封顶 8064 → read_idx 反推逻辑首错位。修复:擦扇区前检查目标扇区是否有数据(读首条 magic),有则 `count -= min(count,128)`
### 单测 (gcc 隔离, tests/test_offlog.c)
| 用例 | 覆盖 |
|------|------|
| test_fresh_init | 全新初始化 + BOOT 事件字段 |
| test_ring_wrap | 8069 条环形回绕: count=7941, 逻辑首 seq=129 |
| test_power_loss_recovery | 掉电重启: boot_seq 递增, seq 跨 boot 连续 |
| test_sector_switch | 128 条扇区切换 + 头 wr_off/wr_sector |
| test_clear | 清空 + LOG_CLEAR 审计 (seq 不重置) |
| test_power_loss_mid_sector | 跨扇区掉电恢复 |
### 待办
- [ ] P1.3 导出: `log_query`/`log_stat`/`log_clear` 命令接入 MQTT/TCP/BLE
- [ ] 快照流 (0xC0 帧原样落盘) 后置
- [ ] 复位原因寄存器布局**待板上验证** (RCC_RSTSCKR 按 STM32F1 兼容写)
- [ ] MRS 工程编译确认 offlog.c 被自动收集
## 2026-08-04 — tcp_json_srv 缓冲宏化: data_json[2048] → TCP_JSON_DATA_BUF_LEN(1024)
三处 `char data_json[2048]`sensor_report 推送×2、loop_param_query 响应×1)改用头文件宏 `TCP_JSON_DATA_BUF_LEN`(默认 1024)。
**容量核验**format_sensor_json 4 通道最大 ~660B、format_loop_param_json ~920B,均 < 1024 且 format 函数有 snprintf 溢出保护(`written >= remaining` 返回 -1)。最终发送帧受 TCP_JSON_MAX_FRAME=800 截断,data_json 只做中间组装,1024 足够。
**附带收益**:栈上缓冲 2048→1024,每处省 1KB 栈(CH32V208 栈紧张,中断路径 832/1322 在回调/主循环调用)。
## 2026-08-04 — offlog: TIME_ANCHOR 严格使用平台下发 unix_ts
`offlog_time_anchor()` 原为丢弃传参、内部重取 `dev_time_now()`(毫秒级误差)。改为 `offlog_evt_ts()` 直接写入平台下发值,锚点事件与平台 ts **严格一致**。新增单测 `test_time_anchor_exact`(传 1800000000 验证写入值),7 例 ALL PASS。
## 修订记录
| 版本 | 时间 | 说明 |
|------|------|------|
| V4.0 | 2026-08-10 | BLE 脱机日志接口: OFFLOG_STAT/QUERY/CLEAR (0x25/0x26/0x27), 32B OfflogEvt 二进制直传, 缓冲扩容 132B, 隔离测试 7 例 + 协议文档 V1.00 |
| V3.9 | 2026-08-05 | offlog 协议导出命令分发: log_stat/log_query/log_clear 落地 MQTT V1.06 + TCP JSON V1.02 (seq_first/seq_last 计算、idx 映射、OFFLOG_MAX_QUERY_RECORDS=4、log_clear 阻塞~2.8s) |
| V3.8 | 2026-08-04 | offlog: TIME_ANCHOR 严格用平台下发 unix_ts (offlog_evt_ts), 单测 7 例 |
| V3.7 | 2026-08-04 | tcp_json_srv 缓冲宏化: data_json[2048]→TCP_JSON_DATA_BUF_LEN(1024), 省栈1KB×3 |
| V3.6 | 2026-08-04 | 脱机事件日志系统: W25Q32 256KB 环形(8064条) + BOOT/网络/事件/线圈/时钟锚点 8 类事件 + 掉电恢复 + gcc 单测 6 例 |
| 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条命令) |