diff --git a/docs/DLD960_BLE协议.md b/docs/DLD960_BLE协议.md index 2f23c2a..e95e9ac 100644 --- a/docs/DLD960_BLE协议.md +++ b/docs/DLD960_BLE协议.md @@ -219,7 +219,7 @@ Magic | Header | Data | CheckByte | type | 事件 | payload | 说明 | |------|------|---------|------| -| `0x01` | boot | `[4]` 复位原因寄存器原始值(大端),位解析:bit31=IWDG(看门狗) bit30=WWDG bit29=LPWR bit26=NRST引脚 bit25=POR(真断电) bit24=软件复位 | 上电/复位 | +| `0x01` | boot | `[4]` 复位原因寄存器原始值(大端),位解析:bit31=LPWR bit30=WWDG bit29=IWDG(看门狗) bit28=软件复位 bit27=POR(真断电) bit26=NRST引脚 | 上电/复位 | | `0x10` | iot_connect | — | MQTT TCP 连接成功 | | `0x11` | iot_ready | — | MQTT 订阅完成 → 发 initialize(重复上线直接证据) | | `0x12` | iot_disconnect | `[1]` reason:1=断开 2=超时 3=CONNACK拒绝 4=连接超时 | MQTT 断连 | diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h index 6aad6d3..c37121c 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h @@ -124,13 +124,15 @@ enum { #define OFFLOG_RCC_BASE 0x40021000UL #define OFFLOG_RCC_RSTSCKR (*(volatile uint32_t *)(OFFLOG_RCC_BASE + 0x24UL)) -/* 复位原因位 (RCC_RSTSCKR, STM32F1/CH32V20x 兼容布局, 待板上验证) */ -#define OFFLOG_RST_IWDG (1UL << 31) /* 独立看门狗复位 (IWDG 超时 → 主循环卡死!) */ +/* 复位原因位 (RCC_RSTSCKR, CH32V20x 布局 — 2026-08-12 对照 WCH ch32v20x.h 修正, + 原定义错位: IWDG/SFT/POR 位置全错, 且 RMVF 清标志写错位导致标志从未清除) */ +#define OFFLOG_RST_LPWR (1UL << 31) /* 低功耗复位 */ #define OFFLOG_RST_WWDG (1UL << 30) /* 窗口看门狗复位 */ -#define OFFLOG_RST_LPWR (1UL << 29) /* 低功耗复位 */ +#define OFFLOG_RST_IWDG (1UL << 29) /* 独立看门狗复位 (IWDG 超时 → 主循环卡死!) */ +#define OFFLOG_RST_SFT (1UL << 28) /* 软件复位 NVIC_SystemReset */ +#define OFFLOG_RST_POR (1UL << 27) /* 上电/掉电复位 (真断电!) */ #define OFFLOG_RST_PIN (1UL << 26) /* NRST 引脚复位 */ -#define OFFLOG_RST_POR (1UL << 25) /* 上电/掉电复位 (真断电!) */ -#define OFFLOG_RST_SFT (1UL << 24) /* 软件复位 NVIC_SystemReset */ +#define OFFLOG_RST_RMVF (1UL << 24) /* 清除复位标志 (写 1 清全部, WCH: RCC_RMVF) */ /*=========================================================================== * API diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c index 2a8135c..ceecfa1 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c @@ -319,7 +319,7 @@ int main(void) { uint32_t rcc_rst = OFFLOG_RCC_RSTSCKR; offlog_boot(rcc_rst); - OFFLOG_RCC_RSTSCKR |= (1UL << 27); /* RMVF: 清复位标志 */ + OFFLOG_RCC_RSTSCKR |= OFFLOG_RST_RMVF; /* RMVF (bit24): 清复位标志 (原写 bit27=PORRSTF 无效) */ } load_cfg_from_flash(); diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index b7fe757..72e928b 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -1,1084 +1,1122 @@ -# vd960DBN 开发日志 - -> MCU: CH32V208 (RISC-V, WCH) | 通信: BLE + ETH (WCHNET) + UART2→Loop MCU | TCP JSON 端口: 5960 -> -> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 - ---- - -## 2026-08-12 — 设备不断重启修复:init/clear 同步全量擦除阻塞超 IWDG - -### 现场症状 - -烧录快照固件后 CH32V208 不断重启,启动打印(`CH32V20x_BLE_LIB` / `SystemCoreClock` / `W25Q32 OK!`)后出现乱码:`>>8>潈8>�0>8�...`。 - -### 根因链 - -| # | 环节 | 问题 | -|---|------|------| -| 1 | `snap_init()` 全新区 | 首次初始化**同步擦全部 751 个数据扇区 ≈ 34s**(SPI 擦除 45ms/扇区) | -| 2 | IWDG 看门狗 4s(iot_mqtt_srv.c:978) | 34s 主循环阻塞 → 看门狗超时 → 复位 | -| 3 | 复位后快照区仍无 "SNAP" magic | 头在擦完数据扇区后才写 → 下次启动又全新区 → **再擦 34s → 再复位 = 死循环** | -| 4 | `offlog_init()` 首擦 5.7s / `offlog_clear()` 2.8s | 同类隐患(事件区全新或清空时) | -| 5 | `snap_clear()` 擦 751 扇区 34s | IWDG 启用后执行 BLE SNAP_CLEAR 命令 → 必复位 | -| 6 | `usart_biz.c:195` 非 0x7F 帧 `%s` 打印 | Loop MCU 数据被当字符串 → 串口乱码刷屏 | - -> 附带修正认知:`lup_process_frame` 实际由**主循环** `uart_srv()` 调用(usart_biz.c:166),USART2 ISR 只逐字节喂 `lup_feed_byte()`。快照 enqueue/flush 全程主循环上下文——此前 snapshot.h 注释误标"ISR 上下文"。 - -### 修复:懒擦(init/clear 只擦当前写扇区,45ms) - -- `snap_init` / `offlog_init` 全新区:只擦数据扇区 1(写指针起点),其余扇区由环形写**切扇区逻辑自动擦**(write_raw 已有 has_data 判定) -- `snap_clear` / `offlog_clear`:**逻辑清除**(count=0 → 旧数据立即不可读)+ 只擦写指针起点扇区;其余扇区写覆盖时逐个擦 -- `usart_biz.c`:非 0x7F 帧 `%s` 打印改 hex(` %02X`),杜绝二进制当字符串 - -启动阻塞 34s → 45ms;SNAP_CLEAR 阻塞 34s → 45ms。环形覆盖语义完全不变。 - -### 测试 - -`test_snapshot` 9 例全过(新增 test_lazy_erase:预埋扇区 2 旧数据,init 后保持,写满 64 条切扇区时正确覆盖);`test_offlog` 8 例、`test_ble_offlog` 7 例回归全过。 - -### 待现场确认 - -- 乱码另一可能源:**Loop MCU ↔ CH32 UART2 波特率/接线**(192000 应为标准 0x7F 帧;若 Loop 发送端配置不同,帧头识别失败 → 非 0x7F 路径)。hex 打印修复后可通过 `Rcv_len:xx,dat: 7F 00 ...` 判断 Loop 帧是否正常到达。 - ---- - -## 2026-08-12 — 传感快照区落地(snapshot.c,BLE 新增 0x28/0x29/0x2A) - -### 背景 - -ROADMAP P1.2 分区规划(参数区→OTA→事件日志区→传感快照区)中,事件区已落地(offlog),快照区此前为预留。本次实现快照流:0xC0 传感帧按上报节奏落盘,断网期间波形照常记录,可离线回放。 - -### 关键设计决策 - -1. **64B 定长记录 SnapRec**:头部 16B(magic=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()` getter(peripheralMTU 是 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 包(每包 20B,pkg_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` | - -### 兼容性 - -- 旧固件事件区在 0x010000(256KB),新固件在 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=0x16,8 例全过(环形回绕自动适配 16256) -- `tests/test_ble_offlog.c`:mock `g_offlog_part`,capacity 断言 16256,7 例全过 -- 地址范围模拟:四芯片事件区 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),全 LE,19B | -| `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 二进制 OfflogEvt(BLE 是二进制协议,无需 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_buf,7 例全过: - - 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.02,commit 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` | 分页上限 4(MQTT ≤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_json(boot 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→56B,DBN 侧 `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 创建(端口 5960,PROTO_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 基础打通 - -### 背景 - -在 CH32V208(RISC-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_id,QoS=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条命令) | +# vd960DBN 开发日志 + +> MCU: CH32V208 (RISC-V, WCH) | 通信: BLE + ETH (WCHNET) + UART2→Loop MCU | TCP JSON 端口: 5960 +> +> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 + +--- + +## 2026-08-12 — 复位原因位定义错位修正 + POR 复位定位(电源方向) + +### 诊断进展(现场 RST_REASON 打印) + +新固件启动首行输出 `RST_REASON: 0x08000000`。 + +对照 WCH 官方库 `ch32v20x.h`(RCC_RSTSCKR 定义)发现 **offlog.h 复位原因位定义整体错位**: + +| 实际位 | 含义 | offlog 原定义 | 状态 | +|--------|------|--------------|------| +| bit31 | LPWR | bit29 | ❌ | +| bit30 | WWDG | bit30 | ✓ | +| bit29 | IWDG | bit31 | ❌ | +| bit28 | SFT 软件复位 | bit24 | ❌ | +| bit27 | **POR 上电/掉电复位** | bit25 | ❌ | +| bit26 | PIN NRST | bit26 | ✓ | +| bit24 | RMVF 清标志 | (main 写 bit27) | ❌ | + +### 两个结论 + +1. **`0x08000000` = PORRSTF(上电/掉电复位)** → 设备被**电源跌落**复位,不是看门狗、不是软件死锁。方向:SPI 擦除电流脉冲(W25Q32 ~20mA 级)或供电余量不足 → 3.3V 跌落触发 PDR。**待现场量测 3.3V 在 SPI 擦除瞬间的跌落**。 +2. **RMVF 清除写错位**(bit27 实为 PORRSTF 只读位)→ 复位标志从未清除、历史累积 → 修正为 bit24。 + +### 修正 + +- offlog.h:复位原因位定义对齐 WCH 库(IWDG=bit29 / SFT=bit28 / POR=bit27 / LPWR=bit31),新增 OFFLOG_RST_RMVF=bit24 +- peripheral_main.c:RMVF 清除 `|= OFFLOG_RST_RMVF` +- 协议文档 boot 事件位解析同步更新 + +### 下一步(板上) + +修正后重新上电,`RST_REASON` 将只显示**最近一次**复位原因: +- 若仍显示 PORRSTF(bit27)→ **电源问题实锤**:查 3.3V 供电余量、W25Q32 VCC 电容、SPI 擦除瞬间跌落 +- 若显示 IWDGRSTF(bit29)→ 主循环死锁,继续查软件 +- 若显示 PINRSTF(bit26)→ NRST 干扰/复位电路 + +--- + +## 2026-08-12 — 设备不断重启修复:init/clear 同步全量擦除阻塞超 IWDG + +### 现场症状 + +烧录快照固件后 CH32V208 不断重启,启动打印(`CH32V20x_BLE_LIB` / `SystemCoreClock` / `W25Q32 OK!`)后出现乱码:`>>8>潈8>�0>8�...`。 + +### 根因链 + +| # | 环节 | 问题 | +|---|------|------| +| 1 | `snap_init()` 全新区 | 首次初始化**同步擦全部 751 个数据扇区 ≈ 34s**(SPI 擦除 45ms/扇区) | +| 2 | IWDG 看门狗 4s(iot_mqtt_srv.c:978) | 34s 主循环阻塞 → 看门狗超时 → 复位 | +| 3 | 复位后快照区仍无 "SNAP" magic | 头在擦完数据扇区后才写 → 下次启动又全新区 → **再擦 34s → 再复位 = 死循环** | +| 4 | `offlog_init()` 首擦 5.7s / `offlog_clear()` 2.8s | 同类隐患(事件区全新或清空时) | +| 5 | `snap_clear()` 擦 751 扇区 34s | IWDG 启用后执行 BLE SNAP_CLEAR 命令 → 必复位 | +| 6 | `usart_biz.c:195` 非 0x7F 帧 `%s` 打印 | Loop MCU 数据被当字符串 → 串口乱码刷屏 | + +> 附带修正认知:`lup_process_frame` 实际由**主循环** `uart_srv()` 调用(usart_biz.c:166),USART2 ISR 只逐字节喂 `lup_feed_byte()`。快照 enqueue/flush 全程主循环上下文——此前 snapshot.h 注释误标"ISR 上下文"。 + +### 修复:懒擦(init/clear 只擦当前写扇区,45ms) + +- `snap_init` / `offlog_init` 全新区:只擦数据扇区 1(写指针起点),其余扇区由环形写**切扇区逻辑自动擦**(write_raw 已有 has_data 判定) +- `snap_clear` / `offlog_clear`:**逻辑清除**(count=0 → 旧数据立即不可读)+ 只擦写指针起点扇区;其余扇区写覆盖时逐个擦 +- `usart_biz.c`:非 0x7F 帧 `%s` 打印改 hex(` %02X`),杜绝二进制当字符串 + +启动阻塞 34s → 45ms;SNAP_CLEAR 阻塞 34s → 45ms。环形覆盖语义完全不变。 + +### 测试 + +`test_snapshot` 9 例全过(新增 test_lazy_erase:预埋扇区 2 旧数据,init 后保持,写满 64 条切扇区时正确覆盖);`test_offlog` 8 例、`test_ble_offlog` 7 例回归全过。 + +### 待现场确认 + +- 乱码另一可能源:**Loop MCU ↔ CH32 UART2 波特率/接线**(192000 应为标准 0x7F 帧;若 Loop 发送端配置不同,帧头识别失败 → 非 0x7F 路径)。hex 打印修复后可通过 `Rcv_len:xx,dat: 7F 00 ...` 判断 Loop 帧是否正常到达。 + +--- + +## 2026-08-12 — 传感快照区落地(snapshot.c,BLE 新增 0x28/0x29/0x2A) + +### 背景 + +ROADMAP P1.2 分区规划(参数区→OTA→事件日志区→传感快照区)中,事件区已落地(offlog),快照区此前为预留。本次实现快照流:0xC0 传感帧按上报节奏落盘,断网期间波形照常记录,可离线回放。 + +### 关键设计决策 + +1. **64B 定长记录 SnapRec**:头部 16B(magic=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()` getter(peripheralMTU 是 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 包(每包 20B,pkg_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` | + +### 兼容性 + +- 旧固件事件区在 0x010000(256KB),新固件在 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=0x16,8 例全过(环形回绕自动适配 16256) +- `tests/test_ble_offlog.c`:mock `g_offlog_part`,capacity 断言 16256,7 例全过 +- 地址范围模拟:四芯片事件区 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),全 LE,19B | +| `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 二进制 OfflogEvt(BLE 是二进制协议,无需 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_buf,7 例全过: + - 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.02,commit 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` | 分页上限 4(MQTT ≤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_json(boot 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→56B,DBN 侧 `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 创建(端口 5960,PROTO_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 基础打通 + +### 背景 + +在 CH32V208(RISC-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_id,QoS=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条命令) |