102 Commits
Author SHA1 Message Date
wangfq 9d6f5ea412 docs(vd_960): MQTT OTA 协议设计稿按修改意见修订 → V1.01
- Slot A/B 容量 60KB→100KB (镜像上限 96KB, 为 DBN CH32V208 镜像预留)
- 删除继电器 GPIO 默认态待验证项, 维持 Loop 现状 (与 BLE OTA 行为一致, 不额外干预)
- 新增会话期间静默: 暂停 MQTT event_report 发送 (队列积压结束补发) + offlog/快照落盘 (开始/结果日志除外)
- 刷写调度明确非阻塞 tick 驱动, 刷写窗口内 MQTT 保活 (PINGREQ/心跳/IWDG 喂狗) 不受影响
- 待板上验证项重新编号 (删 1 项, 保活项升级为实现约束+验证)
2026-08-20 11:37:38 +08:00
wangfq b20fc57954 docs(vd_960): MQTT 远程 OTA 协议设计稿 (Loop MCU 先存后刷, ROADMAP P1.4 ①)
- 三阶段: MQTT 分片下载→W25Qxx 暂存(0x010000 512KB)→本地 0x9F ISP 透传刷写
- 7 命令: ota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status/ota_report
  + event_report 扩展 type=ota_error 告警
- 关键参数: 单片 256B(hex, <RECV_BUF 1024), 刷写块 248B(0x9F LEN uint8 上限 254B),
  镜像上限 60KB, CRC32=ISO-HDLC(zlib.crc32 兼容), Slot A/B 双槽回滚
- 安全底线: 升级窗口检查(有车拒绝)/继电器安全侧/双重 CRC/失败重试×3/审计留痕
- 6 项待板上验证(继电器默认态/DBN TX 缓冲/SPI 写阻塞/RAM 预算/bin 大小/MQTT 保活)

协议先行, 未动固件代码; 待设计拍板后并入 MQTT 协议 V1.08
2026-08-20 11:13:43 +08:00
wangfq d4ce0ba23a chore(vd960DBN): 固件版本 1.02.03 → 1.02.04 (SPI 存储适配)
内容: SPI Flash 识别去厂商代码 + 恢复 factory 配置写入 (板级验证通过)。
README/CHANGELOG/devlog 同步; 手册/规格书配套版本更新 (文档保持 V1.02, 同日补充不升版)
2026-08-19 15:23:35 +08:00
wangfq 881a774c0c docs(vd960DBN): devlog 标注 factory 写入修复板级验证通过 (W25Q128 读写正常) 2026-08-19 15:21:40 +08:00
wangfq 8e58d5fdd1 fix(vd960DBN): 恢复 factory 配置写入 — 换新空片后配置永久丢失
8-13 止血(SPI 写触发复位)根因已 8-17 闭环(栈溢出+printf 重入, 非 SPI 问题),
offlog/snapshot 8-18 起持续 SPI 写入稳定 → 解除止血。
新空片(W25Q128)参数区无 magic + write_net_config 不写 magic → 每次上电
memory defaults, 保存配置永久丢失。恢复: mismatch 分支调 factory_dev_info()
写 magic+默认参数到 flash, 首次上电自动初始化。
2026-08-19 15:13:18 +08:00
wangfq f1d9ac4e48 fix(vd960DBN): SPI Flash 识别去厂商代码判断 — 兼容其他厂家同容量型号
- W25Qxx 宏 0XEF13~17 -> 0X13~17 (去掉厂商前缀, 只留设备 ID)
- SPI_Flash_ReadJEDEC_ID() 删除 id[0]!=0xEF||id[1]!=0x40 校验, 直接返回容量码 id[2]
  (JEDEC 容量码跨厂商标准化, offlog/snapshot 分区判断随之生效)
- storage_init() switch 改 Flash_Model & 0xFF 低字节匹配
- 换非华邦芯片 (如 0x1A 厂商) 时识别不再不一致
2026-08-19 14:45:30 +08:00
wangfq 9209a9f034 docs(vd_960): 产品手册/技术规格书 V1.01→V1.02 配套 DBN V1.02.03
- 产品手册: BLE OTA 升级链路加固说明 (0x9F 响应帧透传回小程序), 故障排查新增 OTA 失败条目, 固件升级补断电重启提示
- 技术规格书: UART2 RX 0x9F OTA 透传机制 (§2/§5/§7), 协议矩阵/版本配套矩阵更新, 已知约束补 OTA 无自动退出
- README 文档索引 V1.01→V1.02
2026-08-19 11:22:17 +08:00
wangfq 832d2953b0 chore(vd960DBN): 固件版本 1.02.01 → 1.02.03 (2026-08-19)
OTA 0x9F 透传修复板级验证通过, 用户更新版本号三段式一致。
V1.02.03 内容: BLE→Loop OTA 修复 (0x9F 帧透传回 BLE) +
8-18 脱机日志快照流/hex 上报 + MQTT 查询命令补齐。
README/CHANGELOG/devlog 同步; 已知约束: OTA 模式无自动退出 (维持现状)
2026-08-19 11:20:03 +08:00
wangfq d67f955fd4 fix(vd960DBN): BLE→Loop OTA 失效修复 — 0x9F 帧透传回 BLE
DMA 改造(fed4335)后 UART2 RX 走 lup_feed_byte 只认 0x7F, Loop bootloader
回的 0x9F 响应帧(pre_ok/addr_ok/data ACK)全被吞; OTA 是停等协议
(0xA7 WITH_BACK 每块必回 ACK), 工具等不到 ACK 升级必然卡死。

- loop_uart_proto: 新增 lup_feed_byte_ota() 0x9F 帧状态机 (SUM 校验无 XOR,
  复用 g_lup_parser), 枚举追加 OTA 专用状态
- usart_biz: uart2_dma_poll 按 g_flag_counter_ota.flag 切换 0x9F/0x7F 解析器,
  模式切换 reset, OTA 溢出阈值放宽整缓冲, 补收帧 tick 归零;
  uart_srv OTA 分支 0x9F 帧透传回 BLE + 清 flag
- 单测 tests/test_lup_ota_parser.c 8 断言全过 (bootloader 真实帧向量)
- 遗留: g_flag_counter_ota.flag 无退出机制, 升级后需断电重启
2026-08-19 11:01:45 +08:00
wangfq 839d79b487 chore(vd960DBN): 移除误提交的测试二进制 test_snap_to_hex 2026-08-18 14:04:34 +08:00
wangfq 5a1893cd1c feat(vd960DBN)+fix(DBNMQTTool): log_query 改 hex 原始字节上报 (2026-08-18)
背景: MQTT 快照流实测 MQTTSerialize_publish failed — JSON 化快照记录 ~810B/条 超 800B 发送缓冲
方案(用户拍板): 对齐 BLE 通道, 原始字节 hex 上报

固件 (V4.3):
- offlog.c/h: 新增 offlog_evt_to_hex() (32B→64 hex)
- snapshot.c/h: 新增 snap_rec_to_hex() (64B→128 hex); 删 SNAP_MAX_QUERY_JSON, 恢复 count=2
- tcp_json_srv.c / iot_mqtt_srv.c: log_query 改 {"seq":N,"hex":"..."}; SEND_BUF 保持 800
- 2 条快照 hex 响应 406B < 800B

文档: TCP JSON V1.03 / MQTT V1.07 §4.17 records 改 hex + 解析表引用 BLE §6.4/§7

工具: parse_offlog_hex/parse_snap_hex/offlog_payload_desc + hex 展示; 验证: gcc 9 断言 + 工具解析全过 + offscreen UI
2026-08-18 14:04:26 +08:00
wangfq 5e9b188c6a feat(DBNMQTTool): 脱机日志翻页 — 上一页/下一页 + 自动填充起始序号 (2026-08-18)
- 按钮行新增 ◀上一页/下一页▶ (拉取成功后启用)
- 下一页: 锚点 = 当前页最后一条 seq+1 (快照/事件流均精确), 空记录按 start+count 步进
- 上一页: max(1, 当前起始-条数), 边界保护
- offscreen 6 场景验证全过
2026-08-18 11:57:11 +08:00
wangfq f2141976f0 feat(vd960DBN)+fix(DBNMQTTool): MQTT 查询命令补齐 + 工具命令集对齐 (2026-08-18)
固件 (V4.2):
- iot_mqtt_srv.c 补齐 ssc_net_query / iot_net_query / iot_topic_query (只读全局组包, 与 TCP JSON §4.5/4.7/4.9 对齐)
- 修复实测: MQTT 通道 ssc_net_query/iot_net_query 返回 code=4 unsupported

工具:
- 禁用固件未实现的 6 个按钮 (ssc/iot_net/iot_topic_set + pwd_set/factory_reset/device_reset) + tooltip 引导 TCP/BLE
- 设备刷新列表移除 loop_param_query (固件未实现)
- offscreen 验证按钮状态正确
2026-08-18 11:44:52 +08:00
wangfq d39ab5ae89 feat(DBNMQTTool): 脱机日志支持快照流 stream=snapshot (2026-08-18)
- protocol.py: STREAM_EVENT/STREAM_SNAPSHOT 常量, data_log_query 加 stream 参数(快照流 count≤2), 新增 data_log_clear(stream), SNAP_MISC_TYPE_DESC
- main.py: 日志流选择器 + 三命令按流发请求 + log_stat/log_query 响应区分流(快照 channels 逐字段展示)
- 验证: protocol 断言 6 例 + offscreen MainWindow 快照/事件流展示实测全过
- devlog 置顶 + .gitignore (venv/)
2026-08-18 09:38:33 +08:00
wangfq f1c9358aad feat(vd960DBN): TCP/MQTT log_* 命令支持快照流 stream=snapshot (2026-08-18)
- snapshot.c/h: 新增 snap_rec_to_json() — SnapRec 64B → JSON (channels 对齐 0xC0, variation 3B 符号扩展, misc_type 全枚举)
- tcp_json_srv.c: handle_log_stat/query/clear 加 stream 解析 + snapshot 分支
- iot_mqtt_srv.c: log_stat/query/clear 加 stream 解析 + snapshot 分支
- 事件流 count=0 按上限处理 (与 BLE 对齐); 快照 QUERY count≤2, CLEAR ~45ms
- 新增 gcc 隔离单测 tests/test_snap_to_json.c 34 断言全过
- devlog V4.1
2026-08-18 09:01:22 +08:00
wangfq e437dce556 docs(协议): TCP JSON V1.03 + IoT MQTT V1.07 — log_* 命令完善传感快照流 (2026-08-18)
- log_stat/log_query/log_clear 复用 stream 字段区分 event/snapshot(与 BLE 0x28/0x2A 同语义)
- 快照统计 capacity 随芯片动态(48064~449472);快照分页 count≤2(SnapRec 64B 原始结构 JSON 化,channels 对齐 0xC0)
- 快照清除审计留痕 + 阻塞时长说明(事件 2.8s / 快照 45ms)
- capacity/count 类型 uint16→uint32 修正(W25Q256 事件流 130944 超 16bit)
- README/技术规格书协议矩阵/产品手册相关文档表同步
2026-08-18 08:54:21 +08:00
wangfq 55192523fe docs(vd960DBN): devlog 2026-08-12 动态分区条目补传感快照区容量对照表 (2026-08-18)
- 原表更名'容量对照(事件日志区)'
- 新增'容量对照(传感快照区)': 起始/大小/数据扇区/max_records/capacity LE 四芯片对照 (0x110000/48064 起)
- 数值与 snapshot.c snap_part_detect 计算一致 (总容量-576KB-事件区, 去头扇区, 64B/条)
2026-08-18 08:45:57 +08:00
wangfq eee7d2459c docs(产品手册): 新增 BLE 脱机日志/传感快照读写说明 (§5.1) + 相关文档表补 BLE 协议 V1.02 (2026-08-18) 2026-08-18 08:24:38 +08:00
wangfq 845f16402c docs(vd_960): 产品手册+技术规格书更新至 V1.01 — 配套 vd960DBN V1.02.01 (2026-08-18)
- 产品手册 V1.00→V1.01: 头部版本/核心特性(通信可靠性加固)/固件配套说明/故障排查新增自动重启条目
- 技术规格书 V1.00→V1.01: 版本矩阵更新(UART2 RX DMA/职责/算法标题)/协议矩阵配套/§10.1 已知约束新增(RAM预算/BLE版本三段式/快照3s/DMA1_Ch6)
- README 文档索引版本同步
2026-08-18 08:18:42 +08:00
wangfq ba4f5e650c docs(vd_960): vd960DBN V1.02.01 发布说明 — CHANGELOG 新条目 + README 版本表更新 (2026-08-17)
- CHANGELOG: 新增 vd960DBN V1.02.01 条目 (DBN 单侧更新, 配套矩阵/变更/约束/验证)
- README: 当前发布 vd960DBN V1.02.01; 子项目版本表 1.0 → 1.02.01
2026-08-17 18:09:48 +08:00
wangfq ae9f5eaf4c chore(vd960DBN): 固件版本 1.02.01 (三段式) + cmcng.h 注释修复 (2026-08-17)
用户更新:
- FIRMWARE_VER "1.02.01" (MAIN=1 SUB=2, 新增 FIRMWARE_VER_SUBSUB=1)
- 注释修复: GBK 乱码注释恢复中文 (文件 GBK→UTF-8 编码转换, 中文注释正常)
- 注意: BLE 上报仍为 MAIN/SUB 两字节 (1.02), SUBSUB 仅字符串上报使用
2026-08-17 17:19:40 +08:00
wangfq 263dc4e05d chore(vd960DBN): 固件版本 1.0 → 1.1 (2026-08-17)
cmcng.h FIRMWARE_VER "1.0"→"1.1" (MAIN=1 SUB=1):
- 修正字符串与数字不一致 (旧 "1.0" vs MAIN=1 SUB=1)
- 版本内容: 栈溢出修复 + RAM 瘦身 + UART2 RX DMA + 清理
- 注释用 ASCII (cmcng.h 为 GBK 编码)
2026-08-17 16:55:51 +08:00
wangfq 8c0f135629 chore(vd960DBN): 结案清理 — 移除临时诊断代码 (2026-08-17)
频繁复位问题闭环后清理:
- peripheral.c performPeriodicTask: 删 _dbg_cnt 500ms 调试心跳打印 (BLE 状态已由上层链路验证)
- peripheral_main.c: 删 g_boot_count(.noinit) + BOOT_CNT/END/SP 诊断打印块
- 保留 fault_diag 基础设施 + RST_REASON 一行 (诊断价值, 未来复用)
- 注意: peripheral.c 为 GBK+CRLF, 新注释用 ASCII
2026-08-17 16:42:59 +08:00
wangfq fed4335947 feat(vd960DBN): UART2 RX DMA 循环接收 — 根治打印关中断丢帧 (2026-08-17)
遗留项'UART2 偶发丢帧(1-3分钟一次 checksum fail)'落地:
- 根因: PRINT 临界区(关中断~7.4ms)屏蔽 USART2 RXNE 中断 → 丢字节
- 方案: DMA1_Ch6 循环模式硬件收字节(不依赖CPU中断), 主循环轮询消费
  - uart2_dma_init(): Ch6 循环模式 + 512B 环形缓冲(aligned(4)) + 关 RXNE
  - uart2_dma_poll(): 主循环读 DMA_GetCurrDataCounter 批量喂 lup_feed_byte
    (状态机移入主循环无竞争); 溢出保护(未消费>256B重置+丢帧计数)
  - uart_init() 末尾调 uart2_dma_init(); USART2_IRQHandler 清空留 IDLE 注释
- 资源: DMA1 全空闲(WCHNET 独立 ETH DMA/BLE 不用 DMA1) → Ch6 独占
- .bss +512B(缓冲), 栈 6.3KB→5.8KB 仍充裕
2026-08-17 15:59:16 +08:00
wangfq f274d70fbd docs(vd960DBN): devlog 重写 2026-08-17 条目 — 根因闭环 + 板级验证通过 (2026-08-17)
- 条目重写为完整闭环: 诊断(Hex日志+实验A/D) → 根因(.bss挤占RAM→栈1.9KB→
  printf栈溢出→PC跑飞) → 修复链(4 commit) → .map分析(BLE栈16KB固定) →
  二轮瘦身 → 板级验证(用户实测无异常重启)
- 验证项标记: 一轮瘦身版无复位(MQTT/BLE正常), 二轮瘦身版测试无异常重启
- 遗留: TCP JSON 回归 + 快照区 3s 初始化回归
2026-08-17 15:34:09 +08:00
wangfq cb63c581be perf(vd960DBN): 二轮RAM瘦身 — RECE_BUF_LEN 1024 + ARP 16 + 中断路径static (2026-08-17)
.map 分析: BLE 栈固定占 RAM 低 16KB 不可裁; 用户 48KB 内 .bss 41.7KB。
- net_config.h: RECE_BUF_LEN MSS×2(1400)→1024 (MSS=700帧+头≈740B, 5数组省~2.6KB)
- net_config.h: WCHNET_NUM_ARP_TABLE 50→16 (停车场景IP少, 省~0.8KB)
- tcp_json_srv.c: tmp_buf[RECE_BUF_LEN]/frame[800] 局部→static (中断上下文栈数组防溢出)

栈 4.7KB→~6.3KB, 中断栈峰值 -1.8KB。devlog 记录 .map 分析结论。
2026-08-17 14:26:22 +08:00
wangfq 89ba602e4f fix(vd960DBN): 栈溢出修复闭环 — 打印恢复 + 字符串0终止保险 (2026-08-17)
RAM 瘦身后栈 1.9KB→~4.75KB (END 0x2000f834→0x2000ed3c 实测), printf 峰值安全:
- cfig_flash.c: 恢复 load_cfg 3 处调试 PRINT (magic mismatch×2 + Sub_Code)
- cfig_flash.c: 字符串 0 终止保险 (remote_addr/client_id/username/password/topic_pub/sub
  末字节强制 '\0', 防 Flash 字符串不足定长时 %s 越界打印)
- peripheral_main.c: 恢复 output_cfg_from_flash() (调试配置打印)
- 注释更新为修复闭环说明

设备实测: 无复位, MQTT 连 159.75.137.141:1883, BLE 广播正常
2026-08-17 14:18:52 +08:00
wangfq 421334ec48 perf(vd960DBN): RAM 瘦身 — .bss 省 4.8KB, 栈 1.9KB→~6.7KB (2026-08-17)
栈溢出根因确认后 .bss 瘦身四刀:
- net_config.h: ETH_MAX_PACKET_SIZE 1520→768 (MSS=700最大帧758B, 省3.76KB)
- iot_mqtt_srv.c: payload[1400]→800 (loop_data最大~604B, 省600B)
- iot_mqtt_srv.h: IOT_MQTT_SEND_BUF_LEN 1024→800 (_iot_send_buf/resp/buf 同步, 省224B)
- net_srv.c: MAX_MQTTBUF_LEN 1024→800 (512仍不够勿回改, 省224B)

⚠ ETH 768 为 MSS=700 最小安全值; 若现场有大UDP包截断改回1024。
待办: .map 确认 BLE 栈占用; RECE_BUF_LEN/ARP 表可再省; 局部大数组转static。
2026-08-17 14:10:52 +08:00
wangfq 26f2326c96 fix(vd960DBN): 栈溢出止血 — load_cfg 去调试打印, output_cfg 暂关 (2026-08-17)
实验D确认 load_cfg/output_cfg 为跑飞触发点。根因: .bss≈46KB
(BLE栈+ETH DMA 7.6KB+SPI_FLASH_BUF 4KB+网络缓冲) 挤占 RAM,
栈仅 ~1.9KB (BOOT_CNT@0x2000f84c 证明 .noinit 被推到 RAM 顶)。
load_cfg 的 printf 栈峰值触顶 → 覆盖 .noinit+返回地址 → PC 跑飞循环。

修复:
- cfig_flash.c: load_cfg 内 3 处调试 PRINT 注释 (magic mismatch×2 + Sub_Code×1)
- peripheral_main.c: load_cfg 恢复(配置加载保留); output_cfg 调用 #if 0 (纯调试打印)
- devlog: 实验D结论 + 修复记录

根治待办: 减 .bss 给栈腾 4KB+ (需 .map 确认大数组);
output_cfg 恢复调试打印前需减栈压力
2026-08-17 11:06:56 +08:00
wangfq 452ff68bae test(vd960DBN): 实验D — 栈空间诊断 + load_cfg/output_cfg 触发点验证 (2026-08-17)
新证据: BOOT_CNT@0x2000f854 = .noinit 被推到 RAM 顶, 栈顶(0x20010000)
之下仅 ~1.9KB → .bss≈46KB (BLE/WCHNET/SPI_FLASH_BUF 等大数组挤占)。
栈溢出 → 覆盖 .noinit(g_boot_count 恒1/g_fault_diag 消失) + 覆盖返回地址
→ PC 跑飞跳回 0 → 循环。快照延后无效因为问题不是快照 SPI 而是栈空间。

- BOOT_CNT 打印加 _end(堆起点, 反推.bss大小) + 当前 SP(栈深)
- load_cfg/output_cfg #if 0 (EXPD 标记): 稳定→触发点确认; 仍崩→更晚
- 待用户提供编译后 .map 确认 .bss 组成
2026-08-17 11:00:03 +08:00
wangfq 8cf01544f6 feat(vd960DBN): 快照区延后初始化(开机3s后) — 避开启动早期SPI重负载窗口 (2026-08-17)
诊断: 08-14恢复快照区后频繁重启。RSTSCKR无复位标志(非硬件复位) +
BOOT_CNT恒=1(.noinit被清, RAM全丢级) + 实验A(跳过offlog_boot)仍崩
→ 启动早期SPI重负载(offlog/snap init 擦+整扇区写回~80ms×2)让系统
进入临界状态, 后续SPI操作(load_cfg读参数区)成为压垮点。

方案:
- snapshot.c/h: 新增 snap_delayed_init(), 主循环每轮调用, 开机3s后
  首次进入执行 snap_init (非阻塞, mstick()>=3000 门控)
- peripheral_main.c: main() 移除 snap_init 调用; 主循环喂狗后加
  snap_delayed_init(); 撤销实验A(恢复offlog_boot)/实验B(恢复load_cfg)
- 3s内传感帧由 snap_enqueue/snap_flush 的 !_ready 门控自动丢弃不落盘
- 保留 BOOT_CNT 诊断打印(.noinit 计数器+地址)
- devlog 置顶 2026-08-17 条目
2026-08-17 10:31:39 +08:00
wangfq 5803255d89 test(vd960DBN): 实验B — 跳过 load_cfg/output_cfg, 跑飞触发点下移 (2026-08-17)
实验A(跳过 offlog_boot)仍循环复位 → offlog_boot 非触发点, 跑飞点下移。
新证据: BOOT_CNT 恒=1 (.noinit 计数器每次被清) → RAM 全丢级重启,
RSTSCKR 却无复位标志 → 疑似掉电-恢复(未触发POR) 而非软件跑飞。

- load_cfg_from_flash + output_cfg_from_flash #if 0, 加 EXPB 标记
- BOOT_CNT 打印加 &g_boot_count 地址, 确认 .noinit 段是否生效
  (地址 >= _ebss≈0x2000612c 且在 .noinit → 复位不清零; 恒1 → RAM被清)

预期: 不崩 → 触发点在参数区SPI读或%s打印; 仍崩 → 触发点更晚/更早
2026-08-17 10:15:21 +08:00
wangfq 0c717e4818 test(vd960DBN): 实验A — 跳过 offlog_boot SPI 写, 验证跑飞触发点 (2026-08-17)
日志诊断: RSTSCKR 全 0(非硬件复位) + snap ok 后 71B 乱码 + 无 ASCII 打印
→ PC 跑飞跳回 0x00000000 重启, 触发点锁定 offlog_boot(SPI 32B 页写)。
与 08-12 实验 99ef634 结论一致。

- offlog_boot() 调用 #if 0, 保留 RMVF 清除
- 新增 .noinit g_boot_count 计数器: 每次 main 重进 +1 并打印 BOOT_CNT
  (跑飞重启不清零, 区分真复位 vs 跑飞)
- 新增 EXPA: offlog_boot skipped 标记打印

预期: 不崩 → offlog_boot 写实锤; 仍崩 → 跑飞点下移 load_cfg/output_cfg
2026-08-17 10:04:52 +08:00
wangfq 7894e67b36 docs(vd_960): 同步 BLE 协议索引 V1.02 + 技术规格书协议矩阵补 BLE 快照命令
- README 协议文档索引: DLD960_BLE协议.md V1.00 -> V1.02, 描述补 SNAP_STAT/QUERY/CLEAR (0x28/0x29/0x2A)
- DLD960_技术规格书.md 5.1 协议矩阵: 补 DLD960 BLE 协议行 (V1.02, 脱机日志 + 传感快照)
2026-08-14 10:13:35 +08:00
wangfq 8873b334d2 feat(vd960DBN): 恢复快照区功能, 移除 sim_snap_spi 实验桩, UART2 波特率注释修正
- peripheral_main.c: snap_init() 恢复 (去 #if 0), offlog_boot() 恢复
  (上电复位原因记入事件日志); 删除 sim_snap_spi 模拟写密度实验函数
  及主循环调用 (printf 重入修复后 SPI 写无罪已确诊, 实验收尾)
- loop_uart_proto.h/c: UART2 波特率注释 115200/19200 -> 192000
  (usart_biz.c 硬编码 192000 为实际值; g_storage_uart_baud=19200
  为独立配置项不驱动 UART2, BLE 显示 19200 不代表实际速率)
- devlog: 新增 2026-08-14 条目, UART2 RX DMA 方案列入待办
2026-08-14 09:46:23 +08:00
wangfq 960d1e83a1 docs(vd960DBN): devlog 置顶 2026-08-13 复位/卡死全链路诊断闭环
printf 重入 + json_sensor_callback 卡死 + UART2 丢字节 三层根因,
SPI 写无罪/QFN28 无 NRST/5min restart 真相/修复清单/遗留待办
2026-08-14 03:02:17 +08:00
wangfq b493e10454 fix(vd960DBN): UART2 粘帧/checksum fail — 高频打印关中断丢字节 + flag 不清
现场: LUP 帧后 checksum fail 刷屏 (坏帧: 7F 03 39 EF... 多帧拼接)。
根因链条:
1. LUP Rx 打印 190B @256000 = 7.4ms 关中断 (PRINT 临界区)
   → UART2(19200) RX 溢出丢字节 → 帧解析错位 → 粘帧
2. g_pkg_uart_2.flag 清理缺失: uart_srv 0xC0 分支不 InitPkgUart,
   tcp_json_push_sensor not authed 提前 return 也不清
   → 坏帧反复处理 → checksum fail 刷屏

修复:
- LUP Rx 打印 #if 0 (高频打印关中断是丢字节根因, 调试时开)
- json_sensor_callback not authed 打印 #if 0 (每帧刷屏)
- uart_srv 所有分支消费后统一 InitPkgUart (0xC0 + 非0xC0无BLE)
- 顺带修 report disabled 双反斜杠
2026-08-13 17:43:24 +08:00
wangfq a36cc15cf2 test(vd960DBN): 二分卡死点 — 注释 json_sensor_callback enter PRINT + 删冗余打印
现场: LUP 帧后卡死 (5分钟后神秘复位 RST=0x10000000, marker丢失)。
卡死点在 lup_process_frame 内 json_sensor_callback 的 enter PRINT
(printf %d 处理), LUP Rx 打印(%s)成功但 enter(%d%d%d)卡死。
实验:
- json_sensor_callback enter PRINT #if 0 (坐实 printf 问题)
- uart_srv 187-190 重复逐字节打印 #if 0 (LUP Rx 已缓冲打印, 冗余)
验证: 不卡死→printf %d 问题实锤; 仍卡死→json_sensor_callback 后续代码
2026-08-13 17:22:31 +08:00
wangfq e7389b95fb fix(vd960DBN): fault_diag 无条件打印 marker — 卡死(非HardFault)也能定位
原只在 magic=0xFA57FA57 (HardFault) 时打印, 卡死/IWDG 复位时
marker 有值但 magic=0 → 轨迹丢失。改为:
- magic 有效 → 完整现场 (mcause/mepc/mtval/marker)
- marker!=0 且无 HardFault → 打印 no-hardfault + marker (卡死定位)
2026-08-13 16:35:30 +08:00
wangfq 4bdf8a7993 fix(vd960DBN): IWDG 无条件启用+喂狗 — 卡死兜底 (卡死定位闭环)
现场: printf 重入修复后乱码消失, 但 LUP 帧处理卡死(连 JSON:
sensor_cb enter 都没打印), 且 iot_enable=0 模式 IWDG 未初始化
(iot_mqtt_poll 才喂狗, tcp_json 分支不喂) → 卡死永久冻结无兜底。

修复:
- main 无条件 iot_watchdog_init() (IWDG 4s 超时)
- 主循环 while 开头无条件 iot_watchdog_kick() (原只在 iot_mqtt_poll)
- json_sensor_callback / iot_evt_sensor_cb 加 FAULT_MARKER(MK_EVT_CB_IN/OUT)

效果: 卡死 → 4s IWDG 复位 → 上电 FAULT_DIAG 打印 marker 定位卡死点
2026-08-13 16:15:26 +08:00
wangfq ee3ade6f00 fix(vd960DBN): PRINT 宏临界区保护 — printf 重入修复 (乱码+HardFault 根因)
现场: LUP 帧后乱码+复位。JSON: sensor 打印被二进制污染、
LUP Rx hex 残留重复 → printf 交叉输出的典型症状。
根因: BLE 协议栈回调(peripheral.c, BB 中断上下文)与主循环
都有 PRINT, 交叉调用 printf → 输出乱码 + newlib 堆/状态破坏
→ HardFault (RST=0x10000000 为 HardFault_Handler 内 NVIC_SystemReset)。

修复:
- debug.h PRINT 宏: 保存/恢复 MSTATUS 临界区 (__get_MSTATUS/
  __disable_irq/printf/__set_MSTATUS), 中断里调用也能正确恢复,
  不会嵌套误开中断
- loop_uart_proto: LUP Rx 逐字节打印改缓冲一次性输出 —
  逐字节 PRINT 关中断 ~350us/字节屏蔽 UART2 ISR 丢帧
2026-08-13 16:06:35 +08:00
wangfq 9f5102e9e8 feat(vd960DBN): fault_diag — .noinit RAM 现场 + 执行轨迹 marker 定位神秘复位
现象: LUP 帧后复位, RST=0x00000000 (无标志) 且 HardFault 打印缺失
(上次 0x10000000=SFT 确认 HardFault 路径, 这次连打印都没有)
方案: .noinit 段复位不清零, 跨复位留痕
- Link.ld: 新增 .noinit 段 (startup 只清 .bss 不碰)
- fault_diag.h: FaultDiag{magic,mcause,mepc,mtval,marker,boot_cnt} + FAULT_MARKER 轨迹宏
- HardFault_Handler: 纯 RAM 写 mcause/mepc/mtval (官方 __get_MCAUSE/MEPC/MTVAL,
  不依赖 UART/printf, 栈坏也能留痕) + 尽力打印
- main 开头 fault_diag_init(): 打印上次现场 + 递增 boot_cnt
- 轨迹 marker: 主循环 LOOP_TOP/UART_SRV/SIM_SPI/POLL_BLE + LUP 链
  LUP_FRAME/INGEST/EVT_FEED 入口出口, 复位后看 marker 停在哪一步
2026-08-13 15:44:07 +08:00
wangfq 1bbaac0046 fix(vd960DBN): HardFault_Handler 抓 RISC-V 异常现场 — 定位崩溃源头
诊断修正(2026-08-13): RST=0x10000000(bit28 SFT) = HardFault_Handler
内 NVIC_SystemReset, 不是硬件复位! 复位紧跟 LUP 传感器帧(30ms内),
0xC0 帧处理路径触发代码异常。模拟实验 SPI 写(cnt=64/128 擦除+头写)
全部成功, SPI 写本身不触发复位, 推翻 VDD 跌落/共线假设。

- HardFault_Handler 打印 mcause(0x342)/mepc(0x341)/mtval(0x343)
  mcause 5/7=野指针(mtval给地址) 2=非法指令 4/6=未对齐
  mepc 用 .map 文件反查函数
2026-08-13 15:31:34 +08:00
wangfq 87c8e01936 test(vd960DBN): SPI写密度模拟实验 + factory写止血 — VDD跌落诊断
- peripheral_main: sim_snap_spi 模拟快照落盘节奏 (20ms 64B页写 + 每64条切扇区擦除+头写)
- cfig_flash: magic不匹配时跳过 factory SPI_Flash_Write, 用内存默认值继续
  (SPI写触发硬件复位死循环止血: 参数区0x00写也崩, RST=0x00000000)
- 诊断修正: CH32V208GBU6 QFN28 无 NRST 引脚, 共线假设作废;
  SPI写/擦除→电流尖峰→VDD跌落→内部POR/PDR(2.4V)复位;
  BLE射频+SPI电流叠加是头号嫌疑
2026-08-13 11:27:08 +08:00
wangfq 99ef6348d7 test(vd960DBN): 跳过 offlog_boot 写 — 二分定位 SPI 触发点
f766530(跳过snap)仍崩 → 非快照SPI; 旧固件也有 offlog_boot 32B 写
→ 硬件一直有问题只是未暴露; 另一差异: 事件区 0x010000(旧) vs 0x090000(新)
本版: #if 0 offlog_boot 写 (保留 RMVF 清除)
不崩 → offlog_boot 写(或切扇区擦除)触发; 崩 → 更早 SPI 读操作/非SPI
2026-08-12 19:39:50 +08:00
wangfq f766530609 test(vd960DBN): 跳过 snap_init 实验 — SPI 序列还原旧固件
回答'为什么旧固件正常': 旧固件也有 offlog_boot 32B 写, 理论上也踩同样硬件问题
差异: 新固件启动早期 SPI 密度翻倍(snap_init 读+擦+写头) + 快照区首擦
实验: #if 0 snap_init → SPI 序列=旧固件
稳定 → 快照 SPI 触发; 崩 → offlog_boot 本身, 旧固件也该崩(硬件一直有问题)
2026-08-12 19:34:55 +08:00
wangfq 8bb576111b test(vd960DBN): SPI 分频 16→128 极慢版 — 验证频率无关性
降频4倍(9053831)后乱码字节不变+仍复位 → 非频率相关串扰
疑似 MOSI(PA7) 写数据与 NRST/调试线物理共线/短路 (乱码=写数据错位采样)
分频128(~0.5-1MHz): 若仍崩 → 100%物理层问题, 软件无解, 必须硬件修
硬件测量: 断电测 PA5/PA7 与 NRST 导通电阻(应无穷大); 查原理图 SPI 走线
2026-08-12 19:31:49 +08:00
wangfq 9053831f27 fix(vd960DBN): SPI 串扰复位软件缓解 — 降时钟+缓边沿, 恢复功能
实验闭环: Flash/SPI 全禁用后设备稳定 (BLE/WCHNET/TCP 全正常)
→ SPI 操作触发复位+乱码实锤; 崩点在 offlog_boot 写 32B (不擦除)
→ 排除电流, 判定 SPI 信号串扰 NRST(复位) + 调试线(乱码)

缓解: storage.c GPIO 50MHz→10MHz 缓边沿 + SPI 分频 4→16 降时钟
恢复: peripheral_main.c 两个隔离块改 #if 0 (Flash + UART2 正常)
硬件根治(待现场): NRST 加 100nF 电容 / SPI 走线远离 NRST / 串阻 22Ω
2026-08-12 19:27:47 +08:00
wangfq 22224416a3 test(vd960DBN): Flash/SPI 全禁用实验 — 定位 SPI 操作触发复位
UART2 隔离后(06fb009)依旧 80ms 复位循环+乱码 → 排除 Loop 数据路径
Hex 日志: 乱码紧跟 snap ok 后的 offlog_boot SPI 写, 复位标志全 0
= NRST 毛刺复位特征, 疑似 SPI 总线(PA4-7)串扰 NRST/调试线 或 电源瞬态
本版: #if 1 跳过 storage/offlog/snap/offlog_boot/load_cfg 全部 SPI 调用
判定: 稳定 → SPI 触发实锤, 硬件查电源/NRST/布线
      仍复位 → 与 SPI 无关, 纯硬件(电源/晶振/其他)
2026-08-12 19:21:57 +08:00
wangfq 06fb009773 test(vd960DBN): UART2 隔离实验 — 软件禁用 Loop 口定位复位源
现场: RST_REASON 全 0 瞬态复位, 总在 Loop 帧后 ~10ms, 指向 Loop 侧干扰
实验: uart_init 后立即禁用 USART2 + PA3 脱离 UART 功能(上拉输入)
     = 等效软件断开 Loop; 同时快照无帧入队, 同步验证 SPI 活动非诱因
判定: 不复位+无乱码 → Loop 数据路径干扰实锤
      还复位 → 物理引脚级干扰 或 CH32 自身电源问题
通过后删除此块
2026-08-12 19:15:37 +08:00
wangfq eadc67584f fix(vd960DBN): 复位原因位定义错位修正 — RST_REASON=0x08000000=PORRSTF
现场: RST_REASON 打印 0x08000000, 对照 WCH ch32v20x.h 发现
offlog.h 位定义整体错位 (IWDG=bit31实为bit29, SFT=bit24实为bit28,
POR=bit25实为bit27, LPWR=bit29实为bit31)
结论: 0x08000000=PORRSTF=上电/掉电复位 → 电源跌落方向 (SPI擦除电流脉冲?)
修正:
- offlog.h 位定义对齐 WCH 库, 新增 OFFLOG_RST_RMVF=bit24
- peripheral_main.c RMVF 清除改 bit24 (原写 bit27=PORRSTF 只读位,
  复位标志从未清除, 历史累积污染诊断)
- 协议文档 boot 事件位解析同步
测试回归全过
2026-08-12 18:58:16 +08:00
wangfq f5b52d5bde fix(vd960DBN): 启动诊断打印 + snap_flush 无条件挂载
现场: 懒擦后仍起不来 (200ms 乱码后死机), 需定位复位/卡死点
- main 开头打印 RST_REASON (RCC_RSTSCKR): 一锤定音区分
  IWDG死锁(bit31) / POR掉电(bit25) / NRST外部复位(bit26) / 软复位(bit24)
- 各 init 步骤加标记 (storage/offlog/snap ok), 定位卡死位置
- snap_flush 从 iot_enable 分支移到 Main_Circulation 无条件调用:
  原实现 TCP 模式(iot_enable=0)下快照永不落盘, RAM 暂存满丢新
2026-08-12 18:42:40 +08:00
wangfq 3d4814bffe fix(vd960DBN): init/clear 懒擦修复不断重启 — 34s 全量擦除超 IWDG 4s
现场: 烧录快照固件后 CH32V208 不断重启 + 串口乱码
根因: snap_init 首擦 751 扇区 ~34s (头在擦完才写→复位后仍全新→死循环);
      snap_clear 34s / offlog_clear 2.8s 同类; 均超 IWDG 4s
修复(懒擦): init/clear 只擦写指针起点扇区 ~45ms, 其余由环形写切扇区
      逻辑自动擦; clear 为逻辑清除 (count=0 旧数据不可读)
附带: usart_biz.c 非 0x7F 帧 %s 打印改 hex (Loop 数据当字符串=乱码源)
      snapshot.h 线程模型注释修正 (lup_process_frame 实际在主循环 uart_srv)
测试: test_snapshot 9例(新增 lazy_erase) + offlog/ble_offlog 回归全过
2026-08-12 18:31:56 +08:00
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
wangfq a8120cc852 feat(vd960DBN): 事件日志区动态分区 — JEDEC ID 选档 (ROADMAP 落地)
- offlog.h: OfflogPart 运行时分区表 + g_offlog_part; AREA_BASE 改 0x090000
  (参数64KB+OTA512KB); 容量宏展开为运行时值(主体零改动)
- offlog.c: offlog_part_detect() 读 JEDEC ID → 事件区容量
  W25Q32=512KB/16256条, Q64=1MB/32640, Q128=2MB/65408, Q256=4MB/130944
- storage.c/h: SPI_Flash_ReadJEDEC_ID() (0x9F, 校验 EF 40)
- 旧固件事件区 0x010000 自动废弃(新地址无 magic → 重建), 预期行为
- 测试: mock JEDEC=0x16 + g_offlog_part, 15 例全过
- 文档: BLE/MQTT/TCP 协议 capacity 8064→动态, 交互示例, devlog
2026-08-12 16:46:19 +08:00
wangfq 6c6ddf8b0f docs(ROADMAP): 容量预算重算 — 事件日志翻倍后快照 4.8万条基准
- W25Q32: 最坏300ms压满 ~4.0h / 繁忙 ~1.5天 / 一般 ~16.7天 (原4.4h/1.5天/18天)
- 新增各芯片快照保留时长对比表 (Q32~Q256, 同落盘节奏)
- 注明事件日志独立 512KB→4MB 不受快照覆盖影响
2026-08-12 16:29:54 +08:00
wangfq 667b5376ab docs(ROADMAP): 事件日志区容量整体翻倍 — W25Q32 512KB 起类推
- W25Q32: 256KB→512KB (~16000条), 快照区 ~3.19MB→~2.94MB
- W25Q64: 512KB→1MB, W25Q128: 1MB→2MB, W25Q256: 2MB→4MB (类推)
- 传感快照区 = 总容量 − 参数64KB − OTA512KB − 日志区
- 容量预算基准同步: 日志512KB / 快照~2.94MB ≈ 4.8万条@64B
2026-08-12 16:27:05 +08:00
wangfq 62c6133632 docs(ROADMAP): 移除 W25Q80 支持 — 最小可部署配置定为 W25Q32
- W25Q80(1MB) 扣除参数64KB+OTA512KB后仅剩448KB, 快照区太紧张
- 容量映射表保留 W25Q32/Q64/Q128/Q256; 事件日志等比 256KB→2MB
- 最小配置 = W25Q32(4MB)
2026-08-12 16:20:37 +08:00
wangfq d7f9c29f25 docs(ROADMAP): 脱机日志分区规划调整 — OTA暂存提前 + 按存储类型动态容量
- 分区顺序: 参数区 → OTA镜像暂存区 → 事件日志区 → 传感快照区
- 参数区(64KB)/OTA暂存(512KB) 固定; 事件日志+传感快照 按 JEDEC ID 动态
- 新增 W25Q80/Q32/Q64/Q128/Q256 容量映射表 (EF 40 14/16/17/18/19)
- 事件日志区等比翻倍 128KB→2MB, 快照吃剩余; W25Q80(1MB) 为最小配置
- 容量预算段注明以 W25Q32 为基准, 其他芯片等比缩放
2026-08-12 16:18:17 +08:00
wangfq 193455e629 docs(vd960DBN): devlog 记录第二分包根因 — 响应缓冲越界踩踏 g_notify_buftemp 2026-08-12 14:51:47 +08:00
wangfq e8b7c6f70c fix(vd960DBN): BLE 响应缓冲越界防御 — 根因: MAX_BLE_DAT_BUF_LEN 宏不一致(ODR)
根因链 (14:46 日志 + .map 铁证):
- .map: g_buf_ble_response = 107B (7 + dat[100]) -> 编译时 MAX_BLE_DAT_BUF_LEN=100
- 但 QUERY 响应 130B 写入 dat[0..129] -> 越界 30B
- 越界踩中相邻 g_notify_buftemp.flag/len (0x20007BDC)
- 第二分包填充后被物理抹除 (无 CLR_NTF = 非 clear_ble_notify_buf 干的)

修复 (兼容新旧宏, 永不越界):
- QUERY: _req_count 按 MAX_BLE_TMP_BUF_LEN 动态限制 (100->3条/132->4条)
- set_response_buf: dat_len 截断到 MAX_BLE_DAT_BUF_LEN
- 验证: 旧宏100 下 3条=98B 分包[93,17] 全合规
2026-08-12 14:51:21 +08:00
wangfq 67734d3b9a debug(vd960DBN): clear_ble_notify_buf 抓现行 — buf指针 + 调用者返回地址
- 沿监控证实: 第二包填充成功 (flag=1 len=49 temp=1) 后被 clear_ble_notify_buf 清空
  (flag=0 len=0), 但无 BLE send 打印 = 不是 performPeriodicTask 发送路径
- clear_ble_notify_buf 入口: 有货被清时打印 buf 指针 + __builtin_return_address(0)
- 若 buf 指针 != g_notify_buftemp → 指针/内存布局问题; ret 对 .map 定位调用者
2026-08-12 14:41:44 +08:00
wangfq 6582b93938 debug(vd960DBN): notify flag / ntf_temp 变化沿监控 — 抓第二包消失瞬间
- ret=00008122 对 .map = set_response_to_notify 内部 (0x7FEC+0x136)
  → CLEAR busy 是第二包填充完成的正常清空, 队列清空不是问题!
- 真正问题: 第二包填充后 notify flag 从 1 被清 0, g_flag_notify_temp=0
  → 下一个 TMOS 周期不发送 (无 BLE send len:49)
- 加 BLE ntf flag x->y (len= temp=) 沿监控, 只在变化时打印
2026-08-12 14:28:41 +08:00
wangfq 7a08e083d6 debug(vd960DBN): clear_buf_dbn_ble 抓现行 — 有货被清时打印调用者返回地址
- 已排除 simpleProfile_Notify (发送后 resp=1 队列完好)
- 队列在 hex 打印后 ~20ms 被清空 (amt/seq/off 全0 = clear_buf_dbn_ble 效果)
- clear_buf_dbn_ble 入口: 仅当 buf->flag=1 (有货被清) 时打印 + __builtin_return_address(0)
- 烧录后 ret=地址 对照 .map 文件直接定位调用者
2026-08-12 14:14:39 +08:00
wangfq 66f156105c debug(vd960DBN): peripheralChar4Notify 内 resp 队列状态实验打印
- 上一轮日志: BLE send 时 resp=1 amt=2 seq=1 off=87 (队列正常),
  但 peripheralChar4Notify 执行后下一周期队列已空
- 锁定嫌疑: simpleProfile_Notify 库函数内部是否清了 g_buf_ble_response
- N4 enter / after simpleProfile_Notify 各打印一次 resp 状态
2026-08-12 14:08:34 +08:00
wangfq 60576b2b33 debug(vd960DBN): 抓现行打印 — 发送前队列状态 + resp flag 变化沿
- 心跳证实: 第一包发出后 resp=0 amt=0, 队列已被意外清空 (第二包不存在)
- poll_dbn_ble 尾部: resp flag 变化沿打印 (0->1/1->0 各一次, 不刷屏)
- performPeriodicTask: 发送前打印 len + resp 队列状态
- 烧录后一次看清 谁在何时清了 g_buf_ble_response
2026-08-12 14:02:23 +08:00
wangfq ef3316f2b3 docs(vd960DBN): BLE 日志查询交互过程文档 + 协议 V1.01 动态分包 + 周期心跳
- 新增 docs/DLD960_BLE日志查询交互.md: 0x25/0x26/0x27 完整交互示例
  (真实日志逐字节解析, 分包规则, 第二分包未发问题记录)
- DLD960_BLE协议.md V1.01: 分包上限改为随协商 MTU 动态 (min(MTU-9,94))
- peripheral.c: performPeriodicTask 加 500ms 心跳打印 (确认 TMOS 周期存活
  + resp 队列状态, 定位第二分包为何不续传)
2026-08-12 13:51:32 +08:00
wangfq 070db9f383 debug(vd960DBN): performPeriodicTask 拉包失败打印 resp 状态 + 注释闭合
- 第二分包未进发送函数, 加条件打印 (仅当队列有货但拉包返回0时)
  BLE pull FAIL: resp flag/amt/seq/off 一烧录即知第二包断在哪
- 补 ble_notify_chunk_max 注释块闭合 */
2026-08-12 11:55:44 +08:00
wangfq 5eef0756f2 debug(vd960DBN): BLE notify 发送数据 hex 打印 — 排查第二分包
- peripheralChar4Notify OK 分支: 打印 len/MTU + 整包 hex (对齐收包打印风格)
- 烧录后设备日志可直接比对发送帧与小程序收到帧
2026-08-12 11:45:06 +08:00
wangfq bcf797285b fix(vd960DBN): BLE 第二分包丢失 — performPeriodicTask 主动拉包 + 发送日志
- 现场: 第一包 93B 正常收到, 第二包 49B 永远收不到 (两包间隔 50ms)
- 根因: 分包续传依赖 poll_dbn_ble 下一轮收包/主循环轮转填充, 与 TMOS 周期交错
- peripheral.c: performPeriodicTask 发完当前包后主动 set_response_to_notify 拉下一分片,
  与收包事件解耦; peripheralChar4Notify 增加 BLE notify OK/FAIL 打印 (发送失败不再静默)
- 隔离测试: 仅靠周期事件驱动, MTU=96 3 周期发出 93B+49B, 重组 130B 一致
2026-08-12 11:35:21 +08:00
wangfq 62f7de0ca2 fix(vd960DBN): BLE 分包粒度动态化 — 修复 offlog_query Too large noti 丢包
- 根因: MAX_BLE_DAT_RESPONSE_LEN=96 写死, MTU 协商 96 后整包 102B > MTU-3=93
  peripheralChar4Notify 直接 return 丢第一包, 小程序重组不完整
- peripheral.c: 新增 peripheral_get_mtu() getter
- dbn_ble_srv.c: ble_notify_chunk_max() 按 min(MTU-9,94) 动态分包
  4 处 MAX_BLE_DAT_RESPONSE_LEN 统一替换 (set_response_buf/to_notify/iot_net/iot_topic)
- 消除 BLE_Notify_Buf.buf[100] 写 102B 越界 2B 隐患
- 隔离 C 测试 MTU=23/96/185/517 四组全过, 重组 130B 逐字节一致
2026-08-12 08:30:44 +08:00
wangfq d6174b9d3f feat(vd960DBN): BLE 读取脱机日志 — OFFLOG_STAT/QUERY/CLEAR (0x25/0x26/0x27)
蓝牙通道补齐脱机日志读取 (此前仅 MQTT V1.06 / TCP JSON V1.02 有 log_* 命令):
- 3 条 BLE 命令, 语义对齐 MQTT log_stat/log_query/log_clear
- QUERY 直接传 32B OfflogEvt 原始结构 (二进制协议, 无需 JSON), 复用
  idx = start_seq - seq_first 定位, 不新增 offlog API
- 缓冲扩容: MAX_BLE_TMP_BUF_LEN 100→132, 新增 MAX_BLE_DAT_BUF_LEN=132
  (QUERY 响应 1+4x32B=129B), clear_buf_dbn_ble_all memset 同步
- 新协议文档 docs/DLD960_BLE协议.md V1.00 (帧格式+分包+命令+记录结构)
- 隔离测试 test_ble_offlog.c 嵌入源文件 3 case 真实文本, 7 例全过;
  offlog 回归 8 例 ALL PASS
- devlog V4.0; README 协议矩阵补 BLE 行
2026-08-10 14:24:20 +08:00
wangfq 5ebd8247f0 feat(DBNMQTTool): 脱机事件日志命令支持 — log_stat/log_query/log_clear (MQTT V1.06)
- protocol.py: CMD_LOG_* 常量 + LOG_COMMANDS + data_log_query 边界钳制
  (start_seq>=1, count 1~4) + LOG_EVENT_TYPE_DESC 事件类型中文描述
- main.py: 参数配置 Tab 新增脱机日志面板 (统计/拉取/清除 + 显示区)
- _on_message 响应分发: log_stat 统计展示, log_query 记录格式化
  (unix_ts 真实时间 / boot+ts_ms 未同步), log_clear 二次确认
- devlog 追加
2026-08-05 09:06:11 +08:00
wangfq 1a01316c7f feat(vd960DBN): offlog 协议导出命令分发 — log_stat/log_query/log_clear 落地
协议先行 (08893d5, MQTT V1.06 + TCP JSON V1.02) 的固件侧实现:
- offlog 新增导出 API: enabled/seq_last/type_str/evt_to_json
  (seq_first=seq_last-count+1, idx=start_seq-seq_first, count≤4)
- MQTT iot_handle_publish 三分支: log_stat/log_query/log_clear
  (log_query 响应 static buf 防大栈; log_clear 阻塞~2.8s 主循环可接受)
- TCP JSON 三 handler + cmd 表 (需鉴权)
- 单测新增 test_export_json (8 例全过): boot rst 大端/coil sub/evt_retry/data:null/seq 公式
- devlog V3.9
2026-08-05 08:53:18 +08:00
wangfq 08893d5134 docs(protocol): 脱机事件日志命令规范 — MQTT V1.06 + TCP JSON V1.02
新增 3 命令 (两协议同步):
- log_stat: 日志统计/分页定位 (stream/boot_seq/count/capacity/seq_first/seq_last)
- log_query: 按全局序号分页拉取, count≤4 (受 500B 发布限制)
- log_clear: 清除+审计留痕 (log_clear 事件不可清除)

日志时间戳语义 (§2.3 补充): 每条记录 ts_ms(相对) + unix_ts(已同步,
0=未同步), 锚点回算规则; 10 类事件类型表 (boot/iot_*/evt_*/coil/
time_anchor/log_clear) 带 data 字段解析。

同步: README/CHANGELOG/产品手册/技术规格书 协议矩阵
(MQTT V1.05→V1.06, TCP JSON V1.01→V1.02)

注: 固件命令分发 (log_stat/log_query/log_clear 处理) 待 P1.3 实现,
本文档为协议先行
2026-08-04 18:34:31 +08:00
wangfq 90755bf465 feat(vd960DBN): offlog TIME_ANCHOR 严格用平台下发 unix_ts
offlog_time_anchor 原丢弃传参内部重取 dev_time_now() (毫秒级误差),
改为 offlog_evt_ts() 直接写入平台下发值, 锚点事件与平台 ts 严格一致。
新增单测 test_time_anchor_exact, 7 例 ALL PASS
2026-08-04 18:10:18 +08:00
wangfq 2b1e888a73 refactor(vd960DBN): 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 溢出保护; 最终帧受 MAX_FRAME=800 截断。
附带: 栈上缓冲 2048→1024 每处省 1KB
2026-08-04 17:06:29 +08:00
wangfq 5155219c92 fix(vd960DBN): 修复 offlog 编译错误 — 声明补齐 + RCC 宏移入公共头
MRS 编译报错:
1. offlog.c: SPI_Flash_Erase_Sector 隐式声明 → storage.h 补声明
2. peripheral_main.c: OFFLOG_RCC_RSTSCKR 未声明 → RCC 宏从 offlog.c
   移到 offlog.h (peripheral_main.c 只 include offlog.h)
3. offlog.c 删除本地重复 RCC 宏定义

单测重跑 ALL PASS
2026-08-04 16:51:53 +08:00
wangfq ac3fb512d5 feat(vd960DBN): 脱机事件日志系统 (W25Q32 环形) — 解决重复上线取证
背景: 设备挂平台测试出现重复上线(现场未断电), 无本地日志无法区分
设备真复位 vs MQTT 断连重连。iot_handle_suback 每次重连都发
initialize 是重复上线的直接证据, 日志需能区分二者。

实现 (V3.6, ROADMAP P1.2 事件流先行):
- offlog.c/h: W25Q32 256KB 环形事件日志 (头扇区+63数据扇区, 8064条)
  - 32B 定长记录: magic/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload[12]
  - 8 类事件: BOOT(复位原因)/IOT_CONNECT/READY/DISCONN/RECONN/
    EVT_RETRY/GIVEUP/COIL/TIME_ANCHOR/LOG_CLEAR
  - 掉电恢复: 头扇区写指针锚点 + 上电 seq 连续性扫描
  - 编译期断言防 32B padding 回归
- 插桩 (全部主循环上下文, socket 中断内不写 SPI):
  - main(): offlog_init + RCC_RSTSCKR 复位原因采集/清除
  - iot_mqtt_poll(): MQTT 状态沿检测 (CONNECT/READY/DISCONN)
  - 重连退避/TCP超时/CONNACK拒绝: RECONN/DISCONN(4)/(3)
  - iot_evt_process/enqueue: EVT_RETRY/GIVEUP/COIL
  - net_srv.c dev_time_sync: TIME_ANCHOR 时钟同步锚点
- tests/test_offlog.c: gcc 隔离单测 6 例 (mock W25Q32 NOR 语义)

关键坑: ①中断内写SPI阻塞 ②结构体36B padding致环形错乱
③扇区级覆盖粒度count扣减语义 (均单测抓出)

待办: P1.3 导出命令(log_query/stat/clear) + 快照流 + 复位原因板上验证
2026-08-04 15:04:12 +08:00
wangfq a5ac54a2bf docs(vd960DBN): 开发日志 V3.5 — 保活精简 + SocketSend 故障恢复 2026-07-23 18:36:40 +08:00
wangfq 98d9b99cf6 fix(vd960DBN): 断连重连时重建 TCP socket (不是复用已 close 的)
root cause 1: 3次失败断连后, iot_connect_broker() 只设
  g_iot_socket=SocketId_TCP, 但 WCHNET_SocketClose 后
  socket 已销毁, 没有重建 → TCP connect 一直超时

root cause 2: old socket close 未完成就立即重连,
  WCHNET_SocketCreat 可能失败

fix:
  1. iot_connect_broker() 先调 WCHNET_CreateTcpMqttSocket()
     真正 create + connect
  2. 3次失败后加 3s 延迟再重连, 给 WCHNET 清理时间
2026-07-23 17:41:41 +08:00
wangfq 7c0de6c566 fix(vd960DBN): SocketSend 连续3次失败 → 主动断连重连
root cause: WCHNET_SocketSend 返回 ret=0x11(sent=0) 后
  TCP超时要等~2分钟才触发 SINT_STAT_TIM_OUT, 期间所有
  loop_data/event_report 全丢, 平台收不到任何数据

fix: iot_mqtt_send 追踪连续失败计数, 3次→强制 close socket
  + g_iot_state=DISCONNECTED, 触发 iot_mqtt_poll 重连
  (成功一次就清零, 新连接也清零)
2026-07-23 17:14:49 +08:00
wangfq 29d1f6a783 refactor(vd960DBN): 去掉 heartbeat JSON 上报, MQTT PINGREQ 已保活
heartbeat 是单向设备上报, 平台不回复; MQTT PINGREQ/PINGRESP
已是双向保活。loop_data 按 g_report_cfg.interval 上报即可。

删除: iot_send_heartbeat() 函数 (省 ~40行 + BSS payload[512]→栈)
2026-07-23 15:28:10 +08:00
wangfq bdc2720d96 docs(vd960DBN): 开发日志 — V3.4 今日MQTT栈重构完整记录 2026-07-23 15:22:29 +08:00
wangfq 8f1bbf75f8 refactor(vd960DBN): 看门狗精简 — 去掉平台响应超时, 只保留硬件 IWDG
heartbeat 是单向设备上报, 平台不回复; 用 '3×心跳无平台回复→重启'
逻辑不成立。保活依赖三层已有机制:
  MQTT层: PINGREQ/PINGRESP (60s) → broker不回 → TCP超时
  TCP层:  KeepAlive → WCHNET SINT_STAT_DISCONNECT/TIMEOUT
  硬件层: IWDG (4s) → 主循环卡死 → 硬件复位
2026-07-23 15:21:59 +08:00
wangfq 127fed4b07 feat(vd960DBN): 硬件 IWDG 看门狗 + 平台响应超时重启
设计:
  1. 硬件 IWDG: LSI≈40kHz, Prescaler=256, Reload=625 → ~4s 超时
     主循环 iot_mqtt_poll() 每轮喂狗, 防死循环/硬故障

  2. 平台响应看门狗: 3×心跳周期(180s)内无平台回复 → NVIC_SystemReset
     _iot_last_recv_ms 刷新点:
     - iot_handle_publish() — 收到平台任意 PUBLISH 命令
     - iot_evt_handle_ack() — event_report 的 ACK
     - iot_handle_suback() — 刚连上 READY 时初始化计时

  3. 仅在 IOT_STATE_READY 时检查 (未连上不触发)
2026-07-23 14:49:07 +08:00
wangfq 199cb5d23f config: WCHNET_TCP_MSS 576→700, TCP_JSON_MAX_FRAME 4096→800
- WCHNET_TCP_MSS=700: 减少 TCP 分段, 配合 iot_mqtt_send 循环重试
  → RECE_BUF_LEN 自动扩至 1400 (原 1152)
- TCP_JSON_MAX_FRAME=800: 匹配实际 JSON 帧大小
2026-07-23 14:30:47 +08:00
wangfq 9e830639d3 fix(vd960DBN): iot_mqtt_send 循环重试防 WCHNET 部分发送致 _raw
root cause: WCHNET_SocketSend 返回部分发送(520/607B),
  剩余字节留在TCP发送队列, 与下一个MQTT包拼接,
  broker收到乱码 → _raw + RST断连

fix: iot_mqtt_send 加 while 循环, 指针偏移重试直到全部发出;
  最多10次重试, slen==0或错误时返回-1
2026-07-23 13:57:54 +08:00
wangfq a9c36e4c9d fix(vd960DBN): 心跳 msg_id 双递增修复 + %d→%lu
root cause: iot_send_heartbeat 先 ++g_iot_msg_id(JSON) 再
  iot_mqtt_publish 内部 ++g_iot_msg_id(MQTT pktid) → 跳号

fix: 心跳改用 g_iot_msg_id + 1 (与其他发布对齐)
2026-07-23 13:43:13 +08:00
wangfq 67976eca8e fix(vd960DBN): IoT MQTT 命令处理完善 — report_config + event_report ACK + initialize 走 IoT 路径
issues:
  1. iot_handle_suback 调 dev_initialize_pub() → 旧 mqttBuf 路径, 与 IoT 栈混淆
  2. report_config 命令无人处理 → 平台收到 code=4 unsupported
  3. event_report ACK (code=0) 被当 unsupported command → 重发闭环断裂

fix:
  1. 新增 iot_send_initialize() — IoT 路径, 用 iot_mqtt_publish
  2. 新增 report_config 处理 — 解析 enable/interval/once/env_eval/sensor_type
  3. 新增 event_report ACK 识别 — code=0 → iot_evt_handle_ack; ACK 不回复
2026-07-23 12:24:09 +08:00
wangfq a577537c38 fix(vd960DBN): 修复 WCHNET_SocketSend 参数类型不匹配致栈破坏 hard fault
root cause: iot_mqtt_send() 声明 uint16_t slen = len,
  传给 WCHNET_SocketSend(..., uint32_t *len) 时取址 &slen。
  WCHNET 写 4 字节返回值到 2 字节栈变量 → 破坏相邻栈内容
  (返回地址/帧指针) → 函数返回时 hard fault 重启

fix: slen 改为 uint32_t
2026-07-23 11:52:58 +08:00
wangfq 32f0edb875 fix(vd960DBN): MQTT CONNECT 改回中断上下文发送, 消除 SocketSend hard fault
root cause: WCHNET SocketSend 在中断上下文外调用会 hard fault。
  旧代码 mqtt_connect() 在 WCHNET_HandleSockInt CONNECT 中断里直接调;
  v4 改成了延后到主循环 iot_mqtt_poll() 里发 → hard fault

fix: iot_mqtt_handle_sock_int 的 CONNECT 分支直接调用
  iot_mqtt_send_connect() (对齐旧代码行为), 不再延后到 poll
2026-07-23 11:47:27 +08:00
wangfq 9521a5e667 fix(vd960DBN): 修复 SocketSend 后重启 — RecvBuf 用错 + 栈溢出
root cause:
  1. iot_mqtt_handle_sock_int 的 CONNECT 处理调了
     WCHNET_ModifyRecvBuf(_iot_wchnet_buf), 覆盖了 WCHNET 原生的
     SocketRecvBuf, 导致后续 SocketSend 时 DMA 访问非法内存 → hard fault
  2. uint8_t tmp[RECE_BUF_LEN](1152B) 在中断栈上, 叠加调用链
     可能撑爆 2KB 栈

fix:
  1. net_srv.c WCHNET_HandleSockInt: 保留 SocketRecvBuf + KeepLive,
     其余事件委托给 iot_mqtt_handle_sock_int
  2. iot_mqtt_handle_sock_int: 移除 ModifyRecvBuf(已在 wrapper 处理)
  3. tmp[1152] 改为 static (移到 BSS, 省 1152B 栈)
2026-07-23 11:39:52 +08:00
wangfq 3d017ccca9 fix(vd960DBN): IoT MQTT 栈独立接管 socket 事件, 消除双 CONNECT 致频繁 initialize
root cause: WCHNET_HandleSockInt 在 iot_enable=1 时仍走旧 MQTT 代码:
  CONNECT → mqtt_connect()     ← 发 MQTT CONNECT #1
  iot_mqtt_poll() → send_connect() ← 发 MQTT CONNECT #2 (同一socket)
  broker 见双 CONNECT → 协议违规踢线 → 4s 后重连 → 循环
  RECV → mqtt_data_manage()   ← 旧代码收 CONNACK
        → dev_initialize_pub() ← initialize msg_id=1, 强设 g_iot_state=READY

fix:
  1. net_srv.c WCHNET_HandleSockInt: iot_enable 时直接调 iot_mqtt_handle_sock_int
  2. peripheral_main.c: poll_mqtt() → iot_mqtt_poll() (IoT 自有 PINGREQ)

BSS savings: -1024B (data_json eliminated), send buffer 隔离 (v3)
2026-07-23 11:25:52 +08:00
wangfq 086dbc6d7c fix(vd960DBN): loop_data 改用 iot_mqtt_publish 发送, 缓冲隔离防 _raw 异常 v3
- root cause (v3): event_report 的 mqtt_publish → MQTTSerialize_publish(mqttBuf)
  写入的 MQTT 二进制泄漏到 loop_data 的 payload 缓冲, 产生 _raw 异常

- fix: loop_data 改用 iot_mqtt_publish() 发送, 其内部 buf[1024] 与
  全局 mqttBuf[1024] 物理隔离, event_report 和 loop_data 不再争用同一缓冲

- 同时: 消灭 data_json[1024] (v1), coil_count 硬限 (v1),
  msg_id 改预读避免 iot_mqtt_publish 内部 ++ 导致跳号

- note: iot_send_heartbeat 有同样 msg_id 双 ++ 问题, 待修
2026-07-23 11:04:17 +08:00
wangfq e11a80c859 fix(vd960DBN): 消灭 data_json[1024] 中间缓冲, 防 BSS 重叠致 MQTT 上报 _raw 异常
- root cause: data_json[1024] + payload[1400] BSS 合计 2424B,
  叠加 iot_mqtt_srv.c 其他缓冲 (~9KB total), 在 48KB RAM 上
  与 mqttBuf[1024] 重叠, event_report 的 MQTT 二进制
  泄漏到 loop_data payload

- fix: 直接在 payload 构建完整 JSON, 省 1024B BSS
- defense: coil_count > 4 硬限, 防 0xC0 坏帧溢出
- JSON 输出格式不变

- also: set_response_tran_to_notify 加返回值
- also: uart_srv BLE 通知逻辑重构
2026-07-23 09:26:18 +08:00
wangfq 95a1d2b5d2 docs(roadmap): 板载W25Q32(4MB)确认, 分区与容量预算按实际重算
- 分区: 参数64KB / 事件流256KB(~8000条) / OTA暂存512KB / 快照区~3.2MB
- 快照容量三场景预算: 快档压满~4.4h / 繁忙车道~1.5天 / 一般车道~18天
- 补充延长保留的两个手段: 落盘与上报解耦 / 沿事件预触发环形
- 移除'W25Q型号确认'前置项
2026-07-17 19:04:28 +08:00
wangfq 70e44e6241 docs(roadmap): 补充脱机日志子系统 + 日志导出管理 + 远程OTA路线细化
- P1.2 脱机日志: W25Qxx 分区规划(参数/事件流/快照流/OTA暂存),
  事件流必录清单, 快照流同频落盘挂 iot_sensor_ingest 同源出口,
  无RTC时间戳方案(boot_seq+mstick+时钟锚点)
- P1.3 导出与管理: MQTT断网补报/命令分页拉取/BLE小程序读取
  三通道, log_stat/log_clear(鉴权+审计自记录)
- P1.4 远程OTA: 先走现网MQTT+复用ISP透传底层(与BLE OTA同构);
  Loop先存后刷+断点续传+回滚, 继电器安全底线;
  DBN自身远程OTA三路线比选(过渡态=仍走BLE最稳妥)
- 风险表新增: 无RTC时间戳、日志写与总线争用
2026-07-17 18:57:13 +08:00
wangfq ea29433633 docs: 新增 ROADMAP — V1.0.0 之后的开发计划 (P0稳固→P1上云→P2算法→P3会车)
- P0 (v1.1.0): BLE透传重构收尾(0xC0三消费方真值表) + TODO清剿
  (loop_ok硬编码/MQTT接收溢出防护) + 工装回归清单
- P1 (v1.2.x): edc_server消费契约(先落库后ACK)、流量API、
  远程OTA方案评审、4G通道预研
- P2 (v2.0.0): 抗变频器干扰(先现场采CAPVD取证)、双线圈测速+
  车型粗分类、半截车双峰识别、方向判别
- P3 (v2.x): 会车控制 — 业务只认event_report、异常分支先行、
  纯平台/纯单机/混合三方案对比
2026-07-17 18:13:29 +08:00
wangfq 579eeecb23 docs: 校准通信接口表述 — DLD960 无 RS485 接口
- 对外通信 = 网口(TCP JSON/MQTT) + 蓝牙(小程序/OTA) + 扩展TTL串口(预留4G上云, RFU)
- 内部双 MCU 经 UART TTL 互联 (0x7F @192000)
- 修正范围: README/CHANGELOG/技术规格书/产品手册/硬件资源文档概述
- acceptance-standard 中 RS-485 为通用验收检查项, 保留
2026-07-16 16:39:02 +08:00
wangfq d61ff830aa fix(vd960DBN): BUFF_STACK_SIZE 512→64 对齐 Loop 侧串口帧缓冲
- 串口帧缓冲装的是 Loop↔DBN 的 0x7F 私有协议帧, 与 Loop 侧发送缓冲(64)同源
- 当前最大帧 0x0C 多线圈上报实测 56B (Len=51), 64 留 8B 余量, 512 严重浪费
- g_pkg_uart_1/2 各省 448B, 合计回收 ~896B SRAM
- 顺带删除零引用死结构体 USART_DMA_UNIT + RX_BUFFER_LEN 宏
2026-07-16 14:52:16 +08:00
wangfq c0bdcd956d docs: 新增产品手册 + 技术规格书 (V1.00, 配套整机 V1.0.0)
- 产品手册: 接口/指示灯/线圈施工要求/快速上手/参数配置/故障排查
- 技术规格书: 双MCU架构/检测规格/灵敏度阈值表(SensTable换算)/算法机制/协议矩阵/配套要求
- README: 文档索引挂接产品文档
2026-07-16 10:31:10 +08:00
49 changed files with 8468 additions and 1021 deletions
+8
View File
@@ -3,6 +3,9 @@
*.i *.i
*.old *.old
# 测试生成物 (extract_offlog_cases.py 输出, 嵌入源文件真实代码)
offlog_cases_embedded.c
# Object files # Object files
*.o *.o
*.ko *.ko
@@ -89,3 +92,8 @@ wchreference/
__pycache__/ __pycache__/
*.pyc *.pyc
# gcc 单测编译产物 (tests/*.c 保留)
test_offlog
test_ble_offlog
test_snapshot
+172 -72
View File
@@ -1,72 +1,172 @@
# CHANGELOG — vd_960 (DLD960) # CHANGELOG — vd_960 (DLD960)
本文件记录 DLD960 整机(双 MCU 配套)的版本发布历史。 本文件记录 DLD960 整机(双 MCU 配套)的版本发布历史。
单侧固件的细粒度演进见 `vd960Loop/docs/devlog.md``vd960DBN/docs/devlog.md` 单侧固件的细粒度演进见 `vd960Loop/docs/devlog.md``vd960DBN/docs/devlog.md`
--- ---
## V1.0.0 — 2026-07-16(首个正式发布 ## vd960DBN V1.02.04 — 2026-08-19DBN 通信板单侧更新
**配套版本矩阵** > 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。
| 组件 | 版本 | **配套版本矩阵**
|------|------|
| vd960Loop 固件 (AT32F421) | 1.0 | | 组件 | 版本 |
| vd960DBN 固件 (CH32V208, DLD960GA) | 1.0 | |------|------|
| DLD960Loop 串口协议(MCU 间 0x7F | V1.05 | | vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.04** |
| DLD960 串口通信协议(RS485/TTL | V1.01 | | vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.01 | | DLD960Loop 串口协议(0x7F / 0x9F OTA | V1.05(不变) |
| DLD960 IoT MQTT 协议 | V1.05 | | DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
| DLD960 IoT MQTT 协议 | V1.07(不变) |
> ⚠ 自协议 V1.05 起,Loop 固件与 DBN 固件必须同版本配套刷写,禁止混跑(variation 字段宽度变更导致帧格式不兼容)。 | DLD960 BLE 协议 | V1.02(不变) |
### vd960Loop(检测 MCUAT32F421 ### 🔧 SPI Flash 存储适配修复
- **识别去厂商代码**`W25Qxx` 宏去 `0XEF` 前缀、`SPI_Flash_ReadJEDEC_ID()` 删厂商校验、`storage_init` 低字节匹配——兼容其他厂家同容量型号(如 0x1A 厂商)
**检测算法**(自 DLD154V4B 移植并四通道化,针对 M4 优化) - **恢复 factory 配置写入**:解除 2026-08-13 止血(根因 8-17 已闭环),换新空片(W25Q128)后不再每次上电进出厂初始化,配置读写恢复正常
- 主循环 tick 50ms → **10ms**,单路 IIRALFA_CAP1=79,τ≈32ms,较 8051 时代快 5 倍) - **板级验证通过**2026-08-19):W25Q128 配置读写正常
- 斜率限幅 MAX_SLOPE_RATE=5%(含低基值 floor=100 防锁死)
- 进入确认 ENTRY_CONFIRM=3(连续 3 次低于阈值才判有车,抗尖峰误触发) ---
- 基线冻结超时 10s + ±2% 稳定性检查:温漂/换线圈可自适应,慢速进车不误吸收
- 稳定期(上电 128 样本)旁路 IIR/限幅,窗口 100 快速收敛;常态基线窗口 500(5s 更新,噪声抑制 22× ## vd960DBN V1.02.03 — 2026-08-19DBN 通信板单侧更新
- 每通道可配置 hold_time(有限存在)与 relay_delay(继电器延时);有限存在超时执行**全通道重启**重建基线
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。
**串口上报(0x7F 协议,UART @192000**
- 0xC0 传感数据主动上报:频率、variation(**3B 有符号**`Origin CAPVD`V1.05)、车状态 **配套版本矩阵**
- 时间量(通过时间/车间距)统一 **50ms 单位**;上电 3 秒抑制主动上报
- 继电器输出次数上报;0x4A 版本查询(CAPVD→实际频率 uint64 精确换算) | 组件 | 版本 |
|------|------|
### vd960DBN(通信 MCUCH32V208 | vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.03** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
**TCP JSON 服务(:5960,协议 V1.01** | DLD960Loop 串口协议(MCU 间 0x7F / 0x9F OTA | V1.05(不变) |
- 鉴权状态机 + 15 条命令;服务异常自动重启(3 条件 + 3 次限额 + 10min 冷却) | DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
- event_report 客户端必答:5s 超时重发同 msg_id/原始 ts ×3 | DLD960 IoT MQTT 协议 | V1.07(不变) |
| DLD960 BLE 协议 | V1.02(不变) |
**IoT MQTT(协议 V1.05**
- 双主题 `dld960/{sn}/srv` + `dld960/{sn}/dev`,消息类型由 `cmd` 区分 ### 🔧 BLE→Loop OTA 升级修复(0x9F 帧透传回 BLE)
- 上电 initialize 上线消息(dev_serial/model/hard_ver/soft_ver - **现象**UART2 RX DMA 改造后,通过 BLE 给地感(Loop MCUOTA 升级失效
- loop_data 三档调度:空闲按配置间隔 / 活动 300ms / **car_state 翻转沿立即上报** - **根因**DMA poll 帧解析 `lup_feed_byte()` 只认 0x7FLoop bootloader 回的 0x9F 响应帧(pre_ok/addr_ok/data ACK)全部被吞;OTA 为**停等协议**0xA7 WITH_BACK 每块必回 ACK),工具等不到 ACK 升级卡死
- event_report 平台必答 + 16 深环形队列,ACK 后出队;不受 report_config.enable 门控 - **修复**:新增 `lup_feed_byte_ota()` 0x9F 帧状态机(SUM 校验无 XOR);`uart2_dma_poll``g_flag_counter_ota.flag` 切换 0x9F/0x7F 解析器;`uart_srv` OTA 分支 0x9F 帧透传回 BLE + 清 flagOTA 溢出阈值放宽 + 收帧 tick 归零
- 设备时钟同步方案B:平台经 report_config 下发 Unix ts,设备无 RTC 也能上报真实时间 - **验证**:gcc 隔离单测 8 断言全过;**板级 BLE OTA 全流程实测通过**2026-08-19
- 稳定性修复:mqtt_publish 缓冲溢出发垃圾包致 broker RST 风暴(现场 P0)、packet_id=0 断连、PINGREQ 保活、分批发送(MSS=576Publish≤500B - **已知约束**:OTA 模式无自动退出机制(`g_flag_counter_ota.flag` 置 1 后需断电重启恢复 0x7F 通信),维持现状
- loop_data 陈旧快照修复:0xC0 帧双消费路径统一摄取,事件与缓存同源
### 📦 脱机日志快照流 + hex 上报(2026-08-18 已含,协议 V1.03/V1.07
**基础设施** - TCP/MQTT `log_*` 命令支持 `stream=snapshot` 传感快照流(复用命令 + stream 字段,不新增命令码)
- BLE 小程序配置 + OTALoop MCU ISP 串口透传升级 - `log_query` records 改 **hex 原始字节**上报(OfflogEvt 32B→64 hex / SnapRec 64B→128 hex2 条快照 406B < 800B 缓冲)
- WCHNET TCP/IPlisten=N / data=N+1)、simple_json 解析器 6 项 bug 修复 - MQTT 查询命令补齐:`ssc_net_query` / `iot_net_query` / `iot_topic_query`(协议命令表全对齐)
### 文档与工具 ---
- 四份协议文档 + 硬件资源文档齐套(见 README 协议矩阵)
- DBNetClientTCP JSON 测试)、DBNMQTToolMQTT 双主题测试 ## vd960DBN V1.02.01 — 2026-08-17DBN 通信板单侧更新
- 双 MCU devlog、验收标准、variation 分析报告、现场事故归档
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议均未变更,可正常配套)。
### 已知约束
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数) **配套版本矩阵**
- CH32V208 栈资源紧张:2KB 任务栈内勿连续 PRINT,勿随意扩大全局 BSS
| 组件 | 版本 |
--- |------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.01** |
## 版本号规则 | vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(MCU 间 0x7F | V1.05(不变) |
- 整机发布 tag`vX.Y.Z`(本仓库 git tag + Gitea Release | DLD960 TCP JSON 协议(:5960 | V1.02(不变) |
- 固件版本:两侧 `cmcng.h``FIRMWARE_VER`,随整机版本同步 | DLD960 IoT MQTT 协议 | V1.06(不变) |
- 协议文档版本:各文档内修订记录表独立演进,发布时在配套矩阵中锁定 | DLD960 BLE 协议 | V1.02(不变) |
### 🔴 P0 稳定性修复:频繁"复位"根因闭环(栈溢出)
- **现象**:恢复快照区功能后设备 100~180ms 一轮"复位"循环(RSTSCKR 无复位标志 = PC 跑飞,非硬件复位)
- **根因**`.bss ≈ 41.7KB` 挤占 RAM → 栈仅 ~1.9KB → `load_cfg/output_cfg` 的 printf 栈峰值触顶 → 覆盖返回地址 → PC 跑飞跳回 0 重启
- **修复**
- RAM 瘦身两轮:ETH_MAX_PACKET_SIZE 1520→768、RECE_BUF_LEN 1400→1024、ARP 表 50→16、MQTT 发送缓冲 1024→800、loop_data payload 1400→800 → **栈 1.9KB → 6.3KB**
- load_cfg 字符串 0 终止保险(防 Flash 配置字符串 %s 越界打印)
- 快照区初始化延后至开机 3s(避开启动早期 SPI 重负载窗口,非根因但减压)
### 🟢 可靠性:UART2 RX DMA 循环接收
- 根治遗留问题"PRINT 关中断(~7.4ms)期间 UART2 丢字节 → checksum fail1-3 分钟一次)"
- DMA1_Ch6 循环模式硬件收字节(不依赖 CPU 中断),主循环轮询消费;512B 环形缓冲 + 溢出保护
- 帧解析状态机移入主循环,消除中断竞争
### 🟢 资源与代码卫生
- .bss 46KB → 41.7KB;中断路径大数组 static 化(栈峰值 -1.8KB
- 调试代码结案清理:移除 BOOT_CNT 诊断打印、_dbg_cnt 心跳打印;保留 fault_diag 基础设施(未来疑难杂症复用)
### 📋 其他
- 固件版本三段式:`FIRMWARE_VER="1.02.01"`MAIN=1 SUB=2 SUBSUB=1
### 已知约束
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)
- BLE 上报版本为 MAIN/SUB 两字节(1.02),SUBSUB 仅字符串上报(MQTT/TCP JSON
- 快照区延后 3s 初始化:开机 3s 内传感数据不落盘(设计接受)
- UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)
### 验证
- ✅ 板级:无异常重启;MQTT159.75.137.141:1883)、BLE 广播、配置加载正常
- [ ] 待回归:UART2 DMA 长时间运行(checksum fail 消失)、TCP JSON 命令交互、快照 BLE 0x28/0x29/0x2A 查询
---
## V1.0.0 — 2026-07-16(首个正式发布)
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960Loop 固件 (AT32F421) | 1.0 |
| vd960DBN 固件 (CH32V208, DLD960GA) | 1.0 |
| DLD960Loop 串口协议(MCU 间 0x7F | V1.05 |
| DLD960 串口通信协议(TTL | V1.01 |
| DLD960 TCP JSON 协议(:5960 | V1.02 |
| DLD960 IoT MQTT 协议 | V1.06 |
> ⚠ 自协议 V1.05 起,Loop 固件与 DBN 固件必须同版本配套刷写,禁止混跑(variation 字段宽度变更导致帧格式不兼容)。
### vd960Loop(检测 MCUAT32F421
**检测算法**(自 DLD154V4B 移植并四通道化,针对 M4 优化)
- 主循环 tick 50ms → **10ms**,单路 IIRALFA_CAP1=79,τ≈32ms,较 8051 时代快 5 倍)
- 斜率限幅 MAX_SLOPE_RATE=5%(含低基值 floor=100 防锁死)
- 进入确认 ENTRY_CONFIRM=3(连续 3 次低于阈值才判有车,抗尖峰误触发)
- 基线冻结超时 10s + ±2% 稳定性检查:温漂/换线圈可自适应,慢速进车不误吸收
- 稳定期(上电 128 样本)旁路 IIR/限幅,窗口 100 快速收敛;常态基线窗口 500(5s 更新,噪声抑制 22×)
- 每通道可配置 hold_time(有限存在)与 relay_delay(继电器延时);有限存在超时执行**全通道重启**重建基线
**串口上报(0x7F 协议,UART @192000**
- 0xC0 传感数据主动上报:频率、variation(**3B 有符号**`Origin CAPVD`V1.05)、车状态
- 时间量(通过时间/车间距)统一 **50ms 单位**;上电 3 秒抑制主动上报
- 继电器输出次数上报;0x4A 版本查询(CAPVD→实际频率 uint64 精确换算)
### vd960DBN(通信 MCUCH32V208
**TCP JSON 服务(:5960,协议 V1.02**
- 密码鉴权 + 18 条命令(新增脱机日志 log_stat/log_query/log_clear
- 鉴权状态机 + 15 条命令;服务异常自动重启(3 条件 + 3 次限额 + 10min 冷却)
- event_report 客户端必答:5s 超时重发同 msg_id/原始 ts ×3
**IoT MQTT(协议 V1.06**
- 双主题 `dld960/{sn}/srv` + `dld960/{sn}/dev`,消息类型由 `cmd` 区分
- 上电 initialize 上线消息(dev_serial/model/hard_ver/soft_ver
- loop_data 三档调度:空闲按配置间隔 / 活动 300ms / **car_state 翻转沿立即上报**
- event_report 平台必答 + 16 深环形队列,ACK 后出队;不受 report_config.enable 门控
- 设备时钟同步方案B:平台经 report_config 下发 Unix ts,设备无 RTC 也能上报真实时间
- 稳定性修复:mqtt_publish 缓冲溢出发垃圾包致 broker RST 风暴(现场 P0)、packet_id=0 断连、PINGREQ 保活、分批发送(MSS=576Publish≤500B
- loop_data 陈旧快照修复:0xC0 帧双消费路径统一摄取,事件与缓存同源
- **脱机事件日志(W25Q32 环形 8064 条,V1.06 新增)**log_stat / log_query / log_clear 三命令;BOOT 复位原因、MQTT 连接/断开、event ACK 超时、线圈事件、时钟锚点共 10 类;掉电不丢
**基础设施**
- BLE 小程序配置 + OTALoop MCU ISP 串口透传升级
- WCHNET TCP/IPlisten=N / data=N+1)、simple_json 解析器 6 项 bug 修复
### 文档与工具
- 四份协议文档 + 硬件资源文档齐套(见 README 协议矩阵)
- DBNetClientTCP JSON 测试)、DBNMQTToolMQTT 双主题测试)
- 双 MCU devlog、验收标准、variation 分析报告、现场事故归档
### 已知约束
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)
- CH32V208 栈资源紧张:2KB 任务栈内勿连续 PRINT,勿随意扩大全局 BSS
---
## 版本号规则
- 整机发布 tag`vX.Y.Z`(本仓库 git tag + Gitea Release
- 固件版本:两侧 `cmcng.h``FIRMWARE_VER`,随整机版本同步
- 协议文档版本:各文档内修订记录表独立演进,发布时在配套矩阵中锁定
+6
View File
@@ -0,0 +1,6 @@
venv/
__pycache__/
*.pyc
*.spec.bak
build/
dist/
+147
View File
@@ -8,6 +8,7 @@ DLD960 IoT MQTT 协议定义
import json import json
import time import time
import struct
from typing import Optional, Any from typing import Optional, Any
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field, asdict
@@ -61,6 +62,11 @@ CMD_LOOP_PARAM_SET = "loop_param_set"
CMD_LOOP_PARAM_QUERY = "loop_param_query" CMD_LOOP_PARAM_QUERY = "loop_param_query"
CMD_REPORT_CONFIG = "report_config" CMD_REPORT_CONFIG = "report_config"
# V1.06: 脱机事件日志
CMD_LOG_STAT = "log_stat"
CMD_LOG_QUERY = "log_query"
CMD_LOG_CLEAR = "log_clear"
# 设备上报 # 设备上报
CMD_LOOP_DATA = "loop_data" CMD_LOOP_DATA = "loop_data"
CMD_EVENT_REPORT = "event_report" CMD_EVENT_REPORT = "event_report"
@@ -71,6 +77,11 @@ CMD_INITIALIZE = "initialize" # V1.03: 设备上电初始化
CONFIG_SET_COMMANDS = {CMD_DEV_SERIAL_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_LOOP_PARAM_SET} CONFIG_SET_COMMANDS = {CMD_DEV_SERIAL_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_LOOP_PARAM_SET}
CONFIG_QUERY_COMMANDS = {CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY} CONFIG_QUERY_COMMANDS = {CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY}
CTRL_COMMANDS = {CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_REPORT_CONFIG} CTRL_COMMANDS = {CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_REPORT_CONFIG}
LOG_COMMANDS = {CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR} # V1.06 脱机日志
# V1.07: log_* 命令日志流 (与 BLE 0x28/0x2A 同语义)
STREAM_EVENT = "event"
STREAM_SNAPSHOT = "snapshot"
# ============================================================ # ============================================================
@@ -104,6 +115,29 @@ FREQ_LEVELS = ["high", "mid_high", "mid_low", "low"]
OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"] OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"]
EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"] EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"]
# 脱机事件日志类型描述 (log_query 响应 records[].type, MQTT V1.06)
LOG_EVENT_TYPE_DESC = {
"boot": "上电/复位",
"iot_connect": "MQTT TCP 连接成功",
"iot_ready": "MQTT 订阅完成→发 initialize",
"iot_disconnect": "MQTT 断连",
"iot_reconn": "重连退避",
"evt_retry": "event ACK 超时重发",
"evt_giveup": "event 重试耗尽挂起",
"coil": "线圈事件",
"time_anchor": "时钟同步锚点",
"log_clear": "日志清除(审计)",
"unknown": "未知类型",
}
# 传感快照 channels[].misc_type 描述 (log_query stream=snapshot, MQTT V1.07)
SNAP_MISC_TYPE_DESC = {
"time": "时间量(50ms)",
"cut_count": "线圈断开次数",
"flow_count": "车流量",
"relay_count": "继电器输出次数",
}
# ============================================================ # ============================================================
# 消息构建 # 消息构建
@@ -214,6 +248,31 @@ def data_report_config(sensor_type: int = 12, enable: bool = True,
} }
def data_log_query(start_seq: int = 1, count: int = 4, stream: str = STREAM_EVENT) -> dict:
"""V1.07 log_query 分页拉取脱机日志
stream=event: 事件流, count 上限 4 (设备侧超限按 4 处理)
stream=snapshot: 快照流, count 上限 2 (hex 原始字节上报); 显式传 stream 字段
"""
d: dict[str, Any] = {
"start_seq": max(1, int(start_seq)),
"count": min(4, max(1, int(count))),
}
if stream == STREAM_SNAPSHOT:
d["stream"] = STREAM_SNAPSHOT
d["count"] = min(2, max(1, int(count)))
return d
def data_log_clear(stream: str = STREAM_EVENT) -> dict:
"""V1.07 log_clear 清除脱机日志 (审计留痕)
stream=snapshot: 快照流清除 (~45ms, 审计写事件流)
event 流省略 data (设备缺省 event, 兼容老固件)
"""
if stream == STREAM_SNAPSHOT:
return {"stream": STREAM_SNAPSHOT}
return {}
# ============================================================ # ============================================================
# 解析设备上报 # 解析设备上报
# ============================================================ # ============================================================
@@ -224,3 +283,91 @@ def parse_topic_dev_serial(topic: str) -> Optional[str]:
if len(parts) >= 2 and parts[0] == "dld960": if len(parts) >= 2 and parts[0] == "dld960":
return parts[1] return parts[1]
return None return None
# ============================================================
# 脱机日志 hex 解析 (V1.07: log_query 响应 records[].hex 原始字节)
# 字段表与《DLD960 BLE 协议》§6.4 (SnapRec) / §7 (OfflogEvt) 一致
# ============================================================
# OfflogEvt 事件类型 (BLE 协议 §7 事件类型表)
OFFLOG_TYPE_NAMES = {
0x01: "boot", 0x10: "iot_connect", 0x11: "iot_ready", 0x12: "iot_disconnect",
0x13: "iot_reconn", 0x30: "evt_retry", 0x31: "evt_giveup", 0x40: "coil",
0x50: "time_anchor", 0x70: "log_clear",
}
_OFFLOG_SUB_NAMES = {1: "car_enter", 2: "car_leave", 3: "loop_cut", 4: "loop_restore"}
_OFFLOG_DISC_REASONS = {1: "断开", 2: "超时", 3: "CONNACK拒绝", 4: "连接超时"}
def parse_offlog_hex(h: str) -> dict:
"""OfflogEvt 32B hex → dict (magic/type/seq/ts_ms/unix_ts/boot_seq/payload)"""
b = bytes.fromhex(h)
if len(b) != 32:
return {"error": f"bad len {len(b)}"}
magic, etype, elen, flags, seq, ts_ms, unix_ts, boot_seq, rsvd, payload = \
struct.unpack('<BBBBIIIHH12s', b)
return {"magic": magic, "type": etype, "len": elen, "flags": flags, "seq": seq,
"ts_ms": ts_ms, "unix_ts": unix_ts, "boot_seq": boot_seq, "payload": payload}
def offlog_payload_desc(etype: int, payload: bytes) -> str:
"""事件 payload 人类可读描述 (BLE 协议 §7 payload 定义, 大端)"""
if etype == 0x01: # boot: 复位原因寄存器
rst = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
bits = [n for n, b in [("LPWR", 31), ("WWDG", 30), ("IWDG", 29),
("SFT", 28), ("POR", 27), ("NRST", 26)] if rst & (1 << b)]
return f"rst=0x{rst:08X} ({'+'.join(bits) if bits else 'none'})"
if etype == 0x12: # iot_disconnect
r = payload[0] if payload else 0
return f"reason={r} ({_OFFLOG_DISC_REASONS.get(r, '?')})"
if etype == 0x13: # iot_reconn
return f"backoff_ms={int.from_bytes(payload[0:4], 'big')}" if len(payload) >= 4 else ""
if etype == 0x30: # evt_retry
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
retry = payload[4] if len(payload) >= 5 else 0
return f"msg_id={mid} retry={retry}"
if etype == 0x31: # evt_giveup
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
return f"msg_id={mid}"
if etype == 0x40: # coil
sub = _OFFLOG_SUB_NAMES.get(payload[0], payload[0]) if payload else "?"
ch = payload[1] if len(payload) >= 2 else 0
val = int.from_bytes(payload[2:6], 'big') if len(payload) >= 6 else 0
return f"sub={sub} ch={ch} value={val}"
if etype in (0x10, 0x11, 0x50, 0x70): # 无 payload
return ""
return payload.hex() if payload else ""
def parse_snap_hex(h: str) -> dict:
"""SnapRec 64B hex → dict (seq/boot_seq/ts_ms/coil_count/channels[])"""
b = bytes.fromhex(h)
if len(b) != 64:
return {"error": f"bad len {len(b)}"}
magic, slen, flags, rsvd, seq, ts_ms, boot_seq, rsvd2, coils = \
struct.unpack('<BBBBIIHH48s', b)
coil_count = min(slen // 12, 4)
channels = []
for c in range(coil_count):
p = coils[c * 12:(c + 1) * 12]
cfg, cond = p[0], p[1]
freq = p[2] | (p[3] << 8) | (p[4] << 16)
var = p[5] | (p[6] << 8) | (p[7] << 16)
if var & 0x800000:
var -= 0x1000000
misc = int.from_bytes(p[8:12], 'little')
channels.append({
"ch": c + 1,
"freq_level": ("high", "mid_high", "mid_low", "low")[(cfg >> 6) & 3],
"direction": (cfg >> 5) & 1,
"freq_type": (cfg >> 4) & 1,
"sensitivity": cfg & 0x0F,
"condition": (cond >> 4) & 0x0F,
"loop_ok": not ((cond >> 3) & 1),
"has_car": bool((cond >> 2) & 1),
"misc_type": ("time", "cut_count", "flow_count", "relay_count")[cond & 3],
"freq": freq, "variation": var, "misc": misc,
})
return {"seq": seq, "boot_seq": boot_seq, "ts_ms": ts_ms,
"coil_count": coil_count, "channels": channels}
+241 -141
View File
@@ -1,141 +1,241 @@
# DBNMQTTool 开发日志 # DBNMQTTool 开发日志
> DLD960 IoT MQTT 桌面工具 | Python 3.11+ | PySide6 | paho-mqtt v2 | 跨平台 (Windows/Linux/macOS) > DLD960 IoT MQTT 桌面工具 | Python 3.11+ | PySide6 | paho-mqtt v2 | 跨平台 (Windows/Linux/macOS)
> >
> 定位: DLD960 车检器 MQTT 调试工具 — 设备管理、实时数据监控、模拟上报、主题订阅/发布 > 定位: DLD960 车检器 MQTT 调试工具 — 设备管理、实时数据监控、模拟上报、主题订阅/发布
--- ---
## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465` ## 2026-08-18 — log_query 响应改 hex 原始字节解析展示(协议 V1.07 修订)
### 1. 模拟上报 Tab ### 背景
- **loop_data / event_report / heartbeat** 三类上报携带独立可编辑 JSON 编辑区 MQTT 快照流实测 `MQTTSerialize_publish failed`——JSON 化快照记录超 800B 发送缓冲。协议修订为**原始字节 hex 上报**(与 BLE 通道同语义),工具同步改为 hex 解析。
- **范例填充**:首次切换类型时自动填入协议定义的标准载荷
- 单次发送 + **周期上报**QSpinBox 可调间隔) ### 变更
- 周期开关由 `QPushButton` toggle 控制
- **protocol.py**:新增 `parse_offlog_hex`OfflogEvt 32B`<BBBBIIIHH12s`)、`parse_snap_hex`SnapRec 64B`<BBBBIIHH48s`)、`offlog_payload_desc`boot 复位位/coil/evt_retry/evt_giveup/iot_disconnect/reconn)、`OFFLOG_TYPE_NAMES`——字段表与 BLE 协议 §6.4/§7 一致
### 2. 协议Topic Tab - `data_log_query` snapshot count 恢复 **2**hex 体积可控)
- **main.py**`_apply_log_query_data` 解析 `records[].hex`hex 长度 128=快照 / 64=事件);事件流展示 type 描述 + payload 可读化,快照流逐通道字段
- 预加载双主题:`dld960/{sn}/srv`(下发)、`dld960/{sn}/dev`(上报)
- QListWidget 展示,双击自动填充到发布区 ### 验证
- 发布区可编辑 JSON 载荷,一键发布
- 主题列表受 SN 输入框驱动,实时更新 - 构造事件/快照二进制→hex→解析回断言全过(POR/SFT 复位位、coil payload、evt_retry、快照 4ch、负 variation 符号扩展)
- 2 条快照 hex 响应 payload = 406B < 800B
### 3. 自定义Topic Tab - offscreen UI:事件流(上电/复位、线圈事件、event 重发)+ 快照流(4ch/2ch)展示 + 翻页正常
- 自由输入 topic + JSON 载荷,QoS (0/1/2) 可调 ---
- **订阅 / 取消订阅**,接收消息实时展示在 QPlainTextEdit
- `#` / `+` 通配符支持,`paho.mqtt.topic_matches_sub` 路由匹配 ## 2026-08-18 — 脱机日志翻页:上一页/下一页 + 自动填充起始序号
- 自定义订阅的消息自动分发到接收区,附带 topic 前缀
### 变更
### 4. 新增依赖
- 脱机日志区按钮行新增 **◀ 上一页 / 下一页 ▶**(初始禁用,拉取成功后启用)
- `paho.mqtt.client` 导入新增 `topic_matches_sub` - **下一页**:起始序号 = 当前页最后一条 `seq` + 1(锚点法,不依赖请求条数,快照流 2 条/事件流 4 条均精确);空记录兜底按 `start + count` 步进
- UI 控件: QComboBox, QSpinBox, QPlainTextEdit - **上一页**:起始序号 = `max(1, 当前起始 - 条数)`,边界保护
- 翻页后自动填充起始序号 SpinBox 并触发拉取;页锚点在 `_apply_log_query_data` 更新(`_log_page_last_seq`
---
### 验证
## 2026-07-06 — paho-mqtt v2 回调修复 `213c0dd`
- offscreen 实测 6 场景全过:初始禁用 / 锚点=103 / 下一页→104 / 上一页→100 / 边界→1 / 空记录步进→504 / 快照流 2 条→12
### 问题
断开连接时报错: ---
```
TypeError: MqttClient._on_disconnect() missing 1 required positional argument: 'reason_code' ## 2026-08-18 — 命令集对齐:禁用固件未实现的 MQTT 命令按钮
```
### 背景
### 根因
paho-mqtt v2 默认 `callback_api_version=VERSION2``on_disconnect` 回调签名为 5 参数 `(client, userdata, flags, reason_code, properties)`,而 v1 为 3 参数 `(client, userdata, rc)` 实测发现 MQTT 通道 `ssc_net_query` / `iot_net_query` 返回 `code=4 unsupported command`——固件 MQTT 分发只实现 6 条命令,工具按协议文档命令表做了全按钮
### 修复 ### 变更
- `_on_disconnect` 改用 `*args` 兼容 v1/v2
- `Client` 构造显式指定 `callback_api_version=CallbackAPIVersion.VERSION2` - **固件同步补齐**vd960DBN V4.2):`ssc_net_query` / `iot_net_query` / `iot_topic_query` 三条查询命令已实现,工具对应查询按钮保持可用
- `_on_connect` 保持 v2 签名不变 - **工具禁用**固件仍未实现的命令按钮:`ssc_net_set` / `iot_net_set` / `iot_topic_set` / `pwd_set` / `factory_reset` / `device_reset``setEnabled(False)` + tooltip 引导走 TCP JSON/BLE 通道)
- 设备刷新列表移除 `CMD_LOOP_PARAM_QUERY`(固件未实现,刷新时不再报 error)
---
### 验证
## 2026-07-06 — tkinter → PySide6 迁移 `e1bf3dc`
- offscreen 实例化:6 个按钮禁用 + tooltip 正确,查询类/验证密码/report_config/log_* 可用
### 决策
放弃 tkinter,改用 PySide6 (Qt for Python),理由: ---
- 跨平台原生外观 (Fusion 风格)
- Qt 信号/槽机制替代 tkinter 回调,更易维护 ## 2026-08-18 — 脱机日志支持快照流 (stream=snapshot, 协议 V1.07)
- 组件丰富 (QGroupBox, QSplitter, QTabWidget 等)
### 变更
### 改动
- `main.py` 全量重写为 PySide6 主窗口 - **protocol.py**:新增 `STREAM_EVENT` / `STREAM_SNAPSHOT` 常量;`data_log_query``stream` 参数(快照流显式传 `stream` 字段 + count≤2);新增 `data_log_clear(stream)`(事件流省略 data 兼容老固件);新增 `SNAP_MISC_TYPE_DESC`time/cut_count/flow_count/relay_count 中文描述)
- 布局: QGroupBox 分组 → QSplitter 分栏 → QTabWidget 功能分区 - **main.py**:脱机日志区加"日志流"选择器(事件日志/传感快照);统计/拉取/清除三按钮按当前流发请求(log_clear 确认框提示阻塞时长 2.8s/45ms);`log_stat`/`log_query` 响应展示区分流——快照记录解析 `channels[]` 逐字段(freq_level/direction/freq_type/sens/cond/loop/car/freq/variation/misc
- `requirements.txt` 新增 `PySide6>=6.6.0`
- tkinter → Qt 映射: `StringVar`→信号/槽, `Listbox``QListWidget`, `Text``QTextEdit` ### 验证
--- - protocol 数据构建器断言 6 例全过(事件流不带 stream / 快照流 count≤2 / log_clear 流参数)
- `QT_QPA_PLATFORM=offscreen` 实例化 MainWindow 实测:`log_stat(snapshot)` capacity=48064 展示、`log_query(snapshot)` 2 条快照记录逐字段展示(含负 variation、misc_type 中文)、事件流展示回归无损、空记录边界
## 2026-07-06 — 项目初始化 `a2cfe46` - 依赖:venv + PySide6 + paho-mqtt`venv/` 已加 .gitignore
### 功能 (初始版本, tkinter) ### 配套
- MQTT Broker 连接管理
- 设备自动发现 (`dld960/+/dev` 通配符订阅) - 固件 vd960DBN `log_*` stream=snapshot 分支(同日实现,devlog V4.1
- 设备信息查询 + 主题配置 - 协议文档:TCP JSON V1.03 / IoT MQTT V1.07
- 实时线圈数据 / 事件上报 / 心跳 监控
- 控制命令: 密码验证/设置、出厂初始化、设备复位 ---
### 项目结构 ## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465`
```
DBNMQTTool/ ### 1. 模拟上报 Tab
├── main.py # 主窗口 (tkinter → 后迁移至 PySide6)
├── dbn_mqtt_tool/ - **loop_data / event_report / heartbeat** 三类上报携带独立可编辑 JSON 编辑区
│ ├── protocol.py # DLD960 IoT MQTT 协议定义 (主题/命令/载荷模板) - **范例填充**:首次切换类型时自动填入协议定义的标准载荷
│ ├── mqtt_client.py # MQTT 客户端封装 (paho-mqtt v2) - 单次发送 + **周期上报**QSpinBox 可调间隔)
│ └── device_manager.py # 设备发现与状态管理 - 周期开关由 `QPushButton` toggle 控制
└── requirements.txt # paho-mqtt>=2.0.0 + PySide6>=6.6.0
``` ### 2. 协议Topic Tab
### 协议依据 - 预加载双主题:`dld960/{sn}/srv`(下发)、`dld960/{sn}/dev`(上报)
`DLD960_IoT_MQTT协议.md` V1.00 - QListWidget 展示,双击自动填充到发布区
- 发布区可编辑 JSON 载荷,一键发布
--- - 主题列表受 SN 输入框驱动,实时更新
## 当前状态 ### 3. 自定义Topic Tab
| 组件 | 状态 | - 自由输入 topic + JSON 载荷,QoS (0/1/2) 可调
|------|------| - **订阅 / 取消订阅**,接收消息实时展示在 QPlainTextEdit
| MQTT 连接/断开 | ✅ 已实现,v1/v2 兼容 | - `#` / `+` 通配符支持,`paho.mqtt.topic_matches_sub` 路由匹配
| 设备发现 | ✅ 通配符订阅 | - 自定义订阅的消息自动分发到接收区,附带 topic 前缀
| 实时数据监控 | ✅ loop_data/event_report/heartbeat |
| 控制命令 | ✅ 15 条命令 | ### 4. 新增依赖
| 模拟上报 | ✅ c19e465 |
| 协议Topic订阅/发布 | ✅ c19e465 | - `paho.mqtt.client` 导入新增 `topic_matches_sub`
| 自定义Topic订阅/发布 | ✅ c19e465 | - UI 控件: QComboBox, QSpinBox, QPlainTextEdit
| 固件联调 | ✅ vd960DBN IoT 模式稳定上报 |
---
---
## 2026-07-06 — paho-mqtt v2 回调修复 `213c0dd`
## 2026-07-07 — MQTT V1.01 适配 + 稳定性修复
### 问题
### 1. MQTT 协议 V1.01 适配 断开连接时报错:
```
- Topic 从多主题(`/loop_data`, `/event_report`, `/heartbeat`, `/cmd`…)压缩为双主题 `dld960/{sn}/dev`(上报)、`dld960/{sn}/srv`(下发) TypeError: MqttClient._on_disconnect() missing 1 required positional argument: 'reason_code'
- `manage_mqtt_recv_message()` 实现 V1.01 协议命令分发,支持 `cmd`/`Method` 双格式 ```
- MQTT PUBLISH 接收修复:topic 过滤 + 通配符路由修正
### 根因
### 2. MqttClient 重构:tkinter 回调 → QObject 信号/槽 paho-mqtt v2 默认 `callback_api_version=VERSION2``on_disconnect` 回调签名为 5 参数 `(client, userdata, flags, reason_code, properties)`,而 v1 为 3 参数 `(client, userdata, rc)`
**问题**: `paho.mqtt` C 层回调直接操作 Qt UI 控件 → `RuntimeError: wrapped C/C++ object has been deleted` 崩溃。 ### 修复
- `_on_disconnect` 改用 `*args` 兼容 v1/v2
**修复**: - `Client` 构造显式指定 `callback_api_version=CallbackAPIVersion.VERSION2`
- `MqttClient` 继承 `QObject`,使用 Qt 信号/槽机制 - `_on_connect` 保持 v2 签名不变
- `on_message``message_received` 信号 → 主线程 slot 安全更新 UI
- 所有 publish 路径增加通用异常捕获,防止未捕获异常导致静默崩溃 ---
- 回退 Qt 信号排队方案,改用 `QMetaObject.invokeMethod` + 日志追踪定位阻塞点
## 2026-07-06 — tkinter → PySide6 迁移 `e1bf3dc`
### 3. DeviceManager 死锁修复
### 决策
`threading.Lock` 在单线程 Qt 事件循环中自锁 → `RLock``add_device()``remove_device()` 同线程互调时 `Lock` 阻塞自身。 放弃 tkinter,改用 PySide6 (Qt for Python),理由:
- 跨平台原生外观 (Fusion 风格)
### 4. UI/UX 改进 - Qt 信号/槽机制替代 tkinter 回调,更易维护
- 组件丰富 (QGroupBox, QSplitter, QTabWidget 等)
- **日志增强**: 收发打印 topic + payload 详情,调试信息更直观
- **查询响应自动回填**: 设备信息查询后自动填充 SN 等输入框 ### 改动
- **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关 - `main.py` 全量重写为 PySide6 主窗口
- `right` 局部变量 → `self._notebook`,修复 tab 引用丢失 - 布局: QGroupBox 分组 → QSplitter 分栏 → QTabWidget 功能分区
- `requirements.txt` 新增 `PySide6>=6.6.0`
- tkinter → Qt 映射: `StringVar`→信号/槽, `Listbox``QListWidget`, `Text``QTextEdit`
---
## 2026-07-06 — 项目初始化 `a2cfe46`
### 功能 (初始版本, tkinter)
- MQTT Broker 连接管理
- 设备自动发现 (`dld960/+/dev` 通配符订阅)
- 设备信息查询 + 主题配置
- 实时线圈数据 / 事件上报 / 心跳 监控
- 控制命令: 密码验证/设置、出厂初始化、设备复位
### 项目结构
```
DBNMQTTool/
├── main.py # 主窗口 (tkinter → 后迁移至 PySide6)
├── dbn_mqtt_tool/
│ ├── protocol.py # DLD960 IoT MQTT 协议定义 (主题/命令/载荷模板)
│ ├── mqtt_client.py # MQTT 客户端封装 (paho-mqtt v2)
│ └── device_manager.py # 设备发现与状态管理
└── requirements.txt # paho-mqtt>=2.0.0 + PySide6>=6.6.0
```
### 协议依据
`DLD960_IoT_MQTT协议.md` V1.00
---
## 当前状态
| 组件 | 状态 |
|------|------|
| MQTT 连接/断开 | ✅ 已实现,v1/v2 兼容 |
| 设备发现 | ✅ 通配符订阅 |
| 实时数据监控 | ✅ loop_data/event_report/heartbeat |
| 控制命令 | ✅ 15 条命令 |
| 模拟上报 | ✅ c19e465 |
| 协议Topic订阅/发布 | ✅ c19e465 |
| 自定义Topic订阅/发布 | ✅ c19e465 |
| 固件联调 | ✅ vd960DBN IoT 模式稳定上报 |
---
## 2026-07-07 — MQTT V1.01 适配 + 稳定性修复
### 1. MQTT 协议 V1.01 适配
- Topic 从多主题(`/loop_data`, `/event_report`, `/heartbeat`, `/cmd`…)压缩为双主题 `dld960/{sn}/dev`(上报)、`dld960/{sn}/srv`(下发)
- `manage_mqtt_recv_message()` 实现 V1.01 协议命令分发,支持 `cmd`/`Method` 双格式
- MQTT PUBLISH 接收修复:topic 过滤 + 通配符路由修正
### 2. MqttClient 重构:tkinter 回调 → QObject 信号/槽
**问题**: `paho.mqtt` C 层回调直接操作 Qt UI 控件 → `RuntimeError: wrapped C/C++ object has been deleted` 崩溃。
**修复**:
- `MqttClient` 继承 `QObject`,使用 Qt 信号/槽机制
- `on_message``message_received` 信号 → 主线程 slot 安全更新 UI
- 所有 publish 路径增加通用异常捕获,防止未捕获异常导致静默崩溃
- 回退 Qt 信号排队方案,改用 `QMetaObject.invokeMethod` + 日志追踪定位阻塞点
### 3. DeviceManager 死锁修复
`threading.Lock` 在单线程 Qt 事件循环中自锁 → `RLock``add_device()``remove_device()` 同线程互调时 `Lock` 阻塞自身。
### 4. UI/UX 改进
- **日志增强**: 收发打印 topic + payload 详情,调试信息更直观
- **查询响应自动回填**: 设备信息查询后自动填充 SN 等输入框
- **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关
- `right` 局部变量 → `self._notebook`,修复 tab 引用丢失
---
## 2026-08-05 — 脱机事件日志命令支持 (MQTT V1.06)
配合 vd960DBN 固件 P1.3commit 1a01316+ 协议 V1.06,工具新增脱机事件日志面板(参数配置 Tab 底部):
### 新增命令
| 命令 | 按钮 | 说明 |
|------|------|------|
| `log_stat` | 统计 | 日志统计/分页定位: enabled/boot_seq/count/capacity/seq_first/seq_last |
| `log_query` | 拉取日志 | 按全局序号分页拉取, 起始序号 + 条数(1~4) 可调 |
| `log_clear` | 清除日志 | 二次确认弹窗, 审计留痕不可撤销 |
### 实现要点
- `protocol.py`: 新增 `CMD_LOG_*` 常量、`LOG_COMMANDS` 集合、`data_log_query()` 构建器(start_seq≥1 / count 1~4 边界钳制)、`LOG_EVENT_TYPE_DESC` 事件类型中文描述(10 类 + unknown
- `main.py`: 参数配置 Tab 新增 g6 组(起始序号/条数 spinbox + 三按钮 + 只读显示区 QPlainTextEdit
- `_on_message` code==0 分支新增 `log_stat`/`log_query`/`log_clear` 响应处理
- 显示优化: log_query 记录带事件类型中文描述; unix_ts 已同步显示真实时间, 未同步显示 `boot+ts_ms(未同步)`
- 清除日志前 QMessageBox 二次确认(防误触, 与出厂初始化/设备复位同模式)
### 验证
- `py_compile` 四个模块通过
- protocol 逻辑单测: data_log_query 边界钳制 (0→1, 99→4, 0→1)、命令枚举一致性、build_request 带 data
+201 -3
View File
@@ -31,9 +31,12 @@ from dbn_mqtt_tool.protocol import (
CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET,
CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET,
CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE, CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE,
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR,
STREAM_EVENT, STREAM_SNAPSHOT,
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC, SNAP_MISC_TYPE_DESC,
OFFLOG_TYPE_NAMES, parse_offlog_hex, offlog_payload_desc, parse_snap_hex,
data_ssc_net_set, data_iot_net_set, data_iot_topic_set, data_ssc_net_set, data_iot_net_set, data_iot_topic_set,
data_pwd_verify, data_pwd_set, data_report_config, data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
build_request, next_msg_id, build_request, next_msg_id,
topic_down, topic_up, topic_down, topic_up,
) )
@@ -194,6 +197,8 @@ class MainWindow(QMainWindow):
b1.clicked.connect(self._query_ssc_net) b1.clicked.connect(self._query_ssc_net)
b2 = QPushButton("设置") b2 = QPushButton("设置")
b2.clicked.connect(self._set_ssc_net) b2.clicked.connect(self._set_ssc_net)
b2.setEnabled(False)
b2.setToolTip("MQTT 固件暂未实现 ssc_net_set,请用 TCP JSON 或蓝牙通道配置")
btn_row.addWidget(b1) btn_row.addWidget(b1)
btn_row.addWidget(b2) btn_row.addWidget(b2)
btn_row.addStretch() btn_row.addStretch()
@@ -222,6 +227,8 @@ class MainWindow(QMainWindow):
b3.clicked.connect(self._query_iot_net) b3.clicked.connect(self._query_iot_net)
b4 = QPushButton("设置") b4 = QPushButton("设置")
b4.clicked.connect(self._set_iot_net) b4.clicked.connect(self._set_iot_net)
b4.setEnabled(False)
b4.setToolTip("MQTT 固件暂未实现 iot_net_set,请用 TCP JSON 或蓝牙通道配置")
btn_row2.addWidget(b3) btn_row2.addWidget(b3)
btn_row2.addWidget(b4) btn_row2.addWidget(b4)
btn_row2.addStretch() btn_row2.addStretch()
@@ -249,6 +256,8 @@ class MainWindow(QMainWindow):
b5.clicked.connect(self._query_iot_topic) b5.clicked.connect(self._query_iot_topic)
b6 = QPushButton("设置") b6 = QPushButton("设置")
b6.clicked.connect(self._set_iot_topic) b6.clicked.connect(self._set_iot_topic)
b6.setEnabled(False)
b6.setToolTip("MQTT 固件暂未实现 iot_topic_set,请用 TCP JSON 或蓝牙通道配置")
btn_row3.addWidget(b5) btn_row3.addWidget(b5)
btn_row3.addWidget(b6) btn_row3.addWidget(b6)
btn_row3.addStretch() btn_row3.addStretch()
@@ -276,6 +285,9 @@ class MainWindow(QMainWindow):
]: ]:
btn = QPushButton(label) btn = QPushButton(label)
btn.clicked.connect(slot) btn.clicked.connect(slot)
if label != "验证密码":
btn.setEnabled(False)
btn.setToolTip(f"MQTT 固件暂未实现 {label},请用 TCP JSON 或蓝牙通道")
btn_row4.addWidget(btn) btn_row4.addWidget(btn)
btn_row4.addStretch() btn_row4.addStretch()
g4_layout.addLayout(btn_row4, 1, 0, 1, 4) g4_layout.addLayout(btn_row4, 1, 0, 1, 4)
@@ -326,6 +338,61 @@ class MainWindow(QMainWindow):
g5_layout.addLayout(btn_row5, 2, 0, 1, 6) g5_layout.addLayout(btn_row5, 2, 0, 1, 6)
layout.addWidget(g5) layout.addWidget(g5)
# -- 脱机日志 (V1.06 事件流 / V1.07 快照流) --
g6 = QGroupBox("脱机日志 (log_stat / log_query / log_clear)")
g6_layout = QGridLayout(g6)
g6_layout.addWidget(QLabel("日志流:"), 0, 0)
self._combo_log_stream = QComboBox()
self._combo_log_stream.addItem("事件日志 (event)", STREAM_EVENT)
self._combo_log_stream.addItem("传感快照 (snapshot)", STREAM_SNAPSHOT)
self._combo_log_stream.setCurrentIndex(0)
g6_layout.addWidget(self._combo_log_stream, 0, 1)
g6_layout.addWidget(QLabel("起始序号:"), 0, 2)
self._edit_log_start_seq = QSpinBox()
self._edit_log_start_seq.setRange(1, 2**31 - 1)
self._edit_log_start_seq.setValue(1)
g6_layout.addWidget(self._edit_log_start_seq, 0, 3)
g6_layout.addWidget(QLabel("条数:"), 0, 4)
self._edit_log_count = QSpinBox()
self._edit_log_count.setRange(1, 4)
self._edit_log_count.setValue(4)
g6_layout.addWidget(self._edit_log_count, 0, 5)
btn_row6 = QHBoxLayout()
b9 = QPushButton("统计")
b9.clicked.connect(self._query_log_stat)
self._btn_log_prev = QPushButton("◀ 上一页")
self._btn_log_prev.clicked.connect(self._page_log_prev)
self._btn_log_prev.setEnabled(False)
self._btn_log_prev.setToolTip("起始序号 = 当前页第一条 - 条数(下限 1)")
b10 = QPushButton("拉取日志")
b10.clicked.connect(self._query_log)
self._btn_log_next = QPushButton("下一页 ▶")
self._btn_log_next.clicked.connect(self._page_log_next)
self._btn_log_next.setEnabled(False)
self._btn_log_next.setToolTip("起始序号 = 当前页最后一条 seq + 1")
b11 = QPushButton("清除日志")
b11.clicked.connect(self._clear_log)
btn_row6.addWidget(b9)
btn_row6.addWidget(self._btn_log_prev)
btn_row6.addWidget(b10)
btn_row6.addWidget(self._btn_log_next)
btn_row6.addWidget(b11)
btn_row6.addStretch()
g6_layout.addLayout(btn_row6, 1, 0, 1, 4)
self._log_rec_text = QPlainTextEdit()
self._log_rec_text.setReadOnly(True)
# 脱机日志翻页状态 (翻页锚点)
self._log_page_last_seq = 0 # 当前页最后一条 seq; 0 = 无有效页
self._log_rec_text.setMaximumHeight(150)
self._log_rec_text.setFont(QFont("Consolas", 9))
g6_layout.addWidget(self._log_rec_text, 2, 0, 1, 4)
layout.addWidget(g6)
layout.addStretch() layout.addStretch()
return w return w
@@ -873,6 +940,12 @@ class MainWindow(QMainWindow):
self._show_json({cmd: data}) self._show_json({cmd: data})
elif cmd == CMD_REPORT_CONFIG: elif cmd == CMD_REPORT_CONFIG:
self._apply_report_config_data(data) self._apply_report_config_data(data)
elif cmd == CMD_LOG_STAT:
self._apply_log_stat_data(data)
elif cmd == CMD_LOG_QUERY:
self._apply_log_query_data(data)
elif cmd == CMD_LOG_CLEAR:
self._log(f"脱机日志已清除 (审计留痕, 需重新统计确认)")
else: else:
self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"}) self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
@@ -967,7 +1040,7 @@ class MainWindow(QMainWindow):
if not sn: if not sn:
return return
for cmd in [CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, for cmd in [CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY,
CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY]: CMD_IOT_TOPIC_QUERY]:
try: try:
self._mqtt.send_command(sn, cmd) self._mqtt.send_command(sn, cmd)
except Exception: except Exception:
@@ -1078,6 +1151,131 @@ class MainWindow(QMainWindow):
timeout=self._edit_report_timeout.value(), timeout=self._edit_report_timeout.value(),
)) ))
# ---- 脱机日志 (V1.06 事件流 / V1.07 快照流) ----
def _selected_log_stream(self) -> str:
"""当前 UI 选择的日志流 (event/snapshot)"""
return self._combo_log_stream.currentData() or STREAM_EVENT
def _query_log_stat(self):
"""log_stat: 日志统计/分页定位 (按当前流)"""
stream = self._selected_log_stream()
data = {"stream": STREAM_SNAPSHOT} if stream == STREAM_SNAPSHOT else None
self._send_cmd(CMD_LOG_STAT, data)
def _query_log(self):
"""log_query: 按全局序号分页拉取 (事件流 count≤4 / 快照流 count≤2)"""
self._send_cmd(CMD_LOG_QUERY, data_log_query(
start_seq=self._edit_log_start_seq.value(),
count=self._edit_log_count.value(),
stream=self._selected_log_stream(),
))
def _page_log_prev(self):
"""上一页: 起始序号 = max(1, 当前起始 - 条数), 自动拉取"""
start = self._edit_log_start_seq.value()
count = self._edit_log_count.value()
new_start = max(1, start - count)
self._edit_log_start_seq.setValue(new_start)
self._log(f"翻页: 上一页 → 起始序号 {new_start}")
self._query_log()
def _page_log_next(self):
"""下一页: 起始序号 = 当前页最后一条 seq + 1, 自动拉取"""
if self._log_page_last_seq > 0:
new_start = self._log_page_last_seq + 1
else:
# 无有效页锚点 (拉取返回空), 按页大小步进
new_start = self._edit_log_start_seq.value() + self._edit_log_count.value()
self._edit_log_start_seq.setValue(new_start)
self._log(f"翻页: 下一页 → 起始序号 {new_start}")
self._query_log()
def _clear_log(self):
"""log_clear: 清除脱机日志 (按当前流, 审计留痕, 设备侧阻塞)"""
stream = self._selected_log_stream()
target = "传感快照" if stream == STREAM_SNAPSHOT else "脱机事件日志"
block = "~45ms (逻辑清除+当前扇区)" if stream == STREAM_SNAPSHOT else "~2.8s (63 扇区擦除)"
r = QMessageBox.question(self, "确认",
f"确定要清除设备{target}吗?\n"
f"清除动作本身会写入审计记录,不可撤销!\n"
f"设备侧阻塞约 {block}",
QMessageBox.Yes | QMessageBox.No)
if r == QMessageBox.Yes:
self._send_cmd(CMD_LOG_CLEAR, data_log_clear(stream))
def _apply_log_stat_data(self, data: dict):
stream = data.get("stream", "event")
stream_name = "传感快照" if stream == "snapshot" else "事件日志"
lines = [
f"日志统计[{stream_name}]: enabled={data.get('enabled', False)} boot_seq={data.get('boot_seq', 0)}",
f"记录数: {data.get('count', 0)} / {data.get('capacity', 0)}",
f"全局序号范围: {data.get('seq_first', 0)} ~ {data.get('seq_last', 0)}",
f"提示: 拉取日志用 log_query, 起始序号填 seq_first"
+ (" (快照流 count 上限 2)" if stream == "snapshot" else ""),
]
self._log_rec_text.setPlainText("\n".join(lines))
self._log(f"log_stat[{stream_name}]: count={data.get('count', 0)} seq={data.get('seq_first', 0)}~{data.get('seq_last', 0)}")
def _apply_log_query_data(self, data: dict):
records = data.get("records", [])
start_seq = data.get("start_seq", 0)
# V1.07: 记录为 {"seq":N,"hex":"..."} 原始字节; hex 长度 128=快照(64B) / 64=事件(32B)
is_snap = bool(records) and len(records[0].get("hex", "")) == 128
stream_name = "传感快照" if is_snap else "事件日志"
lines = [f"log_query[{stream_name}] start_seq={start_seq} → 返回 {len(records)} 条:"]
for rec in records:
hex_str = rec.get("hex", "")
if is_snap:
s = parse_snap_hex(hex_str)
if "error" in s:
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {s['error']}")
continue
lines.append(
f" seq={s['seq']} boot={s['boot_seq']} "
f"boot+{s['ts_ms']}ms ({s['coil_count']}ch)"
)
for ch in s["channels"]:
mt = ch["misc_type"]
misc_desc = SNAP_MISC_TYPE_DESC.get(mt, mt)
lines.append(
f" ch{ch['ch']}: {ch['freq_level']} "
f"dir={ch['direction']} ftype={ch['freq_type']} "
f"sens={ch['sensitivity']} cond={ch['condition']} "
f"loop={'OK' if ch['loop_ok'] else ''} "
f"car={'' if ch['has_car'] else ''} "
f"freq={ch['freq']}Hz Δ={ch['variation']} "
f"[{misc_desc}] misc={ch['misc']}"
)
else:
e = parse_offlog_hex(hex_str)
if "error" in e:
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {e['error']}")
continue
tname = OFFLOG_TYPE_NAMES.get(e["type"], f"0x{e['type']:02x}")
desc = LOG_EVENT_TYPE_DESC.get(tname, tname)
if e["unix_ts"]:
ts_str = datetime.fromtimestamp(e["unix_ts"]).strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = f"boot+{e['ts_ms']}ms (未同步)"
pdesc = offlog_payload_desc(e["type"], e["payload"])
lines.append(
f" seq={e['seq']} boot={e['boot_seq']} "
f"{ts_str} [{desc}] {pdesc}"
)
if not records:
lines.append(" (无记录 / 起始序号越界, 可先用 log_stat 确认范围)")
self._log_rec_text.setPlainText("\n".join(lines))
self._log(f"log_query[{stream_name}]: start_seq={start_seq} got {len(records)}")
# 更新翻页状态: 锚点 = 当前页最后一条 seq; 拉取成功即启用翻页按钮
if records:
self._log_page_last_seq = records[-1].get("seq", 0)
else:
self._log_page_last_seq = 0
self._btn_log_prev.setEnabled(True)
self._btn_log_next.setEnabled(True)
# ================================================================ # ================================================================
# UI 更新 # UI 更新
# ================================================================ # ================================================================
+94 -86
View File
@@ -1,86 +1,94 @@
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构) # vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
> 当前发布版本**V1.0.0**2026-07-16)· 变更历史见 [CHANGELOG.md](CHANGELOG.md) > 当前发布:**vd960DBN V1.02.04**2026-08-19DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md)
## 产品概述 ## 产品概述
DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、RS485、蓝牙(小程序配置 + OTA),面向停车场出入口控制、流量计数、会车控制等场景。 DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、蓝牙(小程序配置 + OTA)、4G 扩展(RFU,面向停车场出入口控制、流量计数、会车控制等场景。
产品型号:**DLD960GA** 产品型号:**DLD960GA**
## 硬件架构(双 MCU ## 硬件架构(双 MCU
``` ```
┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
线圈1~4 ──────────► AT32F421 (vd960Loop) │ 线圈1~4 ──────────► AT32F421 (vd960Loop) │
(TMR3 输入捕获) │ · 四路线圈驱动/频率采样 │ (TMR3 输入捕获) │ · 四路线圈驱动/频率采样 │
继电器1~4 ◄───────│ · 车辆检测算法(IIR/斜率限幅/基线跟踪) │ 继电器1~4 ◄───────│ · 车辆检测算法(IIR/斜率限幅/基线跟踪) │
└───────────────┬─────────────────────────┘ └───────────────┬─────────────────────────┘
│ UART 0x7F 协议 @192000 │ UART 0x7F 协议 @192000
┌───────────────▼─────────────────────────┐ ┌───────────────▼─────────────────────────┐
蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │ 蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │
网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │ 网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │
RS485 ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │ 4G(RFU) ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │
│ · 参数/日志外部存储 │ │ · 参数/日志外部存储 │
└─────────────────────────────────────────┘ └─────────────────────────────────────────┘
``` ```
| 芯片 | 型号 | 主频 | Flash | SRAM | 职责 | | 芯片 | 型号 | 主频 | Flash | SRAM | 职责 |
|------|------|------|-------|------|------| |------|------|------|-------|------|------|
| 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 | | 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 |
| 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz | 128KB | 64KB | 蓝牙、网口、RS485、外部存储 | | 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz | 128KB | 64KB | 蓝牙、网口、双路 TTL 串口(内部互联 + 4G 扩展)、外部存储 |
- 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。 - 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。
- 支持 OTACH32V208 经蓝牙小程序 OTAAT32F421 经 CH32V208 串口透传 ISP 升级。 - 支持 OTACH32V208 经蓝牙小程序 OTAAT32F421 经 CH32V208 串口透传 ISP 升级。
- 地感配置存储于 AT32F421,可经 小程序↔BLE↔CH32V208↔UART↔AT32F421 链路读写。 - 地感配置存储于 AT32F421,可经 小程序↔BLE↔CH32V208↔UART↔AT32F421 链路读写。
详见 [docs/DLD960硬件资源.md](docs/DLD960硬件资源.md)。 详见 [docs/DLD960硬件资源.md](docs/DLD960硬件资源.md)。
## 子项目 ## 子项目
| 子项目 | 目录 | MCU | 固件版本 | 功能 | | 子项目 | 目录 | MCU | 固件版本 | 功能 |
|--------|------|-----|---------|------| |--------|------|-----|---------|------|
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 | | vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
| vd960DBN | `vd960DBN/` | CH32V208 | 1.0 | 蓝牙管理、TCP/IPWCHNET)、MQTT、RS485、串口桥接 | | vd960DBN | `vd960DBN/` | CH32V208 | 1.02.04 | 蓝牙管理、TCP/IPWCHNET)、MQTT、串口桥接、4G 扩展(RFU |
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。 > ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
## 协议文档(docs/ ## 产品文档(docs/
| 文档 | 版本 | 适用层 | 说明 | | 文档 | 版本 | 说明 |
|------|------|--------|------| |------|------|------|
| [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 | | [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.02 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 |
| [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 RS485/TTL | 设备管理、参数配置、数据上报 | | [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.02 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 |
| [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.01 | 以太网 TCP :5960 | 鉴权 + 命令 + event_report 客户端必答 |
| [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.05 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步 | ## 协议文档(docs/
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
| 文档 | 版本 | 适用层 | 说明 |
## 开发文档 |------|------|--------|------|
| [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 |
| 文档 | 说明 | | [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 TTL 串口 | 设备管理、参数配置、数据上报 |
|------|------| | [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.03 | 以太网 TCP :5960 | 鉴权 + 命令 + event_report 客户端必答 + 脱机日志(事件/快照流) |
| [vd960Loop/docs/devlog.md](vd960Loop/docs/devlog.md) | 检测算法开发日志(IIR、斜率限幅、基线冻结、时间量单位演进 | | [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.07 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步、脱机日志(事件/快照流 |
| [vd960DBN/docs/devlog.md](vd960DBN/docs/devlog.md) | 通信固件开发日志(WCHNET、MQTT 稳定性、event_report 可靠上报) | | [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.02 | 蓝牙 BLE | 帧格式 + 分包 + 脱机日志(OFFLOG_STAT/QUERY/CLEAR+ 传感快照(SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
| [vd960Loop/docs/acceptance-standard.md](vd960Loop/docs/acceptance-standard.md) | 检测算法验收标准 | | [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
| [vd960Loop/docs/variation-analysis.md](vd960Loop/docs/variation-analysis.md) | variation 上报量分析报告(基线 vs 当前值三时钟关系) |
| [vd960DBN/docs/incidents/](vd960DBN/docs/incidents/) | 现场事故分析归档 | ## 开发文档
## 配套工具 | 文档 | 说明 |
|------|------|
| 工具 | 目录 | 说明 | | [vd960Loop/docs/devlog.md](vd960Loop/docs/devlog.md) | 检测算法开发日志(IIR、斜率限幅、基线冻结、时间量单位演进) |
|------|------|------| | [vd960DBN/docs/devlog.md](vd960DBN/docs/devlog.md) | 通信固件开发日志(WCHNET、MQTT 稳定性、event_report 可靠上报) |
| DBNetClient | `DBNetClient/` | TCP JSON 协议桌面测试工具(Python + tkinter,零第三方依赖) | | [vd960Loop/docs/acceptance-standard.md](vd960Loop/docs/acceptance-standard.md) | 检测算法验收标准 |
| DBNMQTTool | `DBNMQTTool/` | IoT MQTT 协议测试工具(Python + PySide6 + paho-mqtt,双主题 UI | | [vd960Loop/docs/variation-analysis.md](vd960Loop/docs/variation-analysis.md) | variation 上报量分析报告(基线 vs 当前值三时钟关系 |
| [vd960DBN/docs/incidents/](vd960DBN/docs/incidents/) | 现场事故分析归档 |
## 开发环境
## 配套工具
- **vd960Loop**Keil MDK / AT32 IDEARM Cortex-M4ARMCC V5)、FreeRTOS V10.4.3
- **vd960DBN**MounRiver StudioRISC-V)、WCH BLE 协议栈 + WCHNET | 工具 | 目录 | 说明 |
- 调试口:Loop MCU TTL Tx3.3V 9600 8N1);DBN 侧 PRINT 串口 |------|------|------|
| DBNetClient | `DBNetClient/` | TCP JSON 协议桌面测试工具(Python + tkinter,零第三方依赖) |
## 版本发布流程 | DBNMQTTool | `DBNMQTTool/` | IoT MQTT 协议测试工具(Python + PySide6 + paho-mqtt,双主题 UI |
1. 确认 `vd960Loop``vd960DBN` 固件按同一协议版本联调通过; ## 开发环境
2. 更新两侧 `cmcng.h``FIRMWARE_VER`
3. 更新 `CHANGELOG.md` 与各 devlog 修订记录; - **vd960Loop**Keil MDK / AT32 IDEARM Cortex-M4ARMCC V5)、FreeRTOS V10.4.3
4. 打 tag`vX.Y.Z`)推送 Gitea,并在 Gitea Releases 挂发布说明与固件产物(hex/bin)。 - **vd960DBN**MounRiver StudioRISC-V)、WCH BLE 协议栈 + WCHNET
- 调试口:Loop MCU TTL Tx3.3V 9600 8N1);DBN 侧 PRINT 串口
## 版本发布流程
1. 确认 `vd960Loop``vd960DBN` 固件按同一协议版本联调通过;
2. 更新两侧 `cmcng.h``FIRMWARE_VER`
3. 更新 `CHANGELOG.md` 与各 devlog 修订记录;
4. 打 tag`vX.Y.Z`)推送 Gitea,并在 Gitea Releases 挂发布说明与固件产物(hex/bin)。
+243
View File
@@ -0,0 +1,243 @@
# DLD960 BLE 通信协议(脱机事件日志 + 传感快照)
> 版本: V1.022026-08-12,新增传感快照 3 命令;分包上限随协商 MTU 动态)
> 适用: vd960DBNCH32V208WCH BLE 协议栈)
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Qxx 环形日志(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
---
## 1 帧格式
与既有 BLE 配置命令一致:
```
Magic | Header | Data | CheckByte
| Addr/Sub Len CMD | | Xor Sum
1 Byte | 1B 1B 1B | xx | 1B 1B
```
- `Len = len(CMD) + len(Data)`(即 `pkg[2] = 1 + data_len`
- 校验:`Xor = XOR(pkg[1..Len+2])``Sum = SUM(pkg[1..Len+2])`,覆盖 header + cmd + data(不含 magic
- 本命令族 Magic = `0x8F`MAGIC_BYTE_DBN_DEFAULT
### 分包(长响应)
响应单包数据上限**随协商 MTU 动态变化**V1.01,修复"Too large noti 丢包"):`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B)。分包时 header 字节 = `(pkg_amount << 4) | pkg_seq``pkg_seq` 从 1 递增;收端按 `pkg_amount`/`pkg_seq` 重组,最后一片 `pkg_amount == pkg_seq`
| 协商 MTU | 单包最大 dat | 整包最大长度 |
|---------|-------------|-------------|
| 23(未协商) | 14 | 20 |
| 96(常见 Android | 87 | 93 |
| 185iOS 常见) | 94 | 100 |
脱机日志 QUERY 响应最大 130BMTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
> 分包发送由 `performPeriodicTask`50ms TMOS 周期)主动续传,两包间隔 ≤50ms,不依赖收包事件(2026-08-12 修复)。
---
## 2 命令码
| 命令码 | 名称 | 方向 | 说明 |
|--------|------|------|------|
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) |
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
| `0x28` | `SNAP_STAT` | APP→设备 | 查询传感快照统计(分页定位) |
| `0x29` | `SNAP_QUERY` | APP→设备 | 按全局序号分页拉取快照记录 |
| `0x2A` | `SNAP_CLEAR` | APP→设备 | 清除快照(审计写入事件流) |
> 与 MQTT V1.06 / TCP JSON V1.02 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。
---
## 3 查询日志统计 `OFFLOG_STAT` (0x25)
**请求 data** 无(`Len=1`,仅 cmd 字节)。
**响应 data19B,全小端):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用(Flash 初始化失败) |
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
| 3 | 4 | `count` | 有效记录条数(0~8064,环形覆盖后 < capacity |
| 7 | 4 | `capacity` | 容量上限(8064 |
| 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| 7 | 4 | `capacity` | 容量上限(**随存储芯片动态**W25Q32=16256 / W25Q64=32640 / W25Q128=65408 / W25Q256=130944 |
示例(boot_seq=2, count=1234, capacity=16256, seq_first=100, seq_last=1333):
```
8F 00 14 25 00 02 00 D2 04 00 00 00 80 3F 00 00 00 64 00 00 00 35 05 00 00 00 XX XX
-- -- -- -- -------------------- -------------------- -------------------- -----
| | | | status count=1234 capacity=16256 seq_first=100
| | | | cmd=0x25 boot_seq=2 seq_last=1333
```
---
## 4 分页拉取日志 `OFFLOG_QUERY` (0x26)
**请求 data5B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界(< seq_first 或 > seq_last)返回 0 条 |
| 4 | 1 | `count` | 拉取条数,**上限 4**,超限按 4;0 按 4 处理 |
**响应 data**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用;`0x02`=请求帧过短 |
| 1 | 1 | `count` | 本次实际返回记录条数(0~4) |
| 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §6) |
记录顺序 = 逻辑序号升序(与 `start_seq` 一致)。越界/空日志:`count=0`
请求示例(start_seq=1330, count=4):
```
8F 00 06 26 32 05 00 00 04 XX XX
-- -- -- -- -- -- -- -- ---
| | | | | | | | count=4
| | | | +--+--+--+ start_seq=1330 (LE)
| | | cmd=0x26
| | Len=5+1=6
```
---
## 5 清除日志 `OFFLOG_CLEAR` (0x27)
**请求 data** 无。
**响应 data1B):** `status``0x00`=OK。
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——留痕不可清除)。设备侧应做权限控制(与 MQTT `log_clear` 同语义)。
>
> ⚠ **阻塞 ~2.8s**63 个数据扇区 SPI 擦除(~45ms/扇区),期间主循环阻塞。若设备 IoT MQTT 在线,此期间 WCHNET 无法轮询,可能导致断连重连(60s keepalive 内可恢复)。请勿高频调用。
成功后 `OFFLOG_STAT``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。
---
## 6 传感快照日志(0x28/0x29/0x2AV1.02 新增)
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
### 6.1 查询快照统计 `SNAP_STAT` (0x28)
**请求 data** 无(`Len=1`)。
**响应 data19B,全小端):** 与 §3 `OFFLOG_STAT` 布局相同:
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=快照未启用 |
| 1 | 2 | `boot_seq` | 当前启动序号 |
| 3 | 4 | `count` | 有效记录条数(0~capacity |
| 7 | 4 | `capacity` | 容量上限(**随芯片动态**W25Q32=48064 / W25Q64=105408 / W25Q128=220096 / W25Q256=449472 |
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
| 15 | 4 | `seq_last` | 最新一条全局序号 |
### 6.2 分页拉取快照 `SNAP_QUERY` (0x29)
**请求 data5B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界返回 0 条 |
| 4 | 1 | `count` | 拉取条数,**上限 2**64B×2+2=130B ≤ 单包缓冲),超限按 2;0 按 2 |
**响应 data**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=快照未启用;`0x02`=请求帧过短 |
| 1 | 1 | `count` | 本次实际返回记录条数(0~2) |
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §6.4) |
记录顺序 = 逻辑序号升序。QUERY 响应最大 130BMTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
### 6.3 清除快照 `SNAP_CLEAR` (0x2A)
**请求 data** 无。**响应 data1B):** `status``0x00`=OK。
> ⚠ **高风险操作**:清除动作本身写入**事件流** `log_clear` 审计(payload[0]=2=快照流,不可清除)。
> 阻塞 ~45ms:逻辑清除(count=0 旧数据立即不可读)+ 只擦写指针起点扇区,其余扇区由环形写覆盖时自动擦(原擦全部 751 扇区 ~34s,会触发 IWDG 4s 复位——2026-08-12 修复)。
成功后 `SNAP_STAT``count` 归 0`seq_last` 继续递增(序号不复位)。
### 6.4 快照记录格式(SnapRec64B 定长,小端)
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `magic` | `0xA6`(区别于事件 `0xA5` |
| 1 | 1 | `len` | 线圈数据有效长度 = coil_count×120~48 |
| 2 | 1 | `flags` | bit0=分包帧(预留) |
| 3 | 1 | `rsvd` | 保留(0 |
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,采集时刻) |
| 12 | 2 | `boot_seq` | 所属启动段 |
| 14 | 2 | `rsvd2` | 保留(0 |
| 16 | 48 | `coils` | 4×12B 线圈数据,与 0xC0 线上格式一致 |
**线圈 12B 单元**(与 0xC0 帧内每路线圈完全一致):
| 偏移(单元内) | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `cfg` | freq_level(2) + direction(1) + freq_type(1) + sensitivity(4) |
| 1 | 1 | `cond` | condition(4) + loop_state(1) + car_state(1) + misc_type(2) |
| 2 | 3 | `freq` | 频率,3B 小端无符号 |
| 5 | 3 | `variation` | 变化量,3B 小端有符号补码(= Origin CAPVD |
| 8 | 4 | `misc` | 杂项 4B 小端(misc_type=0 时=通过时间,50ms 单位) |
> 绝对时间回算:用事件流 `time_anchor`boot_seq ↔ unix_ts 映射)+ 本记录 `boot_seq`/`ts_ms`。未同步段仅相对时间。
---
## 7 记录格式(OfflogEvt32B 定长,小端)
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `magic` | `0xA5` |
| 1 | 1 | `type` | 事件类型(下表) |
| 2 | 1 | `len` | payload 有效字节数(0~12 |
| 3 | 1 | `flags` | bit0=1 → `unix_ts` 有效 |
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,断电归零) |
| 12 | 4 | `unix_ts` | 已同步 Unix 秒;`flags.bit0=0` → 0/无效(未同步段仅相对时间) |
| 16 | 2 | `boot_seq` | 所属启动段 |
| 18 | 2 | `rsvd` | 保留(0 |
| 20 | 12 | `payload` | 事件参数(下表) |
### 事件类型与 payload
| type | 事件 | payload | 说明 |
|------|------|---------|------|
| `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]` reason1=断开 2=超时 3=CONNACK拒绝 4=连接超时 | MQTT 断连 |
| `0x13` | iot_reconn | `[4]` 重连退避 ms(大端) | 重连退避 |
| `0x30` | evt_retry | `[5]` msg_id(大端4) + retry(1) | event_report ACK 超时重发 |
| `0x31` | evt_giveup | `[4]` msg_id(大端) | event_report 重试耗尽挂起 |
| `0x40` | coil | `[6]` sub(1) + ch(1) + value(大端4, 50ms 单位) | 线圈事件;sub1=car_enter 2=car_leave 3=loop_cut 4=loop_restore |
| `0x50` | time_anchor | —(`unix_ts` 即平台下发值,严格一致) | 时钟同步锚点 |
| `0x70` | log_clear | — | 日志清除(审计,不可清除) |
> payload 内多字节为**大端**(与 offlog 写入侧一致),OfflogEvt 其余字段为**小端**CPU 原生序,直接 memcpy)。
---
## 8 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |
| V1.01 | 2026-08-12 | 分包上限改为随协商 MTU 动态:`chunk = min(MTU-9, 94)`;修复 Too large noti 丢包 |
| V1.02 | 2026-08-12 | 新增传感快照 3 命令:SNAP_STAT / SNAP_QUERY / SNAP_CLEAR0x28/0x29/0x2A |
+238
View File
@@ -0,0 +1,238 @@
# DLD960 BLE 日志查询交互过程(实测示例)
> 版本: V1.002026-08-12
> 适用: vd960DBNCH32V208+ 微信小程序
> 目的: 以**真实串口日志**为样例,完整描述"小程序发指令 → 设备处理 → 设备回包"的交互过程,供固件/小程序两端排查对照
---
## 1 帧格式回顾(与 DLD960_BLE协议.md 一致)
```
Magic | Header | Data | CheckByte
| Addr/Sub Len CMD | | Xor Sum
1 Byte | 1B 1B 1B | xx | 1B 1B
```
- `Len = len(CMD) + len(Data)`,即 `pkg[2] = 1 + data_len`
- 校验:`Xor = XOR(pkg[1..Len+2])``Sum = SUM(pkg[1..Len+2])`(覆盖 header+cmd+data,不含 magic
- Magic = `0x8F`MAGIC_BYTE_DBN_DEFAULT
- **Header 字节分包语义**`(pkg_amount << 4) | pkg_seq`
- 单包:`0x00`amount=0, seq=0
- 分包:高 4 位 = 总包数,低 4 位 = 当前包序号(从 1 递增),最后一片 `amount == seq`
- **单包数据上限(动态,V1.01+)**:`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B
| 协商 MTU | chunk(单包最大 dat | 整包最大长度 |
|---------|----------------------|-------------|
| 23(未协商) | 14 | 20 |
| 96(本样例手机) | 87 | 93 |
| 185iOS 常见) | 94 | 100 |
---
## 2 命令码
| 命令码 | 名称 | 方向 | 说明 |
|--------|------|------|------|
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计 |
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
---
## 3 交互示例一:查询日志统计 OFFLOG_STAT (0x25)
### 3.1 小程序发送(请求帧)
```
8F 00 01 25 24 26
```
| 字节 | 值 | 含义 |
|------|-----|------|
| pkg[0] | `8F` | magic |
| pkg[1] | `00` | header:单包(amount=0, seq=0 |
| pkg[2] | `01` | Len = 0 data + 1 cmd = 1 |
| pkg[3] | `25` | cmd = OFFLOG_STAT(查询统计,无 data |
| pkg[4..5] | `24 26` | ckbXor=0x24, Sum=0x26 |
### 3.2 设备处理日志
```
[Rx][13:45:41.577] profile ChangeCB CHAR1?..:, len:6
[Rx][13:45:41.577] 8F 00 01 25 24 26
[Rx][13:45:41.577] BLE: offlog_stat count=162 seq_first=1 seq_last=162
[Rx][13:45:41.588] BLE notify OK, len:25, MTU:96
```
### 3.3 设备响应(25B = 帧头4 + dat 19 + ckb 2,单包)
```
8F 00 14 25 <19B data> <ckb2>
```
响应 data(19B,全小端)与设备日志字段对照:
| data 偏移 | 长度 | 字段 | 本次值 | 说明 |
|-----------|------|------|--------|------|
| 0 | 1 | `status` | `00` | OK |
| 1 | 2 | `boot_seq` | 待补(需原始 hex) | 启动序号 |
| 3 | 4 | `count` | `A2 00 00 00` = 162 | 有效记录条数 |
| 7 | 4 | `capacity` | `80 3F 00 00` = 16256W25Q32 512KB127 数据扇区 × 128 条) | 容量上限 |
| 11 | 4 | `seq_first` | `01 00 00 00` = 1 | 逻辑首条全局序号 |
| 15 | 4 | `seq_last` | `A2 00 00 00` = 162 | 最新全局序号 |
> 单包响应无分包,`notify OK len:25`25 = 4+19+2),MTU=96 下 25 ≤ 93,正常。
---
## 4 交互示例二:读取日志记录 OFFLOG_QUERY (0x26)
### 4.1 小程序发送(请求帧)
```
8F 00 06 26 01 00 00 00 04 25 31
```
| 字节 | 值 | 含义 |
|------|-----|------|
| pkg[0] | `8F` | magic |
| pkg[1] | `00` | header:单包请求 |
| pkg[2] | `06` | Len = 5 data + 1 cmd = 6 |
| pkg[3] | `26` | cmd = OFFLOG_QUERY |
| pkg[4..7] | `01 00 00 00` | start_seq = 1LE32,含) |
| pkg[8] | `04` | count = 4(≤ OFFLOG_MAX_QUERY_RECORDS=4 |
| pkg[9..10] | `25 31` | ckb |
### 4.2 设备处理日志
```
[Rx][13:45:47.827] profile ChangeCB CHAR1?..:, len:11
[Rx][13:45:47.827] 8F 00 06 26 01 00 00 00 04 25 31
[Rx][13:45:47.827] BLE: offlog_query start_seq=1 req=4 fetched=4
[Rx][13:45:47.848] BLE notify OK, len:93, MTU:96
[Rx][13:45:47.848] 8F 21 58 26 00 04 A5 01 04 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 18 00 00 00 00 00 00 00 00 00 00 00 A5 01 04 00 02 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 A5 01 04 00 03 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 18 F3 E5
```
### 4.3 响应分包说明
响应 dat 总量 = status(1) + count(1) + 4×32B = **130B** > 单包上限 87BMTU=96)→ **必须分包 2 包**
| 包 | header | 整包长 | dat 内容 |
|----|--------|--------|----------|
| 第 1 包 | `0x21`amount=2, seq=1 | 93B | status+count + 记录1(32B) + 记录2(32B) + 记录3前21B = 87B |
| 第 2 包 | `0x22`amount=2, seq=2 | **49B(预期)** | 记录3剩余11B + 记录4(32B) = 43B |
### 4.4 第一包逐字段解析(93B)
```
8F 21 58 26 00 04 <记录1 32B> <记录2 32B> <记录3 前21B> F3 E5
-- -- -- -- -- -- --------------------------------------- ----
| | | | | | | +-- ckb
| | | | | | +-- dat: status(1)=00, count(1)=04, 然后记录
| | | | | +-- status=00 OK
| | | | +-- cmd=0x26
| | | +-- Len=0x58=88 = 87 data + 1 cmd
| | +-- header=0x21: amount=2, seq=1
| +-- magic=0x8F
```
记录结构(OfflogEvt 32B,小端)——本次 3 条记录均为 `type=0x01 04 00`(事件类型,待对照 offlog.h 事件表):
```
记录1: A5 01 04 00 | 01 00 00 00 | 00×12 | 01 00 00 00 | 18 00 00 00
记录2: A5 01 04 00 | 02 00 00 00 | 00×12 | 02 00 00 00 | 08 00 00 00
记录3: A5 01 04 00 | 03 00 00 00 | 00×12 | 03 00 00 00 | 18(截断于第 21B
```
### 4.5 第二包(预期,未收到——当前问题)
```
8F 22 2C 26 <记录3剩余 11B> <记录4 32B> <ckb2>
```
- header `0x22`amount=2, seq=2(最后一片)
- Len = `0x2C` = 44 = 43 data + 1 cmd
- 整包 49B
### 4.6 当前问题(2026-08-12 排查中)
**现象**:设备只发出了第 1 包(93B),第 2 包(49B)未发出;小程序按 `amount=2` 等第 2 包永远等不到。
**已确认**
- MTU 分包粒度动态化已生效(93B ≤ 96-3=93,无 `Too large noti`
- `performPeriodicTask` 已加主动拉包逻辑(不依赖收包事件)
- 已加 `BLE pull FAIL` 诊断打印,但本次日志**未出现** → 说明拉包时 `g_buf_ble_response.flag==0`(队列已被清)或 `_pull==1` 但发送周期未到/被阻塞
**下一步排查**
1.`performPeriodicTask` 周期心跳打印(每 500ms),确认 TMOS 周期事件是否持续执行
2. 核对 `SBP_PERIODIC_EVT_PERIOD=50` 的 TMOS 时间单位(tick vs ms
3. 检查 `g_buf_ble_response` 在第 1 包发出后是否被意外 clear(`unpack_packs`/`set_response_buf` 调用路径)
---
## 5 交互示例三:清除日志 OFFLOG_CLEAR (0x27)
### 5.1 小程序发送
```
8F 00 01 27 XX XX
```
| 字节 | 值 | 含义 |
|------|-----|------|
| pkg[0] | `8F` | magic |
| pkg[1] | `00` | header:单包 |
| pkg[2] | `01` | Len = 1 |
| pkg[3] | `27` | cmd = OFFLOG_CLEAR |
| pkg[4..5] | `XX XX` | ckb |
### 5.2 设备响应
```
8F 00 02 27 00 XX XX
```
- dat`status(1)=00`OK
- **注意**:擦除 W25Q32 阻塞约 2.8s,期间 BLE 无响应、网络短时失服务(MQTT keepalive 60s 可吸收)
---
## 6 小程序端分包重组规则(伪代码)
```js
// 订阅 onBLECharacteristicValueChange, 按 cmd 维护重组缓冲
let frag = { cmd: null, amount: 0, seq: 0, data: [] };
function onNotify(buf) {
const magic = buf[0], header = buf[1], len = buf[2], cmd = buf[3];
const amount = header >> 4, seq = header & 0x0F;
const data = buf.slice(4, 4 + (len - 1)); // 去掉 cmd 的净数据
const ckb = buf.slice(-2); // 校验可选
if (amount === 0) { // 单包
dispatch(cmd, data);
} else { // 分包
if (seq === 1) { frag = { cmd, amount, seq, data: [] }; }
if (frag.cmd !== cmd) return; // 乱序/新响应, 丢弃
frag.data = frag.data.concat(data);
frag.seq = seq;
if (seq === amount) { // 收齐
dispatch(cmd, frag.data);
frag = null;
}
}
}
```
> 关键点:收到第 1 包(seq=1, amount=2)后**必须继续等待** seq=2 的第二个通知,不要在第 1 包到达时判完成。
---
## 7 排查时间线
| 时间 | 现象 | 结论 |
|------|------|------|
| 08:15 | `Too large noti, len:102, MTU:96` | 分包粒度写死 96B > MTU-3=93,丢包 |
| 11:51 | 修复后第一包 93B 正常发出 | MTU 动态分包生效 |
| 11:51~13:45 | 第二包 49B 未发出,无 pull FAIL 打印 | 分包续传/发送周期链路待查(见 §4.6) |
+161
View File
@@ -110,6 +110,12 @@ dld960/{dev_serial}/{direction}
- 校准前(含首个 `initialize`),`ts` 为上电秒数;平台应能容忍并可按数量级区分。 - 校准前(含首个 `initialize`),`ts` 为上电秒数;平台应能容忍并可按数量级区分。
- 设备重启后需重新校准(无掉电保持)。平台在每次设备 `initialize` 后都应下发一次带 `ts``report_config` - 设备重启后需重新校准(无掉电保持)。平台在每次设备 `initialize` 后都应下发一次带 `ts``report_config`
**脱机日志时间戳语义(V1.06 起)**
- 设备侧事件日志(`log_query` 拉取)每条记录携带双时间戳:`ts_ms`(boot 内相对时间,单位 ms,断电归零)+ `unix_ts`(已同步 Unix 秒,**0 = 未同步**)。
- 设备经本协议同步成功后,其后记录的 `unix_ts` 均为真实 Unix 时间;同步前(含每次上电的 `boot` 事件)`unix_ts = 0`,仅 `ts_ms` 相对时间。
- 回算规则:`绝对时间 = 锚点.unix_ts + (记录.ts_ms - 锚点.ts_ms)/1000`,锚点取该 boot 段内第一条 `time_anchor` 事件(`unix_ts` 即平台下发值,严格一致)。从未同步过的 boot 段只有相对时间。
--- ---
# 3 命令详表 # 3 命令详表
@@ -131,6 +137,9 @@ dld960/{dev_serial}/{direction}
| `loop_param_set` | 设置车检器多路参数 | srv→dev | 0x63 | | `loop_param_set` | 设置车检器多路参数 | srv→dev | 0x63 |
| `loop_param_query` | 读取车检器多路参数 | srv→dev | 0x64 | | `loop_param_query` | 读取车检器多路参数 | srv→dev | 0x64 |
| `report_config` | 设置主动上报 | srv→dev | 0xC5 | | `report_config` | 设置主动上报 | srv→dev | 0xC5 |
| `log_stat` | 查询脱机日志统计(事件/快照流) | srv→dev | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | srv→dev | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | srv→dev | — |
| `initialize` | 设备上电初始化登陆 | dev→srv | — | | `initialize` | 设备上电初始化登陆 | dev→srv | — |
| `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 | | `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 |
| `event_report` | 事件上报(**平台须应答**,见 §5.3 | dev→srv | — | | `event_report` | 事件上报(**平台须应答**,见 §5.3 | dev→srv | — |
@@ -639,6 +648,156 @@ dld960/{dev_serial}/{direction}
--- ---
## 4.16 查询脱机日志统计 `log_stat`
> Topic: `dld960/{sn}/srv`
> 设备本地 W25Qxx 环形日志(事件区/快照区容量随存储芯片动态,掉电不丢)。用于日志拉取前的分页定位。
> 通过 `data.stream` 区分日志流:`event`(事件日志,缺省)/ `snapshot`(传感快照)。
**请求:**
```json
{
"msg_id": 16,
"cmd": "log_stat",
"ts": 1719000000,
"data": {
"stream": "event"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应 datastream=event):**
```json
{
"stream": "event",
"enabled": true,
"boot_seq": 2,
"count": 1234,
"capacity": 16256,
"seq_first": 100,
"seq_last": 1333
}
```
**响应 datastream=snapshot):**
```json
{
"stream": "snapshot",
"enabled": true,
"boot_seq": 2,
"count": 1234,
"capacity": 48064,
"seq_first": 100,
"seq_last": 1333
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event` / `snapshot` |
| `enabled` | bool | 日志功能是否启用(Flash 初始化成功) |
| `boot_seq` | uint16 | 当前启动序号(每次上电 +1,区分复位段) |
| `count` | uint32 | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| `capacity` | uint32 | 容量上限(**随存储芯片与流动态**):事件流 W25Q32=16256 / Q64=32640 / Q128=65408 / Q256=130944;快照流 W25Q32=48064 / Q64=105408 / Q128=220096 / Q256=449472 |
| `seq_first` | uint32 | 逻辑首条记录全局序号(`seq_last - count + 1`count=0 时为 0 |
| `seq_last` | uint32 | 最新一条记录全局序号(跨 boot 单调递增) |
---
## 4.17 分页拉取脱机日志 `log_query`
> Topic: `dld960/{sn}/srv`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**(hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`。
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),平台按《DLD960 BLE 协议》字段表解析。
**请求:**
```json
{
"msg_id": 17,
"cmd": "log_query",
"ts": 1719000000,
"data": {
"stream": "event",
"start_seq": 1330,
"count": 4
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 data**
```json
{
"start_seq": 1330,
"records": [
{
"seq": 1330,
"hex": "a53200000200000001000000..."
}
]
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**(flash 存储字节原样,小端) |
**解析字段表(与 BLE 通道完全一致):**
| 流 | 结构 | 字段表 |
|----|------|--------|
| `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算(见 §2.3)。
---
## 4.18 清除脱机日志 `log_clear`
> Topic: `dld960/{sn}/srv`
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——谁在何时清了日志,留痕不可清除)。平台侧应做权限控制。
**请求:**
```json
{
"msg_id": 18,
"cmd": "log_clear",
"ts": 1719000000,
"data": {
"stream": "event"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应:** 标准成功/失败。
- `stream=event`:成功后 `log_stat``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。**阻塞 ~2.8s**(63 个数据扇区 SPI 擦除),请勿高频调用。
- `stream=snapshot`:成功后 `log_stat``count` 归 0`seq_last` 继续递增;清除动作写入事件流审计(`log_clear`,payload 标记快照流)。**阻塞 ~45ms**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
---
# 5 设备主动上报 # 5 设备主动上报
## 5.1 设备上电登陆信息 `initialize` ## 5.1 设备上电登陆信息 `initialize`
@@ -893,4 +1052,6 @@ dld960/{dev_serial}/{direction}
| V1.03 | 2026-07-09 | 增加设备上电初始化指令 | wangfq | | V1.03 | 2026-07-09 | 增加设备上电初始化指令 | wangfq |
| V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq | | V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq |
| V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq | | V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq |
| V1.06 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);§2.3 补充日志双时间戳语义(`ts_ms` 相对 + `unix_ts` 已同步,0=未同步,锚点回算规则) | wangfq |
| V1.07 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
+507
View File
@@ -0,0 +1,507 @@
# DLD960 MQTT 远程 OTA 协议(Loop MCU 先存后刷)
> 文档版本 V1.01(设计稿·按修改意见修订)· 2026-08-20 · 适用范围:并入《DLD960 IoT 接口协议(MQTT + JSON)》V1.08 后生效
> 依据:`docs/ROADMAP.md` P1.4 ① —— Loop MCU (AT32F421) 远程 OTAv1.2.x 落地
> 设计原则:**下载阶段纯 MQTT 分片 → W25Qxx 暂存;刷写阶段纯本地 ISP 透传(复用 BLE OTA 0x9F 状态机)**;传输层无关,4G 通道原样复用。
---
# 1 背景与目标
## 1.1 现状基线
| 项 | 说明 |
|----|------|
| 双 MCU 架构 | DBN = CH32V208(通信 MCU,跑 MQTT/BLE/TCP+ Loop = AT32F421(地感 MCU,跑检测算法) |
| 现有 OTA 通道 | BLE → DBN 透传 → UART2 0x9F ISP → Loop bootloader2026-08-19 修复闭环,V1.02.03 |
| 本轮目标 | MQTT → DBN 分片下载到 W25Qxx 暂存区 → 校验后**本地** ISP 刷写 Loop |
| 暂存介质 | DBN 外挂 W25QxxSPI1),OTA 镜像暂存区 **0x010000 起 512KB**`SNAP_FIXED_SIZE = 参数区64KB + OTA区512KB`,已在 snapshot.c 分区模型预留) |
## 1.2 为什么"先存后刷"(相对 BLE 流式直透)
| 维度 | 流式直透(BLE 现状) | 先存后刷(本设计) |
|------|---------------------|-------------------|
| 网络抖动影响 | 任一分片丢失 → 停等卡死,全流程重来 | 下载阶段断网无所谓,**断点续传** |
| 刷写窗口 | 与传输时间重合(网络慢则窗口长) | 下载完成后刷写纯本地,窗口短且可控 |
| 校验 | 依赖传输层(BLE 无 CRC) | 单片 CRC32 + 全镜像 CRC32 双重校验 |
| 回滚 | 无 | 暂存区 Slot A/B 双槽,保留上一版重刷 |
| 安全窗口 | 无检查 | 刷写前检查无车压线圈;继电器维持 Loop 现状(与 BLE OTA 一致) |
## 1.3 范围
- 本版只做 **Loop (AT32F421) 远程 OTA**ROADMAP P1.4 ①)
- DBN (CH32V208) 自身 OTAROADMAP P1.4 ②):需 bootloader 改造,**单独评审,不阻塞本设计**;协议命令预留 `target:"dbn"`
- 传输层:本版 MQTT;4G(UART1 AT 透传)复用同一分片下载协议(`target` 字段扩展即可)
---
# 2 总体架构与数据流
```
┌─────────┐ MQTT (dld960/{sn}/srv) ┌──────────────┐ UART2 192000 ┌─────────────┐
│ 平台 │ ── ota_begin / ota_data ─▶ │ DBN (CH32V208) │ ── 0x9F ISP ──▶ │ Loop (AT32) │
│ │ ◀─ 响应 / ota_report ───── │ │ ◀─ ACK ──────── │ bootloader │
└─────────┘ │ W25Qxx 暂存 │ └─────────────┘
│ 0x010000 512KB │
└──────────────┘
阶段1 下载:平台 MQTT 分片 → DBN 写 W25Qxx(单片 CRC32 校验)
阶段2 校验:ota_end 全镜像 CRC32 复核,state → ready
阶段3 刷写:ota_flash → DBN 从 W25Qxx 读出 → 0x9F A5/A6/A7 逐块透传 → Loop bootloader 写 flash
```
**镜像语义**:平台下发的是 **bin 原始字节**hex 编码传输),即 AT32F421 0x08003400 起的 APP 代码映像(hex→bin 转换由平台侧完成,设备不解析 Intel HEX)。刷写时 A6 地址固定 `0x08003400`=`APP_START_ADDR`),A7 数据按 bin 顺序发送。
---
# 3 关键参数
## 3.1 协议参数(平台 ↔ 设备契约)
| 参数 | 值 | 依据 |
|------|-----|------|
| 下载单片原始字节 | **256B** | 2 的幂 + W25Qxx 页(256B)对齐;hex 512 字符 + JSON 外壳 ≈ 640B < `IOT_MQTT_RECV_BUF_LEN=1024` |
| 单片 CRC | **CRC32**(256B 独立计算) | 与全镜像同算法,查表实现复用 |
| 全镜像 CRC | **CRC32**offset 0 ~ size-1 连续) | 标准 CRC-32/ISO-HDLC |
| 镜像上限 | **96KB** | Slot 数据区 100KB 预留 4KB 边界余量;Loop APP 区 0x08003400~0x08010000 = 51KB 硬上限绰绰有余,100KB 槽为 DBN (CH32V208) 自身镜像(~100KB APP 区)预留 |
| 下载片序号语义 | offset 绝对字节偏移(256 对齐) | 支持乱序/重复片幂等处理 |
| 断点续传 | ota_begin 返回已接收字节数(片对齐) | 设备持久化 received 到元数据 |
## 3.2 刷写参数(设备内部实现约束,平台无需感知)
| 参数 | 值 | 依据 |
|------|-----|------|
| A7 数据块 | **≤254B**(实现取 248B | bootloader `LEN` 为 uint8 → DATA = LEN-1 ≤ 254iap.c `CMD_9F_DATA_LEN` |
| 块间超时 | 1s(可配) | 0x9F 停等协议,超时重发该块 |
| 刷写调度 | **非阻塞 tick 驱动** | 刷写状态机挂主循环轮询,每轮最多发送 1~2 块并检查 ACK,绝不阻塞主循环 → 刷写窗口内 MQTT PINGREQ/心跳/IWDG 喂狗节奏完全不受影响 |
| 刷写重试 | 每块失败重发 ×3,整体失败重试 ×3 | ROADMAP 安全底线 |
| 刷写速率 | 248B/帧 ≈ 13ms + ACK~20ms/帧 | 64KB ≈ 265 帧 ≈ **6~8s 窗口**Loop 典型 40~50KB 更快) |
| 升级触发 | DBN 发 `9F 01 00 01 A5 A7` → Loop APP 写 `IAP_UPGRADE_FLAG_9F=0x444C4439` → 复位 | 复用 BLE OTA 启动帧(dbn_ble_srv.c check_pkg 同款) |
## 3.3 CRC32 算法定义(必须双方一致)
- 标准 **CRC-32/ISO-HDLC**poly `0x04C11DB7`reflected `0xEDB88320`),init `0xFFFFFFFF`refin/refout truexorout `0xFFFFFFFF`
- 平台侧 Python `zlib.crc32()` / `binascii.crc32()` 即此算法,直接可用;设备侧查表法实现
- 全镜像 CRC = 从 offset 0 连续计算 size 字节;单片 CRC = 仅该 256B
- JSON 中 crc32 字段以**十进制无符号数**传输(与现有 uint32 字段风格一致)
---
# 4 暂存区布局(设备内部实现,命令接口不依赖)
```
0x010000 ┌─────────────────────────┐
│ OTA 元数据扇区 (4KB) │ SlotA/SlotB 头 + 会话状态 + 审计
0x011000 ├─────────────────────────┤
│ Slot A 数据区 (100KB) │ 当前新镜像(bin 原始字节)
0x02A000 ├─────────────────────────┤
│ Slot B 数据区 (100KB) │ 上一版镜像(回滚重刷)
0x043000 ├─────────────────────────┤
│ 预留 308KB │ DBN 自身镜像 / 4G 扩展
0x090000 └─────────────────────────┘
```
- Slot A/B 各 **100KB 数据区**`0x011000`/`0x02A000` 起,4KB 对齐),镜像上限 96KB(预留 4KB 边界余量)
- 100KB 槽位容量同时覆盖 DBN (CH32V208) 自身镜像(APP 区约 100KB),为 P1.4 ② 预留
- 4KB 边界余量:防止镜像写满后越界擦到下一槽;元数据/数据区起始均 4KB 扇区对齐
元数据结构(建议,实现可调,不进入协议字段):
```c
typedef struct { /* 64B */
uint32_t magic; /* 'DLD9' */
uint32_t state; /* idle/downloading/ready/flashing/flash_failed/aborted */
uint8_t target; /* 0=loop, 1=dbn(预留) */
uint8_t slot; /* 0=A, 1=B */
uint16_t rsv;
uint32_t size; /* 镜像字节数 */
uint32_t crc32; /* 全镜像 CRC32 */
uint32_t received; /* 已下载字节数(片对齐,断点续传依据) */
uint32_t last_result; /* 上次刷写结果码 */
uint32_t begin_ts; /* 会话开始(同步后 Unix 秒,0=未同步) */
char version[16]; /* 目标固件版本字符串(审计) */
uint32_t flash_cnt; /* 刷写尝试次数 */
uint32_t rsv2;
} OtaMeta;
```
**掉电恢复**:元数据每次 ota_begin / 每片落盘 / ota_end / 刷写结果后回写。重启后:
- state=downloading → 平台 ota_begin 时返回已接收 offset 续传
- state=ready → 平台 ota_begin 直接确认可刷写,或 ota_flash 直接触发
- state=flashing → 视为上次刷写中断(掉电/异常),可重试 ota_flash
---
# 5 命令详表(新增,并入 MQTT 协议 V1.08
| cmd | 方向 | 说明 |
|-----|------|------|
| `ota_begin` | srv→dev | 开启 OTA 会话 / 断点续传定位 |
| `ota_data` | srv→dev | 分片下发(256B/片,单片 CRC32 |
| `ota_end` | srv→dev | 结束下载,全镜像 CRC32 复核 |
| `ota_abort` | srv→dev | 中止会话,释放暂存 |
| `ota_flash` | srv→dev | 触发本地 ISP 刷写(仅 state=ready |
| `ota_status` | srv→dev | 查询 OTA 状态(含进度) |
| `ota_report` | dev→srv | 刷写进度/结果主动上报(QoS 1) |
| `event_report` | dev→srv | 扩展 `type=ota_error`:重试 ×3 仍失败告警(**平台必答**,复用 §5.3 ACK 闭环) |
> 兼容:老固件(V1.07 及以下)收到 `ota_*` → 现有 default 分支回 `code=4 unsupported command`。平台下发前可用 `ota_status` 或 `dev_info_query.soft_ver` 探测能力,避免每命令必报错。
## 5.1 开启会话 `ota_begin`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000000,
"data": {
"target": "loop",
"size": 46864,
"crc32": 305419896,
"version": "1.1.0",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | `loop`(当前支持);`dbn` 预留 |
| `size` | uint32 | 镜像 bin 字节数(≤ 98304 = 96KB |
| `crc32` | uint32 | 全镜像 CRC32(十进制) |
| `version` | string | 目标固件版本(写入元数据,审计用;bootloader 不校验版本) |
| `force` | bool | `true` = 覆盖现有暂存镜像 / 忽略冲突(默认 false) |
**设备行为:**
1. 读元数据:若已有镜像且 `size+crc32` 与本次一致 →
- state=ready → 返回 `offset=size`(平台可直接 `ota_flash`
- state=downloading → 返回 `offset=received`(续传)
2. 不一致 → 分配 Slot(A 当前 / B 回滚),写元数据 `state=downloading, received=0`,返回 `offset=0`
3. `force=false` 且目标版本 == 当前运行版本 → 返回 `code=1`(防重复刷写,可 force 绕过)
**响应 data**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000001,
"code": 0,
"msg": "success",
"data": {
"target": "loop",
"slot": "a",
"offset": 0,
"received": 0,
"size": 46864,
"crc32": 305419896,
"state": "downloading"
}
}
```
## 5.2 分片下发 `ota_data`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 402,
"cmd": "ota_data",
"ts": 1719000002,
"data": {
"target": "loop",
"offset": 0,
"crc32": 2524764894,
"data": "6a6173646f6e...512 hex 字符 = 256B"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | 同 `ota_begin` |
| `offset` | uint32 | 本片在镜像中的绝对偏移(**256 对齐**,首片 0) |
| `crc32` | uint32 | 本片 256B 的 CRC32(十进制) |
| `data` | string | 256B 原始字节小写 hex512 字符 |
**设备行为:**
1. `offset == received`(顺序片)→ 单片 CRC32 校验 → 写 W25Qxx256B 页对齐)→ `received += 256`(≥size 时截断为 size)→ 回 `code=0`
2. `offset < received`(重复片,平台重发)→ **幂等直接回 `code=0`**,不重写
3. `offset > received`(缺片/乱序)→ 回 `code=1` + `data.offset=received`(指示平台从该处续传;协议不要求乱序重组,降低设备复杂度)
4. 单片 CRC 失败 → 回 `code=1` + `data.err_code=1`(平台重发本片;连续失败由平台策略控制,可 abort)
5. 会话未开始 / 状态非 downloading → `code=3`(先 `ota_begin`
6. `data` 非 512 hex 字符 / offset 非 256 对齐 / 超 size → `code=1` 参数错误
**响应:** 标准成功/失败 + data 回显 `{offset, received}`
> 单片大小 256B 是协议常量,**不接受协商**(避免双方尺寸漂移)。若未来单片加大需协议升版。
## 5.3 结束下载 `ota_end`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 403,
"cmd": "ota_end",
"ts": 1719000003,
"data": {
"target": "loop",
"crc32": 305419896
}
}
```
**设备行为:**
1. `received != size``code=1` + `data.offset=received`(不完整,续传)
2. `received == size` → 读回暂存区全镜像计算 CRC32,与 ota_begin 声明值比对
- 一致 → 元数据 `state=ready, last_result=0``code=0, data={crc_ok:true}`
- 不一致 → 元数据 `state=downloading`(保留已下载数据,可重发错片)→ `code=5, data={crc_ok:false}`msg 注明 CRC 不匹配)
## 5.4 中止会话 `ota_abort`
> Topic: `dld960/{sn}/srv`
```json
{
"msg_id": 404,
"cmd": "ota_abort",
"ts": 1719000004,
"data": { "target": "loop" }
}
```
设备行为:元数据 `state=aborted`,Slot 标记可覆盖;正在刷写时 abort → 停止发送后续 A7 块(Loop 端由 bootloader 超时复位回 APP 兜底)。响应标准成功。
## 5.5 触发刷写 `ota_flash`
> Topic: `dld960/{sn}/srv`
> ⚠ **会车安全关键命令**:平台应确认现场允许(无车压线圈、非高峰)再下发。
**请求:**
```json
{
"msg_id": 405,
"cmd": "ota_flash",
"ts": 1719000005,
"data": {
"target": "loop",
"slot": "a",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `slot` | string | `a` / `b`(缺省 = 当前 ready 的槽) |
| `force` | bool | `true` = 跳过安全窗口检查(高风险,平台授权) |
**设备行为(同步检查 → 异步刷写):**
1. **安全窗口检查**force=false 时):
- 4 通道 Loop 有车(VD_FLAG 任一置位)→ `code=3` + `data.err_code=1`(有车,拒绝;平台可提示"车辆离开后重试")
- 刷写期间会阻断检测与继电器控制 → 建议平台在低峰执行
2. 元数据非 ready → `code=3`(先 `ota_end` 完成校验)
3. 通过 → 立即回 `code=0`(异步),进入刷写:
- 写 offlog 事件日志:`固件升级开始`target/slot/version/size
- **暂停事件上报与脱机日志(会话期间)**:MQTT `event_report` 暂停发送(入队积压,16 深溢出丢最旧,会话结束恢复后补发);offlog/快照落盘暂停——理由:刷写期间 Loop 复位不产生检测事件,DBN 让出 SPI 总线/主循环给刷写状态机,保证保活与喂狗
- 维持 Loop 现状:复位进 bootloader 后的 GPIO/继电器状态与现网 BLE OTA 升级完全一致(该通道已验证可用),不额外干预
-`9F 01 00 01 A5 A7` 启动帧 → Loop APP 写 flag 复位 → bootloader 回 pre_ok
- 发 A6 地址帧(`0x08003400` 4 字节大端)→ addr_ok
- 从 W25Qxx 读镜像,按 248B/块发 A7(停等 ACK,1s 超时重发 ×3)
- 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP
4. 进度经 `ota_report` 上行(见 §6);失败重试 ×3 仍失败 → 元数据 `state=flash_failed` + `event_report{type:ota_error}` 告警(平台必答)
**响应:** 标准成功/失败(`code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report`/`ota_status` 为准)。
## 5.6 查询状态 `ota_status`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{ "msg_id": 406, "cmd": "ota_status", "ts": 1719000006 }
```
**响应 data**
```json
{
"target": "loop",
"state": "flashing",
"slot": "a",
"size": 46864,
"received": 46864,
"crc32": 305419896,
"version": "1.1.0",
"progress": { "sent": 42112, "total": 46864 },
"last_result": 0,
"last_error": 0
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted` |
| `progress.sent` | uint32 | 刷写阶段已送 Loop 的字节数 |
| `last_result` | uint32 | 上次刷写结果:0=无/成功,非 0=错误码 |
| `last_error` | uint32 | 上次失败细分错误码 |
**状态机(设备侧):**
```
ota_begin(新会话) ota_data×N ota_end(CRC✓)
IDLE ─────────────────▶ DOWNLOADING ────────────▶ READY
▲ │ ▲ │
│ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓)
│ / flash_failed │ └──────────────┐ ▼
└────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ (Loop 重启) ──▶ IDLE(清槽/保留)
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
```
---
# 6 主动上报 `ota_report`
> Topic: `dld960/{sn}/dev` · QoS 1
> 用途:刷写进度与结果主动推送(进度可丢,结果可经 `ota_status` 兜底查询;设备侧元数据持久化 last_result
**上报:**
```json
{
"msg_id": 501,
"cmd": "ota_report",
"ts": 1719000007,
"data": {
"target": "loop",
"stage": "flashing",
"progress": { "sent": 42112, "total": 46864 },
"code": 0,
"msg": ""
}
}
```
| data 字段 | 说明 |
|-----------|------|
| `stage` | `begin`(会话开启)/ `downloading`(片落盘)/ `ready`(校验通过)/ `flashing`(刷写中)/ `done`(刷写成功,Loop 已重启)/ `failed`(刷写失败) |
| `progress` | `sent`/`total` 字节(刷写阶段) |
| `code` | stage 相关结果码 |
**上报节奏**`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按块进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 msg_id/ts),平台去重窗口建议 10 分钟(与 event_report 同策略)。
## 6.1 失败告警(扩展 event_report
刷写整体失败(重试 ×3 仍失败)时,设备经 `event_report` 上报(**平台必答**,复用 §5.3 ACK + 重发机制):
```json
{
"msg_id": 502,
"cmd": "event_report",
"ts": 1719000008,
"data": {
"events": [
{ "type": "ota_error", "ch": 0, "value": 1003 }
]
}
}
```
| type | value | 说明 |
|------|-------|------|
| `ota_error` | 0x1000 起 | `0x1001`=启动帧无响应 / `0x1002`=地址帧错误 / `0x1003`=数据块 ACK 超限 / `0x1004`=全镜像校验失败 / `0x1005`=安全窗口拒绝后强制失败 |
---
# 7 安全设计(会车产品底线)
| # | 措施 | 说明 |
|---|------|------|
| 1 | **升级窗口检查** | `ota_flash` 前检查 4 通道无车;有车 → `code=3`,平台提示延迟。`force=true` 可跳过(高风险,需平台权限控制) |
| 2 | **维持 Loop 现状** | 刷写期间 Loop 行为与现网 BLE OTA 升级一致(复位进 bootloader → ISP 刷写 → 复位跑新 APP),继电器 GPIO 状态按当前硬件实测状态接受,不额外干预(BLE 通道已验证可用) |
| 3 | **双重 CRC** | 单片 CRC32(传输层抓错)+ 全镜像 CRC32(落盘完整性);刷写前读回复核 |
| 4 | **失败可重入** | 下载失败 → 断点续传;刷写失败 → 重试 ×3 → 保留镜像可重刷;AT32 ISP bootloader 兜底,不会真砖 |
| 5 | **版本回滚** | Slot A/B 双槽;平台可 `ota_begin` 指定槽刷旧版(`force=true` 覆盖) |
| 6 | **审计留痕** | 升级开始/结果写 offlog 事件日志(含目标版本、slot、结果码);平台侧 version 字段归档 |
| 7 | **能力探测** | 老固件 `ota_*``code=4`;平台按 `ota_status`/`dev_info_query.soft_ver` 判断,不盲目下发 |
| 8 | **幂等与防呆** | 重复片幂等 ACK;offset 乱序拒绝并要求续传;同版本默认拒绝重刷(force 绕过) |
| 9 | **会话期间静默** | OTA 会话期间(begin ~ 结束)暂停 MQTT `event_report` 发送(队列积压,结束后补发)与 offlog/快照落盘("升级开始"日志在暂停前写入、"升级结果"在恢复后补记);避免刷写窗口与上报/日志抢 SPI 总线与主循环 |
---
# 8 错误码约定
顶层 `code` 沿用通用语义(0 成功 / 1 参数 / 2 密码 / 3 忙 / 4 不支持 / 5 内部 / 6 超长),OTA 细分错误经 `data.err_code` 表达:
| err_code | 场景 | 顶层 code |
|----------|------|-----------|
| 1 | 单片 CRC 失败 / 乱序缺片(data.offset 指示续传点) | 1 |
| 2 | 会话状态不允许(未 begin / 非 downloading 收 ota_data | 3 |
| 3 | 安全窗口拒绝(有车压线圈) | 3 |
| 4 | 版本冲突(同版本且非 force) | 1 |
| 5 | 全镜像 CRC 不匹配(data.crc_ok=false | 5 |
| 6 | 暂存区写失败(SPI 异常/满) | 5 |
---
# 9 平台侧实现要点(edc_server / DBNMQTTool
1. **hex↔bin**`data` hex 解码(512 hex = 256B);bin 由 Intel HEX 转换(固定 0x08003400 偏移),转换在平台完成
2. **CRC32**`zlib.crc32(bin)` 全镜像;单片 `zlib.crc32(chunk)` 与设备逐字节一致(ISO-HDLC 即 Python 内置)
3. **分片循环**`for offset in range(0, size, 256)` 顺序下发,收到 `code=1 + data.offset` 时从该处续传;单片失败重发 ≤3 次后 abort
4. **断点续传**:会话中断后重新 `ota_begin`,读 `data.offset` 续传
5. **结果确认**:刷写启动后轮询 `ota_status`(或订阅 `ota_report`);`stage=done` 后经 dev_info_query 核对 Loop 版本(需 Loop 侧支持版本上报,见 §10 待办)
6. **告警闭环**:收到 `event_report{type:ota_error}` 先落库后应答
7. DBNMQTTool 增加 OTA 页签(工具先行惯例:先模拟分片下发,固件后到)
---
# 10 待板上验证项(实现前必须闭环)
| # | 项 | 影响 |
|---|-----|------|
| 1 | DBN UART2 TX 缓冲容量(能否容纳 254B A7 帧;现 BLE 透传块 ≤94B) | 刷写块大小 |
| 2 | W25Qxx 写 256B 页 + 4KB 扇区擦除在 MQTT 接收回调内的耗时(阻塞窗口 vs MQTT 保活) | 单片落盘是否需移主循环 |
| 3 | DBN RAM 预算:镜像块缓冲(248B)+ CRC 查表(1KB)是否挤占现有栈余量(历史 .bss 事故) | 内存方案 |
| 4 | Loop APP 当前固件实际 bin 大小(验证 100KB 槽余量) | 槽位容量 |
| 5 | 刷写期间 MQTT 保活验证(设计已保证非阻塞 tick 驱动,PINGREQ/心跳/IWDG 喂狗不受阻塞;板级抓包确认 6~8s 窗口内无 PINGREQ 超时断连、IWDG 不复位) | 刷写中断安全 |
---
# 11 与现有协议的关系
| 协议文档 | 变更 |
|----------|------|
| `DLD960_IoT_MQTT协议.md` | 本设计并入后升 **V1.08**:§3 命令表 + §4.19~4.24ota_* 详情)+ §5.5ota_report+ §5.3 事件类型表补 `ota_error` + §8 错误码补 err_code 约定 + 修订记录 |
| `DLD960_TCP_JSON协议.md` | 本版**不扩展**ROADMAP 先走 MQTT);后续按"复用命令 + stream/字段"模式补(与 log_* 扩展同套路) |
| `DLD960_BLE协议.md` | 不变(BLE OTA 维持流式透传现状;本地刷写状态机与 BLE 透传共用 `lup_feed_byte_ota` |
| `README.md` / `DLD960_技术规格书.md` | 协议矩阵同步 V1.08;规格书补 OTA 分区/安全约束(并入主文档时执行) |
**固件代码落点(协议拍板后动):**
- `iot_mqtt_srv.c`:命令分发链加 6 个 ota_* 分支(现有 if-else 链)
- 新增 `ota_srv.c/h`:会话状态机 + W25Qxx 暂存读写 + CRC32 查表
- `usart_biz.c` / `loop_uart_proto.c`:本地刷写状态机(复用 `lup_feed_byte_ota` 解析 + `g_flag_counter_ota` 门控,注意退出机制——现 BLE 透传 flag 无退出点,本地刷写必须自管)
- `offlog.c`:补"固件升级开始/结果"事件类型
- 单测:`tests/test_ota_srv.c`gcc 提取+嵌入模式,mock SPI/MQTT/Loop
---
# 修订记录
| 版本 | 时间 | 说明 |
|------|------|------|
| V1.00 | 2026-08-20 | 设计稿:Loop MCU MQTT 远程 OTA 先存后刷协议(依据 ROADMAP P1.4 ①) |
| V1.01 | 2026-08-20 | 按修改意见修订:①Slot A/B 容量 60KB→**100KB**(镜像上限 96KB,为 DBN 镜像预留)②删除"继电器 GPIO 默认态"待验证项,维持 Loop 现状(与 BLE OTA 行为一致,不额外干预)③新增"OTA 会话期间静默"约束:暂停 MQTT `event_report` 发送(队列积压结束后补发)+ offlog/快照落盘(开始/结果日志除外)④刷写调度明确**非阻塞 tick 驱动**,刷写窗口内 MQTT 保活(PINGREQ/心跳/IWDG 喂狗)不受影响 |
+105
View File
@@ -119,6 +119,9 @@ Client Device (DLD960)
| `loop_param_set` | 设置车检器多路参数 | 是 | 0x63 | | `loop_param_set` | 设置车检器多路参数 | 是 | 0x63 |
| `loop_param_query` | 读取车检器多路参数 | 是 | 0x64 | | `loop_param_query` | 读取车检器多路参数 | 是 | 0x64 |
| `report_config` | 设置主动上报 | 是 | 0xC5 | | `report_config` | 设置主动上报 | 是 | 0xC5 |
| `log_stat` | 查询脱机日志统计(事件/快照流) | 是 | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | 是 | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | 是 | — |
| 主动推送 | | | | | 主动推送 | | | |
| `loop_data` | 线圈传感数据(设备→客户端) | — | 0xC0 | | `loop_data` | 线圈传感数据(设备→客户端) | — | 0xC0 |
| `event_report` | 事件上报(设备→客户端,**客户端须应答**,见 §5.2) | — | — | | `event_report` | 事件上报(设备→客户端,**客户端须应答**,见 §5.2) | — | — |
@@ -342,6 +345,106 @@ Client Device (DLD960)
--- ---
## 4.16 查询脱机日志统计 `log_stat`
> 设备本地 W25Qxx 环形日志(事件区/快照区容量随存储芯片动态,掉电不丢)。用于日志拉取前的分页定位。
> 通过 `data.stream` 区分日志流:`event`(事件日志,缺省)/ `snapshot`(传感快照)。
**请求:**
```json
{"msg_id":16,"cmd":"log_stat","ts":1719000000,"data":{"stream":"event"}}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应 datastream=event):**
```json
{"stream":"event","enabled":true,"boot_seq":2,"count":1234,"capacity":16256,"seq_first":100,"seq_last":1333}
```
**响应 datastream=snapshot):**
```json
{"stream":"snapshot","enabled":true,"boot_seq":2,"count":1234,"capacity":48064,"seq_first":100,"seq_last":1333}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event` / `snapshot` |
| `enabled` | bool | 日志功能是否启用(Flash 初始化成功) |
| `boot_seq` | uint16 | 当前启动序号(每次上电 +1,区分复位段) |
| `count` | uint32 | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| `capacity` | uint32 | 容量上限(**随存储芯片与流动态**):事件流 W25Q32=16256 / Q64=32640 / Q128=65408 / Q256=130944;快照流 W25Q32=48064 / Q64=105408 / Q128=220096 / Q256=449472 |
| `seq_first` | uint32 | 逻辑首条记录全局序号(`seq_last - count + 1`count=0 时为 0 |
| `seq_last` | uint32 | 最新一条记录全局序号(跨 boot 单调递增) |
---
## 4.17 分页拉取脱机日志 `log_query`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**(hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`。
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),上位机按《DLD960 BLE 协议》字段表解析。
**请求:**
```json
{"msg_id":17,"cmd":"log_query","ts":1719000000,"data":{"stream":"event","start_seq":1330,"count":4}}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 data**
```json
{"start_seq":1330,"records":[{"seq":1330,"hex":"a53200000200000001000000..."}]}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**(flash 存储字节原样,小端) |
**解析字段表(与 BLE 通道完全一致):**
| 流 | 结构 | 字段表 |
|----|------|--------|
| `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算。
---
## 4.18 清除脱机日志 `log_clear`
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——谁在何时清了日志,留痕不可清除)。客户端应做权限控制。
**请求:**
```json
{"msg_id":18,"cmd":"log_clear","ts":1719000000,"data":{"stream":"event"}}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应:** 标准成功/失败。
- `stream=event`:成功后 `log_stat``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。**阻塞 ~2.8s**(63 个数据扇区 SPI 擦除),请勿高频调用。
- `stream=snapshot`:成功后 `log_stat``count` 归 0`seq_last` 继续递增;清除动作写入事件流审计(`log_clear`,payload 标记快照流)。**阻塞 ~45ms**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
---
# 5 设备主动推送 # 5 设备主动推送
主动推送帧与请求-响应共用同一条 TCP 连接,由设备在任意时刻发出。 主动推送帧与请求-响应共用同一条 TCP 连接,由设备在任意时刻发出。
@@ -457,3 +560,5 @@ Client Device (DLD960)
|------|----------|----------|--------| |------|----------|----------|--------|
| V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq | | V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq |
| V1.01 | 2026-07-15 | `event_report` 增加**客户端必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、客户端去重与先落库后应答要求(与 MQTT 协议 V1.04 对称) | wangfq | | V1.01 | 2026-07-15 | `event_report` 增加**客户端必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、客户端去重与先落库后应答要求(与 MQTT 协议 V1.04 对称) | wangfq |
| V1.02 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);事件类型表与 MQTT 协议 V1.06 对齐 | wangfq |
| V1.03 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
+183
View File
@@ -0,0 +1,183 @@
# DLD960 四通道车辆检测器 产品手册
> Product Manual · 产品型号:**DLD960GA**
> 文档版本 V1.02 · 2026-08-19 · 适用固件:整机 V1.02.04Loop 1.0 + DBN 1.02.04
---
## 1. 产品简介
DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖四条车道/四个检测断面。采用双 MCU 架构:检测核心专注线圈信号的实时处理,通信核心提供蓝牙、以太网、4G 多种接入方式,可直连道闸控制器,也可对接云平台做流量统计与远程运维。
**典型应用场景**
- 停车场出入口道闸联动(存在检测、防砸车压线检测)
- 分车道流量计数(进出计数、车间距/通过时间统计)
- 单通道会车系统的车辆存在/方向输入
- 传感数据上云(MQTT 接入业务平台)
**核心特性**
- 四路线圈独立检测,10ms 采样周期,进入确认 + 斜率限幅双重抗干扰
- 四档灵敏度、四档工作频率,支持智能选频抑制相邻线圈串扰
- 基线自适应跟踪:温漂、雨天频率漂移自动补偿,不误报不漏检
- 四个双路继电器:存在/脉冲输出、方向判别、输出延时逐路可配
- 蓝牙小程序现场配置,双 MCU 均可无线升级(OTA/ISP 透传)
- 以太网双协议:TCP JSON(局域网对接)+ MQTT(云平台),关键事件确认送达
- 通信可靠性加固:串口硬件 DMA 接收 + 固件内存优化,长时间运行稳定不丢帧、不异常重启
- BLE OTA 升级链路加固:检测 MCU 升级响应帧(0x9F 协议)完整透传回小程序,停等 ACK 不再丢失,升级成功率提升
## 2. 硬件说明
### 2.1 接口一览
| 接口 | 数量 | 说明 |
|------|------|------|
| 线圈接口 | 4 路 | 接环形地感线圈(LPA~LPD) |
| 继电器输出 | 4 个双路 | RLY1~RLY4,默认关联线圈 1~4 |
| 以太网口 | 1 路 | 10MTCP JSON / MQTT |
| TTL串口 | 1 路 | DLD960 内部 两个MCU之间的 串口通信协议 |
| 蓝牙 | — | 小程序配置与升级 |
| 按键 K1 | 1 个 | 多功能按键(复位) |
### 2.2 指示灯
| 指示灯 | 颜色 | 状态说明 |
|--------|------|---------|
| LED_PWR | 红色 | 呼吸闪烁 = 系统正常运行 |
| LED_LP1~LP4 | 绿色 | 对应线圈 1~4:有车亮 / 无车灭 |
| ETH_LED_G / ETH_LED_Y | 绿/黄 | 网口连接/活动指示 |
> 上电后 LED_PWR 呼吸、四路车检灯全灭(无车),约 1.3 秒完成基线建立进入检测状态。若某路车检灯常亮且现场无车,参见 §7 故障排查。
### 2.3 线圈施工要求(关键!)
线圈质量直接决定检测效果,施工务必达标:
| 项目 | 要求 |
|------|------|
| 线圈形状 | 矩形/八角形,典型 2m × 1m(依车道宽度调整) |
| 匝数 | 4~6 匝(视周长,目标电感 80~300 μH) |
| 线材 | ≥1.0mm² 耐高温多股软线(如硅橡胶线) |
| 槽深 | 30~50mm,切槽后清理干净,线圈压实密封 |
| 引线 | 双绞(≥20 绞/米),长度尽量短(≤100m),不与动力电缆同管 |
| 相邻线圈间距 | ≥1m;无法保证时开启智能选频并错开频率档 |
| 绝缘 | 线圈对地绝缘电阻 > 10 MΩ(500V 摇表) |
## 3. 快速上手
1. **接线**:接入线圈、继电器输出至道闸/控制器,需要联网则接网线。
2. **上电**LED_PWR 呼吸闪烁,约 1.3 秒后进入检测状态(上电 3 秒内不主动上报数据)。
3. **配置**:手机小程序经蓝牙连接设备 → 验证密码 → 设置各路灵敏度/频率/输出模式。
4. **验证**:车辆压线,对应绿色车检灯亮、继电器吸合;离开后灯灭、继电器释放。
5. **联网(可选)**:小程序或 TCP 命令配置 IP/MQTT 参数,接入平台。
## 4. 参数配置
四种配置通道功能等价:**蓝牙小程序**(现场推荐)、**UART TTL 串口**、**TCP JSON**、**MQTT**(远程运维)。
### 4.1 每通道可配参数
| 参数 | 出厂默认 | 说明 |
|------|---------|------|
| 灵敏度 | 档位 1(中) | 4 档;大车/干扰大调低,摩托车/高底盘车调高 |
| 工作频率 | 各路错开 | 4 档(高/中高/中低/低);相邻线圈必须错开档位 |
| 输出模式 | 存在输出 | 存在 / 脉冲 |
| 输出延时 | 0 | 继电器动作延时 |
| 方向判别 | 触发模式 | 双线圈可组方向判别 |
| 有限存在 | 0(永久) | 非 0 时,车辆持续占用超时后自动复位重建基线(单位 10 秒) |
| 线圈安全模式 | 0(关闭) | 线圈异常保护超时(单位 10 秒) |
### 4.2 灵敏度选择建议
| 场景 | 建议档位 | 说明 |
|------|---------|------|
| 标准轿车出入口 | 1~2 | 兼顾灵敏与抗干扰 |
| 大型车/货车通道 | 0~1 | 高底盘需要覆盖,但防相邻车道干扰 |
| 摩托车/非机动车混行 | 2~3 | 高灵敏度,注意误触发风险 |
| 强电磁干扰环境(变频器等) | 0~1 + 错开频率档 | 灵敏度让位于稳定性 |
### 4.3 网络配置要点
- **TCP JSON**:默认端口 **5960**,连接后须先 `pwd_verify` 鉴权,未鉴权命令一律拒绝。
- **MQTT**:配置 Broker 地址与设备 Topic 后,设备上电自动发布 `initialize` 上线消息;平台应答 `report_config` 下发时间戳完成时钟同步并配置上报间隔。
- 设备序列码(dev_serial)是 MQTT Topic 的组成部分(`dld960/{sn}/srv|dev`),出厂前须唯一设置。
## 5. 数据上报(联网模式)
| 消息 | 触发 | 内容 |
|------|------|------|
| `initialize` | 上电联网 | 序列码、型号、软硬件版本 |
| `loop_data` | 周期性(空闲按配置间隔 / 有车活动 300ms / 车状态翻变立即) | 各通道频率、变化量、车状态 |
| `event_report` | 事件发生 | 进车/出车/线圈断开,**带确认重发**(5s 超时 ×3 次,平台须应答) |
| `heartbeat` | 周期 | 在线保活、运行时长 |
时间量(通过时间、车间距)分辨率 50ms,可用于速度估计与车型粗分类。
### 5.1 脱机数据取证(蓝牙 BLE)
设备本地 W25Q32 环形存储**事件日志**(容量 8064 条,掉电不丢)与**传感快照**(0xC0 传感波形按上报节奏落盘)。断网/故障期间事件照常记录,可事后经蓝牙小程序或 APP 读取:
- **事件日志**:上电/复位原因、MQTT 连接/断开/重连、event_report 重发/放弃、线圈事件、时钟同步锚点、日志清除审计——用于区分"设备真复位"与"MQTT 断连重连",故障溯源
- **传感快照**:断网期间 4 路线圈波形回放,用于信号质量分析
- **BLE 命令**OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR0x25~0x27+ SNAP_STAT / SNAP_QUERY / SNAP_CLEAR0x28~0x2A),与 MQTT/TCP 的 log_stat / log_query / log_clear 同语义
- **审计留痕**:日志/快照清除操作本身写入事件流,不可清除
详见《DLD960 BLE 协议》V1.02。
## 6. 固件升级
| 对象 | 方式 | 操作 |
|------|------|------|
| 通信 MCU | 蓝牙 OTA | 小程序选择固件直接升级 |
| 检测 MCU | ISP 透传 | 小程序经蓝牙→通信 MCU 串口透传升级 |
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.04。
>
> ⚠ **检测 MCU 升级完成后,若小程序无响应,请将设备断电重启**(DBN OTA 透传模式需重启恢复,V1.02.03 已知约束)。
## 7. 故障排查
| 现象 | 可能原因 | 处理 |
|------|---------|------|
| LED_PWR 不亮/不呼吸 | 供电异常 / 主板故障 | 查电源电压,重新上电 |
| 无车但车检灯常亮 | 线圈附近有大金属体移入;基线未收敛 | 移除金属物;等待 10s 冻结超时自适应,或断电重启重建基线 |
| 有车不触发 | 灵敏度过低;线圈匝数不足/电感异常;底盘过高 | 提高灵敏度档位;实测线圈电感;摩托车场景用 3 档 |
| 车检灯随机闪烁(无车误触发) | 相邻线圈串扰;变频器等电磁干扰;线圈绝缘劣化 | 错开频率档+开智能选频;降低灵敏度;摇表测绝缘 |
| 雨天频繁误报 | 线圈密封失效,频率漂移超阈值 | 基线自适应可吸收慢漂移;快速漂移需重新密封线圈 |
| 继电器输出正常但平台无数据 | 网络配置错误;MQTT Topic/序列码不符 | 查 IP/Broker 配置;核对 dev_serial 与 Topic |
| 设备频繁自动重启 / 偶发数据丢失 | 通信 MCU 旧固件资源不足(V1.02.01 已修复) | 升级整机固件至 V1.02.01 及以上版本 |
| BLE OTA 升级失败/进度卡住 | 通信 MCU 旧固件 0x9F 透传缺陷(V1.02.03 已修复) | 升级整机固件至 V1.02.03 及以上版本 |
| 配置无法保存/每次上电恢复出厂 | 旧固件 factory 写入被禁用;换新存储芯片后参数区无出厂配置(V1.02.04 已修复) | 升级整机固件至 V1.02.04 及以上版本 |
| 事件重复上报 | 平台未按协议应答 event_report | 平台须回显 msg_id 应答(先落库后应答) |
| 线圈断开告警 | 线圈开路/引线断裂 | 万用表量线圈回路电阻(正常几欧姆以内) |
## 8. 技术支持信息
排障时请提供以下信息,可大幅加快定位:
1. 整机固件版本(小程序设备信息页 / `dev_info_query`
2. 现场线圈参数:尺寸、匝数、实测电感、引线长度
3. 干扰源情况:变频器、大功率设备、相邻车道线圈间距
4. 联网问题附 `loop_data` 中该通道的频率与变化量数值(后台可据此判断信号质量)
**相关文档**
| 文档 | 用途 |
|------|------|
| 《DLD960 技术规格书》 | 完整技术参数 |
| 《DLD960 串口通信协议》V1.01 | 串口 对接开发 |
| 《DLD960 TCP JSON 协议》V1.03 | 局域网对接开发 |
| 《DLD960 IoT MQTT 协议》V1.07 | 云平台对接开发 |
| 《DLD960 BLE 协议》V1.02 | 蓝牙小程序/APP 对接开发(脱机日志与传感快照读写) |
| 《环路车辆检测器验收标准》 | 采购/部署验收 |
---
## 修订记录
| 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------|
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04BLE OTA 升级链路加固说明(0x9F 响应帧透传,停等 ACK 完整);故障排查新增 OTA 失败 + 配置无法保存条目;固件升级补断电重启提示;存储兼容适配(去厂商代码 + 恢复 factory 写入) | wangfq |
| V1.01 | 2026-08-18 | 配套整机发布 V1.02.01:通信稳定性修复说明(UART2 DMA 防丢帧、异常重启修复);新增 BLE 脱机日志/传感快照读写说明(§5.1) | wangfq |
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
+191
View File
@@ -0,0 +1,191 @@
# DLD960 四通道车辆检测器 技术规格书
> Technical Specification · 产品型号:**DLD960GA**
> 文档版本 V1.02 · 2026-08-19 · 配套整机发布 V1.02.04
---
## 1. 产品概述
DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆检测器,采用双 MCU 架构:检测 MCU 专注线圈信号采集与检测算法的实时性,通信 MCU 承载蓝牙、以太网、4G 多通道接入与云平台对接。适用于停车场出入口控制、道闸联动、分车道流量计数、会车控制等场景。
## 2. 系统架构
```
线圈1~4 ──► AT32F421(检测 MCU)──► 继电器1~4(存在/脉冲输出)
│ UART 0x7F 私有协议 @192000bps
CH32V208(通信 MCU)──► ETH / BLE / UART TTL / SPI Flash
```
| 项目 | 检测 MCU | 通信 MCU |
|------|---------|---------|
| 型号 | 雅特力 AT32F421ARM Cortex-M4 | 沁恒 CH32V208RISC-V |
| 主频 | 120 MHz | 120 MHz(芯片最高 144 MHz,启用网络时限定 120 MHz |
| Flash / SRAM | 64 KB / 16 KB | 128 KB / 64 KB |
| 职责 | 四路线圈驱动、频率采样、车辆检测算法、继电器输出 | BLE、以太网(TCP JSON / MQTT)、UART TTLRX DMA)、参数与日志外部存储 |
| RTOS/协议栈 | FreeRTOS V10.4.3 | WCH BLE 协议栈 + WCHNET TCP/IP |
双 MCU 间通过 TTL 串口互联(0x7F 私有帧协议,波特率 192000bpsUART2 RX 硬件 DMA 循环接收,抗打印关中断丢帧),传感数据主动上推、配置命令透传。OTA 升级时 UART2 RX 切换 0x9F 帧协议(DMA poll 按 `g_flag_counter_ota` 模式选择解析器),检测 MCU bootloader 的响应帧(pre_ok/addr_ok/data ACK)经 BLE 完整透传回小程序(V1.02.03)。
## 3. 检测规格
### 3.1 检测通道
| 参数 | 规格 |
|------|------|
| 检测通道数 | 4 路独立线圈通道 |
| 检测原理 | LC 振荡频率法(TMR3 四通道输入捕获) |
| 检测采样周期 | 10 ms/通道(算法主循环 tick |
| 工作频率档位 | 4 档(高 / 中高 / 中低 / 低),每路独立两级调频电容(33nF / 10nF)组合切换 |
| 智能选频 | 支持(smart_mode,自动错开相邻通道频率,抑制串扰) |
| 线圈电感适应范围 | 待确认(典型环形线圈 2~6 匝,需现场实测电感与振荡频率) |
### 3.2 灵敏度
进入阈值 = 基线 × SensTable[档位] / 65536,释放阈值带 ~50% 滞回(防止临界抖动)。
| 灵敏度档位 | 进入阈值(Δf/f) | 释放阈值(Δf/f) | 说明 |
|-----------|-----------------|-----------------|------|
| 0 | ≈ 0.330% | ≈ 0.165% | 最低灵敏度(大车/高底盘场景抗干扰) |
| 1 | ≈ 0.165% | ≈ 0.110% | |
| 2 | ≈ 0.055% | ≈ 0.027% | |
| 3 | ≈ 0.015% | ≈ 0.014% | 最高灵敏度(摩托车/高底盘车检出) |
### 3.3 检测算法(Loop 固件 1.0
| 机制 | 参数 | 作用 |
|------|------|------|
| IIR 低通滤波 | α = 79/256,τ ≈ 32 ms @10ms tick | 平滑高频噪声,保留车辆信号 |
| 斜率限幅 | 单步最大变化 5%(低基值下限 100) | 削除瞬态尖峰(继电器动作、电磁干扰) |
| 进入确认 | 连续 3 个采样周期低于阈值 | 抗尖峰误触发 |
| 基线跟踪 | 滑动平均窗口 500 样本(5s 更新周期,噪声抑制 22×) | 跟随温漂/环境慢变 |
| 基线冻结保护 | 偏差超限即冻结基线更新 | 防止过车信号污染基线 |
| 冻结超时自适应 | 10 s 超时 + ±2% 稳定性窗校验 | 温漂/换线圈后自动重建基线;慢速进车不误吸收 |
| 上电稳定期 | 128 样本(≈1.3 s)快速收敛,上电 3 s 内抑制主动上报 | 快速可用且不上报无效数据 |
| 线圈安全模式 | 可配置超时(单位 10 s,0=关闭) | 异常长时间占用强制复位重建基线 |
### 3.4 检测性能指标(设计目标,依据《环路车辆检测器验收标准》v1.0)
| 指标 | 设计目标 | 验收优秀档 |
|------|---------|-----------|
| 存在检测响应时间 T_onV2~V5 车型) | ≤ 100 ms | ≤ 50 ms |
| 释放时间 T_off | ≤ 300 ms | ≤ 100 ms |
| 漏检率(标准车型 30~40 km/h 通过) | 0/50 | 0/50 |
| 无车频率抖动 RMS | ≤ 3 Hz | ≤ 1 Hz |
| 基准频率漂移(10 min | ≤ 0.05% × f₀ | ≤ 0.01% × f₀ |
| 信噪比 SNR(标准轿车) | ≥ 35 dB | ≥ 45 dB |
> 以上指标基于标准化测试线圈与实验室环境,现场性能受线圈施工工艺、引线长度、电磁环境影响,以现场验收为准。
## 4. 输出规格
| 参数 | 规格 |
|------|------|
| 继电器数量 | 4 个双路继电器(RLY1~RLY4,默认一一关联线圈 1~4 |
| 输出模式 | 存在输出 / 脉冲输出(每路独立可配 output_mode |
| 输出延时 | 每路独立可配(delay_time |
| 方向判别 | 支持(direction_mode:触发模式 / 方向判别模式) |
| 有限存在 | 每路独立可配(exist_mode,单位 10 s,0=永久存在;超时全通道重启重建基线) |
| 触点容量 | 待确认(依整机原理图/继电器选型) |
## 5. 通信接口
| 接口 | 规格 | 用途 |
|------|------|------|
| 以太网 | 10MCH32V208 内置 PHY),支持 DHCP/静态 IP | TCP JSON 服务(默认端口 5960)、IoT MQTT 客户端 |
| 蓝牙 | BLE(WCH 低功耗蓝牙协议栈) | 小程序配置、OTA 升级、Loop MCU ISP 透传 |
| TTL 串口 | 1 路 | DLD960 内部两个MCU之间 的串口通信协议(Modbus 风格私有帧);UART2 RX 硬件 DMA 循环接收(抗打印关中断丢帧);OTA 升级态支持 0x9F 帧透传(V1.02.03 |
| TTL 串口 | 1 路(RFU | 预留 4G 模块扩展 |
| 外部存储 | SPI NOR Flash | 参数存储 + 日志存储 |
| 按键 | K1 多功能按键 | 复位等 |
### 5.1 协议矩阵(V1.02.04 配套)
| 协议 | 版本 | 通道 | 要点 |
|------|------|------|------|
| DLD960 串口通信协议 | V1.01 | TTL | 设备管理、参数配置、数据上报 |
| DLD960 TCP JSON 协议 | V1.03 | ETH :5960 | 密码鉴权 + 18 条命令;event_report 客户端必答(5s×3 重发);脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
| DLD960 IoT MQTT 协议 | V1.07 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
| DLD960Loop 串口协议 | V1.05 | MCU 间(内部) | 0x7F 帧、0xC0 传感上报(variation 3B 有符号) |
| DLD960 BLE 协议 | V1.02 | 蓝牙 BLE | 帧格式 + 分包;脱机日志 OFFLOG_STAT/QUERY/CLEAR + 传感快照 SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
### 5.2 数据上报能力
| 上报项 | 说明 |
|--------|------|
| loop_data 传感数据 | 各通道频率、变化量 variation(有符号,正=车辆/裕量,负=反向漂移)、车状态;空闲按配置间隔 / 活动 300ms / 车状态翻转沿立即上报 |
| event_report 事件 | 进车 / 出车 / 线圈断开等关键事件,带确认重发机制(不可再生数据闭环送达) |
| 时间量 | 通过时间、车间距,分辨率 50 ms |
| 继电器输出次数 | 累计计数上报 |
| heartbeat 心跳 | 含 uptime |
| 时钟同步 | 设备无 RTC,平台经 report_config 下发 Unix 时间戳校准,上行 ts 为真实时间 |
## 6. 配置参数(每通道独立)
| 参数 | 范围 | 说明 |
|------|------|------|
| 灵敏度 sensitvity | 4 档 | 见 §3.2 |
| 工作频率 loopFreq_Level | 4 档 | 高/中高/中低/低 |
| 输出延时 delay_time | 0~255 | 继电器动作延时 |
| 输出模式 output_mode | — | 存在/脉冲 |
| 方向判别 direction_mode | 0/非0 | 触发模式 / 方向判别 |
| 线圈安全超时 loopSafe_Timeout | 0~255,单位 10s | 0=关闭 |
| 存在模式 exist_mode | 0~255,单位 10s | 0=永久存在 |
| 智能选频 smart_mode | 0/1 | 全局 |
配置通道:BLE 小程序 / UART TTL / TCP JSON / MQTT 四通道均可读写,配置持久化存储于检测 MCU 内部 Flash。
## 7. 固件升级
| 对象 | 方式 | 路径 |
|------|------|------|
| 通信 MCU (CH32V208) | BLE OTA | 手机小程序直接升级 |
| 检测 MCU (AT32F421) | ISP 透传 | 小程序 → BLE → CH32V208 → UART → AT32F421UART2 0x9F 停等协议(0xA7 每块必回 ACK),DBN 将 ACK 透传回小程序(V1.02.03 |
## 8. 电气与环境(待整机定型确认)
| 参数 | 规格 |
|------|------|
| 供电电压 | 待确认(依电源方案) |
| 整机功耗 | 待确认 |
| 工作温度 | 待确认(建议按 -25℃ ~ +75℃ 车检器行业规格验证) |
| 存储温度 / 湿度 | 待确认 |
| 外形尺寸 / 安装方式 | 待确认(导轨/壁挂,依结构件) |
| 防护等级 | 待确认 |
| EMC | 建议依 EN 50293 / GB/T 26941.3 验证 |
## 9. 引脚资源摘要
详见《DLD960硬件资源》。关键资源:
- **AT32F421**TMR3_CH1~4 线圈捕获(PA6/PA7/PB0/PB1)、8 路调频电容控制、RLY1~4PB4~PB7)、LED_PWR 红色呼吸灯(PA8+ 4 路绿色车检灯、USART1PA9/PA10 ↔ CH32V208
- **CH32V208**:内置 ETH PHYPC6~PC9+ 双色 ETH LED、USART2PA2/PA3 ↔ AT32F421)、扩展 USART1PB6/PB7RFU 4G)、K1 按键(PA0)、SPI1 外部 FlashPA4~PA7
## 10. 版本配套要求
> ⚠ **自内部协议 V1.05 起,vd960Loop 与 vd960DBN 固件必须同版本配套刷写**variation 字段 2B→3B 帧格式变更,混刷导致数据静默错位)。
| 组件 | V1.02.04 配套版本 |
|------|-------------------|
| vd960Loop 固件 | 1.0(不变) |
| vd960DBN 固件 | 1.02.04 |
### 10.1 已知约束(V1.02.01
- 固件版本三段式:`FIRMWARE_VER="1.02.04"`MAIN=1 / SUB=2 / SUBSUB=4);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)。
- CH32V208 RAM 预算:BLE 协议栈固定占用低 16KB(不可裁剪),用户可用 48KB;固件经 RAM 瘦身优化,栈余量约 6KB,现场升级务必使用 V1.02.01 及以上版本。
- 传感快照区延后 3s 初始化(开机 3s 内传感数据不落盘,设计接受)。
- UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)。
- **OTA 透传模式无自动退出机制**:进入 OTA 态(`g_flag_counter_ota.flag=1`)后需断电重启才恢复 0x7F 正常通信(V1.02.03 已知约束,升级完成后重启设备)。
---
## 修订记录
| 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------|
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出;存储适配(SPI 识别去厂商代码 + factory 写入恢复) | wangfq |
| V1.01 | 2026-08-18 | 配套整机发布 V1.02.01:版本矩阵更新、UART2 RX DMA 通信可靠性说明、已知约束补充 | wangfq |
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
+2 -2
View File
@@ -2,9 +2,9 @@
# 概述 # 概述
vd\_960是一个四通道车辆检测器,支持网口、RS485、蓝牙等。功能说明: vd\_960是一个四通道车辆检测器,支持网口、蓝牙、4G扩展(RFU等。功能说明:
* 双核心(双MCU),雅特力芯片AT32F421120MHz,64KB Flash,16KB SRAM)负责线圈功能,沁恒CH32V208144MHz,实际采用的是120MHz 128KB Flash, 64KB SRAM)负责通信(蓝牙、网口,一路RS485和一路TTL串口)和外部存储(参数存储和日志存储) * 双核心(双MCU),雅特力芯片AT32F421120MHz,64KB Flash,16KB SRAM)负责线圈功能,沁恒CH32V208144MHz,实际采用的是120MHz 128KB Flash, 64KB SRAM)负责通信(蓝牙、网口,两路TTL串口:一路内部对接AT32F421,一路预留4G模块上云)和外部存储(参数存储和日志存储)
* 四路线圈,四个双路继电器。 * 四路线圈,四个双路继电器。
+230
View File
@@ -0,0 +1,230 @@
# vd960 (DLD960) 开发计划 — V1.0.0 之后
> 制定日期:2026-07-17 · 基线:整机 V1.0.02026-07-16 发布)
> 原则:先稳固、再上云、后算法深化;任何涉及继电器输出的改动,防砸车逻辑优先。
---
## 0. 现状基线
| 项 | 状态 |
|----|------|
| 整机版本 | V1.0.0(首发),Loop/DBN 固件 1.0 配套 |
| 协议 | Loop 串口 V1.05 / TTL 串口 V1.01 / TCP JSON V1.01 / IoT MQTT V1.05 |
| 在途改动(未提交) | BLE 透传路径重构:`usart_biz.c` 0x7F→0x8F 帧改走 `set_response_tran_to_notify()`,原 `_report_flag` 保留机制移除 |
| 已知约束 | 无 RTC(依赖平台下发 ts);CH32V208 栈紧张;Loop/DBN 固件禁止混刷 |
---
## P0 — 发布后稳固(目标 V1.1.0,约 2~4 周)
### P0.1 BLE 透传路径重构收尾 🔴(在途)
当前工作区改动把 0xC0 传感帧的 BLE 转发从"置 flag 延迟消费"改为"立即 notify"。收尾必须核对三件事:
1. **帧消费方真值表**0xC0 帧现在有 **三个消费方**TCP JSON 回调 / MQTT `iot_sensor_ingest` / BLE notify)。2026-07-15 的"陈旧快照 34 秒"事故根因就是消费路径数据源分裂——BLE 加入后必须重画一张"谁消费、谁清理(InitPkgUart)"的真值表,确保:
- 每条路径都能拿到帧(同源汇聚不漏帧);
- 帧最终一定被清理(无滞留 → 无假"新帧");
- BLE ACS 分支改 Magic 为 0x8F 后,MQTT/TCP 路径若还要读同一缓冲,读到的是被改过的帧头(**待确认是否有影响**)。
2. `set_response_tran_to_notify()` 新增返回值的语义与调用方检查。
3. 板上验证:BLE 小程序连上时,TCP/MQTT 上报不受影响;BLE 断开后无帧泄漏。
### P0.2 代码 TODO 清剿
| 位置 | 问题 | 优先级 |
|------|------|--------|
| `iot_mqtt_srv.c:753` | `loop_ok[i] = 1` 硬编码,未取 Loop MCU 真实线圈状态 → 平台看到的线圈健康度是假的 | 🔴 高 |
| `net_srv.c:528` | MQTT 接收长度未做溢出防护(`len-received` 越界风险)| 🔴 高 |
| `storage.c:494`DBN | Flash_Model 读失败无停机/降级保护 | 中 |
| `net_srv.c:1225` | 序列号修改命令未实现 | 中 |
| `dbn_ble_srv.c:870` | ACS timeout `min→ms` 换算待确认(当前 ×60×100,疑似按 10ms tick,需对时钟源) | 中 |
| `main.c`Loop)多处 | "不整个设备复位,只复位蓝牙以外部分"——历史注释,评估后关闭或立项 | 低 |
### P0.3 回归验证清单(配合 vd_test_fixture 工装)
- [ ] 长车双线圈场景复测:car_leave 事件后**下一条** loop_data 的 iscar 必须已翻 false(同源验证)
- [ ] event_report 断网入队 → 重连补报闭环;5s 超时重发 ×3 后的放弃行为
- [ ] 时钟同步:校准前 ts=上电秒数的平台侧兼容;mstick 49.7 天回绕仿真(gcc 隔离单测)
- [ ] MQTT 稳定性:>512B 帧、PINGREQ 保活、broker 主动踢线后的重连节奏
- [ ] 拨码/灵敏度/hold_time/relay_delay 全参数配置读写一致性(BLE 链路 + MQTT 链路交叉验证)
**P0 出口条件**:以上全绿 → 两侧 `FIRMWARE_VER` 升 1.1 → 整机 tag v1.1.0。
---
## P1 — 传感数据上云闭环(目标 V1.2.x,约 1~2 月,与 edc_server 平台侧并行)
这是你"传感数据云 + 业务平台"目标的主线,设备侧固件已基本就绪,重心在平台侧消费。
### P1.1 平台侧对接(edc_server
- **event_report 消费契约**:平台先落库、后 ACK(code=0)——严禁先 ACK 后落库(设备出队后事件不可再生)
- **loop_data 落库 + 可视化**variation 有符号曲线(正=车/裕量,负=基线污染——免费的诊断维度)、频率漂移趋势图;锯齿波形态识别(5s 基线阶跃是正常形态,别当故障)
- **流量/占用 API**:基于 car_enter/car_leave 事件 + 通过时间/车间距(50ms 单位)产出分通道流量计数、占用率、平均通过时间
- **设备台账**initialize 上线登记、心跳离线判定(阈值=心跳间隔×3)、report_config 远程下发界面
### P1.2 脱机日志子系统 🆕(W25Qxx SPI NOR
**硬件底子**DBN 外挂 SPI NORSPI1)。默认 **W25Q3232Mbit = 4MB,已确认)****兼容 W25Q64 / W25Q128 / W25Q256**,上电先读 JEDEC ID 识别存储类型,再按类型映射各分区容量。
**分区规划(顺序:参数区 → OTA 镜像暂存区 → 事件日志区 → 传感快照区)**:
| 分区 | 容量策略 | 用途 |
|------|---------|------|
| 参数区 | **固定 64KB**0x000000 起,现用 <1KB | 现有配置,留余量 |
| OTA 镜像暂存区 | **固定 512KB** | Loop 固件(64KB×2 版本回滚)+ 预留 DBN 镜像 |
| 事件日志区 | **随存储类型**(见下表) | 环形,关键事件流,不被快照冲掉 |
| 传感快照区 | **随存储类型**(= 总容量 − 参数区 − OTA − 事件日志区) | 环形,0xC0 帧原样落盘 |
**存储类型 ↔ 容量映射表**(上电读 JEDEC ID 后选档):
| JEDEC ID | 芯片 | 总容量 | 参数区 | OTA 暂存 | 事件日志区 | 传感快照区 |
|----------|------|--------|--------|----------|-----------|-----------|
| EF 40 16 | W25Q32(默认) | 4MB | 64KB | 512KB | **512KB**~16000 条) | **~2.94MB** |
| EF 40 17 | W25Q64 | 8MB | 64KB | 512KB | **1MB**~32000 条) | **~6.44MB** |
| EF 40 18 | W25Q128 | 16MB | 64KB | 512KB | **2MB**~64000 条) | **~13.44MB** |
| EF 40 19 | W25Q256 | 32MB | 64KB | 512KB | **4MB**~128000 条) | **~27.44MB** |
- 事件日志区容量随芯片**等比翻倍**(512KB→4MB),保证日志保留时长不随硬件降配而缩水
- 传感快照区吃剩余容量;**W25Q32(4MB)为最小可部署配置**
- 分区边界在**上电初始化时**由 JEDEC ID 计算得出,offlog/快照代码按运行时分区表寻址,不写死
**两条日志流分开存**
1. **事件流(必录,低速率)**car_enter/car_leave(含时间量)、线圈断线/恢复、继电器动作、上电/复位(含复位原因)、MQTT/TCP 连接与断开、event_report ACK 超时/重发/放弃、配置变更(含来源:BLE/MQTT/TCP)、灵敏度换档、**时钟同步锚点**、固件升级开始/结果、日志清除操作(审计自记录)——**已实现(offlog)**
2. **快照流(环形可覆盖)**:0xC0 帧 4 线圈数据原样(12B/线圈)+ 头部(序号/boot_seq/mstick),64B/条,**记录节奏与上报同频**——直接挂在 `iot_sensor_ingest()` 同源出口。**已实现(snapshot2026-08-12**:中断只入 RAM 暂存(8 深满丢新),主循环 flush 落盘;BLE 新增 SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A
**容量预算****以 W25Q32 为基准**:事件日志 512KB ≈ 16384 条 / 快照区 3008KB ≈ **4.8 万条** @64B):
| 场景 | 落盘节奏 | 保留时长(W25Q32) |
|------|---------|---------|
| 最坏情况:快档 300ms 连续压满 | 3.3 条/s | **~4.0 小时** |
| 繁忙出入口(日均 2000 车次,每车次 ~15 条突发 + 空闲 30s 底噪) | ~3.25 万条/天 | **~1.5 天** |
| 一般车道(空闲 30s 为主) | ~2900 条/天 | **~16.7 天** |
**各芯片快照保留时长对比**(同一落盘节奏下):
| 芯片 | 快照区 | 快照条数 | 最坏 300ms 压满 | 繁忙出入口 | 一般车道 |
|------|--------|----------|----------------|-----------|---------|
| W25Q32(默认) | 3008KB | 48128 | ~4.0 小时 | ~1.5 天 | ~16.7 天 |
| W25Q64 | 6592KB | 105472 | ~8.8 小时 | ~3.2 天 | ~36.6 天 |
| W25Q128 | 13760KB | 220160 | ~18.3 小时 | ~6.8 天 | ~76.4 天 |
| W25Q256 | 28096KB | 449536 | ~37.5 小时 | ~13.8 天 | ~156 天 |
> 事件日志区独立于快照:512KB→4MB16384→131072 条 @32B),**不受快照覆盖影响**。
若现场要求更长保留:把空闲档快照落盘间隔与上报解耦(如落盘固定 60s),或快照只存"有沿事件前后 ±N 条"(预触发环形),容量立刻翻数倍——**实施时按现场需求选**。
**⚠ 无 RTC 的时间戳设计(最容易踩的坑)**:断电后 mstick 归零,纯 Unix 时间戳不可行。方案:每条记录带 `(boot_seq, mstick)`;每次时钟同步成功时写一条**时间锚点事件**boot_seq ↔ unix_ts 映射);导出时由平台/小程序用锚点回算绝对时间。从未同步过的 boot 段只有相对时间——协议文档里要写明这个语义。
**磨损与 RAM**:环形顺序写天然磨损均衡(4KB sector 顺序擦除,W25Q 10 万次擦写寿命无忧);**复用现有 `SPI_FLASH_BUF[4096]`,不新增大缓冲**CH32V208 SRAM 紧张的教训)。
### P1.3 日志导出与管理 🆕
**导出通道(三条,统一按序号分页,不按时间——时间不可靠)**:
| 通道 | 机制 | 说明 |
|------|------|------|
| MQTT 自动补报 | 断网期间事件流落盘,重连后按序号续传 | 现有 16 深 RAM 队列扩展为 flash-backed,溢出转日志区;与 event_report ACK 闭环衔接 |
| MQTT/TCP 命令拉取 | `log_query {stream, start_seq, count}` 分页 | 复用现有 cmd 分发框架;注意 Publish≤500B 分批(MSS=576 教训) |
| BLE 小程序读取 | 复用 0x8F 通道新增日志读命令 | BLE MTU 分包(20~244B),小程序侧分页 UI |
**管理命令**
- `log_stat`:各流的序号区间、条数、容量占用
- `log_clear {stream}`:**需鉴权**;分流清除;清除动作本身写入事件流(审计——谁在什么时候清了日志)
- 自动管理:环形覆盖为常态,无需人工干预;满不停写
### P1.4 远程 OTA 🆕(路线已定:先走现网 MQTT,底层复用 ISP 透传)
**关键认知**BLE 给 Loop MCU 升级 = "镜像经手机流式送到 CH32V208 → ISP 串口透传刷写"。远程 OTA 只是把"送镜像"的传输层从 BLE 换成 MQTT,**ISP 透传底层完全复用**——且 Loop MCU 的 OTA 根本不需要 CH32V208 做 A/B 分区,镜像只是路过。
**分两档,难度和节奏完全不同:**
**① Loop MCU (AT32F421) 远程 OTA —— v1.2.x 落地**
采用**先存后刷**(优于流式直透):
1. 平台 MQTT 分片下发(每片带 offset+CRC,≤500B/片,支持断点续传)→ 写入 W25Qxx 暂存区
2. 全镜像 CRC 校验通过后,才启动 ISP 透传本地刷写(复用 BLE OTA 的透传状态机)
3. 抗网络抖动:下载阶段断网无所谓,续传即可;刷写阶段纯本地,窗口短
4. 暂存区保留上一版镜像 → 支持回滚重刷
**安全底线(会车产品,升级失败=继电器行为不可控)**:
- 升级窗口检查:有车压线圈时告警/延迟启动——**待定策略**
- ISP 期间 Loop MCU 停止检测,继电器处于什么状态?(复位后 GPIO 默认态应为断开=常开安全侧,**待板上验证**)
- 刷写失败自动重试 ×3,仍失败 → event_report 告警 + 保持 ISP 可重入(AT32 ISP bootloader 兜底,不会真砖)
- 升级开始/结果写入事件日志
**② DBN (CH32V208) 自身远程 OTA —— 方案评审先行,落地后置**
难点:128KB Flash 装不下 APP 双分区;现有 BLE OTA 走 WCH "OnlyUpdateApp" IAP 框架(bootloader 固定只认 BLE)。候选路线:
- (a) 镜像下到 W25Qxx 暂存 + 校验 → 改 bootloader 支持从外部 Flash 搬运(**动 bootloader 风险高**,刷坏只能拆壳烧录)
- (b) RAM 驻留搬运代码自刷写(掉电=砖,不推荐)
- (c) 维持现状:DBN 升级仍走 BLE(现场人员),只有 Loop 支持远程——**最稳妥的过渡态**
先出方案评审文档比选,v1.2.x 期间不阻塞 ①。
**③ 4G 通道下的 OTA**4G 模组(TTL 对接 UART1)跑 MQTT 后,①②的分片下载协议原样复用——传输层无关是这套设计的出发点。注意 4G 流量成本:64KB 镜像 + 重传开销可忽略,但快照流补报要设流量上限。
### P1.5 设备侧其他补强
- **loop_data 增补字段**(若 P1.1 分析需要):如 Origin 绝对值上报,便于平台重建基线轨迹——走协议 V1.06 流程,两侧同版本配套
- **DBNMQTTool** 同步:日志拉取/OTA 分片下发的模拟与断言(工具先行,固件后到)
### P1.6 4G 上云通道(UART1 RFU 启用,可后置到 P3
- 模组选型(Cat.1 足够:EC800 系列/Air780 级别),AT 透传 vs 模组内置 MQTT 两条路线对比
- 网口 MQTT 与 4G MQTT 复用同一 `iot_mqtt_srv` 状态机,仅换传输层——架构上先留接口
---
## P2 — 检测算法深化(Loop 侧,目标 V2.x,约 2~3 月)
### P2.1 抗变频器/电磁干扰 🎯(你的长期主目标)
1. **先取证再动手**:用工装+现场采集受扰线圈的 CAPVD 原始序列(10ms 粒度),确认干扰形态(周期性拍频 / 白噪 / 突发脉冲)——**待现场验证**
2. **频点错开**:4 路线圈两级调频电容(33nF/10nF)已有硬件基础,上电自检各路振荡频率,间距不足时自动切档,抑制相邻串扰
3. **算法层**:现有 IIR+斜率限幅+进入确认对突发脉冲已有基础免疫;针对周期性拍频评估陷波/中值预滤(注意 10ms tick 下的计算预算)
4. 产出:抗干扰测试报告(干扰源型号、距离、频谱、误检率前后对比)
### P2.2 双线圈测速 + 车型粗分类
- 利用现有 misc 时间量(通过时间/车间距,50ms 单位)+ 已知线圈间距 → 速度估计
- 信号长度(占用时间×速度)→ 车长 → 粗分类(小车/大车/拖挂)
- 拖挂车"半截车"识别:单通道内 variation 双峰形态检测——直接服务会车防砸
- 输出通道:event_report 新增 speed/class 字段(协议升版)
### P2.3 方向判别
- 线圈 1→2 与 2→1 的触发时序 → 方向语义,供会车/双向流量使用
- Loop 侧输出还是平台侧算:**倾向 Loop 侧**(时序精度 50ms 内,平台侧受上报调度抖动影响)
---
## P3 — 会车控制与融合(依赖 P1/P2 产出)
- **会车控制逻辑平台侧先行**:基于多台 DLD960 的 event_report(有 ACK 闭环,可靠交付)做单通道红绿灯会车状态机;业务逻辑只认 event_reportloop_data 仅做展示——两通道职责不混
- **异常分支优先设计**:设备离线(心跳超时)→ 会车灯降级策略;继电器粘连检测(relay_count 与命令数比对);车辆长时间停留(hold_time 超时全通道重启的平台侧感知)
- **单机版会车评估**:DLD960 四通道 + 继电器本地联动能否覆盖小型场景(无平台依赖,可靠性更高)——出对比方案:纯平台 / 纯单机 / 混合
- **雷达/相机融合接口预留**:扩展 UART1 或网口接入目标列表,融合仲裁放平台侧
---
## 版本映射与节奏
| 版本 | 内容 | 前置 |
|------|------|------|
| v1.1.0 | P0 全部(BLE 收尾 + TODO 清剿 + 回归全绿) | — |
| v1.2.x | P1 设备侧(脱机日志 + Loop 远程 OTA + 协议 V1.06 | edc_server 对接联调 |
| v2.0.0 | P2 算法深化(抗干扰 + 测速分类 + 方向) | 现场干扰数据采集 |
| v2.x | P3 会车控制 | P1 平台 + P2 方向判别 |
## 风险与待现场验证
| # | 风险 | 应对 |
|---|------|------|
| 1 | BLE 重构改动帧消费时序,复发"不同源"类 bug | P0.1 真值表 + 长车复测强制过 |
| 2 | 变频器干扰形态未知,算法方案可能推倒重来 | 先采数据后设计,不拍脑袋 |
| 3 | DBN 自身远程 OTA 无安全落点(128KB 无 A/B、bootloader 只认 BLE | 过渡态:DBN 走 BLE、Loop 走远程;bootloader 改造单独评审 |
| 4 | 协议再升版(V1.06)又是双侧死绑定 | 沿用 Len 字节区分格式的兼容方案,发布走配套矩阵 |
| 5 | 无 RTC,脱机日志绝对时间不可靠 | boot_seq+mstick+时钟锚点回算;协议明示"未同步段仅相对时间" |
| 6 | 日志高频写与 MQTT/BLE 共抢主循环与 SPI 总线 | 快照落盘按 page 缓写、擦除放空闲窗口;实测不达标就降快照档频率 |
@@ -241,11 +241,60 @@ void load_cfg_from_flash(void)
if(strncmp(mBuff, DEV_USER_FLASH_MAGIC, sizeof(DEV_USER_FLASH_MAGIC)) != 0) if(strncmp(mBuff, DEV_USER_FLASH_MAGIC, sizeof(DEV_USER_FLASH_MAGIC)) != 0)
{ {
free(mBuff); free(mBuff);
PRINT("Will_Factory_dev_____\n"); /* ===== 恢复 factory 写入 (2026-08-19) =====
8-13 : factory SPI 8-17
(.bss RAM + printf , SPI );
offlog/snapshot 8-18 SPI
(W25Q128) magic memory defaults,
write_net_config magic
: mismatch (magic+) flash, */
factory_dev_info(); factory_dev_info();
PRINT("CFG: magic mismatch - factory defaults written\n");
NVIC_SystemReset(); memset(g_dev_number, 0, 6);
memcpy(g_dev_number, gMacAddr, 6);
sprintf(g_dev_number_str, "%02X%02X%02X%02X%02X%02X",
g_dev_number[0], g_dev_number[1], g_dev_number[2],
g_dev_number[3], g_dev_number[4], g_dev_number[5]);
g_sub_code_enable.code_set = 0x0001;
g_max_counter_bt_min = BT_DISABLE_IDLE_TIMEOUT;
g_max_counter_bt_timeout = g_max_counter_bt_min * 60 * 1000;
g_storage_uart_baud = UART_BAUD_DEFAULT_PORT_1;
g_storage_uart_baud_2 = UART_BAUD_DEFAULT_PORT_2;
memcpy(g_dev_password, "123456", 6);
memcpy(local_net_cfg.mac, g_dev_number, 6);
local_net_cfg.lip[0] = 192; local_net_cfg.lip[1] = 168; local_net_cfg.lip[2] = 1; local_net_cfg.lip[3] = 188;
local_net_cfg.sub[0] = 255; local_net_cfg.sub[1] = 255; local_net_cfg.sub[2] = 255; local_net_cfg.sub[3] = 0;
local_net_cfg.gw[0] = 192; local_net_cfg.gw[1] = 168; local_net_cfg.gw[2] = 1; local_net_cfg.gw[3] = 1;
local_net_cfg.dns[0] = 192; local_net_cfg.dns[1] = 168; local_net_cfg.dns[2] = 1; local_net_cfg.dns[3] = 1;
local_net_cfg.port_ssc_tcp = 5550;
local_net_cfg.port_ssc_udp = NET_REPORT_INTERVAL;
local_net_cfg.port_ssc_udp_message = 5505;
local_net_cfg.port_dev_tcp = 5550;
local_net_cfg.port_dev_udp = 4900;
net_center_info.lssc_ip[0] = 192; net_center_info.lssc_ip[1] = 168; net_center_info.lssc_ip[2] = 1; net_center_info.lssc_ip[3] = 222;
net_center_info.msg_port = 4999;
net_center_info.tcp_port = 5550;
net_center_info.udp_port = NET_REPORT_INTERVAL;
net_center_info.sw_ver[0] = 1; net_center_info.sw_ver[1] = 2;
memset(iot_net_info.remote_addr, 0, 64);
memcpy(iot_net_info.remote_addr, "121.37.20.199", 13);
iot_net_info.mqtt_port = 1883;
memset(iot_net_info.client_id, 0, 64);
memset(iot_net_info.username, 0, 64); memcpy(iot_net_info.username, "admin", 5);
memset(iot_net_info.password, 0, 32); memcpy(iot_net_info.password, "password", 8);
iot_net_info.mode = 0;
g_iot_topic.clientid_enable = 0;
memset(g_iot_topic.topic_pub, 0, 64);
memset(g_iot_topic.topic_sub, 0, 64);
g_sub_code_enable.net_enable = 1;
g_sub_code_enable.iot_enable = 0;
g_sub_code_enable.custom_enable = 0;
g_sub_code_enable.loop_enable = 0;
g_sub_code_enable.dgdus_enable = 0;
g_sub_code_enable.wbdus_enable = 0;
g_sub_code_enable.radar_enable = 0;
PRINT("CFG: memory defaults OK (factory written)\n");
return;
} }
_offset = DEV_NUMER_ADDR_OFFSET; _offset = DEV_NUMER_ADDR_OFFSET;
@@ -305,6 +354,16 @@ void load_cfg_from_flash(void)
/* 2026-08-17: 字符串 0 终止保险 — Flash 中字符串若不足定长且后续 0xFF,
%s () '\0' */
iot_net_info.remote_addr[63] = '\0';
iot_net_info.client_id[63] = '\0';
iot_net_info.username[63] = '\0';
iot_net_info.password[31] = '\0';
g_iot_topic.topic_pub[63] = '\0';
g_iot_topic.topic_sub[63] = '\0';
free(mBuff); free(mBuff);
g_sub_code_enable.net_enable = g_sub_code_enable.code_set & 0x01; g_sub_code_enable.net_enable = g_sub_code_enable.code_set & 0x01;
@@ -315,6 +374,7 @@ void load_cfg_from_flash(void)
g_sub_code_enable.wbdus_enable = (g_sub_code_enable.code_set >> 5) & 0x01; g_sub_code_enable.wbdus_enable = (g_sub_code_enable.code_set >> 5) & 0x01;
g_sub_code_enable.radar_enable = (g_sub_code_enable.code_set >> 6) & 0x01; g_sub_code_enable.radar_enable = (g_sub_code_enable.code_set >> 6) & 0x01;
/* 2026-08-17 栈溢出修复后恢复 */
PRINT("\nSub_Code: 0x%04X, net_enable:%d, iot_enable:%d\n", g_sub_code_enable.code_set, g_sub_code_enable.net_enable, g_sub_code_enable.iot_enable); PRINT("\nSub_Code: 0x%04X, net_enable:%d, iot_enable:%d\n", g_sub_code_enable.code_set, g_sub_code_enable.net_enable, g_sub_code_enable.iot_enable);
// g_sub_code_enable.laser_enable = (g_sub_code_enable.code_set >> 9) & 0x01; // g_sub_code_enable.laser_enable = (g_sub_code_enable.code_set >> 9) & 0x01;
@@ -16,6 +16,8 @@
#include "ch32v20x_it.h" #include "ch32v20x_it.h"
#include "eth_driver.h" #include "eth_driver.h"
#include "CONFIG.h" #include "CONFIG.h"
#include "debug.h"
#include "fault_diag.h"
/********************************************************************* /*********************************************************************
* LOCAL FUNCTIONS * LOCAL FUNCTIONS
@@ -38,6 +40,40 @@ void NMI_Handler(void)
{ {
} }
/* 故障诊断现场: .noinit 段 (复位不清零) */
FaultDiag g_fault_diag __attribute__((section(".noinit")));
/*********************************************************************
* @fn fault_diag_init
*
* @brief main : + + boot_cnt
*
* @return none
*/
void fault_diag_init(void)
{
if (g_fault_diag.magic == 0xFA57FA57UL) {
PRINT("FAULT_DIAG: boot_cnt=%lu mcause=0x%08lx mepc=0x%08lx mtval=0x%08lx marker=0x%08lx\n",
(unsigned long)g_fault_diag.boot_cnt,
(unsigned long)g_fault_diag.mcause,
(unsigned long)g_fault_diag.mepc,
(unsigned long)g_fault_diag.mtval,
(unsigned long)g_fault_diag.marker);
} else if (g_fault_diag.marker != 0) {
/* 非 HardFault 复位 (IWDG 超时/卡死/掉电): marker 仍记录执行轨迹 */
PRINT("FAULT_DIAG: no-hardfault boot_cnt=%lu marker=0x%08lx (卡死/看门狗复位)\n",
(unsigned long)g_fault_diag.boot_cnt,
(unsigned long)g_fault_diag.marker);
}
/* 重置现场, boot_cnt 保留递增 */
g_fault_diag.magic = 0;
g_fault_diag.mcause = 0;
g_fault_diag.mepc = 0;
g_fault_diag.mtval = 0;
g_fault_diag.marker = 0;
g_fault_diag.boot_cnt++;
}
/********************************************************************* /*********************************************************************
* @fn HardFault_Handler * @fn HardFault_Handler
* *
@@ -47,6 +83,16 @@ void NMI_Handler(void)
*/ */
void HardFault_Handler(void) void HardFault_Handler(void)
{ {
/* 纯 RAM 写现场 (不依赖 UART/printf): 栈坏/中断禁用也能留痕 */
g_fault_diag.magic = 0xFA57FA57UL;
g_fault_diag.mcause = __get_MCAUSE();
g_fault_diag.mepc = __get_MEPC();
g_fault_diag.mtval = __get_MTVAL();
/* 尽力打印 (UART 可用时), 失败不影响 RAM 现场 */
PRINT("HARDFAULT: mcause=0x%08lx mepc=0x%08lx mtval=0x%08lx\n",
(unsigned long)g_fault_diag.mcause,
(unsigned long)g_fault_diag.mepc,
(unsigned long)g_fault_diag.mtval);
NVIC_SystemReset(); NVIC_SystemReset();
while(1) while(1)
{ {
@@ -10,7 +10,9 @@
#include "storage.h" #include "storage.h"
#include <string.h> #include <string.h>
#include "net_srv.h" #include "net_srv.h"
#include "offlog.h"
#include "snapshot.h"
extern uint16_t peripheral_get_mtu(void);
uint8_t g_flag_notify_temp = 0; //ÁÙʱ֪ͨnotify flag, 0 disable, 1 enable uint8_t g_flag_notify_temp = 0; //ÁÙʱ֪ͨnotify flag, 0 disable, 1 enable
@@ -29,6 +31,12 @@ extern uint8_t loop1_FLAG_CUT;
void clear_ble_notify_buf(BLE_Notify_Buf * buf) void clear_ble_notify_buf(BLE_Notify_Buf * buf)
{ {
if(buf->flag || buf->len)
{
PRINT("CLR_NTF: buf=%08lx flag=%d len=%d ret=%08lx\n",
(unsigned long)buf, buf->flag, buf->len,
(unsigned long)__builtin_return_address(0));
}
buf->len = 0; buf->len = 0;
buf->flag = 0; buf->flag = 0;
memset(buf, 0, MAX_BLE_Notify_Buf_LEN); memset(buf, 0, MAX_BLE_Notify_Buf_LEN);
@@ -38,6 +46,12 @@ void clear_buf_dbn_ble(Buf_DBN_BLE * buf)
{ {
uint8_t i = 0; uint8_t i = 0;
if(buf->flag)
{
PRINT("CLEAR busy resp: amt=%d seq=%d off=%d ret=%08lx\n",
buf->pkg_amount, buf->pkg_seq, buf->dat_offset,
(unsigned long)__builtin_return_address(0));
}
buf->flag = 0; buf->flag = 0;
buf->magic = 0; buf->magic = 0;
@@ -65,7 +79,7 @@ void clear_buf_dbn_ble_all(Buf_DBN_BLE * buf)
buf->flag = 0; buf->flag = 0;
buf->magic = 0; buf->magic = 0;
buf->cmd = 0; buf->cmd = 0;
memset(buf->dat, 0, MAX_BLE_BUF_LEN); memset(buf->dat, 0, MAX_BLE_DAT_BUF_LEN);
buf->dat_len = 0; buf->dat_len = 0;
buf->dat_offset = 0; buf->dat_offset = 0;
buf->pkg_amount = 0; buf->pkg_amount = 0;
@@ -181,6 +195,25 @@ static uint8_t set_net_info_to_ready(uint8_t * dat_dst)
return i; return i;
} }
/* =============================================================
* ble_notify_chunk_max: max dat bytes per notify packet
* follow negotiated MTU to avoid 'Too large noti' drop:
* whole pkt = 4B header(magic/header/len/cmd) + dat + 2B ckb
* must satisfy whole pkt <= peripheralMTU - 3 (ATT opcode+handle)
* and whole pkt <= MAX_BLE_Notify_Buf_LEN (local buf, no overflow)
* 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; /* fallback before negotiation */
uint16_t _limit = _mtu - 9; /* pkt limit - header4 - ckb2 */
if (_limit > (MAX_BLE_Notify_Buf_LEN - 6))
_limit = MAX_BLE_Notify_Buf_LEN - 6;
return _limit;
}
void set_response_iot_net(Buf_DBN_BLE *response_dst) void set_response_iot_net(Buf_DBN_BLE *response_dst)
{ // config iot_net { // config iot_net
uint8_t ret = 0; uint8_t ret = 0;
@@ -205,10 +238,13 @@ void set_response_iot_net(Buf_DBN_BLE *response_dst)
memcpy(&(response_dst->dat[i]), iot_net_info.password, strlen(iot_net_info.password)); memcpy(&(response_dst->dat[i]), iot_net_info.password, strlen(iot_net_info.password));
i += strlen(iot_net_info.password); i += strlen(iot_net_info.password);
response_dst->dat_len = i; response_dst->dat_len = i;
response_dst->pkg_amount = i / MAX_BLE_DAT_RESPONSE_LEN;
if((i % MAX_BLE_DAT_RESPONSE_LEN) > 0)
{ {
response_dst->pkg_amount += 1; uint16_t _chunk = ble_notify_chunk_max();
response_dst->pkg_amount = i / _chunk;
if((i % _chunk) > 0)
{
response_dst->pkg_amount += 1;
}
} }
response_dst->pkg_seq = 0; response_dst->pkg_seq = 0;
@@ -232,10 +268,13 @@ void set_response_iot_topic(Buf_DBN_BLE *response_dst)
memcpy(&(response_dst->dat[i]), g_iot_topic.topic_sub, strlen(g_iot_topic.topic_sub)); memcpy(&(response_dst->dat[i]), g_iot_topic.topic_sub, strlen(g_iot_topic.topic_sub));
i += strlen(g_iot_topic.topic_sub); i += strlen(g_iot_topic.topic_sub);
response_dst->dat_len = i; response_dst->dat_len = i;
response_dst->pkg_amount = i / MAX_BLE_DAT_RESPONSE_LEN;
if((i % MAX_BLE_DAT_RESPONSE_LEN) > 0)
{ {
response_dst->pkg_amount += 1; uint16_t _chunk = ble_notify_chunk_max();
response_dst->pkg_amount = i / _chunk;
if((i % _chunk) > 0)
{
response_dst->pkg_amount += 1;
}
} }
response_dst->pkg_seq = 0; response_dst->pkg_seq = 0;
response_dst->flag = 1; response_dst->flag = 1;
@@ -246,12 +285,14 @@ uint8_t set_response_buf(Buf_DBN_BLE *response_dst, uint8_t magic, uint8_t cmd,
{ {
uint8_t ret = 0; uint8_t ret = 0;
uint8_t i = 0; uint8_t i = 0;
if(dat_len > MAX_BLE_DAT_BUF_LEN) dat_len = MAX_BLE_DAT_BUF_LEN; /* overflow guard */
clear_buf_dbn_ble(response_dst); clear_buf_dbn_ble(response_dst);
response_dst->magic = magic; response_dst->magic = magic;
response_dst->cmd = cmd; response_dst->cmd = cmd;
uint8_t _amount = dat_len / MAX_BLE_DAT_RESPONSE_LEN; uint16_t _chunk = ble_notify_chunk_max();
if((dat_len % MAX_BLE_DAT_RESPONSE_LEN) > 0) uint8_t _amount = dat_len / _chunk;
if((dat_len % _chunk) > 0)
{ {
_amount++; _amount++;
} }
@@ -308,6 +349,7 @@ uint8_t set_response_tran_to_notify(uint8_t *dat_ori, uint8_t dat_len, BLE_Notif
} }
notify_dst->len = dat_len; notify_dst->len = dat_len;
notify_dst->flag = 1; notify_dst->flag = 1;
return notify_dst->flag;
} }
@@ -368,9 +410,12 @@ uint8_t set_response_to_notify(Buf_DBN_BLE *response_ori, BLE_Notify_Buf * notif
uint8_t _pkg_amount = response_ori->pkg_amount; uint8_t _pkg_amount = response_ori->pkg_amount;
uint8_t _pkg_seq = response_ori->pkg_seq; uint8_t _pkg_seq = response_ori->pkg_seq;
uint8_t _remain_len = response_ori->dat_len - response_ori->dat_offset; uint8_t _remain_len = response_ori->dat_len - response_ori->dat_offset;
if(_remain_len > MAX_BLE_DAT_RESPONSE_LEN)
{ {
_remain_len = MAX_BLE_DAT_RESPONSE_LEN; uint16_t _chunk = ble_notify_chunk_max();
if(_remain_len > _chunk)
{
_remain_len = (uint8_t)_chunk;
}
} }
_pkg_seq += 1; _pkg_seq += 1;
notify_dst->buf[1] = (_pkg_amount << 4)|(_pkg_seq); notify_dst->buf[1] = (_pkg_amount << 4)|(_pkg_seq);
@@ -875,6 +920,190 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len)
} }
PRINT("Rcv_acs_enable_flag: %d\n", g_dbn_ble_state_acs_enable.flag); PRINT("Rcv_acs_enable_flag: %d\n", g_dbn_ble_state_acs_enable.flag);
} break; } break;
case CMD_DBN_OFFLOG_STAT: {
/* offlog stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */
uint8_t _i = 0;
uint32_t _total = offlog_count();
uint32_t _seq_last = offlog_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint32_t _boot = offlog_boot_seq();
uint32_t _cap = OFFLOG_MAX_RECORDS;
if (!offlog_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
} else {
tmp_ble_buf[_i++] = 0x00; /* ok */
tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF);
}
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: offlog_stat count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last);
} break;
case CMD_DBN_OFFLOG_QUERY: {
/* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..4)
resp: status(1) count(1) + N x 32B OfflogEvt raw (LE) */
uint32_t _start_seq = 0;
uint32_t _req_count = 0;
uint32_t _total = offlog_count();
uint32_t _seq_last = offlog_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint8_t _i = 0, _j = 0;
if (!offlog_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */
tmp_ble_buf[_i++] = 0x02; /* bad request */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
_start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8)
| ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24);
_req_count = pkg[8];
if (_req_count > OFFLOG_MAX_QUERY_RECORDS) _req_count = OFFLOG_MAX_QUERY_RECORDS;
if (_req_count == 0) _req_count = OFFLOG_MAX_QUERY_RECORDS;
/* overflow guard: resp dat = 2 + N*32 must fit tmp buf.
history: MAX_BLE_TMP_BUF_LEN was 100, 130B write overflowed
into g_notify_buftemp and erased pending chunk 2 */
{ uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ?
((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(OfflogEvt)) : 0;
if (_req_count > _max_rec) _req_count = _max_rec; }
tmp_ble_buf[_i++] = 0x00; /* status ok */
tmp_ble_buf[_i++] = 0; /* count placeholder */
if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) {
uint32_t _idx = _start_seq - _seq_first;
while (_j < _req_count && (_idx + _j) < _total) {
OfflogEvt _evt;
if (offlog_read_idx((uint16_t)(_idx + _j), &_evt) != 0) break;
memcpy(&tmp_ble_buf[_i], &_evt, sizeof(OfflogEvt));
_i += sizeof(OfflogEvt);
_j++;
}
}
tmp_ble_buf[1] = _j;
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: offlog_query start_seq=%lu req=%lu fetched=%u\n",
(unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j);
} break;
case CMD_DBN_OFFLOG_CLEAR: {
/* clear offlog (audit trail). blocking ~45ms (logical clear + current sector), main-loop ctx ok */
uint8_t _i = 0;
offlog_clear();
tmp_ble_buf[_i++] = 0x00; /* ok */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: offlog_clear done\n");
} break;
case CMD_DBN_SNAP_STAT: {
/* snap stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */
uint8_t _i = 0;
uint32_t _total = snap_count();
uint32_t _seq_last = snap_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint32_t _boot = snap_boot_seq();
uint32_t _cap = SNAP_MAX_RECORDS;
if (!snap_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
} else {
tmp_ble_buf[_i++] = 0x00; /* ok */
tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF);
}
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: snap_stat count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last);
} break;
case CMD_DBN_SNAP_QUERY: {
/* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..2)
resp: status(1) count(1) + N x 64B SnapRec raw (LE) */
uint32_t _start_seq = 0;
uint32_t _req_count = 0;
uint32_t _total = snap_count();
uint32_t _seq_last = snap_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint8_t _i = 0, _j = 0;
if (!snap_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */
tmp_ble_buf[_i++] = 0x02; /* bad request */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
_start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8)
| ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24);
_req_count = pkg[8];
if (_req_count > SNAP_MAX_QUERY_RECORDS) _req_count = SNAP_MAX_QUERY_RECORDS;
if (_req_count == 0) _req_count = SNAP_MAX_QUERY_RECORDS;
/* overflow guard: resp dat = 2 + N*64 must fit tmp buf (ODR lesson) */
{ uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ?
((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(SnapRec)) : 0;
if (_req_count > _max_rec) _req_count = _max_rec; }
tmp_ble_buf[_i++] = 0x00; /* status ok */
tmp_ble_buf[_i++] = 0; /* count placeholder */
if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) {
uint32_t _idx = _start_seq - _seq_first;
while (_j < _req_count && (_idx + _j) < _total) {
SnapRec _rec;
if (snap_read_idx(_idx + _j, &_rec) != 0) break;
memcpy(&tmp_ble_buf[_i], &_rec, sizeof(SnapRec));
_i += sizeof(SnapRec);
_j++;
}
}
tmp_ble_buf[1] = _j;
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: snap_query start_seq=%lu req=%lu fetched=%u\n",
(unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j);
} break;
case CMD_DBN_SNAP_CLEAR: {
/* clear snapshot (audit trail). blocking ~45ms (logical clear + current sector) */
uint8_t _i = 0;
snap_clear();
tmp_ble_buf[_i++] = 0x00; /* ok */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: snap_clear done\n");
} break;
default: default:
{ {
} break; } break;
@@ -893,6 +1122,7 @@ void manage_dbn_ble_transparent(uint8_t *pkg, uint8_t len)
void poll_dbn_ble(void) void poll_dbn_ble(void)
{ {
static uint8_t _last_ntf_flag = 0;
uint8_t i = 0; uint8_t i = 0;
uint8_t magic = 0; uint8_t magic = 0;
if(g_ble_rcv_buf.flag) if(g_ble_rcv_buf.flag)
@@ -935,8 +1165,24 @@ void poll_dbn_ble(void)
} }
if(g_notify_buftemp.flag == 0) if(g_notify_buftemp.flag == 0)
{ {
// report_sens_acs(); static uint8_t _last_resp_flag = 0;
g_flag_notify_temp = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp); uint8_t _pull = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
g_flag_notify_temp = _pull;
if(g_buf_ble_response.flag != _last_resp_flag)
{
PRINT("BLE resp flag %d->%d (pull=%d amt=%d seq=%d off=%d)\n",
_last_resp_flag, g_buf_ble_response.flag, _pull,
g_buf_ble_response.pkg_amount, g_buf_ble_response.pkg_seq,
g_buf_ble_response.dat_offset);
_last_resp_flag = g_buf_ble_response.flag;
}
}
if(g_notify_buftemp.flag != _last_ntf_flag)
{
PRINT("BLE ntf flag %d->%d (len=%d temp=%d)\n",
_last_ntf_flag, g_notify_buftemp.flag, g_notify_buftemp.len,
g_flag_notify_temp);
_last_ntf_flag = g_notify_buftemp.flag;
} }
} }
@@ -11,45 +11,46 @@
#include <stdint.h> #include <stdint.h>
#define PRODUCT_MODEL "DLD960GA" #define PRODUCT_MODEL "DLD960GA"
#define FIRMWARE_VER "1.0" #define FIRMWARE_VER "1.02.04"
#define HARDWARE_VER "1.0" #define HARDWARE_VER "1.0"
#define FIRMWARE_VER_MAIN 1 #define FIRMWARE_VER_MAIN 1
#define FIRMWARE_VER_SUB 1 #define FIRMWARE_VER_SUB 2
#define FIRMWARE_VER_SUBSUB 4
#define HARDWARE_VER_MAIN 1 #define HARDWARE_VER_MAIN 1
#define HARDWARE_VER_SUB 1 #define HARDWARE_VER_SUB 1
#define BUFF_STACK_SIZE 512 //128 #define BUFF_STACK_SIZE 64
#define MAX_UART_TXRX_LEN 32 #define MAX_UART_TXRX_LEN 32
#define MAX_TOUCHUAN_DISABLE_TIME 15 #define MAX_TOUCHUAN_DISABLE_TIME 15
#define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min #define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min
typedef enum typedef enum
{ {
DDType_DNT900 = 1, //1 ???????? DNT900 DDType_DNT900 = 1, //1 区位有线 DNT900
DDType_WireDUS, //2???ù?¨???????? DG DDType_WireDUS, //2超声波车位有线 DG
DDType_4G_1, DDType_4G_1,
DDType_WireDus_Wb, //4 ???ù?¨???????? ?ò?? DDType_WireDus_Wb, //4 超声波车位有线 万泊
DDType_LoraAreaMain, //5 Lora ?????÷?ú DDType_LoraAreaMain, //5 Lora 区位主机
DDType_LoraAreaSub, //6 Lora ???????ú DDType_LoraAreaSub, //6 Lora 区位从机
DDType_LoraLotMain, //7 Lora ?????÷?ú DDType_LoraLotMain, //7 Lora 车位主机
DDType_LoraLotSub, //8 Lora ???????ú DDType_LoraLotSub, //8 Lora 车位从机
DDType_LoraBroadcast, //9 Lora ???????? DDType_LoraBroadcast, //9 Lora 广播模式
DDType_LoraRelay, //10 Lora ?????? ???? DLR101 DDType_LoraRelay, //10 Lora 中继, 使用 DLR101
DDType_Relay, //11 ?????????? ?¨??RS485-2 ??·? DDType_Relay, //11 中继模式, 通过RS485-2 收发
DDType_4G_IOT, //12 4G ??±¨??????????????MQTT DDType_4G_IOT, //12 4G 上报传感数据,使用MQTT
DDType_Wire_Collect_IOT, //13 ????????±¨??????????????MQTT DDType_Wire_Collect_IOT, //13 有线,上报传感数据,使用MQTT
DDType_Wire_Display_IOT, //14 ????????·?????????MQTT DDType_Wire_Display_IOT, //14 有线显示发布,使用MQTT
DDType_Radar_Lot, //15 ?×?????? DDType_Radar_Lot, //15 雷达车位
DDType_DLD950 = 20, //DLD950 DDType_DLD950 = 20, //DLD950
DDType_DBN101 = 21, DDType_DBN101 = 21,
DDType_DNT910 = 22, //DNT910,DNT920 DDType_DNT910 = 22, //DNT910,DNT920
DDType_DNT920 = 23, DDType_DNT920 = 23,
DDType_DLD950V4 = 24, DDType_DLD950V4 = 24, // DLD960
}DG_Device_Type; }DG_Device_Type;
typedef enum typedef enum
@@ -67,12 +68,12 @@ typedef struct _SUB_CODE_ENABLE_
uint8_t net_enable; uint8_t net_enable;
uint8_t iot_enable; uint8_t iot_enable;
uint8_t custom_enable; uint8_t custom_enable;
uint8_t loop_enable; // ?÷?????? uint8_t loop_enable; // 流量计数
uint8_t dgdus_enable; uint8_t dgdus_enable;
uint8_t wbdus_enable; uint8_t wbdus_enable;
uint8_t radar_enable; uint8_t radar_enable;
uint8_t laser_enable; // lora?¤???×?????? uint8_t laser_enable; // lora激光雷达车位
uint8_t lora_enable; // Lora ???? uint8_t lora_enable; // Lora 车位
//uint8_t input_enable; //uint8_t input_enable;
} Sub_Code_Enable; } Sub_Code_Enable;
@@ -91,21 +92,12 @@ extern Pkg_Uart g_pkg_uart_1;
extern Pkg_Uart g_pkg_uart_2; extern Pkg_Uart g_pkg_uart_2;
#define RX_BUFFER_LEN BUFF_STACK_SIZE
typedef struct
{
volatile uint8_t DMA_USE_BUFFER;
uint8_t Rx_Buffer[2][RX_BUFFER_LEN];
} USART_DMA_UNIT;
typedef struct _DBN_BLE_STATE_ typedef struct _DBN_BLE_STATE_
{ {
uint8_t flag; // if need to report uint8_t flag; // if need to report
uint8_t enable; // 是否使能 uint8_t enable; // 使能
uint8_t send_flag; uint8_t send_flag;
uint8_t obj_amount; uint8_t obj_amount;
uint8_t cmd; uint8_t cmd;
@@ -49,6 +49,13 @@
#define CMD_DBN_SET_CJQ_PARAM 0x23 // 设置 车检器参数 #define CMD_DBN_SET_CJQ_PARAM 0x23 // 设置 车检器参数
#define CMD_DBN_GET_CJQ_PARAM 0x24 // 读取 车检器参数 #define CMD_DBN_GET_CJQ_PARAM 0x24 // 读取 车检器参数
#define CMD_DBN_OFFLOG_STAT 0x25 // query offlog stat
#define CMD_DBN_OFFLOG_QUERY 0x26 // query offlog records by seq
#define CMD_DBN_OFFLOG_CLEAR 0x27 // clear offlog (audit trail)
#define CMD_DBN_SNAP_STAT 0x28 // query snapshot stat
#define CMD_DBN_SNAP_QUERY 0x29 // query snapshot records by seq
#define CMD_DBN_SNAP_CLEAR 0x2A // clear snapshot (audit trail)
#define CMD_DBN_RW_UART_BAUD 0x31 #define CMD_DBN_RW_UART_BAUD 0x31
@@ -73,7 +80,8 @@
#define MAX_BLE_DAT_RESPONSE_LEN (BLE_BUFF_MAX_LEN - 4) // 14 //返回 的所有字节数不能超过20个字节,除去 magic、header和ckb校验字节,剩下数据的字节数为14 #define MAX_BLE_DAT_RESPONSE_LEN (BLE_BUFF_MAX_LEN - 4) // 14 //返回 的所有字节数不能超过20个字节,除去 magic、header和ckb校验字节,剩下数据的字节数为14
#define MAX_BLE_Notify_Buf_LEN BLE_BUFF_MAX_LEN //24 #define MAX_BLE_Notify_Buf_LEN BLE_BUFF_MAX_LEN //24
#define MAX_BLE_BUF_LEN BLE_BUFF_MAX_LEN //128 #define MAX_BLE_BUF_LEN BLE_BUFF_MAX_LEN //128
#define MAX_BLE_TMP_BUF_LEN BLE_BUFF_MAX_LEN #define MAX_BLE_TMP_BUF_LEN 132 // offlog QUERY: 1 status + 4x32B records
#define MAX_BLE_DAT_BUF_LEN 132 // response data buffer (align tmp buf)
typedef struct _BLE_Notify_Buf_ typedef struct _BLE_Notify_Buf_
{ {
@@ -91,7 +99,7 @@ typedef struct _BUF_DBN_BLE_
uint8_t dat_offset; uint8_t dat_offset;
uint8_t pkg_amount; uint8_t pkg_amount;
uint8_t pkg_seq; uint8_t pkg_seq;
uint8_t dat[MAX_BLE_BUF_LEN]; uint8_t dat[MAX_BLE_DAT_BUF_LEN];
} Buf_DBN_BLE; } Buf_DBN_BLE;
extern Buf_DBN_BLE g_buf_ble_response; extern Buf_DBN_BLE g_buf_ble_response;
@@ -0,0 +1,56 @@
/********************************** (C) COPYRIGHT *******************************
* File Name : fault_diag.h
* Author : wfq
* Version : V1.0
* Date : 2026-08-13
* Description : .noinit RAM + marker
*
* : HardFault UART/printf (/), ;
* RST=0x00000000 , RSTSCKR
* : .noinit (, ):
* - HardFault_Handler RAM mcause/mepc/mtval ( UART)
* - FAULT_MARKER()
* - main fault_diag_init() + boot_cnt
* : FAULT_MARKER(MK_XXX) ; marker
*******************************************************************************/
#ifndef __FAULT_DIAG_H
#define __FAULT_DIAG_H
#include <stdint.h>
typedef struct {
uint32_t magic; /* 0xFA57FA57 = 现场有效 */
uint32_t mcause; /* RISC-V 异常原因 (5/7=野指针 2=非法指令 4/6=未对齐) */
uint32_t mepc; /* 异常指令 PC (对照 .map 反查函数) */
uint32_t mtval; /* 错误访问地址 */
uint32_t marker; /* 最后执行位置标记 */
uint32_t boot_cnt; /* 复位计数 */
} FaultDiag;
/* 定义在 ch32v20x_it.c (.noinit 段) */
extern FaultDiag g_fault_diag;
/* main 开头调用: 打印上次现场 + 重置 + boot_cnt++ */
void fault_diag_init(void);
/* 执行轨迹标记 */
#define FAULT_MARKER(s) (g_fault_diag.marker = (s))
#define MK_MAIN_ENTRY 0xA0000001UL /* main 入口 */
#define MK_INIT_DONE 0xA0000002UL /* 初始化完成, 进主循环前 */
#define MK_LOOP_TOP 0xA0000010UL /* 主循环 while 开头 */
#define MK_UART_SRV_IN 0xA0000011UL /* uart_srv 调用前 */
#define MK_UART_SRV_OUT 0xA0000012UL /* uart_srv 返回后 */
#define MK_SIM_SPI_IN 0xA0000013UL /* sim_snap_spi 调用前 */
#define MK_POLL_BLE_IN 0xA0000014UL /* poll_dbn_ble 调用前 */
#define MK_LUP_FRAME_IN 0xA0000020UL /* lup_process_frame 入口 */
#define MK_LUP_FRAME_OUT 0xA0000021UL /* lup_process_frame 出口 */
#define MK_INGEST_IN 0xA0000030UL /* iot_sensor_ingest 入口 */
#define MK_INGEST_OUT 0xA0000031UL /* iot_sensor_ingest 出口 */
#define MK_EVT_FEED_IN 0xA0000032UL /* iot_evt_feed 入口 */
#define MK_EVT_FEED_OUT 0xA0000033UL /* iot_evt_feed 出口 */
#define MK_EVT_CB_IN 0xA0000034UL /* iot_evt_sensor_cb 入口 */
#define MK_EVT_CB_OUT 0xA0000035UL /* iot_evt_sensor_cb 出口 */
#endif /* __FAULT_DIAG_H */
@@ -22,7 +22,7 @@
#define IOT_MQTT_RECONNECT_MAX_MS 60000 // 最大重连间隔 (指数退避上限) #define IOT_MQTT_RECONNECT_MAX_MS 60000 // 最大重连间隔 (指数退避上限)
#define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s) #define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s)
#define IOT_MQTT_RECV_BUF_LEN 1024 // 接收缓冲区 (单次交互 ≤1024) #define IOT_MQTT_RECV_BUF_LEN 1024 // 接收缓冲区 (单次交互 ≤1024)
#define IOT_MQTT_SEND_BUF_LEN 1024 // 发送缓冲区 (单次交互 ≤1024) #define IOT_MQTT_SEND_BUF_LEN 800 // 发送缓冲区 (2026-08-18: hex 上报方案, 快照 2 条 128hex×2+头 ~420B; loop_data ~604B; 不扩容)
#define IOT_MQTT_TOPIC_MAX_LEN 128 // Topic 最大长度 #define IOT_MQTT_TOPIC_MAX_LEN 128 // Topic 最大长度
/*=========================================================================== /*===========================================================================
@@ -52,5 +52,7 @@ void iot_mqtt_poll(void); // 主循环轮询 (状态机 + 心跳 +
void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理 void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理
void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT
void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04) void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04)
void iot_watchdog_init(void); // 初始化硬件 IWDG (无条件, 主循环卡死兜底)
void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用)
#endif /* __IOT_MQTT_SRV_H__ */ #endif /* __IOT_MQTT_SRV_H__ */
@@ -7,7 +7,7 @@
* @brief DLD960Loop (7F) CH32V208 DLD960Loop MCU * @brief DLD960Loop (7F) CH32V208 DLD960Loop MCU
* *
* : 7F | Addr | LEN | CMD | Data... | XOR | SUM * : 7F | Addr | LEN | CMD | Data... | XOR | SUM
* UART: USART2, 115200bps, PA2(Tx)/PA3(Rx) * UART: USART2, 192000bps, PA2(Tx)/PA3(Rx)
* *
* MCU(CH32V208)MCU(DLD960Loop/AT32F421) * MCU(CH32V208)MCU(DLD960Loop/AT32F421)
* (TCP JSON/5960)MCU下发命令并获取数据 * (TCP JSON/5960)MCU下发命令并获取数据
@@ -212,7 +212,11 @@ typedef enum {
LUP_FRAME_STATE_HEADER, // 接收 Header (Addr, LEN, CMD) LUP_FRAME_STATE_HEADER, // 接收 Header (Addr, LEN, CMD)
LUP_FRAME_STATE_VALUE, // 接收 Value LUP_FRAME_STATE_VALUE, // 接收 Value
LUP_FRAME_STATE_CHECK, // 接收 Checksum LUP_FRAME_STATE_CHECK, // 接收 Checksum
LUP_FRAME_STATE_COMPLETE // 帧完成 LUP_FRAME_STATE_COMPLETE, // 帧完成
/* 0x9F OTA 专用状态 (2026-08-19): 帧格式 9F SubL SubH LEN CMD DATA CHECK */
LUP_FRAME_STATE_OTA_LEN, // 0x9F: 收 LEN (含 CMD)
LUP_FRAME_STATE_OTA_CMD, // 0x9F: 收 CMD
LUP_FRAME_STATE_OTA_CHECK // 0x9F: 收 CHECK (仅 SUM, 无 XOR)
} LUP_FrameState; } LUP_FrameState;
typedef struct { typedef struct {
@@ -228,6 +232,11 @@ extern LUP_FrameParser g_lup_parser;
/* 喂一个字节给帧解析器,返回 1 表示帧接收完成 */ /* 喂一个字节给帧解析器,返回 1 表示帧接收完成 */
int lup_feed_byte(uint8_t byte); int lup_feed_byte(uint8_t byte);
/* 0x9F OTA 帧解析器 (OTA 模式专用, g_flag_counter_ota.flag=1 时使用)
* : 9F SubL SubH LEN CMD DATA(LEN-1) CHECK(SUM)
* 0x7F g_lup_parser /, (LUP_FRAME_STATE_OTA_*) */
int lup_feed_byte_ota(uint8_t byte);
/* 取完整帧的指针和数据长度 */ /* 取完整帧的指针和数据长度 */
const uint8_t *lup_frame_data(void); const uint8_t *lup_frame_data(void);
uint16_t lup_frame_len(void); uint16_t lup_frame_len(void);
@@ -36,7 +36,7 @@ extern "C" {
/* The number of sockets, the maximum is 31 */ /* The number of sockets, the maximum is 31 */
#define WCHNET_MAX_SOCKET_NUM (WCHNET_NUM_IPRAW+WCHNET_NUM_UDP+WCHNET_NUM_TCP+WCHNET_NUM_TCP_LISTEN) #define WCHNET_MAX_SOCKET_NUM (WCHNET_NUM_IPRAW+WCHNET_NUM_UDP+WCHNET_NUM_TCP+WCHNET_NUM_TCP_LISTEN)
#define WCHNET_TCP_MSS 576 //1024 //1460 /* Size of TCP MSS*/ #define WCHNET_TCP_MSS 700 //576 //1024 //1460 /* Size of TCP MSS*/
#define WCHNET_NUM_POOL_BUF (WCHNET_NUM_TCP*2+2) /* The number of POOL BUFs, the number of receive queues */ #define WCHNET_NUM_POOL_BUF (WCHNET_NUM_TCP*2+2) /* The number of POOL BUFs, the number of receive queues */
@@ -47,6 +47,12 @@ extern "C" {
#define ETH_RXBUFNB 4 /* Number of MAC received descriptors */ #define ETH_RXBUFNB 4 /* Number of MAC received descriptors */
/* 2026-08-17 RAM 瘦身: ETH_MAX_PACKET_SIZE 1520 → 768.
TCP (MSS=700) = 700+20+20+14+4 = 758B 768 ;
(TCP JSON/MQTT/SSC UDP ) >728B UDP
.bss: (1520-768)×(RX 4 + TX 1) = 3.76KB ( +3.76KB)
UDP , 1024 ( 2.48KB) */
#define ETH_MAX_PACKET_SIZE 768
#ifndef ETH_MAX_PACKET_SIZE #ifndef ETH_MAX_PACKET_SIZE
#define ETH_RX_BUF_SZE 1520 /* MAC receive buffer length, an integer multiple of 4 */ #define ETH_RX_BUF_SZE 1520 /* MAC receive buffer length, an integer multiple of 4 */
#define ETH_TX_BUF_SZE 1520 /* MAC send buffer length, an integer multiple of 4 */ #define ETH_TX_BUF_SZE 1520 /* MAC send buffer length, an integer multiple of 4 */
@@ -84,7 +90,10 @@ extern "C" {
/* If you want to achieve a higher transmission speed, /* If you want to achieve a higher transmission speed,
* try to increase RECE_BUF_LEN to (WCHNET_TCP_MSS*4) * try to increase RECE_BUF_LEN to (WCHNET_TCP_MSS*4)
* and increase WCHNET_NUM_TCP_SEG to (WCHNET_NUM_TCP*4)*/ * and increase WCHNET_NUM_TCP_SEG to (WCHNET_NUM_TCP*4)*/
#define RECE_BUF_LEN (WCHNET_TCP_MSS*2) /* socket receive buffer size */ /* 2026-08-17 RAM 瘦身: MSS×2(1400)→1024 — MSS=700 时完整 TCP 段+头≈740B,
1024 ; 5 RECE_BUF_LEN (SocketRecvBuf/MyBuf/tmp/_iot_wchnet_buf/
g_json_wchnet_buf) ~2.6KB .bss ( +2.6KB) */
#define RECE_BUF_LEN 1024 /* socket receive buffer size */
#define WCHNET_NUM_PBUF WCHNET_NUM_POOL_BUF /* Number of PBUF structures */ #define WCHNET_NUM_PBUF WCHNET_NUM_POOL_BUF /* Number of PBUF structures */
@@ -92,7 +101,7 @@ extern "C" {
#define WCHNET_MEM_HEAP_SIZE (((WCHNET_TCP_MSS+0x10+54+8)*WCHNET_NUM_TCP_SEG)+ETH_TX_BUF_SZE+64+2*0x18) /* memory heap size */ #define WCHNET_MEM_HEAP_SIZE (((WCHNET_TCP_MSS+0x10+54+8)*WCHNET_NUM_TCP_SEG)+ETH_TX_BUF_SZE+64+2*0x18) /* memory heap size */
#define WCHNET_NUM_ARP_TABLE 50 /* Number of ARP lists */ #define WCHNET_NUM_ARP_TABLE 16 /* 2026-08-17: 50→16 (停车设备同时连接 IP 少, 省 ~0.8KB .bss) */
#define WCHNET_MEM_ALIGNMENT 4 /* 4 byte alignment */ #define WCHNET_MEM_ALIGNMENT 4 /* 4 byte alignment */
@@ -0,0 +1,169 @@
/**
******************************************************************************
* @file offlog.h
* @author wfq
* @version V1.0
* @date 2026-08-04
* @brief DBN (W25Qxx SPI NOR, )
*
* : "平台重复上线/异常重启但现场无日志"
* vsMQTT ( initialize):
* - : OFFLOG_EVT_BOOT , boot_seq
* - : boot_seq , IOT_*
*
* (2026-08-12 ROADMAP ):
* | 0x000000 64KB ()
* | OTA 0x010000 512KB ()
* | 0x090000 : W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB
* | (snapshot.c, 2026-08-12 )
* JEDEC ID (0x9F) (g_offlog_part )
*
* :
* - 0 () = , (boot_seq//),
* - 1..63 = , 128 x 32B
* - , (),
* - , = ()
*
* (32B ):
* magic(1) type(1) len(1) flags(1) seq(4) boot_seq(2) rsvd(2)
* ts_ms(4) unix_ts(4) payload(14)
******************************************************************************
*/
#ifndef _OFFLOG_H__
#define _OFFLOG_H__
#include <stdint.h>
/*===========================================================================
*
*===========================================================================*/
/* 固定分区 (ROADMAP 2026-08-12): 参数区 64KB + OTA 512KB, 事件区紧随其后 */
#define OFFLOG_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */
#define OFFLOG_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */
#define OFFLOG_AREA_BASE (OFFLOG_PARAM_SIZE + OFFLOG_OTA_SIZE) /* 0x090000 */
#define OFFLOG_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
#define OFFLOG_EVT_PER_SECTOR 128 /* 4096 / 32 */
/* 芯片 JEDEC ID (0x9F 命令第 3 字节, EF 40 xx) */
#define OFFLOG_CHIP_W25Q32 0x16
#define OFFLOG_CHIP_W25Q64 0x17
#define OFFLOG_CHIP_W25Q128 0x18
#define OFFLOG_CHIP_W25Q256 0x19
/* 运行时分区表 (offlog_init 由 JEDEC ID 填充; 默认 W25Q32) */
typedef struct {
uint8_t chip; /* OFFLOG_CHIP_xxx */
uint32_t area_size; /* 事件日志区大小 */
uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */
uint32_t max_records; /* 最大记录数 = data_sectors * 128 */
} OfflogPart;
extern OfflogPart g_offlog_part;
/* 兼容宏: 展开为运行时值 (offlog.c 主体引用不变) */
#define OFFLOG_AREA_SIZE (g_offlog_part.area_size)
#define OFFLOG_DATA_SECTOR_CNT (g_offlog_part.data_sectors)
#define OFFLOG_DATA_SIZE ((g_offlog_part.data_sectors) * 4096UL)
#define OFFLOG_MAX_RECORDS (g_offlog_part.max_records)
#define OFFLOG_DATA_BASE (OFFLOG_AREA_BASE + 4096UL)
/* 头扇区记录 (32B, 扇区切换时更新) */
typedef struct {
uint8_t magic[4]; /* "OFFL" */
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
uint32_t wr_sector; /* 上次写扇区号 (区内 1..63) */
uint32_t wr_off; /* 上次写偏移 (相对 OFFLOG_AREA_BASE) */
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
uint32_t count; /* 有效记录数 (0..max_records) */
uint32_t rsvd[2];
} OfflogHead; /* 28B, 按 32B 对齐使用 */
#define OFFLOG_HEAD_MAGIC0 'O'
#define OFFLOG_HEAD_MAGIC1 'F'
#define OFFLOG_HEAD_MAGIC2 'L'
#define OFFLOG_HEAD_MAGIC3 'L'
/* 事件记录 (32B 定长, 字段重排保证自然对齐无 padding:
4×uint8 uint32×3 uint16×2 payload[12] = 4+12+4+12 = 32) */
typedef struct {
uint8_t magic; /* 0xA5 */
uint8_t type; /* OFFLOG_EVT_xxx */
uint8_t len; /* payload 有效字节数 (0..12) */
uint8_t flags; /* bit0: unix_ts 有效 */
uint32_t seq; /* 全局序号 (跨 boot 递增) */
uint32_t ts_ms; /* boot 内相对时间 mstick() */
uint32_t unix_ts; /* 已同步 Unix 秒 (flags.bit0=1 时有效) */
uint16_t boot_seq; /* 启动序号 */
uint16_t rsvd;
uint8_t payload[12]; /* 事件参数 */
} OfflogEvt; /* 32B, 无 padding */
#define OFFLOG_EVT_MAGIC 0xA5
#define OFFLOG_EVT_SIZE sizeof(OfflogEvt)
#define OFFLOG_UNIX_VALID_FLAG 0x01
/*===========================================================================
*
*===========================================================================*/
enum {
OFFLOG_EVT_BOOT = 0x01, /* 上电/复位; payload[0..3]=复位原因寄存器 RCC_RSTSCKR */
OFFLOG_EVT_IOT_CONNECT = 0x10, /* MQTT TCP 连接成功 (socket 中断) */
OFFLOG_EVT_IOT_READY = 0x11, /* MQTT SUBACK → 发 initialize (重复上线直接原因!) */
OFFLOG_EVT_IOT_DISCONN = 0x12, /* MQTT 断连; payload[0]=原因(1=断开 2=超时 3=CONNACK拒绝 4=连接超时) */
OFFLOG_EVT_IOT_RECONN = 0x13, /* 重连退避; payload[0..3]=退避 ms */
OFFLOG_EVT_EVT_RETRY = 0x30, /* event_report ACK 超时重发; payload[0..3]=msg_id, payload[4]=retry */
OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */
OFFLOG_EVT_COIL = 0x40, /* 线圈事件; payload[0]=sub(1=进 2=出 3=断开 4=恢复), payload[1]=ch, payload[2..5]=value(50ms) */
OFFLOG_EVT_TIME_ANCHOR = 0x50, /* 时钟同步锚点; unix_ts=同步值 */
OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */
};
/* 复位原因寄存器 (CH32V20x RCC 基址 0x40021000, RSTSCKR 偏移 0x24,
STM32F1 ; , , ) */
#define OFFLOG_RCC_BASE 0x40021000UL
#define OFFLOG_RCC_RSTSCKR (*(volatile uint32_t *)(OFFLOG_RCC_BASE + 0x24UL))
/* 复位原因位 (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_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_RMVF (1UL << 24) /* 清除复位标志 (写 1 清全部, WCH: RCC_RMVF) */
/*===========================================================================
* API
*===========================================================================*/
void offlog_init(void); /* 上电初始化 (扫描恢复) + 内部准备 BOOT 事件 */
void offlog_boot(uint32_t reset_reason); /* 记录 BOOT 事件 (带复位原因) */
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len);
void offlog_iot_connect(void);
void offlog_iot_ready(void);
void offlog_iot_disconnect(uint8_t reason);
void offlog_iot_reconn(uint32_t backoff_ms);
void offlog_evt_retry(uint32_t msg_id, uint8_t retry);
void offlog_evt_giveup(uint32_t msg_id);
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value);
void offlog_time_anchor(uint32_t unix_ts);
uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */
uint16_t offlog_count(void); /* 有效记录数 */
int offlog_read_idx(uint16_t idx, OfflogEvt *out); /* 按逻辑序号读, 0=成功 -1=越界 */
void offlog_clear(void); /* 清空事件区 (清空后写 LOG_CLEAR 审计) */
/*===========================================================================
* (MQTT V1.06 / TCP JSON V1.02)
*===========================================================================*/
uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */
uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */
const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 (旧方案, 保留) */
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len); /* 32B → 64 hex (TCP/MQTT log_query, V1.03/V1.07) */
#define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */
#endif /* _OFFLOG_H__ */
@@ -0,0 +1,139 @@
/**
******************************************************************************
* @file snapshot.h
* @author wfq
* @version V1.0
* @date 2026-08-12
* @brief DBN (W25Qxx SPI NOR, , )
*
* : 0xC0 (4 线) , ;
* "发生了什么", "波形长什么样"
*
* (ROADMAP 2026-08-12 ):
* | 0x000000 64KB ()
* | OTA 0x010000 512KB ()
* | 0x090000 (offlog , g_offlog_part.area_size)
* | ( - - )
* JEDEC ID (0x9F) / (g_snap_part )
*
* ( offlog ):
* - 0 () = , (boot_seq//),
* - 64 x 64B (4096/64)
* - , (),
* - , = ()
*
* 线 (, 2026-08-12 ):
* - USART2 ISR lup_feed_byte(), uart_srv()
* lup_process_frame iot_sensor_ingest snap_enqueue()
* - snap_enqueue(): + RAM ( SPI, );
* - snap_flush(): iot_mqtt_publish_sensor , RAM SPI
* ( ~45ms , offlog 线)
* - : tail/count, head
*
* SnapRec (64B , padding):
* magic(1) len(1) flags(1) rsvd(1) | seq(4) ts_ms(4) boot_seq(2) rsvd2(2) | coils(48)
* coils = 4 x 12B, 0xC0 线线:
* [0] 1 freq_level(2)|direction(1)|freq_type(1)|sensitivity(4)
* [1] condition(4)|loop_state(1)|car_state(1)|misc_type(2)
* [2..4] 3B LE
* [5..7] 3B LE ()
* [8..11] 4B LE
******************************************************************************
*/
#ifndef _SNAPSHOT_H__
#define _SNAPSHOT_H__
#include <stdint.h>
#include "offlog.h" /* OFFLOG_CHIP_xxx + g_offlog_part (事件区大小) */
#include "loop_uart_proto.h" /* LUP_SensorReport / LUP_CoilSensor */
/*===========================================================================
*
*===========================================================================*/
/* 固定分区 (与 offlog 一致): 参数区 64KB + OTA 512KB */
#define SNAP_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */
#define SNAP_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */
#define SNAP_FIXED_SIZE (SNAP_PARAM_SIZE + SNAP_OTA_SIZE) /* 0x090000 */
#define SNAP_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
#define SNAP_REC_PER_SECTOR 64 /* 4096 / 64 */
/* 运行时分区表 (snap_init 由 JEDEC ID + g_offlog_part 填充; 默认 W25Q32) */
typedef struct {
uint8_t chip; /* OFFLOG_CHIP_xxx */
uint32_t area_base; /* 快照区起始 (事件区后) */
uint32_t area_size; /* 快照区大小 (剩余容量) */
uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */
uint32_t max_records; /* 最大记录数 = data_sectors * 64 */
} SnapPart;
extern SnapPart g_snap_part;
/* 兼容宏: 展开为运行时值 */
#define SNAP_DATA_BASE (g_snap_part.area_base + 4096UL)
#define SNAP_AREA_SIZE (g_snap_part.area_size)
#define SNAP_DATA_SECTOR_CNT (g_snap_part.data_sectors)
#define SNAP_DATA_SIZE ((g_snap_part.data_sectors) * 4096UL)
#define SNAP_MAX_RECORDS (g_snap_part.max_records)
/* 头扇区记录 (32B, 扇区切换时更新) */
typedef struct {
uint8_t magic[4]; /* "SNAP" */
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
uint32_t wr_sector; /* 上次写扇区号 (区内 1..n) */
uint32_t wr_off; /* 上次写偏移 (绝对地址) */
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
uint32_t count; /* 有效记录数 (0..max_records) */
uint32_t rsvd[2];
} SnapHead; /* 28B, 按 32B 对齐使用 */
#define SNAP_HEAD_MAGIC0 'S'
#define SNAP_HEAD_MAGIC1 'N'
#define SNAP_HEAD_MAGIC2 'A'
#define SNAP_HEAD_MAGIC3 'P'
/* 快照记录 (64B 定长, 字段重排保证自然对齐无 padding:
4×uint8 uint32×2 uint16×2 coils[48] = 16+48 = 64) */
typedef struct {
uint8_t magic; /* 0xA6 (区别于事件 0xA5) */
uint8_t len; /* 线圈数据有效长度 = coil_count*12 (0..48) */
uint8_t flags; /* bit0: 分包帧 (预留) */
uint8_t rsvd;
uint32_t seq; /* 全局序号 (跨 boot 递增, flush 时分配) */
uint32_t ts_ms; /* boot 内相对时间 mstick() (采集时刻) */
uint16_t boot_seq; /* 启动序号 */
uint16_t rsvd2;
uint8_t coils[48]; /* 4 × 12B 线圈数据 (0xC0 线上格式原样) */
} SnapRec; /* 64B, 无 padding */
#define SNAP_MAGIC 0xA6
#define SNAP_REC_SIZE sizeof(SnapRec)
#define SNAP_COIL_BYTES 12
#define SNAP_COILS_MAX 4
/* RAM 暂存深度 (中断 enqueue → 主循环 flush 之间的缓冲)
Loop 150ms, publish_sensor flush, 8 1.2s ;
CH32V208 SRAM : 64B × 8 = 512B, SPI_FLASH_BUF 4KB */
#define SNAP_RAM_DEPTH 8
#define SNAP_MAX_QUERY_RECORDS 2 /* QUERY 分页上限: BLE 原始 64B×2 (132B 缓冲); TCP/MQTT hex 上报 64B->128 字符×2 亦安全 (2026-08-18) */
/*===========================================================================
* API
*===========================================================================*/
void snap_init(void); /* 上电初始化 (扫描恢复) */
void snap_delayed_init(void); /* 主循环延后初始化 (开机 3s 后, 2026-08-17) */
uint8_t snap_enabled(void); /* Flash 初始化成功 */
void snap_enqueue(const LUP_SensorReport *sr); /* 主循环入队: 打包→RAM 暂存 (满丢新) */
void snap_flush(void); /* 主循环: RAM 暂存→SPI 落盘 */
uint32_t snap_count(void); /* 有效记录数 */
uint32_t snap_seq_last(void); /* 最新一条记录全局序号 (0=空) */
uint32_t snap_boot_seq(void); /* 当前 boot 序号 */
int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */
void snap_clear(void); /* 清空快照区 (审计写入事件流) */
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len); /* 记录 -> JSON (旧方案, 保留) */
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len); /* 64B -> 128 hex (TCP/MQTT log_query, V1.03/V1.07) */
#endif /* _SNAPSHOT_H__ */
@@ -32,7 +32,10 @@ void load_cfg_from_flash(void);
void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size); void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size);
void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size); void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size);
void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size);
void SPI_Flash_Erase_Sector(uint32_t Dst_Addr);
void storage_init(void); void storage_init(void);
uint8_t SPI_Flash_ReadJEDEC_ID(void); /* 0x9F JEDEC device id (W25Q32=0x16...) */
#endif #endif
@@ -17,7 +17,8 @@
* Protocol Constants * Protocol Constants
*===========================================================================*/ *===========================================================================*/
#define TCP_JSON_PORT 5960 // TCP JSON protocol default port #define TCP_JSON_PORT 5960 // TCP JSON protocol default port
#define TCP_JSON_MAX_FRAME 4096 // Max JSON frame length #define TCP_JSON_MAX_FRAME 800 //4096 // Max JSON frame length
#define TCP_JSON_DATA_BUF_LEN 1024 // 本地 JSON 组装缓冲长度 (data_json)
#define TCP_JSON_AUTH_TIMEOUT_MS 60000 // 60s auth timeout (ms) #define TCP_JSON_AUTH_TIMEOUT_MS 60000 // 60s auth timeout (ms)
#define TCP_JSON_RECV_BUF_LEN (TCP_JSON_MAX_FRAME * 2) #define TCP_JSON_RECV_BUF_LEN (TCP_JSON_MAX_FRAME * 2)
#define TCP_JSON_MAX_PWD_RETRY 3 // Max password retries before lockout #define TCP_JSON_MAX_PWD_RETRY 3 // Max password retries before lockout
@@ -23,6 +23,10 @@
#include "simple_json.h" #include "simple_json.h"
#include "tcp_json_srv.h" #include "tcp_json_srv.h"
#include "storage.h" #include "storage.h"
#include "offlog.h"
#include "snapshot.h"
#include "fault_diag.h"
#include "ch32v20x_iwdg.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@@ -36,8 +40,9 @@ extern ReportConfig g_report_cfg; // from tcp_json_srv.c
extern uint8_t SocketId_TCP; // from net_srv.c, MQTT socket created by WCHNET_CreateTcpMqttSocket extern uint8_t SocketId_TCP; // from net_srv.c, MQTT socket created by WCHNET_CreateTcpMqttSocket
uint8_t g_iot_socket = 0xFF; // MQTT TCP socket ID uint8_t g_iot_socket = 0xFF; // MQTT TCP socket ID
IotMqttState g_iot_state = IOT_STATE_DISCONNECTED; IotMqttState g_iot_state = IOT_STATE_DISCONNECTED;
static IotMqttState _prev_iot_state = IOT_STATE_DISCONNECTED; // 事件日志: 状态沿检测
uint8_t g_iot_msg_id = 0; // MQTT packet identifier uint8_t g_iot_msg_id = 0; // MQTT packet identifier
static uint32_t _iot_last_heartbeat = 0; // 上次心跳时刻 (ms) static uint32_t _iot_last_heartbeat = 0; // 上次 PINGREQ 时刻 (ms), MQTT 保活
static uint32_t _iot_reconnect_deadline = 0; static uint32_t _iot_reconnect_deadline = 0;
static uint32_t _iot_reconnect_backoff = 0; static uint32_t _iot_reconnect_backoff = 0;
static uint32_t _iot_connect_start = 0; // TCP connect 开始时刻 static uint32_t _iot_connect_start = 0; // TCP connect 开始时刻
@@ -107,6 +112,8 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH]; IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH];
e->type = type; e->ch = ch; e->value = value; e->type = type; e->ch = ch; e->value = value;
_evt_count++; _evt_count++;
/* 事件日志: 线圈事件 (主循环上下文) */
offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */
} }
_evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报 _evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报
PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count); PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count);
@@ -115,12 +122,14 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
/* 沿检测: 每收到一帧有效 0xC0 调用一次 (两条消费路径都要喂) */ /* 沿检测: 每收到一帧有效 0xC0 调用一次 (两条消费路径都要喂) */
static void iot_evt_feed(const LUP_SensorReport *sr) { static void iot_evt_feed(const LUP_SensorReport *sr) {
uint8_t i; uint8_t i;
FAULT_MARKER(MK_EVT_FEED_IN);
if (!_evt_prev_valid) { // 首帧: 只建快照 (上电已有车不算进入沿) if (!_evt_prev_valid) { // 首帧: 只建快照 (上电已有车不算进入沿)
for (i = 0; i < LUP_COIL_COUNT; i++) { for (i = 0; i < LUP_COIL_COUNT; i++) {
_evt_prev_car[i] = (i < sr->coil_count) ? sr->coils[i].car_state : 0; _evt_prev_car[i] = (i < sr->coil_count) ? sr->coils[i].car_state : 0;
_evt_prev_loop[i] = (i < sr->coil_count) ? sr->coils[i].loop_state : 0; _evt_prev_loop[i] = (i < sr->coil_count) ? sr->coils[i].loop_state : 0;
} }
_evt_prev_valid = 1; _evt_prev_valid = 1;
FAULT_MARKER(MK_EVT_FEED_OUT);
return; return;
} }
for (i = 0; i < sr->coil_count && i < LUP_COIL_COUNT; i++) { for (i = 0; i < sr->coil_count && i < LUP_COIL_COUNT; i++) {
@@ -148,6 +157,7 @@ static void iot_evt_feed(const LUP_SensorReport *sr) {
_evt_prev_car[i] = cs->car_state; _evt_prev_car[i] = cs->car_state;
_evt_prev_loop[i] = cs->loop_state; _evt_prev_loop[i] = cs->loop_state;
} }
FAULT_MARKER(MK_EVT_FEED_OUT);
} }
/* 统一帧摄取: 事件沿检测 + 刷新 loop_data 缓存 (单一数据源) /* 统一帧摄取: 事件沿检测 + 刷新 loop_data 缓存 (单一数据源)
@@ -156,18 +166,23 @@ static void iot_evt_feed(const LUP_SensorReport *sr) {
<2% loop_data 34s ( iscar <2% loop_data 34s ( iscar
true), diff fast_mode 300ms ! */ true), diff fast_mode 300ms ! */
static void iot_sensor_ingest(const LUP_SensorReport *sr) { static void iot_sensor_ingest(const LUP_SensorReport *sr) {
FAULT_MARKER(MK_INGEST_IN);
iot_evt_feed(sr); // 事件沿检测 iot_evt_feed(sr); // 事件沿检测
memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照 memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照
_cached_sr_valid = 1; _cached_sr_valid = 1;
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */
FAULT_MARKER(MK_INGEST_OUT);
} }
/* lup 传感回调: uart_srv 消费路径的帧从这里喂入 (lup_process_frame 已过校验) */ /* lup 传感回调: uart_srv 消费路径的帧从这里喂入 (lup_process_frame 已过校验) */
static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) { static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) {
LUP_SensorReport sr; LUP_SensorReport sr;
FAULT_MARKER(MK_EVT_CB_IN);
memset(&sr, 0, sizeof(sr)); memset(&sr, 0, sizeof(sr));
if (lup_parse_sensor_report(pkg, len, &sr) == 0) { if (lup_parse_sensor_report(pkg, len, &sr) == 0) {
iot_sensor_ingest(&sr); iot_sensor_ingest(&sr);
} }
FAULT_MARKER(MK_EVT_CB_OUT);
} }
/* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */ /* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */
@@ -241,11 +256,13 @@ static void iot_evt_process(void) {
if (_evt_retry >= IOT_EVT_MAX_RETRY) { if (_evt_retry >= IOT_EVT_MAX_RETRY) {
PRINT("EVT: retry exhausted msg_id=%lu, hold %d events\n", PRINT("EVT: retry exhausted msg_id=%lu, hold %d events\n",
_evt_pend_id, _evt_count); _evt_pend_id, _evt_count);
offlog_evt_giveup(_evt_pend_id); // 事件日志: 重试耗尽
_evt_pend_id = 0; _evt_pend_id = 0;
_evt_gaveup = 1; // 挂起: 新事件或重连时再触发 _evt_gaveup = 1; // 挂起: 新事件或重连时再触发
return; return;
} }
_evt_retry++; _evt_retry++;
offlog_evt_retry(_evt_pend_id, _evt_retry); // 事件日志: ACK 超时重发
iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); // 同 id/ts 重发 iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); // 同 id/ts 重发
_evt_sent_ms = now; _evt_sent_ms = now;
return; return;
@@ -290,13 +307,36 @@ static int iot_make_topic(char *out, uint16_t out_len, const char *direction,
* MQTT Packet Helpers * MQTT Packet Helpers
*===========================================================================*/ *===========================================================================*/
/* 发送 MQTT 报文到 broker */ /* 发送 MQTT 报文到 broker — 循环重试防 WCHNET 部分发送
(2026-07-23: SocketSend 520/607B broker _raw+RST)
: ret=0x11 TCP /, 3
(TCP超时要等~2, , ) */
static int _iot_send_fail_cnt = 0; // 连续发送失败计数
static int iot_mqtt_send(const uint8_t *buf, uint16_t len) { static int iot_mqtt_send(const uint8_t *buf, uint16_t len) {
uint16_t slen = len; uint16_t total_sent = 0;
PRINT("IOT: SocketSend sock=%d len=%d\n", g_iot_socket, len); int retries = 0;
uint8_t ret = WCHNET_SocketSend(g_iot_socket, (uint8_t *)buf, &slen); while (total_sent < len) {
PRINT("IOT: SocketSend ret=0x%02X sent=%d\n", ret, slen); uint32_t slen = len - total_sent;
return (ret == WCHNET_ERR_SUCCESS) ? 0 : -1; uint8_t ret = WCHNET_SocketSend(g_iot_socket, (uint8_t *)(buf + total_sent), &slen);
total_sent += (uint16_t)slen;
if (slen == 0 || ret != WCHNET_ERR_SUCCESS || ++retries > 10) {
_iot_send_fail_cnt++;
PRINT("IOT: SocketSend FAIL#%d ret=0x%02X sent=%u/%u\n",
_iot_send_fail_cnt, ret, total_sent, len);
if (_iot_send_fail_cnt >= 3) {
PRINT("IOT: SocketSend 3 consecutive failures, forcing reconnect\n");
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
g_iot_socket = 0xFF;
g_iot_state = IOT_STATE_DISCONNECTED;
_iot_send_fail_cnt = 0;
_iot_reconnect_deadline = mstick() + 3000; // 3s 后重连, 让 WCHNET 清旧 socket
}
return -1;
}
}
_iot_send_fail_cnt = 0; // 成功一次就清零
return 0;
} }
/* 发送 MQTT CONNECT — 参考 net_srv.c mqtt_connect 实现 */ /* 发送 MQTT CONNECT — 参考 net_srv.c mqtt_connect 实现 */
@@ -429,6 +469,48 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
g_sub_code_enable.iot_enable ? "true" : "false"); g_sub_code_enable.iot_enable ? "true" : "false");
iot_mqtt_publish(resp_topic, data_json, strlen(data_json), 1); iot_mqtt_publish(resp_topic, data_json, strlen(data_json), 1);
} else if (strcmp(cmd_str, "report_config") == 0) {
/* 平台下发配置 → 更新 g_report_cfg, 回 ACK */
char data[512] = {0};
simple_parse_json(json, "\"data\"", data);
if (strlen(data) > 0) {
char tmp2[64];
memset(tmp2, 0, sizeof(tmp2));
simple_parse_json(data, "\"enable\"", tmp2);
if (strlen(tmp2) > 0) g_report_cfg.enable = (strcmp(tmp2, "true") == 0);
memset(tmp2, 0, sizeof(tmp2));
simple_parse_json(data, "\"interval\"", tmp2);
if (strlen(tmp2) > 0) g_report_cfg.interval = (uint16_t)strtoul(tmp2, NULL, 10);
memset(tmp2, 0, sizeof(tmp2));
simple_parse_json(data, "\"once\"", tmp2);
if (strlen(tmp2) > 0) g_report_cfg.once = (strcmp(tmp2, "true") == 0);
memset(tmp2, 0, sizeof(tmp2));
simple_parse_json(data, "\"env_eval\"", tmp2);
if (strlen(tmp2) > 0) g_report_cfg.env_eval = (strcmp(tmp2, "true") == 0);
memset(tmp2, 0, sizeof(tmp2));
simple_parse_json(data, "\"sensor_type\"", tmp2);
if (strlen(tmp2) > 0) g_report_cfg.sensor_type = (uint8_t)strtoul(tmp2, NULL, 10);
}
char resp[256];
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"report_config\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\"}",
msg_id, dev_time_now());
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} else if (strcmp(cmd_str, "event_report") == 0) {
/* 平台对 event_report 的 ACK: 检查 code 字段 */
char tmp3[64] = {0};
int code = -1;
simple_parse_json(json, "\"code\"", tmp3);
if (strlen(tmp3) > 0) code = (int)strtol(tmp3, NULL, 10);
if (code == 0) {
iot_evt_handle_ack(msg_id, 0);
} else {
iot_evt_handle_ack(msg_id, code);
}
// ACK 不回复 (避免乒乓)
} else if (strcmp(cmd_str, "pwd_verify") == 0) { } else if (strcmp(cmd_str, "pwd_verify") == 0) {
// 验证密码 // 验证密码
char password[16] = {0}; char password[16] = {0};
@@ -466,6 +548,185 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} }
} else if (strcmp(cmd_str, "log_stat") == 0) {
/* 日志统计/分页定位: event/snapshot 流 (协议 V1.07) */
char stream_buf[16];
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
char resp[300];
if (strcmp(stream_buf, "snapshot") == 0) {
uint32_t total = snap_count();
uint32_t seq_last = snap_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"log_stat\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"stream\":\"snapshot\",\"enabled\":%s,\"boot_seq\":%lu,"
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}}",
msg_id, dev_time_now(),
snap_enabled() ? "true" : "false",
(unsigned long)snap_boot_seq(),
(unsigned long)total, (unsigned long)SNAP_MAX_RECORDS,
(unsigned long)seq_first, (unsigned long)seq_last);
PRINT("IOT: log_stat(snapshot) count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)total, (unsigned long)seq_first, (unsigned long)seq_last);
} else {
uint32_t total = offlog_count();
uint32_t seq_last = offlog_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"log_stat\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu,"
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}}",
msg_id, dev_time_now(),
offlog_enabled() ? "true" : "false",
(unsigned long)offlog_boot_seq(),
(unsigned long)total, (unsigned long)OFFLOG_MAX_RECORDS,
(unsigned long)seq_first, (unsigned long)seq_last);
PRINT("IOT: log_stat count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)total, (unsigned long)seq_first, (unsigned long)seq_last);
}
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} else if (strcmp(cmd_str, "log_query") == 0) {
/* 分页拉取脱机日志: 按全局序号, event/snapshot 流, hex 原始字节上报 (协议 V1.07) */
static char resp[IOT_MQTT_SEND_BUF_LEN]; /* static: 避免大栈开销 */
char stream_buf[16];
uint32_t start_seq = 0, req_count = 0;
int is_snap = 0;
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"start_seq\"", tmp);
if (strlen(tmp) > 0) start_seq = (uint32_t)strtoul(tmp, NULL, 10);
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"count\"", tmp);
if (strlen(tmp) > 0) req_count = (uint32_t)strtoul(tmp, NULL, 10);
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
if (strcmp(stream_buf, "snapshot") == 0) is_snap = 1;
int pos = snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"log_query\",\"ts\":%lu,\"code\":0,"
"\"msg\":\"success\",\"data\":{\"start_seq\":%lu,\"records\":[",
msg_id, dev_time_now(), (unsigned long)start_seq);
uint32_t fetched = 0;
if (is_snap) {
uint32_t total = snap_count();
uint32_t seq_last = snap_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
if (req_count > SNAP_MAX_QUERY_RECORDS) req_count = SNAP_MAX_QUERY_RECORDS;
if (req_count == 0) req_count = SNAP_MAX_QUERY_RECORDS;
if (snap_enabled() && total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
SnapRec rec;
char hex[132]; /* 64B -> 128 hex + NUL */
if (snap_read_idx(idx + fetched, &rec) != 0) break;
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ",");
snap_rec_to_hex(&rec, hex, sizeof(hex));
pos += snprintf(resp + pos, sizeof(resp) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)rec.seq, hex);
fetched++;
}
}
PRINT("IOT: log_query(snapshot) start_seq=%lu req=%lu fetched=%lu\n",
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
} else {
uint32_t total = offlog_count();
uint32_t seq_last = offlog_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS;
if (req_count == 0) req_count = OFFLOG_MAX_QUERY_RECORDS;
if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
OfflogEvt evt;
char hex[68]; /* 32B -> 64 hex + NUL */
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ",");
offlog_evt_to_hex(&evt, hex, sizeof(hex));
pos += snprintf(resp + pos, sizeof(resp) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)evt.seq, hex);
fetched++;
}
}
PRINT("IOT: log_query start_seq=%lu req=%lu fetched=%lu\n",
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
}
snprintf(resp + pos, sizeof(resp) - pos, "]}}");
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} else if (strcmp(cmd_str, "log_clear") == 0) {
/* 清除日志 (审计留痕). event 阻塞 ~2.8s (63 扇区), snapshot 阻塞 ~45ms (逻辑清除+当前扇区) */
char stream_buf[16];
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
char resp[256];
if (strcmp(stream_buf, "snapshot") == 0) {
snap_clear();
PRINT("IOT: log_clear(snapshot) done\n");
} else {
offlog_clear();
PRINT("IOT: log_clear done\n");
}
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"log_clear\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\"}",
msg_id, dev_time_now());
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} else if (strcmp(cmd_str, "ssc_net_query") == 0) {
/* SSC 网络配置查询 (只读全局, 协议 V1.02) */
char resp[400];
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"ssc_net_query\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"dev_ip\":\"%d.%d.%d.%d\",\"subnet_mask\":\"%d.%d.%d.%d\","
"\"route_ip\":\"%d.%d.%d.%d\",\"lssc_ip\":\"%d.%d.%d.%d\","
"\"dns\":\"%d.%d.%d.%d\",\"port\":%d}}",
msg_id, dev_time_now(),
local_net_cfg.lip[0], local_net_cfg.lip[1], local_net_cfg.lip[2], local_net_cfg.lip[3],
local_net_cfg.sub[0], local_net_cfg.sub[1], local_net_cfg.sub[2], local_net_cfg.sub[3],
local_net_cfg.gw[0], local_net_cfg.gw[1], local_net_cfg.gw[2], local_net_cfg.gw[3],
net_center_info.lssc_ip[0], net_center_info.lssc_ip[1],
net_center_info.lssc_ip[2], net_center_info.lssc_ip[3],
local_net_cfg.dns[0], local_net_cfg.dns[1], local_net_cfg.dns[2], local_net_cfg.dns[3],
net_center_info.tcp_port);
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
PRINT("IOT: ssc_net_query dev_ip=%d.%d.%d.%d\n",
local_net_cfg.lip[0], local_net_cfg.lip[1], local_net_cfg.lip[2], local_net_cfg.lip[3]);
} else if (strcmp(cmd_str, "iot_net_query") == 0) {
/* IoT 网络配置查询 (只读全局) */
char resp[400];
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"iot_net_query\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"host\":\"%s\",\"port\":%d,\"client_id\":\"%s\","
"\"username\":\"%s\",\"password\":\"%s\"}}",
msg_id, dev_time_now(),
iot_net_info.remote_addr, iot_net_info.mqtt_port,
iot_net_info.client_id, iot_net_info.username, iot_net_info.password);
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
PRINT("IOT: iot_net_query host=%s port=%d\n",
iot_net_info.remote_addr, iot_net_info.mqtt_port);
} else if (strcmp(cmd_str, "iot_topic_query") == 0) {
/* IoT Topic 配置查询 (只读全局) */
char resp[300];
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"iot_topic_query\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"client_id_enable\":%s,\"topic_pub\":\"%s\",\"topic_sub\":\"%s\"}}",
msg_id, dev_time_now(),
g_iot_topic.clientid_enable ? "true" : "false",
g_iot_topic.topic_pub, g_iot_topic.topic_sub);
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
PRINT("IOT: iot_topic_query pub=%s sub=%s\n",
g_iot_topic.topic_pub, g_iot_topic.topic_sub);
} else { } else {
// 暂不支持的命令, 返回错误 // 暂不支持的命令, 返回错误
char resp[256]; char resp[256];
@@ -477,14 +738,29 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
} }
} }
/* 发送 initialize 上线消息 (IoT 路径) */
static void iot_send_initialize(void) {
char payload[512];
char topic[IOT_MQTT_TOPIC_MAX_LEN];
snprintf(payload, sizeof(payload),
"{\"msg_id\":%lu,\"cmd\":\"initialize\",\"ts\":%lu,"
"\"data\":{\"dev_serial\":\"%s\",\"model\":\"%s\","
"\"hard_ver\":\"%s\",\"soft_ver\":\"%s\","
"\"extra_info\":{\"code\":\"0\",\"csq\":\"0\",\"location\":\"\"}}}",
(unsigned long)(g_iot_msg_id + 1), dev_time_now(),
g_iot_dev_serial, PRODUCT_MODEL, HARDWARE_VER, FIRMWARE_VER);
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
iot_mqtt_publish(topic, payload, strlen(payload), 0);
}
/* 处理 MQTT SUBACK */ /* 处理 MQTT SUBACK */
static void iot_handle_suback(void) { static void iot_handle_suback(void) {
PRINT("IOT: ← SUBACK, topics subscribed\n"); PRINT("IOT: ← SUBACK, topics subscribed\n");
g_iot_state = IOT_STATE_READY; g_iot_state = IOT_STATE_READY;
_iot_reconnect_backoff = 0; // 连接成功, 重置退避 _iot_reconnect_backoff = 0; // 连接成功, 重置退避
// V1.03: 订阅成功后发布 initialize 告知服务器上线 // V1.03: 订阅成功后发布 initialize 告知服务器上线 (IoT 路径)
dev_initialize_pub(); iot_send_initialize();
} }
/* 处理收到的 MQTT 数据 */ /* 处理收到的 MQTT 数据 */
@@ -532,6 +808,7 @@ static void iot_process_recv(void) {
iot_mqtt_send_subscribe(); iot_mqtt_send_subscribe();
} else { } else {
PRINT("IOT: CONNACK rejected, rc=%d\n", connack_rc); PRINT("IOT: CONNACK rejected, rc=%d\n", connack_rc);
offlog_iot_disconnect(3); // 事件日志: broker 拒绝
g_iot_state = IOT_STATE_DISCONNECTED; g_iot_state = IOT_STATE_DISCONNECTED;
} }
} }
@@ -685,20 +962,29 @@ void iot_mqtt_publish_sensor(void) {
} }
} }
/*--- Step 4: 构建 JSON → 发布 (字段与 V1.02 协议一致) ---*/ /*--- Step 4: 构建 JSON → 发布 (字段与 V1.02 协议一致) ---
static char data_json[1024]; payload, data_json[1024]:
static char payload[1400]; - iot_mqtt_publish() mqtt_publish(),
char *p = data_json; (event_reportmqttBuf vs loop_dataiot_mqtt_publish::buf[1024])
int remaining = sizeof(data_json); - coil_count , 0xC0 */
static char payload[800]; /* 2026-08-17: 1400→800, loop_data 最大~604B, 省 600B .bss */
char *p = payload;
int remaining = sizeof(payload);
int written; int written;
const char *freq_level_names[] = {"high", "mid_high", "mid_low", "low"}; const char *freq_level_names[] = {"high", "mid_high", "mid_low", "low"};
uint8_t i; uint8_t i;
uint8_t coil_n = _cached_sr.coil_count;
uint32_t my_msg_id = g_iot_msg_id + 1; /* iot_mqtt_publish 内部 ++g_iot_msg_id, 这里只预读 */
if (coil_n > 4) coil_n = 4; /* 硬限: 防坏帧致 snprintf 循环溢出 */
written = snprintf(p, remaining, "{\"channels\":["); /* 先写外层包装头 (msg_id/cmd/ts/data) */
written = snprintf(p, remaining,
"{\"msg_id\":%lu,\"cmd\":\"loop_data\",\"ts\":%lu,\"data\":{\"channels\":[",
(unsigned long)my_msg_id, dev_time_now());
if (written < 0 || written >= remaining) return; if (written < 0 || written >= remaining) return;
p += written; remaining -= written; p += written; remaining -= written;
for (i = 0; i < _cached_sr.coil_count; i++) { for (i = 0; i < coil_n; i++) {
const LUP_CoilSensor *cs = &_cached_sr.coils[i]; const LUP_CoilSensor *cs = &_cached_sr.coils[i];
const char *misc_type_str = "time"; const char *misc_type_str = "time";
uint32_t misc_val = 0; uint32_t misc_val = 0;
@@ -725,56 +1011,25 @@ void iot_mqtt_publish_sensor(void) {
p += written; remaining -= written; p += written; remaining -= written;
} }
snprintf(p, remaining, "]}"); snprintf(p, remaining, "]}}");
/* Wrap and publish — V1.01 双主题模型: dld960/{sn}/dev */
snprintf(payload, sizeof(payload),
"{\"msg_id\":%d,\"cmd\":\"loop_data\",\"ts\":%lu,\"data\":%s}",
++g_iot_msg_id, dev_time_now(), data_json);
/* 用 iot_mqtt_publish: 发送缓冲 buf[1024] 与 mqttBuf[1024] 物理隔离,
event_report MQTT loop_data (2026-07-23 _raw v3) */
{ {
char topic[IOT_MQTT_TOPIC_MAX_LEN]; char topic[IOT_MQTT_TOPIC_MAX_LEN];
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial); snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
mqtt_publish(topic, payload, 0); iot_mqtt_publish(topic, payload, strlen(payload), 0);
} }
} }
/*===========================================================================
*
*===========================================================================*/
static void iot_send_heartbeat(void) {
if (g_iot_state != IOT_STATE_READY) return;
char payload[512];
uint8_t loop_ok[4] = {1, 1, 1, 1};
uint8_t i;
for (i = 0; i < 4; i++) {
/* 简化: 读取线圈状态 */
loop_ok[i] = 1; // TODO: 从 Loop MCU 获取实际状态
}
snprintf(payload, sizeof(payload),
"{\"msg_id\":%d,\"cmd\":\"heartbeat\","
"\"ts\":%lu,\"data\":{"
"\"uptime\":%lu,\"loop_status\":[%s,%s,%s,%s],"
"\"net_status\":true,\"iot_status\":true}}",
++g_iot_msg_id, dev_time_now(),
mstick() / 1000,
loop_ok[0] ? "true" : "false", loop_ok[1] ? "true" : "false",
loop_ok[2] ? "true" : "false", loop_ok[3] ? "true" : "false");
char topic[IOT_MQTT_TOPIC_MAX_LEN];
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
iot_mqtt_publish(topic, payload, strlen(payload), 0);
}
/*=========================================================================== /*===========================================================================
* *
*===========================================================================*/ *===========================================================================*/
/* 连接到 MQTT Broker — 复用 SocketId_TCP(已由 WCHNET_CreateTcpMqttSocket 创建并连接 */ /* 连接到 MQTT Broker — 重新创建 TCP socket 并发起连接 */
static void iot_connect_broker(void) { static void iot_connect_broker(void) {
g_iot_socket = SocketId_TCP; // 复用已创建的 MQTT socket WCHNET_CreateTcpMqttSocket(); // 重新 create + connect (不是复用旧的!)
g_iot_socket = SocketId_TCP;
g_iot_state = IOT_STATE_TCP_CONNECTING; g_iot_state = IOT_STATE_TCP_CONNECTING;
_iot_connect_start = mstick(); _iot_connect_start = mstick();
PRINT("IOT: TCP connecting to broker (sock=%d)...\n", g_iot_socket); PRINT("IOT: TCP connecting to broker (sock=%d)...\n", g_iot_socket);
@@ -786,13 +1041,15 @@ static void iot_connect_broker(void) {
void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat) { void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat) {
if (socketid != g_iot_socket) return; if (socketid != g_iot_socket) return;
/* CONNECT 成功 — 仅标记状态,由 poll 延迟发送 CONNECT */ /* CONNECT 成功 — 在中断上下文直接发 MQTT CONNECT
( mqtt_connect : WCHNET SocketSend hard fault) */
if (intstat & SINT_STAT_CONNECT) { if (intstat & SINT_STAT_CONNECT) {
PRINT("IOT: TCP connected (sock=%d)\n", socketid); PRINT("IOT: TCP connected (sock=%d)\n", socketid);
WCHNET_ModifyRecvBuf(socketid, (uint32_t)_iot_wchnet_buf, RECE_BUF_LEN);
g_iot_state = IOT_STATE_TCP_CONNECTED; g_iot_state = IOT_STATE_TCP_CONNECTED;
_iot_recv_len = 0; _iot_recv_len = 0;
// 不在此发送 CONNECT,等下一轮 poll 处理 _iot_send_fail_cnt = 0; // 新连接, 重置失败计数
iot_mqtt_send_connect(); // 在中断上下文发, 不延后到 poll
g_iot_state = IOT_STATE_MQTT_CONNECTING;
} }
/* 收到数据 */ /* 收到数据 */
@@ -802,7 +1059,7 @@ void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat) {
uint16_t space = IOT_MQTT_RECV_BUF_LEN - _iot_recv_len; uint16_t space = IOT_MQTT_RECV_BUF_LEN - _iot_recv_len;
if (recv_len > space) recv_len = space; if (recv_len > space) recv_len = space;
uint32_t rd_len = recv_len; uint32_t rd_len = recv_len;
uint8_t tmp[RECE_BUF_LEN]; static uint8_t tmp[RECE_BUF_LEN]; /* static: 1152B 栈分配在中断上下文中会撑爆 2KB 栈 (2026-07-23) */
WCHNET_SocketRecv(socketid, tmp, &rd_len); WCHNET_SocketRecv(socketid, tmp, &rd_len);
memcpy(_iot_recv_buf + _iot_recv_len, tmp, (uint16_t)rd_len); memcpy(_iot_recv_buf + _iot_recv_len, tmp, (uint16_t)rd_len);
_iot_recv_len += (uint16_t)rd_len; _iot_recv_len += (uint16_t)rd_len;
@@ -823,10 +1080,46 @@ void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat) {
} }
} }
/*===========================================================================
* IWDG
* LSI40kHz, Prescaler=256, Reload=625 ~4s
* : MQTT PINGREQ/PINGRESP TCP KeepAlive IWDG
* (heartbeat , , )
*===========================================================================*/
/* IWDG 初始化 */
void iot_watchdog_init(void) {
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
IWDG_SetPrescaler(IWDG_Prescaler_256);
IWDG_SetReload(625); // 625 × (256/40000) ≈ 4.0s
IWDG_ReloadCounter();
IWDG_Enable();
PRINT("IOT: IWDG init ok, timeout≈4s\n");
}
/* 喂狗 — 主循环每轮调用 */
void iot_watchdog_kick(void) {
IWDG_ReloadCounter();
}
/*=========================================================================== /*===========================================================================
* *
*===========================================================================*/ *===========================================================================*/
void iot_mqtt_poll(void) { void iot_mqtt_poll(void) {
iot_watchdog_kick(); // 喂硬件 IWDG
/* 事件日志: MQTT 状态沿检测 (主循环上下文, 不在 socket 中断里写 SPI)
BOOT + boot_seq ; */
if (g_iot_state != _prev_iot_state) {
switch (g_iot_state) {
case IOT_STATE_MQTT_CONNECTED: offlog_iot_connect(); break; // TCP+CONNACK 成功
case IOT_STATE_READY: offlog_iot_ready(); break; // SUBACK → 将发 initialize
case IOT_STATE_DISCONNECTED: offlog_iot_disconnect(1); break; // 断连(细分原因在各分支单独记)
default: break;
}
_prev_iot_state = g_iot_state;
}
/* 断线重连 */ /* 断线重连 */
if (g_iot_state == IOT_STATE_DISCONNECTED && g_iot_socket == 0xFF) { if (g_iot_state == IOT_STATE_DISCONNECTED && g_iot_socket == 0xFF) {
if (_iot_reconnect_deadline == 0 || mstick() > _iot_reconnect_deadline) { if (_iot_reconnect_deadline == 0 || mstick() > _iot_reconnect_deadline) {
@@ -841,6 +1134,7 @@ void iot_mqtt_poll(void) {
_iot_reconnect_backoff = IOT_MQTT_RECONNECT_MAX_MS; _iot_reconnect_backoff = IOT_MQTT_RECONNECT_MAX_MS;
} }
_iot_reconnect_deadline = mstick() + _iot_reconnect_backoff; _iot_reconnect_deadline = mstick() + _iot_reconnect_backoff;
offlog_iot_reconn(_iot_reconnect_backoff); // 事件日志: 重连退避
PRINT("IOT: reconnect in %lu ms\n", _iot_reconnect_backoff); PRINT("IOT: reconnect in %lu ms\n", _iot_reconnect_backoff);
} }
} }
@@ -851,6 +1145,7 @@ void iot_mqtt_poll(void) {
if (g_iot_state == IOT_STATE_TCP_CONNECTING) { if (g_iot_state == IOT_STATE_TCP_CONNECTING) {
if (mstick() - _iot_connect_start > 10000) { if (mstick() - _iot_connect_start > 10000) {
PRINT("IOT: TCP connect timeout\n"); PRINT("IOT: TCP connect timeout\n");
offlog_iot_disconnect(4); // 事件日志: 连接超时
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL); WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
g_iot_socket = 0xFF; g_iot_socket = 0xFF;
g_iot_state = IOT_STATE_DISCONNECTED; g_iot_state = IOT_STATE_DISCONNECTED;
@@ -864,7 +1159,8 @@ void iot_mqtt_poll(void) {
g_iot_state = IOT_STATE_MQTT_CONNECTING; g_iot_state = IOT_STATE_MQTT_CONNECTING;
} }
/* MQTT Keepalive — PINGREQ */ /* MQTT Keepalive — PINGREQ (60s, broker 不回 → TCP 超时 → 重连)
loop_data g_report_cfg.interval , heartbeat JSON */
if (g_iot_state == IOT_STATE_READY) { if (g_iot_state == IOT_STATE_READY) {
uint32_t now = mstick(); uint32_t now = mstick();
if (now - _iot_last_heartbeat > IOT_MQTT_HEARTBEAT_MS) { if (now - _iot_last_heartbeat > IOT_MQTT_HEARTBEAT_MS) {
@@ -872,8 +1168,6 @@ void iot_mqtt_poll(void) {
int ping_len = MQTTSerialize_pingreq(ping_buf, sizeof(ping_buf)); int ping_len = MQTTSerialize_pingreq(ping_buf, sizeof(ping_buf));
iot_mqtt_send(ping_buf, (uint16_t)ping_len); iot_mqtt_send(ping_buf, (uint16_t)ping_len);
_iot_last_heartbeat = now; _iot_last_heartbeat = now;
// 心跳上报
iot_send_heartbeat();
} }
} }
} }
@@ -899,4 +1193,7 @@ void iot_mqtt_init(void) {
_iot_reconnect_backoff = 0; _iot_reconnect_backoff = 0;
_iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接 _iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接
g_iot_state = IOT_STATE_DISCONNECTED; g_iot_state = IOT_STATE_DISCONNECTED;
/* 初始化硬件看门狗 IWDG (LSI≈40kHz, 4s 超时, 主循环喂狗) */
iot_watchdog_init();
} }
@@ -14,6 +14,7 @@
#include "CONFIG.h" #include "CONFIG.h"
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "cmcng.h" #include "cmcng.h"
#include "fault_diag.h"
#include <string.h> #include <string.h>
/*=========================================================================== /*===========================================================================
@@ -654,6 +655,110 @@ int lup_feed_byte(uint8_t byte)
// 不应该到达这里,调用方应在 COMPLETE 后 reset // 不应该到达这里,调用方应在 COMPLETE 后 reset
lup_frame_reset(); lup_frame_reset();
break; break;
default:
/* 未知状态 (含 0x9F OTA 专用状态值): 防御性复位, 防状态机跑飞 */
lup_frame_reset();
break;
}
return 0;
}
/*===========================================================================
* 0x9F OTA (OTA , 2026-08-19)
*
* (DLD960LoopBootloader ):
* [9F] [SubL] [SubH] [LEN] [CMD] [DATA: LEN-1] [CHECK: SUM]
* - SubL/SubH: , 2 ()
* - LEN: CMD , DATA = LEN - 1
* - CHECK: SUM ( XOR), SubL DATA
* = LEN + 5 ( 0x7F )
*
* g_lup_parser (/), (LUP_FRAME_STATE_OTA_*)
* (uart2_dma_poll) OTA lup_frame_reset()
*===========================================================================*/
int lup_feed_byte_ota(uint8_t byte)
{
LUP_FrameParser *p = &g_lup_parser;
switch (p->state) {
case LUP_FRAME_STATE_IDLE:
if (byte == LUP_MAGIC_OTA) {
p->buf[0] = byte;
p->idx = 1;
p->state = LUP_FRAME_STATE_HEADER; /* 复用 HEADER: 收 SubL/SubH */
}
break;
case LUP_FRAME_STATE_HEADER:
p->buf[p->idx++] = byte;
if (p->idx == 3) { /* SubL + SubH 收完 */
p->state = LUP_FRAME_STATE_OTA_LEN;
}
break;
case LUP_FRAME_STATE_OTA_LEN:
p->buf[p->idx++] = byte;
{
uint8_t len_field = p->buf[3];
uint16_t data_bytes;
if (len_field < 1) {
data_bytes = 0; /* LEN<1 非法, 按无数据直接到 CHECK */
} else {
data_bytes = len_field - 1; /* LEN 含 CMD */
}
if (data_bytes > LUP_MAX_VALUE_LEN) {
lup_frame_reset(); /* 非法长度, 丢弃 */
break;
}
p->value_len = data_bytes;
p->value_idx = 0;
}
p->state = LUP_FRAME_STATE_OTA_CMD;
break;
case LUP_FRAME_STATE_OTA_CMD:
p->buf[p->idx++] = byte;
if (p->value_len > 0) {
p->state = LUP_FRAME_STATE_VALUE; /* 复用 VALUE: 收 DATA */
} else {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_VALUE:
p->buf[p->idx++] = byte;
p->value_idx++;
if (p->value_idx >= p->value_len) {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_OTA_CHECK:
p->buf[p->idx++] = byte;
{
/* SUM = SubL+SubH+LEN+CMD+DATA (不含 magic 0x9F, 不含本 CHECK) */
uint8_t sum = 0;
uint16_t i;
for (i = 1; i < (uint16_t)(p->idx - 1); i++) {
sum += p->buf[i];
}
if (sum == p->buf[p->idx - 1]) {
p->state = LUP_FRAME_STATE_COMPLETE;
return 1; /* 帧完成 */
}
lup_frame_reset(); /* 校验失败, 丢弃 */
}
break;
case LUP_FRAME_STATE_COMPLETE:
// 不应该到达这里,调用方应在 COMPLETE 后 reset
lup_frame_reset();
break;
default:
lup_frame_reset();
break;
} }
return 0; return 0;
@@ -681,14 +786,24 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
int csr; int csr;
uint8_t i; uint8_t i;
if (len < 6) return; FAULT_MARKER(MK_LUP_FRAME_IN);
// Debug: print raw if (len < 6) { FAULT_MARKER(MK_LUP_FRAME_OUT); return; }
PRINT("LUP Rx:");
for (i = 0; i < len; i++) { // Debug: print raw (2026-08-13: 缓冲一次性输出 — 逐字节 PRINT 关中断 ~350us/字节
PRINT(" %02X", pkg[i]); // 会屏蔽 UART2 ISR 丢帧, 且高频打印放大 printf 重入窗口。
// 再关闭: 190B 打印 @256000 = 7.4ms 关中断 → UART2(192000) 溢出丢字节 → 粘帧!
// 需要调试时打开 #if 1)
#if 0
{
char _hex[224];
int _p = snprintf(_hex, sizeof(_hex), "LUP Rx:");
for (i = 0; i < len && _p >= 0 && _p < (int)sizeof(_hex) - 5; i++) {
_p += snprintf(_hex + _p, sizeof(_hex) - (size_t)_p, " %02X", pkg[i]);
}
PRINT("%s\n", _hex);
} }
PRINT("\n"); #endif
// --- Checksum --- // --- Checksum ---
csr = lup_verify_checksum(pkg, len); csr = lup_verify_checksum(pkg, len);
@@ -712,4 +827,5 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
// Try matching with pending command // Try matching with pending command
lup_cmd_on_response(pkg, len); lup_cmd_on_response(pkg, len);
FAULT_MARKER(MK_LUP_FRAME_OUT);
} }
@@ -19,6 +19,7 @@
#include "simple_json.h" #include "simple_json.h"
#include "tcp_json_srv.h" #include "tcp_json_srv.h"
#include "iot_mqtt_srv.h" #include "iot_mqtt_srv.h"
#include "offlog.h"
uint32_t slen; uint32_t slen;
@@ -64,7 +65,7 @@ uint8_t MyBuf[RECE_BUF_LEN];
#define MAX_TMP_BUF_LEN 64 #define MAX_TMP_BUF_LEN 64
#define MAX_MQTTBUF_LEN 1024 //512: loop_data(4通道)≈604B > 512 → 序列化失败发全零垃圾包被broker RST (2026-07-15) #define MAX_MQTTBUF_LEN 800 // 2026-08-17: 1024→800 (loop_data~604B+头<800; 512仍不够勿改回) 省224B .bss
char mqtt_username[64] = {0}; char mqtt_username[64] = {0};
char mqtt_password[32] = {0}; char mqtt_password[32] = {0};
char mqtt_clientid[64] = {0}; char mqtt_clientid[64] = {0};
@@ -97,6 +98,7 @@ void dev_time_sync(uint32_t unix_ts)
if (unix_ts < 1600000000UL) return; // 合法性门槛(>2020-09), 挡掉上电秒数/0/异常 if (unix_ts < 1600000000UL) return; // 合法性门槛(>2020-09), 挡掉上电秒数/0/异常
_dev_time_base_unix = unix_ts; _dev_time_base_unix = unix_ts;
_dev_time_base_tick = mstick(); _dev_time_base_tick = mstick();
offlog_time_anchor(unix_ts); // 事件日志: 时钟同步锚点 (boot_seq↔unix 回算)
PRINT("TIME: synced unix=%lu\n", (unsigned long)unix_ts); PRINT("TIME: synced unix=%lu\n", (unsigned long)unix_ts);
} }
@@ -578,28 +580,20 @@ uint8_t i;
return; return;
} }
// IoT MQTT mode — inline MQTT handling (ref: DBN101GA) // IoT MQTT mode — delegate all socket events to iot_mqtt_srv
// (2026-07-23 修复: 旧 mqtt_connect/mqtt_data_manage 与 IoT 栈抢 socket, 致双 CONNECT → broker 踢线 → 频繁 initialize)
if (g_sub_code_enable.iot_enable) { if (g_sub_code_enable.iot_enable) {
if (intstat & SINT_STAT_RECV) {
mqtt_data_manage(socketid);
}
if (intstat & SINT_STAT_CONNECT) { if (intstat & SINT_STAT_CONNECT) {
#if KEEPLIVE_ENABLE #if KEEPLIVE_ENABLE
WCHNET_SocketSetKeepLive(socketid, ENABLE); WCHNET_SocketSetKeepLive(socketid, ENABLE);
#endif #endif
WCHNET_ModifyRecvBuf(socketid, (uint32_t)SocketRecvBuf[socketid], RECE_BUF_LEN); WCHNET_ModifyRecvBuf(socketid, (uint32_t)SocketRecvBuf[socketid], RECE_BUF_LEN);
PRINT("TCP Connect Success (MQTT)\n"); PRINT("TCP Connect Success (MQTT IoT)\n");
mqtt_connect();
} }
if (intstat & SINT_STAT_DISCONNECT) { if (intstat & SINT_STAT_DISCONNECT || intstat & SINT_STAT_TIM_OUT) {
PRINT("TCP Disconnect (MQTT)\n"); PRINT("TCP Disconnect/Timeout (MQTT IoT)\n");
g_iot_state = IOT_STATE_DISCONNECTED;
g_net_state.flag = 1;
}
if (intstat & SINT_STAT_TIM_OUT) {
PRINT("TCP Timeout (MQTT)\n");
g_net_state.flag = 1;
} }
iot_mqtt_handle_sock_int(socketid, intstat);
return; return;
} }
@@ -0,0 +1,505 @@
/**
******************************************************************************
* @file offlog.c
* @author wfq
* @version V1.0
* @date 2026-08-04
* @brief DBN W25Q32
*
* : storage.c (SPI_Flash_*), cmcng.h (mstick), net_srv.c (dev_time_now)
* (), SPI ~45ms
******************************************************************************
*/
#include "offlog.h"
#include "storage.h"
#include "cmcng.h"
#include "net_srv.h"
#include <string.h>
/* 编译期断言: 记录必须是 32B (定长环形索引依赖, 踩过 36B padding 的坑) */
typedef char offlog_evt_size_must_be_32[(sizeof(OfflogEvt) == 32) ? 1 : -1];
typedef char offlog_head_size_must_be_32[(sizeof(OfflogHead) == 32) ? 1 : -1];
/*===========================================================================
*
*===========================================================================*/
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..63) */
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
static uint16_t _boot_seq; /* 当前 boot 序号 */
static uint32_t _count; /* 有效记录数 (0..max_records) */
static uint8_t _ready; /* init 完成标志 */
/*===========================================================================
* (ROADMAP 2026-08-12: JEDEC ID )
*===========================================================================*/
OfflogPart g_offlog_part = {
OFFLOG_CHIP_W25Q32, /* 默认 */
0x80000UL, /* 512KB */
127, /* (512KB/4096) - 1 头扇区 */
16256, /* 127 * 128 */
};
/* 读 JEDEC ID → 填分区表。未知芯片按 W25Q32 兜底 (与 storage_init 打印对照) */
static void offlog_part_detect(void)
{
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
uint32_t size = 0x80000UL;
uint8_t chip = OFFLOG_CHIP_W25Q32;
switch (dev) {
case OFFLOG_CHIP_W25Q32: size = 0x80000UL; break; /* 512KB */
case OFFLOG_CHIP_W25Q64: size = 0x100000UL; break; /* 1MB */
case OFFLOG_CHIP_W25Q128: size = 0x200000UL; break; /* 2MB */
case OFFLOG_CHIP_W25Q256: size = 0x400000UL; break; /* 4MB */
default: chip = OFFLOG_CHIP_W25Q32; break; /* 未知→默认 */
}
g_offlog_part.chip = chip;
g_offlog_part.area_size = size;
g_offlog_part.data_sectors = (size / 4096UL) - 1;
g_offlog_part.max_records = g_offlog_part.data_sectors * OFFLOG_EVT_PER_SECTOR;
}
/*===========================================================================
*
*===========================================================================*/
static void offlog_head_write(const OfflogHead *h)
{
uint8_t buf[32];
memset(buf, 0xFF, sizeof(buf));
memcpy(buf, h, sizeof(OfflogHead));
SPI_Flash_Write(buf, OFFLOG_AREA_BASE, sizeof(buf));
}
static void offlog_head_read(OfflogHead *h)
{
uint8_t buf[32];
SPI_Flash_Read(buf, OFFLOG_AREA_BASE, sizeof(buf));
memcpy(h, buf, sizeof(OfflogHead));
}
static int offlog_evt_read_at(uint32_t abs_off, OfflogEvt *out)
{
SPI_Flash_Read((uint8_t *)out, abs_off, OFFLOG_EVT_SIZE);
return (out->magic == OFFLOG_EVT_MAGIC) ? 0 : -1;
}
/*===========================================================================
*
* , magic + seq ,
* () , ;
*===========================================================================*/
static void offlog_scan_recover(const OfflogHead *h)
{
uint32_t pos = h->wr_off;
uint32_t scanned = 0;
uint32_t expect_seq = h->wr_seq;
OfflogEvt e;
/* 防御: wr_off 必须在数据区范围内且 32B 对齐 */
if (pos < OFFLOG_DATA_BASE || pos >= OFFLOG_AREA_BASE + OFFLOG_AREA_SIZE) {
pos = OFFLOG_DATA_BASE;
}
if ((pos - OFFLOG_DATA_BASE) % OFFLOG_EVT_SIZE != 0) {
pos = OFFLOG_DATA_BASE; /* 未对齐 → 从头开始 */
}
while (scanned < OFFLOG_MAX_RECORDS) {
if (offlog_evt_read_at(pos, &e) != 0) {
break; /* 找到真实写位置 */
}
if (expect_seq != 0 && e.seq != expect_seq) {
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
}
pos += OFFLOG_EVT_SIZE;
if (pos >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
pos = OFFLOG_DATA_BASE; /* 回绕数据区 */
}
scanned++;
expect_seq++;
}
_wr_off = pos;
_wr_sector = (uint16_t)((pos - OFFLOG_DATA_BASE) / 4096) + 1; /* 区内 1..63 */
_wr_seq = expect_seq; /* 下一条 = 最后有效 seq + 1 */
_count = h->count + scanned;
if (_count > OFFLOG_MAX_RECORDS) _count = OFFLOG_MAX_RECORDS;
_boot_seq = (uint16_t)(h->boot_seq + 1);
if (_boot_seq == 0) _boot_seq = 1;
}
/*===========================================================================
*
*===========================================================================*/
void offlog_init(void)
{
OfflogHead h;
offlog_part_detect(); /* JEDEC ID → 事件区容量 (W25Q32/Q64/Q128/Q256) */
memset(&h, 0xFF, sizeof(h));
offlog_head_read(&h);
if (!(h.magic[0] == OFFLOG_HEAD_MAGIC0 && h.magic[1] == OFFLOG_HEAD_MAGIC1 &&
h.magic[2] == OFFLOG_HEAD_MAGIC2 && h.magic[3] == OFFLOG_HEAD_MAGIC3)) {
/* 全新区: 懒擦 — 只擦当前写扇区 ~45ms (原擦全部 127 扇区 ~5.7s,
IWDG 4s ), */
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
memset(&h, 0xFF, sizeof(h));
h.magic[0] = OFFLOG_HEAD_MAGIC0;
h.magic[1] = OFFLOG_HEAD_MAGIC1;
h.magic[2] = OFFLOG_HEAD_MAGIC2;
h.magic[3] = OFFLOG_HEAD_MAGIC3;
h.boot_seq = 0;
h.wr_sector = 1;
h.wr_off = OFFLOG_DATA_BASE;
h.wr_seq = 0;
h.count = 0;
offlog_head_write(&h);
_wr_off = OFFLOG_DATA_BASE;
_wr_sector = 1;
_wr_seq = 1;
_count = 0;
_boot_seq = 1;
} else {
offlog_scan_recover(&h);
}
/* 回写头: boot_seq 递增 */
h.magic[0] = OFFLOG_HEAD_MAGIC0;
h.magic[1] = OFFLOG_HEAD_MAGIC1;
h.magic[2] = OFFLOG_HEAD_MAGIC2;
h.magic[3] = OFFLOG_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
offlog_head_write(&h);
_ready = 1;
}
/*===========================================================================
* ()
*===========================================================================*/
static void offlog_flush_head(void)
{
OfflogHead h;
h.magic[0] = OFFLOG_HEAD_MAGIC0;
h.magic[1] = OFFLOG_HEAD_MAGIC1;
h.magic[2] = OFFLOG_HEAD_MAGIC2;
h.magic[3] = OFFLOG_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
offlog_head_write(&h);
}
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic。
, , 0xA5 */
static int offlog_sector_has_data(uint32_t abs_sec_off)
{
OfflogEvt e;
SPI_Flash_Read((uint8_t *)&e, abs_sec_off, OFFLOG_EVT_SIZE);
return (e.magic == OFFLOG_EVT_MAGIC) ? 1 : 0;
}
static void offlog_write_raw(const OfflogEvt *e)
{
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
if (_wr_off >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
_wr_off = OFFLOG_DATA_BASE;
}
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区。
: (sec_pos==0, )
(sec_pos + 32 > 4096) */
{
uint32_t sec_index = (_wr_off - OFFLOG_DATA_BASE) / 4096; /* 0..62 */
if ((uint32_t)(_wr_sector - 1) != sec_index) {
uint32_t target_abs = (OFFLOG_DATA_BASE / 4096 + sec_index) * 4096;
/* 仅环形覆盖 (目标扇区有旧数据) 才扣减 count;
*/
if (offlog_sector_has_data(target_abs)) {
uint32_t erased = (_count > OFFLOG_EVT_PER_SECTOR)
? OFFLOG_EVT_PER_SECTOR : _count;
_count -= erased;
}
SPI_Flash_Erase_Sector(target_abs / 4096);
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..63 */
offlog_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
}
}
SPI_Flash_Write_NoCheck((uint8_t *)e, _wr_off, OFFLOG_EVT_SIZE);
_wr_off += OFFLOG_EVT_SIZE;
_wr_seq++;
if (_count < OFFLOG_MAX_RECORDS) _count++;
}
/* 内部: 支持外部指定 unix_ts (TIME_ANCHOR 用平台下发值, 严格一致) */
static void offlog_evt_ts(uint8_t type, const uint8_t *payload, uint8_t len, uint32_t unix_ts)
{
OfflogEvt e;
uint32_t now = mstick();
if (!_ready) return;
memset(&e, 0, sizeof(e));
e.magic = OFFLOG_EVT_MAGIC;
e.type = type;
e.len = (len > 12) ? 12 : len;
e.seq = _wr_seq;
e.boot_seq= _boot_seq;
e.ts_ms = now;
e.unix_ts = unix_ts;
if (unix_ts >= 1600000000UL) { /* 与 dev_time_sync 合法性门槛一致 */
e.flags |= OFFLOG_UNIX_VALID_FLAG;
}
if (payload != NULL && e.len > 0) {
memcpy(e.payload, payload, e.len);
}
offlog_write_raw(&e);
}
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len)
{
offlog_evt_ts(type, payload, len, dev_time_now());
}
/*===========================================================================
* 便
*===========================================================================*/
void offlog_boot(uint32_t reset_reason)
{
uint8_t p[4];
p[0] = (uint8_t)(reset_reason >> 24);
p[1] = (uint8_t)(reset_reason >> 16);
p[2] = (uint8_t)(reset_reason >> 8);
p[3] = (uint8_t)reset_reason;
offlog_evt(OFFLOG_EVT_BOOT, p, 4);
}
void offlog_iot_connect(void)
{
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
}
void offlog_iot_ready(void)
{
offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0);
}
void offlog_iot_disconnect(uint8_t reason)
{
offlog_evt(OFFLOG_EVT_IOT_DISCONN, &reason, 1);
}
void offlog_iot_reconn(uint32_t backoff_ms)
{
uint8_t p[4];
p[0] = (uint8_t)(backoff_ms >> 24);
p[1] = (uint8_t)(backoff_ms >> 16);
p[2] = (uint8_t)(backoff_ms >> 8);
p[3] = (uint8_t)backoff_ms;
offlog_evt(OFFLOG_EVT_IOT_RECONN, p, 4);
}
void offlog_evt_retry(uint32_t msg_id, uint8_t retry)
{
uint8_t p[5];
p[0] = (uint8_t)(msg_id >> 24);
p[1] = (uint8_t)(msg_id >> 16);
p[2] = (uint8_t)(msg_id >> 8);
p[3] = (uint8_t)msg_id;
p[4] = retry;
offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5);
}
void offlog_evt_giveup(uint32_t msg_id)
{
uint8_t p[4];
p[0] = (uint8_t)(msg_id >> 24);
p[1] = (uint8_t)(msg_id >> 16);
p[2] = (uint8_t)(msg_id >> 8);
p[3] = (uint8_t)msg_id;
offlog_evt(OFFLOG_EVT_EVT_GIVEUP, p, 4);
}
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value)
{
uint8_t p[6];
p[0] = sub;
p[1] = ch;
p[2] = (uint8_t)(value >> 24);
p[3] = (uint8_t)(value >> 16);
p[4] = (uint8_t)(value >> 8);
p[5] = (uint8_t)value;
offlog_evt(OFFLOG_EVT_COIL, p, 6);
}
void offlog_time_anchor(uint32_t unix_ts)
{
offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */
}
/*===========================================================================
*
*===========================================================================*/
uint32_t offlog_boot_seq(void)
{
return _boot_seq;
}
uint16_t offlog_count(void)
{
return (uint16_t)_count;
}
int offlog_read_idx(uint16_t idx, OfflogEvt *out)
{
uint32_t cnt = _count;
if (!_ready || idx >= cnt) return -1;
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
uint32_t phys = (_wr_off - OFFLOG_DATA_BASE); /* 0..DATA_SIZE */
uint32_t start = (phys + OFFLOG_DATA_SIZE - (uint32_t)cnt * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
uint32_t read_off = OFFLOG_DATA_BASE + (start + (uint32_t)idx * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
return offlog_evt_read_at(read_off, out);
}
/*===========================================================================
* (MQTT V1.06 / TCP JSON V1.02 )
*===========================================================================*/
uint8_t offlog_enabled(void)
{
return _ready;
}
uint32_t offlog_seq_last(void)
{
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
}
const char *offlog_type_str(uint8_t type)
{
switch (type) {
case OFFLOG_EVT_BOOT: return "boot";
case OFFLOG_EVT_IOT_CONNECT: return "iot_connect";
case OFFLOG_EVT_IOT_READY: return "iot_ready";
case OFFLOG_EVT_IOT_DISCONN: return "iot_disconnect";
case OFFLOG_EVT_IOT_RECONN: return "iot_reconn";
case OFFLOG_EVT_EVT_RETRY: return "evt_retry";
case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup";
case OFFLOG_EVT_COIL: return "coil";
case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor";
case OFFLOG_EVT_LOG_CLEAR: return "log_clear";
default: return "unknown";
}
}
/* 线圈事件子类型: 1=进 2=出 3=断开 4=恢复
( iot_mqtt_srv.c offlog_coil ) */
static const char *offlog_coil_sub_str(uint8_t sub)
{
switch (sub) {
case 1: return "car_enter";
case 2: return "car_leave";
case 3: return "loop_cut";
case 4: return "loop_restore";
default: return "unknown";
}
}
/* payload 大端 uint32 (与 offlog_boot/offlog_coil 写入一致) */
static uint32_t offlog_payload_u32(const uint8_t *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
/* 记录 → JSON 对象 (data 按事件类型组装; 无参数事件 data=null)
snprintf ; buf_len (130B) */
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len)
{
const char *type = offlog_type_str(e->type);
char data[96];
switch (e->type) {
case OFFLOG_EVT_BOOT:
snprintf(data, sizeof(data), "{\"rst\":%lu}",
(unsigned long)offlog_payload_u32(e->payload));
break;
case OFFLOG_EVT_IOT_DISCONN:
snprintf(data, sizeof(data), "{\"reason\":%u}", e->payload[0]);
break;
case OFFLOG_EVT_IOT_RECONN:
snprintf(data, sizeof(data), "{\"backoff_ms\":%lu}",
(unsigned long)offlog_payload_u32(e->payload));
break;
case OFFLOG_EVT_EVT_RETRY:
snprintf(data, sizeof(data), "{\"msg_id\":%lu,\"retry\":%u}",
(unsigned long)offlog_payload_u32(e->payload), e->payload[4]);
break;
case OFFLOG_EVT_EVT_GIVEUP:
snprintf(data, sizeof(data), "{\"msg_id\":%lu}",
(unsigned long)offlog_payload_u32(e->payload));
break;
case OFFLOG_EVT_COIL:
snprintf(data, sizeof(data), "{\"sub\":\"%s\",\"ch\":%u,\"value\":%lu}",
offlog_coil_sub_str(e->payload[0]), e->payload[1],
(unsigned long)offlog_payload_u32(e->payload + 2));
break;
default:
data[0] = '\0'; /* iot_connect/iot_ready/time_anchor/log_clear → null */
break;
}
if (data[0] == '\0') {
return snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":null}",
(unsigned long)e->seq, (unsigned)e->boot_seq,
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type);
}
return snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":%s}",
(unsigned long)e->seq, (unsigned)e->boot_seq,
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type, data);
}
/*===========================================================================
* (: LOG_CLEAR)
*===========================================================================*/
void offlog_clear(void)
{
if (!_ready) return;
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms, 原 2.8s 接近 IWDG 4s 风险):
count=0 使, */
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
_wr_off = OFFLOG_DATA_BASE;
_wr_sector = 1;
_count = 0;
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证日志全局唯一 */
offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0);
offlog_flush_head();
}
/* OfflogEvt -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §7 字段表解析)
Used by TCP JSON / MQTT log_query stream=event (protocol V1.03/V1.07).
Returns snprintf written length; 32B -> 64 hex chars. */
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)e;
for (i = 0; i < (int)sizeof(OfflogEvt); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
@@ -147,6 +147,11 @@ static uint8_t attDeviceName[GAP_DEVICE_NAME_LEN] = "DLD960GA"; // "Simple Perip
static peripheralConnItem_t peripheralConnList; static peripheralConnItem_t peripheralConnList;
static uint16_t peripheralMTU = ATT_MTU_SIZE; static uint16_t peripheralMTU = ATT_MTU_SIZE;
/* getter for dbn_ble_srv.c: dynamic notify chunk by negotiated MTU */
uint16_t peripheral_get_mtu(void)
{
return peripheralMTU;
}
/********************************************************************* /*********************************************************************
* LOCAL FUNCTIONS * LOCAL FUNCTIONS
*/ */
@@ -679,13 +684,30 @@ static void peripheralStateNotificationCB(gapRole_States_t newState, gapRoleEven
*/ */
static void performPeriodicTask(void) static void performPeriodicTask(void)
{ {
// uint8_t notiData[SIMPLEPROFILE_CHAR4_LEN] = {0x88}; /* 2026-08-17 cleanup: removed _dbg_cnt 500ms debug heartbeat print */
// peripheralChar4Notify(notiData, SIMPLEPROFILE_CHAR4_LEN);
if(g_flag_notify_temp){ if(g_flag_notify_temp){
g_flag_notify_temp = 0; g_flag_notify_temp = 0;
PRINT("BLE send: len=%d resp=%d amt=%d seq=%d off=%d\n",
g_notify_buftemp.len, g_buf_ble_response.flag,
g_buf_ble_response.pkg_amount, g_buf_ble_response.pkg_seq,
g_buf_ble_response.dat_offset);
peripheralChar4Notify(g_notify_buftemp.buf, g_notify_buftemp.len); peripheralChar4Notify(g_notify_buftemp.buf, g_notify_buftemp.len);
clear_ble_notify_buf(&g_notify_buftemp); clear_ble_notify_buf(&g_notify_buftemp);
} }
/* chunk continuation: when notify buf idle and response queue still
has more fragments, actively pull next packet here (50ms period),
do NOT rely on poll_dbn_ble rx events */
if(g_notify_buftemp.flag == 0)
{
uint8_t _pull = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
g_flag_notify_temp = _pull;
if(_pull == 0 && g_buf_ble_response.flag)
{
PRINT("BLE pull FAIL: resp flag=%d amt=%d seq=%d off=%d\n",
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
}
}
} }
/********************************************************************* /*********************************************************************
@@ -701,9 +723,12 @@ static void performPeriodicTask(void)
static void peripheralChar4Notify(uint8_t *pValue, uint16_t len) static void peripheralChar4Notify(uint8_t *pValue, uint16_t len)
{ {
attHandleValueNoti_t noti; attHandleValueNoti_t noti;
PRINT("N4 enter: len=%d resp=%d amt=%d seq=%d off=%d\n", len,
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
if(len > (peripheralMTU - 3)) if(len > (peripheralMTU - 3))
{ {
PRINT("Too large noti\n"); PRINT("Too large noti, len:%d, peripheralMTU:%d\n", len, peripheralMTU);
return; return;
} }
noti.len = len; noti.len = len;
@@ -713,8 +738,22 @@ static void peripheralChar4Notify(uint8_t *pValue, uint16_t len)
tmos_memcpy(noti.pValue, pValue, noti.len); tmos_memcpy(noti.pValue, pValue, noti.len);
if(simpleProfile_Notify(peripheralConnList.connHandle, &noti) != SUCCESS) if(simpleProfile_Notify(peripheralConnList.connHandle, &noti) != SUCCESS)
{ {
PRINT("BLE notify FAIL, len:%d, MTU:%d\n", len, peripheralMTU);
GATT_bm_free((gattMsg_t *)&noti, ATT_HANDLE_VALUE_NOTI); GATT_bm_free((gattMsg_t *)&noti, ATT_HANDLE_VALUE_NOTI);
} }
else
{
uint8_t _ti = 0;
PRINT("N4 after simpleProfile_Notify: resp=%d amt=%d seq=%d off=%d\n",
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
PRINT("BLE notify OK, len:%d, MTU:%d\n", len, peripheralMTU);
for(_ti = 0; _ti < len; _ti++)
{
PRINT(" %02X", pValue[_ti]);
}
PRINT("\n");
}
} }
} }
@@ -22,9 +22,12 @@
#include "eth_driver.h" #include "eth_driver.h"
#include "net_srv.h" #include "net_srv.h"
#include "storage.h" #include "storage.h"
#include "offlog.h"
#include "snapshot.h"
#include "tcp_json_srv.h" #include "tcp_json_srv.h"
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "iot_mqtt_srv.h" #include "iot_mqtt_srv.h"
#include "fault_diag.h"
/********************************************************************* /*********************************************************************
* GLOBAL TYPEDEFS * GLOBAL TYPEDEFS
@@ -229,6 +232,9 @@ void Main_Circulation(void)
uint32_t _counter = 0; uint32_t _counter = 0;
while(1) while(1)
{ {
FAULT_MARKER(MK_LOOP_TOP);
iot_watchdog_kick(); /* 无条件喂狗 (2026-08-13): 原只在 iot_mqtt_poll, tcp_json 分支不喂 */
snap_delayed_init(); /* 快照区延后初始化: 开机 3s 后首次进入执行 (2026-08-17) */
TMOS_SystemProcess(); TMOS_SystemProcess();
if(g_net_state.flag < 2) if(g_net_state.flag < 2)
@@ -265,13 +271,16 @@ void Main_Circulation(void)
WCHNET_HandleGlobalInt(); WCHNET_HandleGlobalInt();
} }
FAULT_MARKER(MK_UART_SRV_IN);
uart_srv(); uart_srv();
FAULT_MARKER(MK_UART_SRV_OUT);
snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */
FAULT_MARKER(MK_POLL_BLE_IN);
poll_dbn_ble(); poll_dbn_ble();
if (g_sub_code_enable.iot_enable) { if (g_sub_code_enable.iot_enable) {
iot_mqtt_publish_sensor(); // Push 0xC0 sensor data to MQTT broker iot_mqtt_publish_sensor(); // Push 0xC0 sensor data to MQTT broker
poll_mqtt(); // MQTT PINGREQ heartbeat (in net_srv.c) iot_mqtt_poll(); // IoT MQTT state machine + PINGREQ keepalive
} else { } else {
tcp_json_push_sensor(); // Push 0xC0 sensor data to TCP JSON client tcp_json_push_sensor(); // Push 0xC0 sensor data to TCP JSON client
tcp_json_poll(); tcp_json_poll();
@@ -298,12 +307,47 @@ int main(void)
// USART_Printf_Init( 115200 ); // USART_Printf_Init( 115200 );
USART_Printf_Init( 256000 ); USART_Printf_Init( 256000 );
#endif #endif
/* 诊断: 复位原因寄存器 (bit31=IWDG bit30=WWDG bit29=LPWR bit26=NRST bit25=POR bit24=软复位)
, vs vs */
PRINT("RST_REASON: 0x%08lx\n", (unsigned long)OFFLOG_RCC_RSTSCKR);
fault_diag_init(); /* 打印上次复位现场 (HardFault mcause/mepc + 执行轨迹 marker) */
iot_watchdog_init(); /* 无条件 IWDG (2026-08-13): iot_enable=0 时 iot_mqtt_init 不调,
IWDG 4s + */
PRINT("%s\n", VER_LIB); PRINT("%s\n", VER_LIB);
PRINT("SystemCoreClock:%d\n", SystemCoreClock); PRINT("SystemCoreClock:%d\n", SystemCoreClock);
/* ===== 实验: 禁用全部 Flash/SPI 操作 (2026-08-12) =====
: SPI +
: storage.c SPI + GPIO 沿 ( SPI_Flash_Init)
: NRST 100nF / SPI 线 NRST / 沿
#if 0 = 恢复全部 SPI; 保留本块便于回退实验 */
#if 0
PRINT("ISOLATE: flash/SPI disabled (flash isolation test)\n");
#else
GetMacAddr(gMacAddr); GetMacAddr(gMacAddr);
storage_init(); storage_init();
PRINT("INIT: storage ok\n");
offlog_init();
PRINT("INIT: offlog ok\n");
/* 快照区: 延后初始化 (2026-08-17) — snap_init 移到主循环 3s 后
(snap_delayed_init), SPI (+ ~80ms);
3s snap_enqueue/snap_flush !_ready */
/* offlog_boot: 记录本次复位原因到事件日志。
A(2026-08-17, offlog_boot仍崩) */
{
uint32_t rcc_rst = OFFLOG_RCC_RSTSCKR;
offlog_boot(rcc_rst);
OFFLOG_RCC_RSTSCKR |= OFFLOG_RST_RMVF; /* RMVF (bit24): 清复位标志 */
}
/* ===== 栈溢出修复闭环 (2026-08-17) =====
: .bss46KB ~1.9KB; load_cfg/output_cfg printf
.noinit+ PC
: D定位触发点 RAM (ETH 768/payload 800/MQTT buf 800,
1.9KB~4.75KB) load_cfg 0 */
load_cfg_from_flash(); load_cfg_from_flash();
output_cfg_from_flash(); output_cfg_from_flash(); /* 2026-08-17: 栈瘦身后恢复 (栈~4.75KB, printf 峰值安全) */
#endif
PRINT("MAC: %02X %02X %02X %02X %02X %02X\r\n", gMacAddr[0],gMacAddr[1], gMacAddr[2], gMacAddr[3],gMacAddr[4],gMacAddr[5]); PRINT("MAC: %02X %02X %02X %02X %02X %02X\r\n", gMacAddr[0],gMacAddr[1], gMacAddr[2], gMacAddr[3],gMacAddr[4],gMacAddr[5]);
PRINT("net version:%x\n", WCHNET_GetVer()); PRINT("net version:%x\n", WCHNET_GetVer());
@@ -311,6 +355,24 @@ int main(void)
HAL_Init(); HAL_Init();
uart_init(); uart_init();
/* ===== 实验: 禁用 UART2 (Loop 口) 隔离测试 (2026-08-12) =====
: UART2 Loop ( SPI )
#if 0 = 恢复 UART2 正常 */
#if 0
{
GPIO_InitTypeDef gpio;
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
USART_Cmd(USART2, DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_3;
gpio.GPIO_Mode = GPIO_Mode_IPU; /* PA3 脱离 UART2, 上拉输入 */
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);
PRINT("ISOLATE: UART2 disabled (Loop isolation test)\n");
}
#endif
TIM3_Init(); TIM3_Init();
TIM2_Init(); TIM2_Init();
@@ -0,0 +1,496 @@
/**
******************************************************************************
* @file snapshot.c
* @author wfq
* @version V1.0
* @date 2026-08-12
* @brief DBN ( offlog , )
*
* : storage.c (SPI_Flash_*), cmcng.h (mstick), offlog.c (g_offlog_part + )
* 线: enqueue (USART2 ISR), flush/write_raw/clear
* SPI ( ~45ms , offlog 线)
******************************************************************************
*/
#include "snapshot.h"
#include "storage.h"
#include "cmcng.h"
#include <string.h>
#include <stdio.h>
/* 编译期断言: 记录必须是 64B (定长环形索引依赖) */
typedef char snap_rec_size_must_be_64[(sizeof(SnapRec) == 64) ? 1 : -1];
typedef char snap_head_size_must_be_32[(sizeof(SnapHead) == 32) ? 1 : -1];
/*===========================================================================
* (访: init/flush/clear/)
*===========================================================================*/
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..n) */
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
static uint16_t _boot_seq; /* 当前 boot 序号 */
static uint32_t _count; /* 有效记录数 (0..max_records) */
static uint8_t _ready; /* init 完成标志 */
/* 中断安全 RAM 暂存 (单生产者=中断 enqueue / 单消费者=主循环 flush, 无锁)
g_snap_wr / g_snap_count, g_snap_rd; count */
static SnapRec g_snap_pending[SNAP_RAM_DEPTH];
static volatile uint8_t g_snap_rd; /* 消费者 (主循环) 读指针 */
static volatile uint8_t g_snap_wr; /* 生产者 (中断) 写指针 */
static volatile uint8_t g_snap_count; /* 暂存条数 (容量 = SNAP_RAM_DEPTH) */
static volatile uint32_t g_snap_drop; /* 满时丢弃计数 (调试用) */
/*===========================================================================
* (ROADMAP 2026-08-12: JEDEC ID + = )
*===========================================================================*/
SnapPart g_snap_part = {
OFFLOG_CHIP_W25Q32, /* 默认 */
0x110000UL, /* W25Q32: 0x090000 + 512KB 事件区 */
0x2F0000UL, /* 3008KB = 4MB - 576KB - 512KB */
751, /* 3008KB/4096 - 1 头扇区 */
48064, /* 751 * 64 */
};
/* 芯片总容量 (W25Q32 4MB → W25Q256 32MB) */
static uint32_t snap_chip_total(uint8_t chip)
{
switch (chip) {
case OFFLOG_CHIP_W25Q32: return 0x00400000UL;
case OFFLOG_CHIP_W25Q64: return 0x00800000UL;
case OFFLOG_CHIP_W25Q128: return 0x01000000UL;
case OFFLOG_CHIP_W25Q256: return 0x02000000UL;
default: return 0x00400000UL; /* 未知→默认 4MB */
}
}
/* 读 JEDEC ID → 填分区表。事件区大小与 offlog 同一事实来源 (g_offlog_part),
= - - W25Q32 */
static void snap_part_detect(void)
{
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
uint8_t chip = OFFLOG_CHIP_W25Q32;
uint32_t evt_size;
uint32_t total;
switch (dev) {
case OFFLOG_CHIP_W25Q32: chip = dev; break;
case OFFLOG_CHIP_W25Q64: chip = dev; break;
case OFFLOG_CHIP_W25Q128: chip = dev; break;
case OFFLOG_CHIP_W25Q256: chip = dev; break;
default: chip = OFFLOG_CHIP_W25Q32; break;
}
evt_size = g_offlog_part.area_size; /* 与事件区严格一致 */
total = snap_chip_total(chip);
g_snap_part.chip = chip;
g_snap_part.area_base = SNAP_FIXED_SIZE + evt_size;
g_snap_part.area_size = total - SNAP_FIXED_SIZE - evt_size;
g_snap_part.data_sectors = (g_snap_part.area_size / 4096UL) - 1;
g_snap_part.max_records = g_snap_part.data_sectors * SNAP_REC_PER_SECTOR;
}
/*===========================================================================
*
*===========================================================================*/
static void snap_head_write(const SnapHead *h)
{
uint8_t buf[32];
memset(buf, 0xFF, sizeof(buf));
memcpy(buf, h, sizeof(SnapHead));
SPI_Flash_Write(buf, g_snap_part.area_base, sizeof(buf));
}
static void snap_head_read(SnapHead *h)
{
uint8_t buf[32];
SPI_Flash_Read(buf, g_snap_part.area_base, sizeof(buf));
memcpy(h, buf, sizeof(SnapHead));
}
static int snap_rec_read_at(uint32_t abs_off, SnapRec *out)
{
SPI_Flash_Read((uint8_t *)out, abs_off, SNAP_REC_SIZE);
return (out->magic == SNAP_MAGIC) ? 0 : -1;
}
/*===========================================================================
* ( offlog : , magic + seq )
*===========================================================================*/
static void snap_scan_recover(const SnapHead *h)
{
uint32_t pos = h->wr_off;
uint32_t scanned = 0;
uint32_t expect_seq = h->wr_seq;
SnapRec r;
/* 防御: wr_off 必须在数据区范围内且 64B 对齐 */
if (pos < SNAP_DATA_BASE || pos >= g_snap_part.area_base + g_snap_part.area_size) {
pos = SNAP_DATA_BASE;
}
if ((pos - SNAP_DATA_BASE) % SNAP_REC_SIZE != 0) {
pos = SNAP_DATA_BASE;
}
while (scanned < SNAP_MAX_RECORDS) {
if (snap_rec_read_at(pos, &r) != 0) {
break; /* 找到真实写位置 */
}
if (expect_seq != 0 && r.seq != expect_seq) {
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
}
pos += SNAP_REC_SIZE;
if (pos >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
pos = SNAP_DATA_BASE; /* 回绕数据区 */
}
scanned++;
expect_seq++;
}
_wr_off = pos;
_wr_sector = (uint16_t)((pos - SNAP_DATA_BASE) / 4096) + 1; /* 区内 1..n */
_wr_seq = expect_seq;
_count = h->count + scanned;
if (_count > SNAP_MAX_RECORDS) _count = SNAP_MAX_RECORDS;
_boot_seq = (uint16_t)(h->boot_seq + 1);
if (_boot_seq == 0) _boot_seq = 1;
}
/*===========================================================================
*
*===========================================================================*/
void snap_init(void)
{
SnapHead h;
snap_part_detect(); /* JEDEC ID + 事件区大小 → 快照区容量 */
memset(&h, 0xFF, sizeof(h));
snap_head_read(&h);
if (!(h.magic[0] == SNAP_HEAD_MAGIC0 && h.magic[1] == SNAP_HEAD_MAGIC1 &&
h.magic[2] == SNAP_HEAD_MAGIC2 && h.magic[3] == SNAP_HEAD_MAGIC3)) {
/* 全新区: 懒擦 — 只擦当前写扇区 (数据扇区1) ~45ms, 其余扇区由环形写
751 ~34s IWDG 4s */
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
memset(&h, 0xFF, sizeof(h));
h.magic[0] = SNAP_HEAD_MAGIC0;
h.magic[1] = SNAP_HEAD_MAGIC1;
h.magic[2] = SNAP_HEAD_MAGIC2;
h.magic[3] = SNAP_HEAD_MAGIC3;
h.boot_seq = 0;
h.wr_sector = 1;
h.wr_off = SNAP_DATA_BASE;
h.wr_seq = 0;
h.count = 0;
snap_head_write(&h);
_wr_off = SNAP_DATA_BASE;
_wr_sector = 1;
_wr_seq = 1;
_count = 0;
_boot_seq = 1;
} else {
snap_scan_recover(&h);
}
/* 回写头: boot_seq 递增 */
h.magic[0] = SNAP_HEAD_MAGIC0;
h.magic[1] = SNAP_HEAD_MAGIC1;
h.magic[2] = SNAP_HEAD_MAGIC2;
h.magic[3] = SNAP_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
snap_head_write(&h);
g_snap_rd = 0;
g_snap_wr = 0;
g_snap_count = 0;
g_snap_drop = 0;
_ready = 1;
}
/*===========================================================================
* (2026-08-17): SNAP_START_DELAY_MS
* : SPI ( + ~80ms),
* VDD / (08-17 : BOOT_CNT =1, RSTSCKR )
* : 3s snap_enqueue/snap_flush !_ready ,
* ()
* , (/ return)
*===========================================================================*/
#define SNAP_START_DELAY_MS 3000
void snap_delayed_init(void)
{
if (_ready) return; /* 已初始化 */
if (mstick() < SNAP_START_DELAY_MS) return; /* 未到延时窗口 */
snap_init();
}
/*===========================================================================
* ()
*===========================================================================*/
static void snap_flush_head(void)
{
SnapHead h;
h.magic[0] = SNAP_HEAD_MAGIC0;
h.magic[1] = SNAP_HEAD_MAGIC1;
h.magic[2] = SNAP_HEAD_MAGIC2;
h.magic[3] = SNAP_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
snap_head_write(&h);
}
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic */
static int snap_sector_has_data(uint32_t abs_sec_off)
{
SnapRec r;
SPI_Flash_Read((uint8_t *)&r, abs_sec_off, SNAP_REC_SIZE);
return (r.magic == SNAP_MAGIC) ? 1 : 0;
}
/* 写一条记录 (仅主循环; rec->seq 已由 snap_flush 分配) */
static void snap_write_raw(const SnapRec *rec)
{
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
if (_wr_off >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
_wr_off = SNAP_DATA_BASE;
}
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区 */
{
uint32_t sec_index = (_wr_off - SNAP_DATA_BASE) / 4096; /* 0..n-2 */
if ((uint32_t)(_wr_sector - 1) != sec_index) {
uint32_t target_abs = (SNAP_DATA_BASE / 4096 + sec_index) * 4096;
if (snap_sector_has_data(target_abs)) {
uint32_t erased = (_count > SNAP_REC_PER_SECTOR)
? SNAP_REC_PER_SECTOR : _count;
_count -= erased;
}
SPI_Flash_Erase_Sector(target_abs / 4096);
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..n */
snap_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
}
}
SPI_Flash_Write_NoCheck((uint8_t *)rec, _wr_off, SNAP_REC_SIZE);
_wr_off += SNAP_REC_SIZE;
_wr_seq++;
if (_count < SNAP_MAX_RECORDS) _count++;
}
/*===========================================================================
* ( 0xC0 线线 12B )
*===========================================================================*/
static void snap_pack_coil(const LUP_CoilSensor *cs, uint8_t *o)
{
o[0] = (uint8_t)((cs->freq_level << 6) | (cs->direction << 5)
| (cs->freq_type << 4) | (cs->sensitivity & 0x0F));
o[1] = (uint8_t)((cs->condition << 4) | (cs->loop_state << 3)
| (cs->car_state << 2) | (cs->misc_type & 0x03));
o[2] = (uint8_t)(cs->freq & 0xFF);
o[3] = (uint8_t)((cs->freq >> 8) & 0xFF);
o[4] = (uint8_t)((cs->freq >> 16) & 0xFF);
o[5] = (uint8_t)(cs->variation & 0xFF);
o[6] = (uint8_t)((cs->variation >> 8) & 0xFF);
o[7] = (uint8_t)((cs->variation >> 16) & 0xFF);
o[8] = (uint8_t)(cs->misc.passtime_ms & 0xFF);
o[9] = (uint8_t)((cs->misc.passtime_ms >> 8) & 0xFF);
o[10] = (uint8_t)((cs->misc.passtime_ms >> 16) & 0xFF);
o[11] = (uint8_t)((cs->misc.passtime_ms >> 24) & 0xFF);
}
/* 打包传感帧 → SnapRec (seq 留待 flush 时分配; ts_ms/boot_seq 用采集时刻) */
static void snap_pack(const LUP_SensorReport *sr, SnapRec *rec)
{
uint8_t i, n;
memset(rec, 0, sizeof(*rec));
rec->magic = SNAP_MAGIC;
n = (sr->coil_count > SNAP_COILS_MAX) ? SNAP_COILS_MAX : sr->coil_count;
rec->len = n * SNAP_COIL_BYTES;
rec->ts_ms = mstick();
rec->boot_seq = _boot_seq;
for (i = 0; i < n; i++) {
snap_pack_coil(&sr->coils[i], rec->coils + i * SNAP_COIL_BYTES);
}
}
/*===========================================================================
* (: uart_srv lup_process_frame iot_sensor_ingest)
* USART2 ISR lup_feed_byte, uart_srv
* , RAM + flush : SPI
* - g_snap_wr / g_snap_pending / g_snap_count
* - g_snap_rd; count volatile ,
* - : count >= DEPTH ( = SNAP_RAM_DEPTH)
*===========================================================================*/
void snap_enqueue(const LUP_SensorReport *sr)
{
SnapRec rec;
if (!_ready || sr == NULL) return;
snap_pack(sr, &rec);
if (g_snap_count >= SNAP_RAM_DEPTH) {
g_snap_drop++; /* 暂存满 → 丢新帧 (快照环形, 可接受) */
return;
}
g_snap_pending[g_snap_wr] = rec;
g_snap_wr = (uint8_t)((g_snap_wr + 1) & (SNAP_RAM_DEPTH - 1));
g_snap_count++;
}
/*===========================================================================
* (iot_mqtt_publish_sensor ; /使)
* - seq ( _wr_seq, )
*===========================================================================*/
void snap_flush(void)
{
if (!_ready) return;
while (g_snap_count > 0) {
SnapRec *rec = &g_snap_pending[g_snap_rd];
rec->seq = _wr_seq; /* 主循环独占分配 */
snap_write_raw(rec);
g_snap_rd = (uint8_t)((g_snap_rd + 1) & (SNAP_RAM_DEPTH - 1));
g_snap_count--;
}
if (g_snap_drop != 0) {
g_snap_drop = 0; /* 丢弃计数清零 (调试观察点) */
}
}
/*===========================================================================
*
*===========================================================================*/
uint8_t snap_enabled(void)
{
return _ready;
}
uint32_t snap_count(void)
{
return _count;
}
uint32_t snap_seq_last(void)
{
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
}
uint32_t snap_boot_seq(void)
{
return _boot_seq;
}
int snap_read_idx(uint32_t idx, SnapRec *out)
{
uint32_t cnt = _count;
if (!_ready || idx >= cnt) return -1;
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
uint32_t phys = (_wr_off - SNAP_DATA_BASE); /* 0..DATA_SIZE */
uint32_t start = (phys + SNAP_DATA_SIZE - cnt * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
uint32_t read_off = SNAP_DATA_BASE + (start + idx * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
return snap_rec_read_at(read_off, out);
}
/*===========================================================================
* (: LOG_CLEAR, payload[0]=2=)
*===========================================================================*/
void snap_clear(void)
{
if (!_ready) return;
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms): count=0 使旧数据不可读,
751 ~34s > IWDG 4s */
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
_wr_off = SNAP_DATA_BASE;
_wr_sector = 1;
_count = 0;
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证快照全局唯一 */
{
uint8_t p[1] = {2}; /* 2 = 快照流 (审计区分) */
offlog_evt(OFFLOG_EVT_LOG_CLEAR, p, 1);
}
snap_flush_head();
}
/* SnapRec -> JSON object (channels aligned with 0xC0 wire format).
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
Returns snprintf written length; caller must ensure buf_len enough. */
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len)
{
int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES))
? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES);
if (coil_count < 0) coil_count = 0;
int pos = snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[",
(unsigned long)r->seq, (unsigned)r->boot_seq,
(unsigned long)r->ts_ms, (unsigned)coil_count);
int c;
for (c = 0; c < coil_count; c++) {
const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES];
uint8_t cfg = p[0];
uint8_t cond = p[1];
uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16);
int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16));
uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8)
| ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24);
uint8_t freq_level = (cfg >> 6) & 0x03;
uint8_t direction = (cfg >> 5) & 0x01;
uint8_t freq_type = (cfg >> 4) & 0x01;
uint8_t sensitivity = cfg & 0x0F;
uint8_t condition = (cond >> 4) & 0x0F;
uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */
uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */
uint8_t misc_type = cond & 0x03;
const char *fl = "high";
const char *mt = "time";
if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */
if (freq_level == 1) fl = "mid_high";
else if (freq_level == 2) fl = "mid_low";
else if (freq_level == 3) fl = "low";
if (misc_type == 1) mt = "cut_count";
else if (misc_type == 2) mt = "flow_count";
else if (misc_type == 3) mt = "relay_count";
if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ",");
pos += snprintf(buf + pos, buf_len - pos,
"{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u,"
"\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s,"
"\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}",
(unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type,
(unsigned)sensitivity, (unsigned)condition,
loop_state ? "false" : "true", car_state ? "true" : "false",
mt, (unsigned long)freq, (long)variation, (unsigned long)misc);
}
pos += snprintf(buf + pos, buf_len - pos, "]}");
return pos;
}
/* SnapRec -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §6.4 字段表解析)
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
Returns snprintf written length; 64B -> 128 hex chars. */
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)r;
for (i = 0; i < (int)sizeof(SnapRec); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
@@ -19,11 +19,11 @@
/* Winbond SPIFalsh ID */ /* Winbond SPIFalsh ID */
#define W25Q80 0XEF13 #define W25Q80 0X13 // device ID only, no vendor code
#define W25Q16 0XEF14 #define W25Q16 0X14
#define W25Q32 0XEF15 #define W25Q32 0X15
#define W25Q64 0XEF16 //64Mbit, 8MByte #define W25Q64 0X16 //64Mbit, 8MByte
#define W25Q128 0XEF17 #define W25Q128 0X17
/* Winbond SPIFalsh Instruction List */ /* Winbond SPIFalsh Instruction List */
#define W25X_WriteEnable 0x06 #define W25X_WriteEnable 0x06
@@ -90,13 +90,13 @@ void SPI_Flash_Init(void)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
@@ -105,7 +105,7 @@ void SPI_Flash_Init(void)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
@@ -114,7 +114,7 @@ void SPI_Flash_Init(void)
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure); SPI_Init(SPI1, &SPI_InitStructure);
@@ -225,6 +225,33 @@ u16 SPI_Flash_ReadID(void)
} }
/*********************************************************************
* @fn SPI_Flash_ReadJEDEC_ID
*
* @brief Read JEDEC ID (0x9F). Return 3rd byte (capacity code).
* W25Q32=0x16 Q64=0x17 Q128=0x18 Q256=0x19.
* 2026-08-19: no vendor code check (id[0]/id[1]) - compatible
* with other vendors same-capacity parts.
*
* @return device id byte
*/
uint8_t SPI_Flash_ReadJEDEC_ID(void)
{
uint8_t id[3] = {0, 0, 0};
GPIO_WriteBit(GPIOA, GPIO_Pin_4, 0);
SPI1_ReadWriteByte(W25X_JedecDeviceID);
id[0] = SPI1_ReadWriteByte(0xFF);
id[1] = SPI1_ReadWriteByte(0xFF);
id[2] = SPI1_ReadWriteByte(0xFF);
GPIO_WriteBit(GPIOA, GPIO_Pin_4, 1);
/* 2026-08-19: no vendor code check - JEDEC capacity code (id[2])
is standardized across vendors (e.g. 0x16=32Mbit for ISSI/GD too) */
return id[2];
}
/********************************************************************* /*********************************************************************
* @fn SPI_Flash_Erase_Sector * @fn SPI_Flash_Erase_Sector
* *
@@ -481,7 +508,7 @@ void storage_init(void)
SPI_Flash_Init(); SPI_Flash_Init();
uint16_t Flash_Model = SPI_Flash_ReadID(); uint16_t Flash_Model = SPI_Flash_ReadID();
switch(Flash_Model) switch(Flash_Model & 0xFF) /* device ID low byte only, no vendor check */
{ {
case W25Q80: PRINT("W25Q80 OK!\r\n"); break; case W25Q80: PRINT("W25Q80 OK!\r\n"); break;
case W25Q16: PRINT("W25Q16 OK!\r\n"); break; case W25Q16: PRINT("W25Q16 OK!\r\n"); break;
@@ -18,6 +18,9 @@
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "simple_json.h" #include "simple_json.h"
#include "storage.h" #include "storage.h"
#include "offlog.h"
#include "snapshot.h"
#include "fault_diag.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@@ -636,7 +639,129 @@ static void handle_report_config(uint8_t socket, uint32_t msg_id, const char *js
g_report_cfg.enable, g_report_cfg.interval, g_report_cfg.sensor_type); g_report_cfg.enable, g_report_cfg.interval, g_report_cfg.sensor_type);
} }
/* 4.16 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */ /* 4.16 log_stat — 查询脱机日志统计 (event/snapshot 流, 协议 V1.03) */
static void handle_log_stat(uint8_t socket, uint32_t msg_id, const char *json) {
char data_json[256];
char stream_buf[16];
uint32_t count, seq_last, seq_first, capacity;
const char *stream = "event";
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
if (strcmp(stream_buf, "snapshot") == 0) stream = "snapshot";
if (strcmp(stream, "snapshot") == 0) {
count = snap_count();
seq_last = snap_seq_last();
seq_first = (count > 0) ? (seq_last - count + 1) : 0;
capacity = SNAP_MAX_RECORDS;
snprintf(data_json, sizeof(data_json),
"{\"stream\":\"snapshot\",\"enabled\":%s,\"boot_seq\":%lu,"
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}",
snap_enabled() ? "true" : "false",
(unsigned long)snap_boot_seq(),
(unsigned long)count, (unsigned long)capacity,
(unsigned long)seq_first, (unsigned long)seq_last);
PRINT("JSON: log_stat(snapshot) count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last);
} else {
count = offlog_count();
seq_last = offlog_seq_last();
seq_first = (count > 0) ? (seq_last - count + 1) : 0;
capacity = OFFLOG_MAX_RECORDS;
snprintf(data_json, sizeof(data_json),
"{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu,"
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}",
offlog_enabled() ? "true" : "false",
(unsigned long)offlog_boot_seq(),
(unsigned long)count, (unsigned long)capacity,
(unsigned long)seq_first, (unsigned long)seq_last);
PRINT("JSON: log_stat count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last);
}
json_send_ok(socket, msg_id, "log_stat", data_json);
}
/* 4.17 log_query — 分页拉取脱机日志 (event/snapshot 流, hex 原始字节上报, 协议 V1.03) */
static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json) {
char data_json[TCP_JSON_DATA_BUF_LEN];
char stream_buf[16];
uint32_t start_seq = json_get_uint_field(json, "\"start_seq\"");
uint32_t req_count = json_get_uint_field(json, "\"count\"");
int is_snap = 0;
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
if (strcmp(stream_buf, "snapshot") == 0) is_snap = 1;
int pos = snprintf(data_json, sizeof(data_json),
"{\"start_seq\":%lu,\"records\":[",
(unsigned long)start_seq);
uint32_t fetched = 0;
if (is_snap) {
uint32_t total = snap_count();
uint32_t seq_last = snap_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
if (req_count > SNAP_MAX_QUERY_RECORDS) req_count = SNAP_MAX_QUERY_RECORDS;
if (req_count == 0) req_count = SNAP_MAX_QUERY_RECORDS;
if (snap_enabled() && total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
SnapRec rec;
char hex[132]; /* 64B -> 128 hex + NUL */
if (snap_read_idx(idx + fetched, &rec) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
snap_rec_to_hex(&rec, hex, sizeof(hex));
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)rec.seq, hex);
fetched++;
}
}
PRINT("JSON: log_query(snapshot) start_seq=%lu req=%lu fetched=%lu\n",
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
} else {
uint32_t total = offlog_count();
uint32_t seq_last = offlog_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS;
if (req_count == 0) req_count = OFFLOG_MAX_QUERY_RECORDS;
if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
OfflogEvt evt;
char hex[68]; /* 32B -> 64 hex + NUL */
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
offlog_evt_to_hex(&evt, hex, sizeof(hex));
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)evt.seq, hex);
fetched++;
}
}
PRINT("JSON: log_query start_seq=%lu req=%lu fetched=%lu\n",
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
}
snprintf(data_json + pos, sizeof(data_json) - pos, "]}");
json_send_ok(socket, msg_id, "log_query", data_json);
}
/* 4.18 log_clear — 清除脱机日志 (event/snapshot 流, 审计留痕, 协议 V1.03) */
static void handle_log_clear(uint8_t socket, uint32_t msg_id, const char *json) {
char stream_buf[16];
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
if (strcmp(stream_buf, "snapshot") == 0) {
snap_clear(); /* 阻塞 ~45ms (逻辑清除 + 当前写扇区) */
PRINT("JSON: log_clear(snapshot) done\n");
} else {
offlog_clear(); /* 阻塞 ~2.8s (63 扇区擦除) */
PRINT("JSON: log_clear done\n");
}
json_send_ok(socket, msg_id, "log_clear", NULL);
}
/* 4.19 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */
static void handle_loop_version_query(uint8_t socket, uint32_t msg_id, const char *json) { static void handle_loop_version_query(uint8_t socket, uint32_t msg_id, const char *json) {
lup_send_get_version(); lup_send_get_version();
g_json_pending.active = 1; g_json_pending.active = 1;
@@ -754,6 +879,9 @@ static const JsonCmdEntry g_cmd_table[] = {
{"loop_sens_read", handle_loop_sens_read, 1}, {"loop_sens_read", handle_loop_sens_read, 1},
{"loop_sens_write", handle_loop_sens_write, 1}, {"loop_sens_write", handle_loop_sens_write, 1},
{"report_config", handle_report_config, 1}, {"report_config", handle_report_config, 1},
{"log_stat", handle_log_stat, 1},
{"log_query", handle_log_query, 1},
{"log_clear", handle_log_clear, 1},
}; };
#define JSON_CMD_COUNT (sizeof(g_cmd_table) / sizeof(g_cmd_table[0])) #define JSON_CMD_COUNT (sizeof(g_cmd_table) / sizeof(g_cmd_table[0]))
@@ -813,13 +941,23 @@ static int format_loop_param_json(char *buf, uint16_t buf_size, const LUP_ParamG
static void json_sensor_callback(const uint8_t *pkg, uint16_t len) static void json_sensor_callback(const uint8_t *pkg, uint16_t len)
{ {
FAULT_MARKER(MK_EVT_CB_IN);
/* 2026-08-13 实验: enter PRINT 是 LUP 帧后卡死嫌疑点 (printf %d 处理死循环),
printf , _write/ */
#if 0
PRINT("JSON: sensor_cb enter socket=%d auth=%d report=%d\n", PRINT("JSON: sensor_cb enter socket=%d auth=%d report=%d\n",
g_json_socket_listen, g_json_auth_state, g_report_cfg.enable); g_json_socket_listen, g_json_auth_state, g_report_cfg.enable);
#endif
// Check: socket active, authed, report enabled // Check: socket active, authed, report enabled
if (g_json_socket_listen == 0xFF) { PRINT("JSON: sensor_cb skip: no socket\n"); return; } if (g_json_socket_listen == 0xFF) { PRINT("JSON: sensor_cb skip: no socket\n"); return; }
/* 2026-08-13: not authed 每帧打印太吵 (Loop MCU 高频上报), 且占用主循环
UART2 */
#if 0
if (g_json_auth_state != JSON_STATE_AUTHED) { PRINT("JSON: sensor_cb skip: not authed\n"); return; } if (g_json_auth_state != JSON_STATE_AUTHED) { PRINT("JSON: sensor_cb skip: not authed\n"); return; }
if (!g_report_cfg.enable) { PRINT("JSON: sensor_cb skip: report disabled\\n"); return; } #endif
if (g_json_auth_state != JSON_STATE_AUTHED) { return; }
if (!g_report_cfg.enable) { PRINT("JSON: sensor_cb skip: report disabled\n"); return; }
LUP_SensorReport sr; LUP_SensorReport sr;
memset(&sr, 0, sizeof(sr)); memset(&sr, 0, sizeof(sr));
@@ -829,7 +967,7 @@ static void json_sensor_callback(const uint8_t *pkg, uint16_t len)
return; return;
} }
char data_json[2048]; char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) { if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
char *out = (char *)malloc(TCP_JSON_MAX_FRAME); char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
if (out) { if (out) {
@@ -933,13 +1071,13 @@ void tcp_json_handle_sock_int(uint8_t socketid, uint8_t intstat) {
uint16_t space = TCP_JSON_RECV_BUF_LEN - g_json_recv_len; uint16_t space = TCP_JSON_RECV_BUF_LEN - g_json_recv_len;
if (recv_len > space) recv_len = space; if (recv_len > space) recv_len = space;
uint32_t rd_len = recv_len; uint32_t rd_len = recv_len;
uint8_t tmp_buf[RECE_BUF_LEN]; static uint8_t tmp_buf[RECE_BUF_LEN]; /* 2026-08-17: static — 中断上下文 1KB 栈数组 (防溢出) */
WCHNET_SocketRecv(socketid, tmp_buf, &rd_len); WCHNET_SocketRecv(socketid, tmp_buf, &rd_len);
g_json_last_comm_ts = mstick(); // 有数据交互 g_json_last_comm_ts = mstick(); // 有数据交互
memcpy(g_json_recv_buf + g_json_recv_len, tmp_buf, (uint16_t)rd_len); memcpy(g_json_recv_buf + g_json_recv_len, tmp_buf, (uint16_t)rd_len);
g_json_recv_len += (uint16_t)rd_len; g_json_recv_len += (uint16_t)rd_len;
char frame[TCP_JSON_MAX_FRAME]; static char frame[TCP_JSON_MAX_FRAME]; /* 2026-08-17: static — 中断上下文 800B 栈数组 (防溢出) */
while (json_extract_frame(g_json_recv_buf, &g_json_recv_len, frame, sizeof(frame))) { while (json_extract_frame(g_json_recv_buf, &g_json_recv_len, frame, sizeof(frame))) {
PRINT("JSON recv: %s\n", frame); PRINT("JSON recv: %s\n", frame);
json_process_frame(socketid, frame); json_process_frame(socketid, frame);
@@ -1102,7 +1240,7 @@ static void json_check_pending(void) {
memset(&pg, 0, sizeof(pg)); memset(&pg, 0, sizeof(pg));
int ret = lup_parse_param_get(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &pg); int ret = lup_parse_param_get(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &pg);
if (ret == 0) { if (ret == 0) {
char data_json[2048]; char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_loop_param_json(data_json, sizeof(data_json), &pg) == 0) { if (format_loop_param_json(data_json, sizeof(data_json), &pg) == 0) {
json_send_ok(g_json_pending.socket, g_json_pending.msg_id, json_send_ok(g_json_pending.socket, g_json_pending.msg_id,
g_json_pending.cmd, data_json); g_json_pending.cmd, data_json);
@@ -1319,7 +1457,7 @@ void tcp_json_push_sensor(void) {
return; return;
} }
char data_json[2048]; char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) { if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
char *out = (char *)malloc(TCP_JSON_MAX_FRAME); char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
if (out) { if (out) {
@@ -16,6 +16,102 @@ void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
/*==================== UART2 RX DMA (2026-08-17 方案A) ====================
* : PRINT ( ~7.4ms) USART2 RXNE checksum fail
* : DMA1_Ch6 ( CPU ),
* - 512B : ~7 (70B), (SPI 45ms)
* - ( RXNE, DMA ) ; WCHNET ETH DMA,
* BLE DMA1 Ch6
* - lup_feed_byte ()
*======================================================================*/
#define UART2_DMA_BUF_LEN 512
static uint8_t uart2_dma_buf[UART2_DMA_BUF_LEN] __attribute__((aligned(4)));
static uint16_t uart2_dma_last = 0; /* 主循环消费位置 (DMA 写指针由硬件维护) */
static uint32_t uart2_dma_drop = 0; /* 溢出丢弃计数 (主循环消费不及时) */
static uint8_t uart2_dma_ota_mode = 0xFF; /* 上次 OTA 模式 (0xFF=未初始化, 首轮强制 reset) */
void uart2_dma_init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel6);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DATAR);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart2_dma_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = UART2_DMA_BUF_LEN;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel6, &DMA_InitStructure);
/* DMA 接管 USART2 RX: 硬件收字节, 打印关中断不丢 */
USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE);
DMA_Cmd(DMA1_Channel6, ENABLE);
/* 关闭 RXNE 中断 (DMA 接管后逐字节中断不再需要) */
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
uart2_dma_last = 0;
}
/* 主循环每轮调用: 消费 DMA 环形缓冲新字节 → 批量喂 lup_feed_byte 状态机 */
void uart2_dma_poll(void)
{
uint16_t cur = (uint16_t)(UART2_DMA_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel6));
uint16_t n = (uint16_t)((cur + UART2_DMA_BUF_LEN - uart2_dma_last) & (UART2_DMA_BUF_LEN - 1));
if (n == 0) return;
/* 溢出保护: 未消费 > 半缓冲 (256B) → 消费太慢被 DMA 覆盖, 重置解析器丢帧计数
2026-08-19: OTA 0x9F ACK 7B, Loop 1 ,
256B TX ACK */
{
uint16_t drop_thr = g_flag_counter_ota.flag ? UART2_DMA_BUF_LEN : (UART2_DMA_BUF_LEN / 2);
if (n > drop_thr) {
uart2_dma_drop++;
lup_frame_reset();
uart2_dma_last = cur;
return;
}
}
/* OTA 模式切换 (0x7F ↔ 0x9F) 时重置帧解析器, 防旧模式残留状态污染 */
if (g_flag_counter_ota.flag != uart2_dma_ota_mode) {
lup_frame_reset();
uart2_dma_ota_mode = g_flag_counter_ota.flag;
}
while (n--) {
uint8_t b = uart2_dma_buf[uart2_dma_last];
uart2_dma_last = (uint16_t)((uart2_dma_last + 1) & (UART2_DMA_BUF_LEN - 1));
/* OTA 模式走 0x9F 帧解析 (bootloader 的 pre_ok/addr_ok/data ACK),
OTA 0x7F */
int done = g_flag_counter_ota.flag ? lup_feed_byte_ota(b) : lup_feed_byte(b);
if (done) {
/* 帧接收完成 → 复制到 g_pkg_uart_2 (与中断版同逻辑, 主循环无竞争) */
const uint8_t *frame = lup_frame_data();
uint16_t frame_len = lup_frame_len();
if (frame_len <= BUFF_STACK_SIZE) {
memcpy(g_pkg_uart_2.pkg, frame, frame_len);
g_pkg_uart_2.offset = frame_len;
g_pkg_uart_2.flag = 1;
g_pkg_uart_2.tick = 0;
}
lup_frame_reset();
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
/* 收帧中 → tick 归零 (对齐中断版行为, 防 TIM3 半帧兜底误判) */
g_pkg_uart_2.tick = 0;
}
}
}
void uart_init(void){ void uart_init(void){
GPIO_InitTypeDef GPIO_InitStructure = {0}; GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0}; USART_InitTypeDef USART_InitStructure = {0};
@@ -44,8 +140,7 @@ void uart_init(void){
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART2, &USART_InitStructure); USART_Init(USART2, &USART_InitStructure);
// USART_ITConfig(USART2, USART_IT_IDLE, ENABLE); /* RXNE 中断由 uart2_dma_init 关闭 (DMA 接管, 2026-08-17) */
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
@@ -55,6 +150,8 @@ void uart_init(void){
USART_Cmd(USART2, ENABLE); USART_Cmd(USART2, ENABLE);
uart2_dma_init(); /* DMA 循环接收接管 (2026-08-17) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
@@ -90,27 +187,9 @@ void USART1_IRQHandler(void)
*/ */
void USART2_IRQHandler(void) void USART2_IRQHandler(void)
{ {
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) /* 2026-08-17: RXNE 中断已关 (DMA 接管), 本 handler 不再触发。
{ + IDLE : ,
uint8_t _dat = USART_ReceiveData(USART2); USART_IT_IDLE ( SR + DR , ) */
// 喂给帧解析器
if (lup_feed_byte(_dat)) {
// 帧接收完成,复制到 g_pkg_uart_2
const uint8_t *frame = lup_frame_data();
uint16_t frame_len = lup_frame_len();
if (frame_len <= BUFF_STACK_SIZE) {
memcpy(g_pkg_uart_2.pkg, frame, frame_len);
g_pkg_uart_2.offset = frame_len;
g_pkg_uart_2.flag = 1;
g_pkg_uart_2.tick = 0;
}
lup_frame_reset();
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
// 正在接收帧中,tick 归零
g_pkg_uart_2.tick = 0;
}
}
} }
@@ -151,6 +230,8 @@ void uart_srv(void)
uint8_t i; uint8_t i;
uint8_t _report_flag = 0; uint8_t _report_flag = 0;
uart2_dma_poll(); /* DMA 环形缓冲 → 帧解析 (2026-08-17) */
// 检查命令超时 // 检查命令超时
lup_cmd_check_timeout(); lup_cmd_check_timeout();
@@ -172,43 +253,61 @@ void uart_srv(void)
if(g_dbn_ble_state_acs_enable.flag != 0){ if(g_dbn_ble_state_acs_enable.flag != 0){
// BLE ACS 已连接 → 改 Magic 为 0x8F 发给 BLE // BLE ACS 已连接 → 改 Magic 为 0x8F 发给 BLE
g_pkg_uart_2.pkg[0] = 0x8F; g_pkg_uart_2.pkg[0] = 0x8F;
_report_flag = 1; // 保留给 BLE g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp); // 保留给 BLE
} }
// else: 回调已推送 TCP,直接清理 /* 2026-08-13 修复: 0xC0 帧消费后必须清 flag —
, auth tcp_json_push_sensor return
/ (checksum fail ) */
InitPkgUart(&g_pkg_uart_2);
}
else {
if(g_flag_bt_state){
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
InitPkgUart(&g_pkg_uart_2);
}
else {
/* 2026-08-13 修复: 非 0xC0 + 无 BLE 连接也清 flag (原缺失) */
InitPkgUart(&g_pkg_uart_2);
}
} }
// 调试打印 // 调试打印 (2026-08-13: LUP Rx 已在 lup_process_frame 缓冲打印,
// 此处重复逐字节打印冗余且高频 — 暂时关闭)
#if 0
for(i = 0; i < g_pkg_uart_2.offset; i++){ for(i = 0; i < g_pkg_uart_2.offset; i++){
PRINT(" %02X", g_pkg_uart_2.pkg[i]); PRINT(" %02X", g_pkg_uart_2.pkg[i]);
} }
PRINT("\n"); PRINT("\n");
#endif
if(_report_flag){
// 传感器帧保留在 pkg 中供上层 (tcp_json_srv) 处理
// 不 InitPkgUart — 由 tcp_json_push_sensor 消费后清理
}
} }
else { else {
// 非 0x7F 魔法字节 // 非 0x7F 魔法字节 — hex 打印 (原 %s 会把二进制当字符串 → 乱码)
PRINT("Rcv_len:%d,dat: %s\n", g_pkg_uart_2.offset, g_pkg_uart_2.pkg); PRINT("Rcv_len:%d,dat:", g_pkg_uart_2.offset);
for(i = 0; i < g_pkg_uart_2.offset; i++){
PRINT(" %02X", g_pkg_uart_2.pkg[i]);
}
PRINT("\n");
} }
} }
else{ else{
// OTA 模式 — 忽略 Loop MCU 数据 /* OTA 模式 (2026-08-19): Loop bootloader 回的 0x9F 帧
(back_9F_pre_ok / addr_ok / data ACK) BLE ,
(0xA7 WITH_BACK) ACK */
if(g_flag_bt_state){
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
}
/* OTA 模式同样清 flag — 残留帧会在 OTA 结束后被误处理 */
InitPkgUart(&g_pkg_uart_2);
} }
if(g_flag_bt_state){ if(g_flag_bt_state){
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
} }
else{ else{
g_dbn_ble_state_acs_enable.flag = 0; g_dbn_ble_state_acs_enable.flag = 0;
} }
// 只有非 _report_flag 时才立即清空
// _report_flag 的帧由 tcp_json_push_sensor 消费后清理
if (!_report_flag) {
InitPkgUart(&g_pkg_uart_2);
}
} }
} }
@@ -171,15 +171,23 @@ SECTIONS
{ {
. = ALIGN(4); . = ALIGN(4);
PROVIDE( _sbss = .); PROVIDE( _sbss = .);
*(.sbss*) *(.sbss*)
*(.gnu.linkonce.sb.*) *(.gnu.linkonce.sb.*)
*(.bss*) *(.bss*)
*(.gnu.linkonce.b.*) *(.gnu.linkonce.b.*)
*(COMMON*) *(COMMON*)
. = ALIGN(4); . = ALIGN(4);
PROVIDE( _ebss = .); PROVIDE( _ebss = .);
} >RAM AT>FLASH } >RAM AT>FLASH
/* 故障诊断现场: 复位不清零 (startup 只清 .bss, 不碰 .noinit) */
.noinit (NOLOAD) :
{
. = ALIGN(4);
*(.noinit*)
. = ALIGN(4);
} >RAM
PROVIDE( _end = _ebss); PROVIDE( _end = _ebss);
PROVIDE( end = . ); PROVIDE( end = . );
+10 -1
View File
@@ -46,7 +46,16 @@ void USART_Printf_Init(uint32_t baudrate);
void SDI_Printf_Enable(void); void SDI_Printf_Enable(void);
#if(DEBUG) #if(DEBUG)
#define PRINT(format, ...) printf(format, ##__VA_ARGS__) /* 2026-08-13 printf 重入修复: BLE 协议栈回调(peripheral.c, BB 中断上下文)
PRINT, printf + / HardFault
/ MSTATUS: ,
: PRINT ( LUP Rx hex) , */
#define PRINT(format, ...) do { \
uint32_t _print_ms = __get_MSTATUS(); \
__disable_irq(); \
printf(format, ##__VA_ARGS__); \
__set_MSTATUS(_print_ms); \
} while(0)
#else #else
#define PRINT(X...) #define PRINT(X...)
#endif #endif
+1522 -510
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""从 dbn_ble_srv.c 提取脱机日志 3 case 文本, 供 test_ble_offlog.c 隔离验证。
用法: python3 extract_offlog_cases.py
输出: ./offlog_cases_embedded.c ( tests/ 目录下运行)
原理: 测的是源文件真实代码而非拷贝 提取 `case CMD_DBN_OFFLOG_STAT`
`default:` 之间的文本 ( STAT/QUERY/CLEAR 3 case), 嵌入测试框架编译
"""
import os
SRC = os.path.join(os.path.dirname(__file__),
"../BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c")
OUT = os.path.join(os.path.dirname(__file__), "offlog_cases_embedded.c")
with open(SRC, "rb") as f:
content = f.read()
text = content.decode("utf-8", errors="replace").replace("\r\n", "\n")
start = text.find("case CMD_DBN_OFFLOG_STAT")
end = text.find("default:\n", start)
if start == -1 or end == -1:
raise SystemExit("ERROR: 未找到 offlog 3 case (确认 dbn_ble_srv.c 已修改)")
cases = text[start:end]
for c in ("CMD_DBN_OFFLOG_STAT", "CMD_DBN_OFFLOG_QUERY", "CMD_DBN_OFFLOG_CLEAR"):
if c not in cases:
raise SystemExit(f"ERROR: 提取内容缺少 {c}")
with open(OUT, "w") as f:
f.write(cases)
print(f"OK: {OUT} ({len(cases)} bytes, 3 cases)")
+245
View File
@@ -0,0 +1,245 @@
/**
* test_ble_offlog.c dbn_ble_srv.c 3 case
*
* dbn_ble_srv.c case ,
* Mock: offlog_* / set_response_buf / PRINT / Buf_DBN_BLE
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \
* -o test_ble_offlog test_ble_offlog.c ( extract_offlog_cases.py)
* : ./test_ble_offlog
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ============ 类型/宏 (来自 dbn_ble_srv.h 编辑后, 避免 include BLE 栈) ============ */
#define MAGIC_BYTE_DBN_DEFAULT 0x8F
#define CMD_DBN_OFFLOG_STAT 0x25
#define CMD_DBN_OFFLOG_QUERY 0x26
#define CMD_DBN_OFFLOG_CLEAR 0x27
#define MAX_BLE_TMP_BUF_LEN 132
#define MAX_BLE_DAT_BUF_LEN 132
#define MAX_BLE_DAT_RESPONSE_LEN (100 - 4) /* 单包数据上限 96B */
typedef struct _BLE_Notify_Buf_ {
uint8_t flag;
uint8_t len;
uint8_t buf[100];
} BLE_Notify_Buf;
typedef struct _BUF_DBN_BLE_ {
uint8_t flag;
uint8_t magic;
uint8_t cmd;
uint8_t dat_len ;
uint8_t dat_offset;
uint8_t pkg_amount;
uint8_t pkg_seq;
uint8_t dat[MAX_BLE_DAT_BUF_LEN];
} Buf_DBN_BLE;
#include "offlog.h"
#define PRINT(...) ((void)0)
/* ============ 全局 (来自 dbn_ble_srv.c) ============ */
static uint8_t tmp_ble_buf[MAX_BLE_TMP_BUF_LEN];
static Buf_DBN_BLE g_buf_ble_response;
/* ============ offlog mock ============ */
static uint32_t mock_count = 0;
static uint32_t mock_seq_last = 0;
static uint32_t mock_boot = 0;
static uint8_t mock_enabled = 1;
uint16_t offlog_count(void) { return (uint16_t)mock_count; }
uint32_t offlog_seq_last(void) { return mock_seq_last; }
uint32_t offlog_boot_seq(void) { return mock_boot; }
uint8_t offlog_enabled(void) { return mock_enabled; }
void offlog_clear(void) { mock_count = 1; mock_seq_last++; } /* 审计留痕: count 归 1, seq 继续 */
int offlog_read_idx(uint16_t idx, OfflogEvt *out)
{
uint32_t seq_first;
if (idx >= mock_count) return -1;
seq_first = mock_seq_last - mock_count + 1;
memset(out, 0, sizeof(OfflogEvt));
out->magic = OFFLOG_EVT_MAGIC;
out->type = (idx % 3 == 0) ? OFFLOG_EVT_BOOT :
(idx % 3 == 1) ? OFFLOG_EVT_IOT_READY : OFFLOG_EVT_COIL;
out->seq = seq_first + idx;
out->boot_seq = 2;
out->ts_ms = 1000 + idx * 100;
out->unix_ts = 1784768575UL;
if (idx % 3 == 2) { /* COIL: sub=1 ch=1 value=97 */
out->payload[0] = 1; out->payload[1] = 1;
out->payload[2] = 0; out->payload[3] = 0; out->payload[4] = 0; out->payload[5] = 97;
}
return 0;
}
/* g_offlog_part: OFFLOG_MAX_RECORDS 宏展开为运行时值, 此处 mock W25Q32 (512KB) */
OfflogPart g_offlog_part = { OFFLOG_CHIP_W25Q32, 0x80000UL, 127, 16256 };
/* ============ set_response_buf mock: 只记录调用参数 ============ */
static uint8_t last_cmd = 0;
static uint8_t last_len = 0;
static uint8_t last_dat[160];
uint8_t set_response_buf(Buf_DBN_BLE *response_dst, uint8_t magic, uint8_t cmd,
uint8_t *dat, uint8_t dat_len)
{
uint8_t i;
memset(last_dat, 0, sizeof(last_dat));
last_cmd = cmd;
last_len = dat_len;
for (i = 0; i < dat_len; i++) last_dat[i] = dat[i];
return 0;
}
/* ============ 被测: 从 dbn_ble_srv.c 提取的 3 case (嵌入 switch) ============ */
static void run_case(uint8_t cmd, uint8_t *pkg, uint8_t len)
{
uint8_t _cmd = cmd;
switch (_cmd) {
#include "offlog_cases_embedded.c"
default: break;
}
}
/* ============ 断言 ============ */
static int failures = 0;
#define CHECK(cond) do { \
if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \
} while (0)
static uint8_t pkg[16];
static void test_stat_ok(void)
{
mock_enabled = 1; mock_count = 1234; mock_seq_last = 1333; mock_boot = 2;
memset(tmp_ble_buf, 0, sizeof(tmp_ble_buf));
run_case(0x25, NULL, 0);
CHECK(last_cmd == 0x25);
CHECK(last_len == 19);
/* status=0 */
CHECK(last_dat[0] == 0x00);
/* boot_seq=2 LE */
CHECK(last_dat[1] == 0x02 && last_dat[2] == 0x00);
/* count=1234 LE */
CHECK(last_dat[3] == 0xD2 && last_dat[4] == 0x04 && last_dat[5] == 0x00 && last_dat[6] == 0x00);
/* capacity=16256 LE (W25Q32 512KB: 127 数据扇区 x 128) */
CHECK(last_dat[7] == 0x80 && last_dat[8] == 0x3F && last_dat[9] == 0x00 && last_dat[10] == 0x00);
/* seq_first=100 LE */
CHECK(last_dat[11] == 0x64 && last_dat[12] == 0x00 && last_dat[13] == 0x00 && last_dat[14] == 0x00);
/* seq_last=1333 LE */
CHECK(last_dat[15] == 0x35 && last_dat[16] == 0x05 && last_dat[17] == 0x00 && last_dat[18] == 0x00);
printf("PASS test_stat_ok\n");
}
static void test_stat_disabled(void)
{
mock_enabled = 0;
run_case(0x25, NULL, 0);
CHECK(last_cmd == 0x25);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x01);
printf("PASS test_stat_disabled\n");
}
static void test_query_ok(void)
{
/* 10 条: seq 11..20, 请求 start_seq=13 count=4 → 返回 seq 13,14,15,16 */
mock_enabled = 1; mock_count = 10; mock_seq_last = 20; mock_boot = 2;
memset(pkg, 0, sizeof(pkg));
pkg[4] = 13; /* start_seq LE */
pkg[5] = 0; pkg[6] = 0; pkg[7] = 0;
pkg[8] = 4; /* count */
memset(tmp_ble_buf, 0, sizeof(tmp_ble_buf));
run_case(0x26, pkg, 11);
CHECK(last_cmd == 0x26);
CHECK(last_len == 2 + 4 * 32); /* status + count + 4×32B */
CHECK(last_dat[0] == 0x00); /* status ok */
CHECK(last_dat[1] == 4); /* fetched 4 */
/* 记录0 (idx=2, seq=13): type=coil (mock: idx%3==2 → COIL), 仅查 magic+seq */
CHECK(last_dat[2] == OFFLOG_EVT_MAGIC);
CHECK(last_dat[6] == 13); /* seq LE32 偏移4 */
CHECK(last_dat[7] == 0);
/* 记录1 (idx=3, seq=14): type=boot */
CHECK(last_dat[2+32+1] == OFFLOG_EVT_BOOT);
CHECK(last_dat[2+32+4] == 14);
/* 记录2 (idx=4, seq=15): type=iot_ready, payload 空 */
CHECK(last_dat[2+64+1] == OFFLOG_EVT_IOT_READY);
CHECK(last_dat[2+64+4] == 15);
/* 记录3 (idx=5, seq=16): type=coil, payload sub=1 ch=1 value=97 */
CHECK(last_dat[2+96+1] == OFFLOG_EVT_COIL);
CHECK(last_dat[2+96+4] == 16);
CHECK(last_dat[2+96+20] == 1); /* payload[0] sub */
CHECK(last_dat[2+96+21] == 1); /* payload[1] ch */
CHECK(last_dat[2+96+25] == 97); /* payload[5] value LSB */
printf("PASS test_query_ok\n");
}
static void test_query_range_error(void)
{
mock_enabled = 1; mock_count = 10; mock_seq_last = 20; mock_boot = 2;
memset(pkg, 0, sizeof(pkg));
pkg[4] = 100; /* 越界 > seq_last */
pkg[8] = 4;
run_case(0x26, pkg, 11);
CHECK(last_cmd == 0x26);
CHECK(last_len == 2);
CHECK(last_dat[0] == 0x00);
CHECK(last_dat[1] == 0); /* 空 records */
printf("PASS test_query_range_error\n");
}
static void test_query_short_frame(void)
{
mock_enabled = 1;
pkg[8] = 4;
run_case(0x26, pkg, 9); /* len < 11 */
CHECK(last_cmd == 0x26);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x02); /* bad request */
printf("PASS test_query_short_frame\n");
}
static void test_query_disabled(void)
{
mock_enabled = 0;
pkg[8] = 4;
run_case(0x26, pkg, 11);
CHECK(last_cmd == 0x26);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x01); /* disabled */
printf("PASS test_query_disabled\n");
}
static void test_clear(void)
{
mock_enabled = 1; mock_count = 5; mock_seq_last = 30;
run_case(0x27, NULL, 0);
CHECK(last_cmd == 0x27);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x00);
/* offlog_clear mock: count 归 1, seq_last 递增 */
CHECK(mock_count == 1);
CHECK(mock_seq_last == 31);
printf("PASS test_clear\n");
}
int main(void)
{
test_stat_ok();
test_stat_disabled();
test_query_ok();
test_query_range_error();
test_query_short_frame();
test_query_disabled();
test_clear();
if (failures == 0) printf("\nALL PASS\n");
else printf("\n%d FAILURES\n", failures);
return failures ? 1 : 0;
}
+262
View File
@@ -0,0 +1,262 @@
/*
* test_lup_ota_parser.c lup_feed_byte_ota 0x9F
*
* DLD960LoopBootloader (back_9F_pre_ok/addr_ok/data):
* pre_ok : 9F 01 00 02 A5 00 A8 (CHECK = 01+00+02+A5+00 = A8)
* addr_ok: 9F 01 00 02 A6 00 A9
* addr_err:9F 01 00 02 A6 01 AA
* data : 9F <sub_l> <sub_h> 02 A7 <status> <sum>
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include -o test_lup_ota_parser test_lup_ota_parser.c
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ---- mock: 从 loop_uart_proto.h 复制的最小依赖 ---- */
#define LUP_MAGIC 0x7F
#define LUP_MAGIC_OTA 0x9F
#define LUP_MAX_VALUE_LEN 64
#define LUP_MAX_PKG_LEN (1 + 3 + LUP_MAX_VALUE_LEN + 2)
typedef enum {
LUP_FRAME_STATE_IDLE = 0,
LUP_FRAME_STATE_HEADER,
LUP_FRAME_STATE_VALUE,
LUP_FRAME_STATE_CHECK,
LUP_FRAME_STATE_COMPLETE,
LUP_FRAME_STATE_OTA_LEN,
LUP_FRAME_STATE_OTA_CMD,
LUP_FRAME_STATE_OTA_CHECK
} LUP_FrameState;
typedef struct {
LUP_FrameState state;
uint8_t buf[LUP_MAX_PKG_LEN];
uint16_t idx;
uint16_t value_len;
uint16_t value_idx;
} LUP_FrameParser;
static LUP_FrameParser g_lup_parser;
static void lup_frame_reset(void)
{
g_lup_parser.state = LUP_FRAME_STATE_IDLE;
g_lup_parser.idx = 0;
g_lup_parser.value_len = 0;
g_lup_parser.value_idx = 0;
memset(g_lup_parser.buf, 0, sizeof(g_lup_parser.buf));
}
static const uint8_t *lup_frame_data(void) { return g_lup_parser.buf; }
static uint16_t lup_frame_len(void) { return g_lup_parser.idx; }
/* ---- 嵌入被测函数体 (从 loop_uart_proto.c 提取) ---- */
int lup_feed_byte_ota(uint8_t byte)
{
LUP_FrameParser *p = &g_lup_parser;
switch (p->state) {
case LUP_FRAME_STATE_IDLE:
if (byte == LUP_MAGIC_OTA) {
p->buf[0] = byte;
p->idx = 1;
p->state = LUP_FRAME_STATE_HEADER;
}
break;
case LUP_FRAME_STATE_HEADER:
p->buf[p->idx++] = byte;
if (p->idx == 3) {
p->state = LUP_FRAME_STATE_OTA_LEN;
}
break;
case LUP_FRAME_STATE_OTA_LEN:
p->buf[p->idx++] = byte;
{
uint8_t len_field = p->buf[3];
uint16_t data_bytes;
if (len_field < 1) {
data_bytes = 0;
} else {
data_bytes = len_field - 1;
}
if (data_bytes > LUP_MAX_VALUE_LEN) {
lup_frame_reset();
break;
}
p->value_len = data_bytes;
p->value_idx = 0;
}
p->state = LUP_FRAME_STATE_OTA_CMD;
break;
case LUP_FRAME_STATE_OTA_CMD:
p->buf[p->idx++] = byte;
if (p->value_len > 0) {
p->state = LUP_FRAME_STATE_VALUE;
} else {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_VALUE:
p->buf[p->idx++] = byte;
p->value_idx++;
if (p->value_idx >= p->value_len) {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_OTA_CHECK:
p->buf[p->idx++] = byte;
{
uint8_t sum = 0;
uint16_t i;
for (i = 1; i < (uint16_t)(p->idx - 1); i++) {
sum += p->buf[i];
}
if (sum == p->buf[p->idx - 1]) {
p->state = LUP_FRAME_STATE_COMPLETE;
return 1;
}
lup_frame_reset();
}
break;
case LUP_FRAME_STATE_COMPLETE:
lup_frame_reset();
break;
default:
lup_frame_reset();
break;
}
return 0;
}
/* ---- 测试辅助 ---- */
static int fails = 0;
/* 喂完整帧, 断言: 帧完成 且 len 正确 */
static void feed_frame(const uint8_t *frame, int len, const char *name)
{
int i, done = 0;
lup_frame_reset();
for (i = 0; i < len; i++) {
if (lup_feed_byte_ota(frame[i])) {
done = 1;
break;
}
}
if (!done) {
printf("FAIL %s: frame not completed\n", name);
fails++;
return;
}
if ((int)lup_frame_len() != len) {
printf("FAIL %s: len=%d expected %d\n", name, (int)lup_frame_len(), len);
fails++;
return;
}
if (memcmp(lup_frame_data(), frame, len) != 0) {
printf("FAIL %s: content mismatch\n", name);
fails++;
return;
}
printf("PASS %s\n", name);
}
/* 喂错误校验帧, 断言: 解析失败 (reset 回 IDLE) */
static void feed_bad(const uint8_t *frame, int len, const char *name)
{
int i, done = 0;
lup_frame_reset();
for (i = 0; i < len; i++) {
if (lup_feed_byte_ota(frame[i])) {
done = 1;
break;
}
}
if (done) {
printf("FAIL %s: bad checksum accepted\n", name);
fails++;
return;
}
if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
printf("FAIL %s: parser not reset to IDLE (state=%d)\n", name, g_lup_parser.state);
fails++;
return;
}
printf("PASS %s\n", name);
}
int main(void)
{
uint8_t f1[] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8}; /* pre_ok */
uint8_t f2[] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9}; /* addr_ok */
uint8_t f3[] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x01, 0xAA}; /* addr_err */
uint8_t f4[] = {0x9F, 0x05, 0x00, 0x02, 0xA7, 0x00, 0xAE}; /* data ACK sub=5 ok */
uint8_t f5[] = {0x9F, 0x34, 0x12, 0x02, 0xA7, 0x01, 0xF0}; /* data ACK sub=0x1234 err */
uint8_t bad[] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA9}; /* CHECK 错 */
uint8_t v7f[] = {0x7F, 0x00, 0x04, 0x4A, 0x00, 0x4E, 0x4E}; /* 0x7F 帧不应被解析 */
feed_frame(f1, sizeof(f1), "pre_ok (9F 01 00 02 A5 00 A8)");
feed_frame(f2, sizeof(f2), "addr_ok (9F 01 00 02 A6 00 A9)");
feed_frame(f3, sizeof(f3), "addr_err(9F 01 00 02 A6 01 AA)");
feed_frame(f4, sizeof(f4), "data ack sub=5 (9F 05 00 02 A7 00 AE)");
feed_frame(f5, sizeof(f5), "data ack sub=0x1234 err (9F 34 12 02 A7 01 F0)");
feed_bad(bad, sizeof(bad), "bad checksum rejected");
/* 0x7F 帧: IDLE 不认 → 应无帧完成, 状态回 IDLE */
{
int i, done = 0;
lup_frame_reset();
for (i = 0; i < (int)sizeof(v7f); i++) {
if (lup_feed_byte_ota(v7f[i])) { done = 1; break; }
}
if (done) {
printf("FAIL 0x7F frame parsed by ota parser\n");
fails++;
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
printf("FAIL 0x7F frame left parser non-IDLE (state=%d)\n", g_lup_parser.state);
fails++;
} else {
printf("PASS 0x7F frame ignored by ota parser\n");
}
}
/* 粘帧: 两帧连续 → 各自解析成功 */
{
uint8_t glue[14];
int i, done_cnt = 0;
memcpy(glue, f1, 7);
memcpy(glue + 7, f2, 7);
lup_frame_reset();
for (i = 0; i < 14; i++) {
if (lup_feed_byte_ota(glue[i])) {
done_cnt++;
if (done_cnt == 1 && memcmp(lup_frame_data(), f1, 7) != 0) {
printf("FAIL glue frame1 mismatch\n"); fails++;
}
if (done_cnt == 2 && memcmp(lup_frame_data(), f2, 7) != 0) {
printf("FAIL glue frame2 mismatch\n"); fails++;
}
lup_frame_reset();
}
}
if (done_cnt != 2) {
printf("FAIL glue: %d frames completed, expected 2\n", done_cnt);
fails++;
} else {
printf("PASS glued frames parsed independently\n");
}
}
printf("\n%s (%d failures)\n", fails ? "FAILED" : "ALL PASS", fails);
return fails ? 1 : 0;
}
+312
View File
@@ -0,0 +1,312 @@
/**
* test_offlog.c offlog gcc
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \
* -o test_offlog test_offlog.c
* : ./test_offlog
*
* Mock: W25Q32 NOR (=AND, =0xFF), mstick/dev_time_now
* : / / / /
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ============ mock W25Q32 (4MB, NOR 语义) ============ */
static uint8_t flash[4 * 1024 * 1024];
void SPI_Flash_Erase_Sector(uint32_t sec)
{
memset(flash + sec * 4096, 0xFF, 4096);
}
void SPI_Flash_Read(uint8_t *buf, uint32_t addr, uint16_t size)
{
memcpy(buf, flash + addr, size);
}
void SPI_Flash_Write_NoCheck(uint8_t *buf, uint32_t addr, uint16_t size)
{
uint16_t i;
for (i = 0; i < size; i++) flash[addr + i] &= buf[i]; /* NOR: 只能 1→0 */
}
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* mock W25Q32 */
void SPI_Flash_Write(uint8_t *buf, uint32_t addr, uint16_t size)
{
/* 模拟 storage.c 行为: 目标区域非全 FF 则擦整扇区再写 */
uint16_t i;
int need_erase = 0;
for (i = 0; i < size; i++) {
if (flash[addr + i] != 0xFF) { need_erase = 1; break; }
}
if (need_erase) SPI_Flash_Erase_Sector(addr / 4096);
SPI_Flash_Write_NoCheck(buf, addr, size);
}
/* ============ mock 时间 (已同步: >16亿) ============ */
static uint32_t mock_ms = 0;
uint32_t mstick(void) { return mock_ms; }
uint32_t dev_time_now(void) { return 1784768575UL; }
/* ch32v20x 库类型 (单测环境无 SDK) */
typedef unsigned char u8;
/* ============ 被测模块 (直接包含实现) ============ */
#include "../BLE/OnlyUpdateApp_Peripheral/APP/offlog.c"
/* ============ 断言 ============ */
static int failures = 0;
#define CHECK(cond) do { \
if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \
} while (0)
static void reset_flash(void)
{
memset(flash, 0xFF, sizeof(flash));
mock_ms = 0;
}
/* 测试1: 全新初始化 → BOOT 事件可读回 */
static void test_fresh_init(void)
{
OfflogEvt e;
uint8_t payload[4] = {0x18, 0, 0, 0}; /* 假复位原因 (大端序写入 payload) */
reset_flash();
offlog_init();
CHECK(offlog_boot_seq() == 1);
CHECK(offlog_count() == 0); /* init 不写事件 */
offlog_boot(0x18000000); /* bit27+bit24 之类 */
CHECK(offlog_count() == 1);
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.magic == OFFLOG_EVT_MAGIC);
CHECK(e.type == OFFLOG_EVT_BOOT);
CHECK(e.seq == 1);
CHECK(e.boot_seq == 1);
CHECK(e.flags & OFFLOG_UNIX_VALID_FLAG);
CHECK(e.len == 4);
CHECK(memcmp(e.payload, payload, 4) == 0);
CHECK(offlog_read_idx(1, &e) == -1); /* 越界 */
printf("PASS test_fresh_init\n");
}
/* 测试2: 环形回绕 — 写满 8064+5 条, count 封顶, 序号单调 */
static void test_ring_wrap(void)
{
OfflogEvt e;
uint32_t i, n = OFFLOG_MAX_RECORDS + 5;
reset_flash();
offlog_init();
for (i = 0; i < n; i++) {
offlog_evt(OFFLOG_EVT_IOT_RECONN, NULL, 0);
}
CHECK(offlog_count() == OFFLOG_MAX_RECORDS - OFFLOG_EVT_PER_SECTOR + 5); /* 8064-128+5 */
/* 最新一条是最后写入的, seq 应等于 n (从 1 开始) */
CHECK(offlog_read_idx(offlog_count() - 1, &e) == 0);
CHECK(e.seq == n);
/* 逻辑首: 回绕时整扇区(128条)被擦 + 写回5条 → 最旧 = 129 */
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.seq == n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4);
printf("PASS test_ring_wrap (seq %lu..%lu, count %u)\n",
(unsigned long)(n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4),
(unsigned long)n, offlog_count());
}
/* 测试3: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */
static void test_power_loss_recovery(void)
{
OfflogEvt e;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 10; i++) {
offlog_evt(OFFLOG_EVT_COIL, NULL, 0);
}
CHECK(offlog_count() == 10);
/* 模拟掉电重启 (flash 保留, RAM 全失) */
offlog_init();
CHECK(offlog_boot_seq() == 2);
CHECK(offlog_count() == 10); /* 上电恢复出之前 10 条 */
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
CHECK(offlog_count() == 11);
CHECK(offlog_read_idx(10, &e) == 0);
CHECK(e.type == OFFLOG_EVT_IOT_CONNECT);
CHECK(e.seq == 11); /* seq 跨 boot 连续 */
CHECK(e.boot_seq == 2);
printf("PASS test_power_loss_recovery\n");
}
/* 测试4: 扇区切换 — 写满 128 条后头扇区更新, 数据完整 */
static void test_sector_switch(void)
{
OfflogEvt e;
OfflogHead h;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 128; i++) {
offlog_evt(OFFLOG_EVT_BOOT, NULL, 0);
}
/* 第 129 条触发扇区切换 */
offlog_evt(OFFLOG_EVT_TIME_ANCHOR, NULL, 0);
CHECK(offlog_count() == 129);
SPI_Flash_Read((uint8_t *)&h, OFFLOG_AREA_BASE, sizeof(h));
CHECK(h.magic[0] == OFFLOG_HEAD_MAGIC0);
CHECK(h.wr_sector == 2);
/* 头在扇区切换时刷新: wr_off = 新扇区起点 (尚未写入第 129 条) */
CHECK(h.wr_off == OFFLOG_DATA_BASE + 4096);
CHECK(offlog_read_idx(127, &e) == 0);
CHECK(e.type == OFFLOG_EVT_BOOT);
CHECK(e.seq == 128);
CHECK(offlog_read_idx(128, &e) == 0);
CHECK(e.type == OFFLOG_EVT_TIME_ANCHOR);
CHECK(e.seq == 129);
printf("PASS test_sector_switch\n");
}
/* 测试5: 清空 — 擦数据区 + LOG_CLEAR 审计 */
static void test_clear(void)
{
OfflogEvt e;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 5; i++) {
offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0);
}
offlog_clear();
CHECK(offlog_count() == 1); /* 只剩审计 */
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.type == OFFLOG_EVT_LOG_CLEAR);
CHECK(e.seq == 6); /* seq 不重置 */
printf("PASS test_clear\n");
}
/* 测试6: 掉电恢复跨扇区 — 写到扇区 2 中间掉电 */
static void test_power_loss_mid_sector(void)
{
OfflogEvt e;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 130; i++) { /* 已跨入扇区 2 */
offlog_evt(OFFLOG_EVT_COIL, NULL, 0);
}
CHECK(offlog_count() == 130);
offlog_init(); /* 掉电重启 */
CHECK(offlog_boot_seq() == 2);
CHECK(offlog_count() == 130);
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
CHECK(offlog_count() == 131);
CHECK(offlog_read_idx(130, &e) == 0);
CHECK(e.seq == 131);
printf("PASS test_power_loss_mid_sector\n");
}
/* 测试7: TIME_ANCHOR 严格使用平台下发 unix_ts (不走 dev_time_now) */
static void test_time_anchor_exact(void)
{
OfflogEvt e;
reset_flash();
offlog_init();
offlog_time_anchor(1800000000UL); /* 平台下发值, 与 mock dev_time_now() 不同 */
CHECK(offlog_count() == 1);
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.type == OFFLOG_EVT_TIME_ANCHOR);
CHECK(e.unix_ts == 1800000000UL); /* 严格等于传参 */
CHECK(e.flags & OFFLOG_UNIX_VALID_FLAG);
printf("PASS test_time_anchor_exact\n");
}
/* 测试8: 协议导出 — enabled/seq_last/type_str/evt_to_json (log_stat/log_query 数据源) */
static void test_export_json(void)
{
OfflogEvt e;
char buf[256];
reset_flash();
offlog_init();
CHECK(offlog_enabled() == 1);
CHECK(offlog_seq_last() == 0); /* 尚无记录 */
offlog_boot(0x80000000UL); /* IWDG 复位 → rst=2147483648 */
CHECK(offlog_count() == 1);
CHECK(offlog_seq_last() == 1);
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(strcmp(offlog_type_str(e.type), "boot") == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"type\":\"boot\"") != NULL);
CHECK(strstr(buf, "\"data\":{\"rst\":2147483648}") != NULL);
/* coil: payload = sub(1=进) ch(2) value(97, 大端) */
{
uint8_t p[6] = {1, 2, 0, 0, 0, 97};
offlog_evt(OFFLOG_EVT_COIL, p, 6);
}
CHECK(offlog_read_idx(1, &e) == 0);
CHECK(strcmp(offlog_type_str(e.type), "coil") == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"type\":\"coil\"") != NULL);
CHECK(strstr(buf, "\"sub\":\"car_enter\"") != NULL);
CHECK(strstr(buf, "\"ch\":2") != NULL);
CHECK(strstr(buf, "\"value\":97") != NULL);
/* evt_retry: msg_id=5 retry=2 */
{
uint8_t p[5] = {0, 0, 0, 5, 2};
offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5);
}
CHECK(offlog_read_idx(2, &e) == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"msg_id\":5") != NULL);
CHECK(strstr(buf, "\"retry\":2") != NULL);
/* 无 payload 事件 → data:null */
offlog_iot_ready();
CHECK(offlog_read_idx(3, &e) == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"type\":\"iot_ready\"") != NULL);
CHECK(strstr(buf, "\"data\":null") != NULL);
/* seq_first 公式: seq_last - count + 1 (4 条: seq 1..4) */
CHECK(offlog_seq_last() == 4);
CHECK(offlog_count() == 4);
printf("PASS test_export_json (seq_first=%lu seq_last=%lu)\n",
(unsigned long)(offlog_seq_last() - offlog_count() + 1),
(unsigned long)offlog_seq_last());
}
int main(void)
{
test_fresh_init();
test_ring_wrap();
test_power_loss_recovery();
test_sector_switch();
test_clear();
test_power_loss_mid_sector();
test_time_anchor_exact();
test_export_json();
if (failures) {
printf("\n%d FAILURE(S)\n", failures);
return 1;
}
printf("\nALL PASS\n");
return 0;
}
+76
View File
@@ -0,0 +1,76 @@
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* mock SnapRec (与 snapshot.h 一致, 64B) */
typedef struct {
uint8_t magic;
uint8_t len;
uint8_t flags;
uint8_t rsvd;
uint32_t seq;
uint32_t ts_ms;
uint16_t boot_seq;
uint16_t rsvd2;
uint8_t coils[48];
} SnapRec;
#define SNAP_COILS_MAX 4
#define SNAP_COIL_BYTES 12
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)r;
for (i = 0; i < (int)sizeof(SnapRec); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
#define CHECK(cond) do { if (!(cond)) { printf("FAIL %d: %s\n", __LINE__, #cond); fails++; } else { passes++; } } while (0)
static int passes = 0, fails = 0;
int main(void)
{
char buf[256];
SnapRec r;
/* 用例1: 与 test_snap_to_json case2 相同数据, 验证 hex 与 JSON 语义一致
ch1: cfg=0xE8(11 1 0 0000: ///0) cond=0xFF(cond15///relay)
freq=0x00C383=50051, variation=-10, misc=128 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 48; r.seq = 999; r.ts_ms = 1234; r.boot_seq = 3;
r.coils[0] = 0xE8; r.coils[1] = 0xFF;
r.coils[2] = 0x83; r.coils[3] = 0xC3; r.coils[4] = 0x00; /* 50051 */
r.coils[5] = 0xF6; r.coils[6] = 0xFF; r.coils[7] = 0xFF; /* -10 */
r.coils[8] = 0x80; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00; /* 128 */
int n = snap_rec_to_hex(&r, buf, sizeof(buf));
printf("hex(%d): %s\n", n, buf);
CHECK(n == 128);
CHECK(strlen(buf) == 128);
/* 头部: magic=0xA6 len=0x30(48) flags=0 rsvd=0 seq=0xE7030000(999 LE) */
CHECK(strncmp(buf, "a6300000e7030000d204", 20) == 0);
/* ch1 12B: e8 ff 83 c3 00 f6 ff ff 80 00 00 00 */
CHECK(strncmp(buf + 32, "e8ff83c300f6ffff80000000", 24) == 0);
/* 全部小写 */
CHECK(strspn(buf, "0123456789abcdef") == 128);
/* 用例2: 空记录 len=0 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6;
n = snap_rec_to_hex(&r, buf, sizeof(buf));
CHECK(n == 128);
CHECK(strncmp(buf, "a60000000000000000000000", 24) == 0);
/* 用例3: 缓冲不足截断安全 (buf_len=10 不越界) */
memset(&r, 0, sizeof(r));
n = snap_rec_to_hex(&r, buf, 10);
CHECK(n == 8);
CHECK(strlen(buf) == 8);
printf("PASS=%d FAIL=%d\n", passes, fails);
return fails ? 1 : 0;
}
+164
View File
@@ -0,0 +1,164 @@
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* mock SnapRec (与 snapshot.h 一致, 64B) */
typedef struct {
uint8_t magic;
uint8_t len;
uint8_t flags;
uint8_t rsvd;
uint32_t seq;
uint32_t ts_ms;
uint16_t boot_seq;
uint16_t rsvd2;
uint8_t coils[48];
} SnapRec;
#define SNAP_COILS_MAX 4
#define SNAP_COIL_BYTES 12
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len)
{
int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES))
? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES);
if (coil_count < 0) coil_count = 0;
int pos = snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[",
(unsigned long)r->seq, (unsigned)r->boot_seq,
(unsigned long)r->ts_ms, (unsigned)coil_count);
int c;
for (c = 0; c < coil_count; c++) {
const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES];
uint8_t cfg = p[0];
uint8_t cond = p[1];
uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16);
int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16));
uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8)
| ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24);
uint8_t freq_level = (cfg >> 6) & 0x03;
uint8_t direction = (cfg >> 5) & 0x01;
uint8_t freq_type = (cfg >> 4) & 0x01;
uint8_t sensitivity = cfg & 0x0F;
uint8_t condition = (cond >> 4) & 0x0F;
uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */
uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */
uint8_t misc_type = cond & 0x03;
const char *fl = "high";
const char *mt = "time";
if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */
if (freq_level == 1) fl = "mid_high";
else if (freq_level == 2) fl = "mid_low";
else if (freq_level == 3) fl = "low";
if (misc_type == 1) mt = "cut_count";
else if (misc_type == 2) mt = "flow_count";
else if (misc_type == 3) mt = "relay_count";
if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ",");
pos += snprintf(buf + pos, buf_len - pos,
"{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u,"
"\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s,"
"\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}",
(unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type,
(unsigned)sensitivity, (unsigned)condition,
loop_state ? "false" : "true", car_state ? "true" : "false",
mt, (unsigned long)freq, (long)variation, (unsigned long)misc);
}
pos += snprintf(buf + pos, buf_len - pos, "]}");
return pos;
}
#define CHECK(cond) do { if (!(cond)) { printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); fails++; } else { passes++; } } while (0)
static int passes = 0, fails = 0;
int main(void)
{
char buf[2048];
SnapRec r;
/* 用例1: 单线圈, 高频(00)/触发/实时频率/灵敏度2, 正常无车, 时间量
freq=0x010F2A=69418, variation=+7, misc=0 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 12; r.seq = 12345; r.ts_ms = 456789; r.boot_seq = 2;
r.coils[0] = 0x12; /* freq_level=00 direction=0 freq_type=1 sens=0010 */
r.coils[1] = 0x00; /* condition=0 loop_state=0 car_state=0 misc_type=00 */
r.coils[2] = 0x2A; r.coils[3] = 0x0F; r.coils[4] = 0x01; /* 69418 */
r.coils[5] = 0x07; r.coils[6] = 0x00; r.coils[7] = 0x00; /* +7 */
r.coils[8] = 0x00; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00;
snap_rec_to_json(&r, buf, sizeof(buf));
printf("case1: %s\n", buf);
CHECK(strstr(buf, "\"seq\":12345") != NULL);
CHECK(strstr(buf, "\"boot_seq\":2") != NULL);
CHECK(strstr(buf, "\"ts_ms\":456789") != NULL);
CHECK(strstr(buf, "\"coil_count\":1") != NULL);
CHECK(strstr(buf, "\"ch\":1") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"high\"") != NULL);
CHECK(strstr(buf, "\"direction\":0") != NULL);
CHECK(strstr(buf, "\"freq_type\":1") != NULL);
CHECK(strstr(buf, "\"sensitivity\":2") != NULL);
CHECK(strstr(buf, "\"condition\":0") != NULL);
CHECK(strstr(buf, "\"loop_ok\":true") != NULL);
CHECK(strstr(buf, "\"has_car\":false") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"time\"") != NULL);
CHECK(strstr(buf, "\"freq\":69418") != NULL);
CHECK(strstr(buf, "\"variation\":7") != NULL);
CHECK(strstr(buf, "\"misc\":0") != NULL);
/* 用例2: 4线圈全量 + 负 variation + 低频 + relay_count + 有车/断开
ch1: (11)/(1)/(0)/0, condition=0xF, loop_state=1(), car_state=1(), misc_type=11(relay)
freq=0x00C383=50051, variation=-10 (F6 FF FF), misc=128 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 48; r.seq = 999; r.ts_ms = 1234; r.boot_seq = 3;
r.coils[0] = 0xE8; /* 11 1 0 0000 */
r.coils[1] = 0xFF; /* 1111 1 1 11 */
r.coils[2] = 0x83; r.coils[3] = 0xC3; r.coils[4] = 0x00; /* 50051 */
r.coils[5] = 0xF6; r.coils[6] = 0xFF; r.coils[7] = 0xFF; /* -10 */
r.coils[8] = 0x80; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00; /* 128 */
/* ch2: 中高(01), 正常无车, cut_count */
r.coils[12] = 0x50; /* 01 0 1 0000 */
r.coils[13] = 0x01; /* 0000 0 0 01 */
r.coils[14] = 0x00; r.coils[15] = 0x02; r.coils[16] = 0x00; /* 512 */
r.coils[17] = 0x01; r.coils[18] = 0x00; r.coils[19] = 0x00; /* +1 */
/* ch3: 中低(10), flow_count */
r.coils[24] = 0xA0; /* 10 1 0 0000 */
r.coils[25] = 0x02; /* 0000 0 0 10 */
r.coils[26] = 0x00; r.coils[27] = 0x01; r.coils[28] = 0x00; /* 256 */
/* ch4: 高频, 空 */
snap_rec_to_json(&r, buf, sizeof(buf));
printf("case2: %s\n", buf);
CHECK(strstr(buf, "\"coil_count\":4") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"low\"") != NULL);
CHECK(strstr(buf, "\"direction\":1") != NULL);
CHECK(strstr(buf, "\"freq_type\":0") != NULL);
CHECK(strstr(buf, "\"sensitivity\":0") != NULL);
CHECK(strstr(buf, "\"condition\":15") != NULL);
CHECK(strstr(buf, "\"loop_ok\":false") != NULL);
CHECK(strstr(buf, "\"has_car\":true") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"relay_count\"") != NULL);
CHECK(strstr(buf, "\"freq\":50051") != NULL);
CHECK(strstr(buf, "\"variation\":-10") != NULL);
CHECK(strstr(buf, "\"misc\":128") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"mid_high\"") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"cut_count\"") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"mid_low\"") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"flow_count\"") != NULL);
/* 用例3: len=0 边界 (空记录) */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 0;
snap_rec_to_json(&r, buf, sizeof(buf));
printf("case3: %s\n", buf);
CHECK(strstr(buf, "\"coil_count\":0") != NULL);
CHECK(strstr(buf, "\"channels\":[]") != NULL);
printf("\nPASS=%d FAIL=%d\n", passes, fails);
return fails ? 1 : 0;
}
+389
View File
@@ -0,0 +1,389 @@
/**
* test_snapshot.c gcc
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \
* -o test_snapshot test_snapshot.c
* : ./test_snapshot
*
* Mock: W25Q32 NOR (=AND, =0xFF), mstick ,
* g_offlog_part ( 512KB), offlog_evt ( mock)
* : include offlog.c .c static ( _wr_off)
* (: snap_clear )
* : / (enqueue ) / flush /
* / / / / /
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include "../BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h"
/* ============ mock W25Q32 (4MB, NOR 语义) ============ */
static uint8_t flash[4 * 1024 * 1024];
void SPI_Flash_Erase_Sector(uint32_t sec)
{
memset(flash + sec * 4096, 0xFF, 4096);
}
void SPI_Flash_Read(uint8_t *buf, uint32_t addr, uint16_t size)
{
memcpy(buf, flash + addr, size);
}
void SPI_Flash_Write_NoCheck(uint8_t *buf, uint32_t addr, uint16_t size)
{
uint16_t i;
for (i = 0; i < size; i++) flash[addr + i] &= buf[i]; /* NOR: 只能 1→0 */
}
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* mock W25Q32 */
void SPI_Flash_Write(uint8_t *buf, uint32_t addr, uint16_t size)
{
/* 模拟 storage.c 行为: 目标区域非全 FF 则擦整扇区再写 */
uint16_t i;
int need_erase = 0;
for (i = 0; i < size; i++) {
if (flash[addr + i] != 0xFF) { need_erase = 1; break; }
}
if (need_erase) SPI_Flash_Erase_Sector(addr / 4096);
SPI_Flash_Write_NoCheck(buf, addr, size);
}
/* ============ mock 时间 ============ */
static uint32_t mock_ms = 0;
uint32_t mstick(void) { return mock_ms; }
/* ============ mock offlog 依赖 (不 include offlog.c, 避免 static 冲突) ============ */
OfflogPart g_offlog_part = {
OFFLOG_CHIP_W25Q32, /* W25Q32 */
0x80000UL, /* 事件区 512KB */
127,
16256,
};
static uint8_t g_audit_type;
static uint8_t g_audit_payload[4];
static uint8_t g_audit_len;
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len)
{
g_audit_type = type;
g_audit_len = len;
if (payload && len > 0 && len <= 4) memcpy(g_audit_payload, payload, len);
}
/* ch32v20x 库类型 (单测环境无 SDK) */
typedef unsigned char u8;
/* ============ 被测模块 (直接包含实现) ============ */
#include "../BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c"
/* ============ 断言 ============ */
static int failures = 0;
#define CHECK(cond) do { \
if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \
} while (0)
static void reset_flash(void)
{
memset(flash, 0xFF, sizeof(flash));
mock_ms = 0;
}
/* 构造一帧传感报告: 4 线圈, 各线圈填可辨识值 */
static void make_report(LUP_SensorReport *sr, uint32_t base_freq, int32_t base_var)
{
uint8_t i;
memset(sr, 0, sizeof(*sr));
sr->sens_type = 0x0C;
sr->coil_count = 4;
for (i = 0; i < 4; i++) {
sr->coils[i].freq_level = 2; /* 中低 66nF */
sr->coils[i].direction = 1;
sr->coils[i].freq_type = 0;
sr->coils[i].sensitivity = 9;
sr->coils[i].condition = 5;
sr->coils[i].loop_state = 1;
sr->coils[i].car_state = (i & 1); /* 0/1 交替 */
sr->coils[i].misc_type = 0;
sr->coils[i].freq = base_freq + i * 1000;
sr->coils[i].variation = base_var + i * 100;
sr->coils[i].misc.passtime_ms = 0x10000000 + i;
}
}
/* 测试1: 全新初始化 → enqueue 不落盘(中断安全), flush 落盘可读回 */
static void test_fresh_init_and_flush(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
snap_init();
CHECK(snap_enabled() == 1);
CHECK(snap_count() == 0);
make_report(&sr, 0x123456, 1000);
mock_ms = 12345;
/* 中断路径: 只 enqueue, 不 flush → flash 不得有任何快照数据 */
snap_enqueue(&sr);
CHECK(snap_count() == 0); /* 未落盘 */
for (i = 0; i < 64; i++) {
CHECK(flash[SNAP_DATA_BASE + i] == 0xFF); /* 数据区仍全 FF */
}
/* 主循环: flush → 落盘 1 条 */
snap_flush();
CHECK(snap_count() == 1);
CHECK(snap_read_idx(0, &r) == 0);
CHECK(r.magic == SNAP_MAGIC);
CHECK(r.len == 48); /* 4 线圈 × 12B */
CHECK(r.seq == 1);
CHECK(r.boot_seq == 1);
CHECK(r.ts_ms == 12345);
CHECK(snap_seq_last() == 1);
printf("PASS test_fresh_init_and_flush\n");
}
/* 测试2: 打包格式与 0xC0 线上线圈单元 12B 一致 */
static void test_pack_format(void)
{
LUP_SensorReport sr;
SnapRec r;
reset_flash();
snap_init();
make_report(&sr, 0x123456, -12345); /* 负变化量 */
/* 覆盖值: freq_level=2 direction=1 freq_type=0 sens=9 → o[0]=0xA9
condition=5 loop=1 car=0 misc=3 o[1]=0x5B
freq=0x123456 56 34 12
variation=-12345 0xFFFFCFC7 24bit C7 CF FF
misc=0x10000000 00 00 00 10 */
sr.coils[0].freq = 0x123456;
sr.coils[0].variation = -12345;
sr.coils[0].misc_type = 3;
sr.coils[0].misc.relay_count = 0x10000000;
snap_enqueue(&sr);
snap_flush();
snap_read_idx(0, &r);
CHECK(r.coils[0] == 0xA9);
CHECK(r.coils[1] == 0x5B);
CHECK(r.coils[2] == 0x56 && r.coils[3] == 0x34 && r.coils[4] == 0x12);
CHECK(r.coils[5] == 0xC7 && r.coils[6] == 0xCF && r.coils[7] == 0xFF);
CHECK(r.coils[8] == 0x00 && r.coils[9] == 0x00 && r.coils[10] == 0x00 && r.coils[11] == 0x10);
/* 线圈 2: car_state=1 → o[1] bit2=1 → 0x5F (其他字段相同) */
CHECK((r.coils[12 + 1] & 0x04) == 0x04);
CHECK((r.coils[0 + 1] & 0x04) == 0x00);
printf("PASS test_pack_format\n");
}
/* 测试3: 环形回绕 — 写满 max+5 条, count 封顶, 序号单调 */
static void test_ring_wrap(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i, n = SNAP_MAX_RECORDS + 5;
reset_flash();
snap_init();
make_report(&sr, 0x100, 100);
for (i = 0; i < n; i++) {
snap_enqueue(&sr);
snap_flush();
}
CHECK(snap_count() == SNAP_MAX_RECORDS - SNAP_REC_PER_SECTOR + 5);
CHECK(snap_read_idx(snap_count() - 1, &r) == 0);
CHECK(r.seq == n);
CHECK(snap_seq_last() == n);
CHECK(snap_read_idx(snap_count(), &r) == -1); /* 越界 */
printf("PASS test_ring_wrap (count %lu, max %lu)\n",
(unsigned long)snap_count(), (unsigned long)SNAP_MAX_RECORDS);
}
/* 测试4: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */
static void test_power_loss_recovery(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
snap_init();
make_report(&sr, 0x200, 200);
for (i = 0; i < 10; i++) {
snap_enqueue(&sr);
snap_flush();
}
CHECK(snap_count() == 10);
snap_init(); /* 模拟掉电重启 */
CHECK(snap_boot_seq() == 2);
CHECK(snap_count() == 10);
snap_enqueue(&sr);
snap_flush();
CHECK(snap_count() == 11);
CHECK(snap_read_idx(10, &r) == 0);
CHECK(r.seq == 11);
CHECK(r.boot_seq == 2);
printf("PASS test_power_loss_recovery\n");
}
/* 测试5: 扇区切换 — 写满 64 条后头扇区更新, 数据完整 */
static void test_sector_switch(void)
{
LUP_SensorReport sr;
SnapRec r;
SnapHead h;
uint32_t i;
reset_flash();
snap_init();
make_report(&sr, 0x300, 300);
for (i = 0; i < 64; i++) {
snap_enqueue(&sr);
snap_flush();
}
snap_enqueue(&sr);
snap_flush(); /* 第 65 条触发扇区切换 */
CHECK(snap_count() == 65);
SPI_Flash_Read((uint8_t *)&h, g_snap_part.area_base, sizeof(h));
CHECK(h.magic[0] == SNAP_HEAD_MAGIC0);
CHECK(h.wr_sector == 2);
CHECK(h.wr_off == SNAP_DATA_BASE + 4096);
CHECK(snap_read_idx(63, &r) == 0);
CHECK(r.seq == 64);
CHECK(snap_read_idx(64, &r) == 0);
CHECK(r.seq == 65);
printf("PASS test_sector_switch\n");
}
/* 测试6: RAM 暂存满丢新 — 8 深, 第 9 条丢, flush 只落 8 条 */
static void test_ram_overflow_drop(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
snap_init();
make_report(&sr, 0x400, 400);
for (i = 0; i < SNAP_RAM_DEPTH + 3; i++) { /* 11 条, 深 8 */
snap_enqueue(&sr); /* 不 flush, 模拟中断风暴 */
}
CHECK(snap_count() == 0); /* 全部还在 RAM */
snap_flush();
CHECK(snap_count() == 8); /* 只落 8 条, 丢 3 条 */
CHECK(snap_read_idx(0, &r) == 0);
CHECK(r.seq == 1); /* 丢的是最新(尾部) */
CHECK(snap_read_idx(7, &r) == 0);
CHECK(r.seq == 8);
printf("PASS test_ram_overflow_drop\n");
}
/* 测试7: 清空 — 擦数据区 + 事件流审计 (payload[0]=2=快照流) */
static void test_clear_audit(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
g_audit_type = 0; g_audit_len = 0; memset(g_audit_payload, 0, sizeof(g_audit_payload));
snap_init();
make_report(&sr, 0x500, 500);
for (i = 0; i < 5; i++) {
snap_enqueue(&sr);
snap_flush();
}
CHECK(snap_count() == 5);
snap_clear();
CHECK(snap_count() == 0);
CHECK(snap_seq_last() == 5); /* seq 不重置 */
CHECK(snap_read_idx(0, &r) == -1); /* 数据区已空 */
/* 审计: offlog_evt 被调用, type=LOG_CLEAR, payload[0]=2 */
CHECK(g_audit_type == OFFLOG_EVT_LOG_CLEAR);
CHECK(g_audit_len == 1);
CHECK(g_audit_payload[0] == 2);
printf("PASS test_clear_audit\n");
}
/* 测试8: 运行时分区 — W25Q32 快照区 0x110000, 3008KB, 48064 条 */
static void test_part_table(void)
{
reset_flash();
snap_init();
CHECK(g_snap_part.area_base == 0x110000UL);
CHECK(g_snap_part.area_size == 0x2F0000UL); /* 3008KB */
CHECK(g_snap_part.data_sectors == 751);
CHECK(g_snap_part.max_records == 751 * 64);
CHECK(SNAP_DATA_BASE == 0x111000UL); /* base + 4KB 头扇区 */
printf("PASS test_part_table\n");
}
/* 测试9: 懒擦 — init 只擦当前写扇区, 其余扇区由环形写切扇区时覆盖
(: init 751 ~34s IWDG 4s ) */
static void test_lazy_erase(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
/* 在数据扇区 2 预埋旧数据 (模拟 Flash 残留) */
flash[SNAP_DATA_BASE + 4096] = 0xAA;
flash[SNAP_DATA_BASE + 4096 + 1] = 0x55;
snap_init();
/* 懒擦: 只擦扇区 1, 扇区 2 保持旧数据 */
CHECK(flash[SNAP_DATA_BASE] == 0xFF); /* 扇区1 已擦 */
CHECK(flash[SNAP_DATA_BASE + 4096] == 0xAA); /* 扇区2 未动 */
CHECK(flash[SNAP_DATA_BASE + 4096 + 1] == 0x55);
make_report(&sr, 0x600, 600);
for (i = 0; i < 64; i++) { /* 写满扇区 1 */
snap_enqueue(&sr);
snap_flush();
}
CHECK(flash[SNAP_DATA_BASE + 4096] == 0xAA); /* 仍在扇区 1 内, 扇区2 未动 */
snap_enqueue(&sr); /* 第 65 条切扇区 */
snap_flush();
CHECK(flash[SNAP_DATA_BASE + 4096] != 0xAA); /* 扇区2 已擦并写入新数据 */
CHECK(snap_read_idx(64, &r) == 0);
CHECK(r.seq == 65);
CHECK(snap_count() == 65);
printf("PASS test_lazy_erase\n");
}
int main(void)
{
test_fresh_init_and_flush();
test_pack_format();
test_ring_wrap();
test_power_loss_recovery();
test_sector_switch();
test_ram_overflow_drop();
test_clear_audit();
test_part_table();
test_lazy_erase();
if (failures) {
printf("\n%d FAILURE(S)\n", failures);
return 1;
}
printf("\nALL PASS\n");
return 0;
}