137 Commits
Author SHA1 Message Date
wangfq 12618578d8 docs: §6.9 补 BLE 分包承载与缓冲边界 + 4 条实现前置项 (V1.15)
- 核实 BLE 链路分包机制: 收/发双向对称, 分包头编码于 pkg[1] 高/低 4 位
  (高 4 位 = 总包数, 低 4 位 = 当前包序), 每包 dat <= 94B @MTU>=103,
  4 位总包数 => 最多 15 包; 发送侧先自增再编码, 线上首包 seq==1, 与接收侧
  low==1 起点 / high==low 收尾完全配对
- 修正 229B 载荷定性: 分包使 229B 确可送达 => 「载荷 > 131B 即越界写
  g_buf_ble_response.dat / tmp_ble_buf(132B)」属可达缺陷, 而非纸面能力
- §6.9.9 实施状态新增 4 条前置改造项: 缓冲扩 >= 260B / set_response_iot_net|topic
  补长度钳制 / 接收侧累加钳制(防 uint8 回绕) / unpack_packs 用起 len 形参
- devlog 归档本轮勘察, vd960DBN 源码零改动
2026-09-10 22:59:30 +08:00
wangfq 2044efe14f docs: 新增《DLD960_IoT_MQTT协议》§6.9 4G 配置同步(V1.14),0x7D 帧方案作废
- 协议 §6.9:BLE→DBN→Air780 配置同步(权威源 / 载体 0x8F / 命令复用 / 载荷拆两条 /
  拉取为主+推送为辅 / 明确不同步项 / 异常处理 / 实施状态)
- 版本 V1.13 → V1.14,修订记录加行(插 V1.13 前);§6.6 增补充说明
- 行尾:全文纯 LF,改动零行尾污染
- devlog:新增 2026-09-10 条目(结论 + 6 条代码事实级硬约束 + 文件改动 + 待办)
2026-09-10 21:46:00 +08:00
wangfq c879ed04e2 fix(vd960DBN): 0x8F 接入 DBN 私有指令处理器 + net_srv 裸 printf + devlog 行尾
按用户 2026-09-10 四条指示:

1) 0x8F 下行的"本地处理"明确为 CH32V208GBU6 私有指令 —— 只作用在自身读写配置上,
   一般不转发。实现: 不再留空壳计数, 直接复用 BLE 侧同一个分发器
   manage_dbn_ble_default() (dbn_ble_srv.c:729)。两侧帧布局完全一致
   (pkg[1]=Addr, pkg[2]=LEN, pkg[3]=CMD, pkg[4..]=Value), 零适配。
   响应仍走 g_buf_ble_response -> BLE 通知; 不回 4G —— Air780 侧 parser 只认 0x7F,
   回 0x8F 也没人接。
   增加 OTA 门控: g_flag_counter_ota.flag != 0 时拒绝私有指令 ——
   私有指令会写自身配置(碰 flash), 与 OTA 刷写并发是自找麻烦。
   魔数宏改用 dbn_ble_srv.h 的 MAGIC_BYTE_DBN_DEFAULT, 不再硬编码 0x8F。

2) 0x7D 帧: 删除/不理会。V1.11 曾提的 DBN<->Air780 握手/配置同步构想作废,
   代码里本就没实现, 正式协议也从未收录。devlog 待办项移除, 记入"已定案"。

3) net_srv.c:125 裸 printf() -> PRINT。该文件本已用 PRINT 53 处, 无需补 include。
   DEBUG=0 时随 PRINT 一起消失, 不再出现"往未初始化的 USART1 写字节"。

4) devlog.md 行尾统一 CRLF (修复 132 个孤立 LF: 顶部 V1.13 条目原为 LF)。

验证 (host 单测, 代码从 usart_biz.c 原样抽取, gcc -Wall -Wextra -Werror 零警告):
  15 组用例全部通过, 其中新增:
  - 0x8F -> manage_dbn_ble_default 被调用 1 次, 传入 len==全帧长, 内容逐字节一致, 未转发
  - OTA 期间 0x8F -> 处理器未被调用, 但仍计数 (可观测)
  原有 13 组 (魔数分流/最大帧/坏帧回找魔数/截断帧不丢帧/LEN 非法/0x9F 忽略...) 保持通过。

待板级验证: 需 MounRiver 工具链编译 + 实机 (本机无 riscv 工具链)。
2026-09-10 16:41:09 +08:00
wangfq d4d57f6487 feat(vd960DBN): UART1 ↔ Air780 4G 通道 (协议 §6.1 纯字节流透传)
需求(2026-09-10): 打通 vd960DBN 与 vd960Air(Air780) 两侧串口对接逻辑 ——
DBN 侧 UART1(PB6/PB7) 通道 + UART2↔UART1 双向透传。

协议依据 DLD960_IoT_MQTT协议 §6.1(V1.12 方案C):
  上行 Loop 0x7F(UART2) --原样转发--> UART1 --> Air780 解析转 JSON --> MQTT
  下行 平台 JSON --> Air780 --转 0x7F 帧--> DBN --0x7F(UART2)--> Loop
  DBN 只做字节流透传, 零业务转换; 魔数分流: 0x7F -> UART2, 0x8F -> DBN 本地

实现:
- usart_biz.c +242 行: UART1 通道整段
  * uart1_dma_init(): USART1 重映射 PB6(TX)/PB7(RX), 115200 8N1,
    RX 走 DMA1_Ch5 循环 512B (UART2 占 Ch6/Ch7, 不冲突)
  * uart1_feed_byte()/uart1_frame_ready()/uart1_resync(): 独立帧装配器
  * uart1_dispatch_frame(): 按魔数分流
  * uart1_dma_poll(): 主循环消费 DMA 缓冲
  * uart_srv() 内插入上行转发(484-490): 紧接 lup_process_frame 之后、
    InitPkgUart 消费之前, 且只转发 lup_verify_checksum()==0 的完整帧
    (位置必须在 BLE 分支改写 pkg[0]=0x8F 之前, 否则会给 Air780 发错魔数帧)
- cmcng.h: 声明 uart1_dma_init/poll + 5 个通道计数
- peripheral_main.c: 主循环加 uart1_dma_poll() (置于 MK_UART_SRV fault marker
  之外, 不改变既有 marker 语义)

三个关键决定:
1. 接收走 DMA 而非 RXNE 中断 —— 理由同 UART2 方案A(2026-08-17): PRINT 临界区
   关中断 ~7.4ms / BLE 栈回调 / SPI 擦除 45ms 等长阻塞窗口会屏蔽 RXNE 丢字节。
   "不丢帧"是 Air780 沿检测(loop_state 变化沿)判车的硬前提。
2. 帧装配器独立于 g_lup_parser —— 后者硬编码专供 UART2/Loop 通道, 两条流混进
   同一状态机必然互相残杀; 只复用纯函数 lup_verify_checksum()。
3. 坏帧回找魔数 resync —— 防"单帧损坏 → 后续帧全部失步、链路长时间瞎掉"。

与 printf 共用 USART1 (按用户 2026-09-10 指示: printf 串口不动, 4G 时禁用 debug):
- 开关 UART1_AIR780_EN, 默认 (DEBUG == 0)
- 4G 版本 -DDEBUG=0 编译即自动启用: #if(DEBUG) 为假 -> PRINT 空宏, 且
  USART_Printf_Init() 里 USART1 配置分支整段不编译(已核对 USART_Init/USART_Cmd
  均在 #if(DEBUG == DEBUG_UART1) 内)
- 调试版本通道为空实现; debug.h/debug.c 一行未改, printf 行为完全不变

验证:
- host 单测: 从 usart_biz.c 原样抽取装配器 + 逐字复刻 loop_uart_proto.c 的
  XOR/SUM 校验, gcc -Wall -Wextra -Werror 零警告, 13 组用例全部通过 ——
  含"坏帧后紧跟好帧仍送达"、"截断帧后只交付 1 帧完整好帧"、70B 最大帧、
  LEN 非法/超长、0x9F(Loop OTA 魔数)忽略、上行闸门只放行校验通过帧
- 待板级验证: 本机无 RISC-V 工具链, 固件编译与实机联调尚未进行

现场核对: 新增 g_uart1_fwd_to_air / _to_loop / _local_cnt / _badchk / _drop
计数, 判据为"g_uart1_fwd_to_air 与 Air780 侧实收帧数相等"

文档: devlog 新增 2026-09-10 条目(含接线说明 PB6/PB7 交叉 + 待办清单)
2026-09-10 16:31:54 +08:00
wangfq b90842f89d feat(vd960Loop): V1.03 — 上电自检基准判稳同步 V4B V4.23
绿灯上电自检快闪: 原固定 128 样本(≈1.3s)即判稳, 与基准是否真稳无关;
stable_cnt 仅 init_vd_single 清零, 有限存在超时/安全复位(LC_Reset=1)
重学阶段残留计数 → 二次稳定期 1 样本瞬间判稳 (同 V4B 旧缺陷)。

修复 (四路 unit 独立):
- TaskLoop.h: +STABLE_ORIGIN_PPT 1 / STABLE_SETTLE_WINDOWS 2 /
  STABLE_MAX_SAMPLES 500(≈5s兜底); Loop154_Unit +settle_cnt
- TaskLoop.c vd1_task_per_channel 稳定期: 每窗(100样本≈1s)完成比较
  Origin 均值漂移 ≤0.1% → settle++, 否则清零; 判稳=最少128样本+
  连续2窗稳定 或 500 样本兜底; 判稳退出清零
- init_vd_single / 有限存在超时 / 安全复位超时重学路径统一清零自检计数
- poll_green_led 条件不变: 绿灯闪至本路基准稳定才停 (四路独立)

验证: tests/test_stable_settle.c 5 项 PASS (常量200/首窗尖峰400/
      持续漂移500兜底/重学复位200防呆/Origin=0守卫);
      gcc -fsyntax-only TaskLoop.c 0 error
文档: vd960Loop/docs/devlog V1.03 置顶; DLD960_技术规格书/产品手册
      V1.04 (上电稳定期/绿灯自检语义 + 修订记录)
cmcng.h: FIRMWARE_VER "1.02"→"1.03" (MAIN=1/SUB=3, SSUB=1保持)

待现场验证: 绿灯自检时长(干净≈2s/扰动3~4s/兜底5s)与四路独立闪烁
2026-09-02 17:02:15 +08:00
wangfq 77b667f1a0 chore(vd960Loop): V1.02 — 基线跟踪双向对称保护后版本号bump + devlog
- cmcng.h: FIRMWARE_VER 1.01→1.02, SUB 1→2 (SSUB=1保持)
- devlog: V1.02 条目(双向对称基线保护同步V4B V4.20 + 版本轨迹)
- 代码基线保护同步见前提交 4c4000e
2026-09-02 15:02:43 +08:00
wangfq 4c4000eb7d fix(vd960Loop): 基线跟踪双向对称保护同步V4B V4.20 — 负variation铁块防Origin污染锁死
负偏差(dev>0,CAPVD>Origin)只冻结永不超时更新Origin, 对称窗口[-4×dlt,+4×dlt]
四路unit结构同步, 与DLD154V4B V4.20修复一致
2026-09-02 14:52:09 +08:00
wangfq 3d9df52d1c feat(MQTT): 协议 V1.13 — initialize extra_info 增加 imei/iccid 可选字段
用户需求 2026-08-31:
- 协议 §5.1: extra_info 增加 imei(4G 模块 IMEI)/ iccid(流量卡 ICCID)可选字段
- 语义: 无 4G 模块省略/空串;4G 通道由 Air780 填真实值(§6.2)
- 固件 iot_send_initialize: extra_info 补 imei/iccid(空串占位, DBN 无 4G 信息;
  待"BLE→Air780 配置同步"实现后可回读填充)
- 同步: README 索引 V1.13 + devlog 置顶条目
2026-08-31 15:37:40 +08:00
wangfq 3c9a6f900b docs(MQTT): DLD960_IoT_MQTT协议 V1.12 — 4G 通道适配修订为方案 C(Air780 协议转换)
方案 B → 方案 C(用户 2026-08-31 决策):
- Air780 解析 0x7F 帧转换为标准 JSON 命令(loop_data/event_report/initialize/heartbeat),平台零改动
- V1.11 的 frame_report/frame_cmd 降级为可选兜底
- 4G 事件面 = 仅线圈事件(car_enter/car_leave/loop_cut/loop_restore),不含 DBN 内部网络事件
- event_report 复刻 DBN iot_evt_* 逻辑(Lua: 沿检测+ACK+5s×3+16深队列+跨重连同 msg_id),可行性已评估无硬障碍
- 命令响应: Air780 单命令状态机 + 超时回 code=5(与 g_lup_cmd 同模式)
- 链路层: DBN UART2↔UART1 纯转发(魔数分流),转发不丢帧是沿检测前提

同步: README 索引 V1.12 + devlog 置顶条目
2026-08-31 15:24:31 +08:00
wangfq d6873f4b45 docs(MQTT): DLD960_IoT_MQTT协议 V1.10→V1.11 — 4G 通道适配(方案B hex透传)
方案 B(原始帧透传 + hex 封装)正式并入主协议:
- 新增 §6 4G 通道适配: 架构(0x7F帧透传)/命令面差异/上行 frame_report/下行 frame_cmd/不支持命令/link 对象/平台要求
- §3 命令详表加 frame_cmd + frame_report(4G 通道专用标注)
- 头部版本行 + 修订记录 V1.11
- 4G 通道: 不使用有线标准 JSON 业务命令;网络配置类命令回 code=4
- 实施主体: Air8781P(Air780EPM) vd960Air 工程;vd960DBN UART1 通道列入计划(固件未实现,如实标注)
- 同步: README 索引 V1.11 + devlog 置顶条目(协议先行)
2026-08-31 14:45:46 +08:00
wangfq 2e73155388 docs(BLE): DLD960_BLE协议 V1.02→V1.03 — 命令总表补齐 + 未实现如实标注
对照 dbn_ble_srv.c(GBK+CRLF) 逐命令梳理,补齐既有配置命令:
- 0x09/0x10/0x11/0x12: 序列码/设备信息/SSC网络读写
- 0x13/0x14/0x15/0x16: IoT网络/Topic读写(字符串数组 0x00 分隔)
- 0x1C/0x1D: 密码验证/修改(0x1D 在 unpack_packs,不在 switch)
- 0x1E/0x92/0x1F/0x22: 出厂/复位/子功能码
- 0x31: UART波特率读写(⚠ 写响应多 1B 重复低字节)
- 0xC5: 传感上报使能(⚠ report_sens_acs 无定义,0xC0 上报未实现)
- 透传通道 0x7F/0x9F
未实现如实标注: 0x17/0x18/0x20/0x21/0x87/0x88/0x89 无 case;
0x23/0x24/0x8A 空壳/空响应

同步: README 文档索引 V1.03 + devlog 置顶条目(2026-08-31)
2026-08-31 13:58:59 +08:00
wangfq 31ca3d66c3 bump(vd960Loop): FIRMWARE_VER 1.00 -> 1.01 (MAIN=1/SUB=1/SSUB=1)
CMD_DEV_Ver 上报字节: 1.1.1; 字符串版本 '1.01'
2026-08-28 09:48:06 +08:00
wangfq d1fb357989 feat(vd960Loop): 看门狗升级双保险喂狗模式 + 超时对齐 V4B(1.64s)
对照 DLD154V4B V2.10(同款 AT32F421):
- 原直喂模式缺 ISR 活性检测: TMR15 ISR 死但主循环活 -> 看门狗永不复位(假活)
- 升级: TMR15 ISR 5ms 递增 g_wdg_counter + 主循环 poll_wdg() 500ms 才 reload
- 超时 3s(div64/rld1875) -> 1.64s(div16枚举/rld4095)
- wdt_feed 定义移至 TaskLoop.c 改名 poll_wdg, 与 V4B 同构
- 修正 main.c 注释矛盾(4687 vs 1875); 沿用 WDT_CLK_DIV_16 枚举写法避免
  V4B 同源传值 bug
- 隔离测试 3 项断言全过(分频16/超时1638ms>500ms/10s喂20次/ISR死不喂)
2026-08-28 09:15:54 +08:00
wangfq 40e6648445 feat(vd960Loop): LOOP_MEASURE_BASE 归一化基数上报 (协议 V1.09)
跨产品 MEASUREMENT_BASE 对齐(2026-08-26 DLD110S ×4 事故):variation 是周期域
计数值,归一化必须除设备自身的 MEASUREMENT_BASE。960Loop 此前 0x64 读参响应裸发
参数,第三方平台无从换算(写死 131072 恰好蒙对,写 524288 就 ÷4 失真)。

- docs(DLD960Loop_串口通信协议.md): V1.09 — 0x64 响应尾部追加 3B LE
  LOOP_MEASURE_BASE(响应 34B→37B)+ 换算公式 ΔL/L≈-2×variation/BASE、
  Δf/f≈variation/BASE + 修订记录
- inc/cmcng.h: #define LOOP_MEASURE_BASE 131072L(注释标明与 TaskLoop.h
  MEASUREMENT_BASE 同步)
- src/main.c: CMD_DBN_GET_MCJQ_PARAM (0x64) handler for 循环后追加 3B LE
- docs/devlog.md: 置顶 2026-08-27 条目

设计决策:常量走 0x64 查询响应(低频)不进 0xC0 高频帧(避免分包);
向后兼容老客户端(只读前 34B 不受影响)。
联动待办:DBN 侧对外(BLE/TCP/MQTT)暴露基数未落地。
2026-08-27 08:27:02 +08:00
wangfq e9a19b40d3 docs(vd960Loop): 9级制灵敏度表设计 (0.01%~5.00% ΔL/L, 等比公比2.198)
- variation-analysis.md §9.3.1: 9级 sens_in/sens_out 表 (0=最低, 8=最高)
  - sens_in {1638,745,339,154,70,32,15,7,3} 端点精确 0.01%/5.00%
  - sens_out 滞回渐变 50%→65%→90%/100% (高档防不释放)
  - 4级→9级迁移映射 0→3,1→4,2→5,3→7
  - 工程警示: SENS=8 贴噪声底, SENS=0 只认重型车
- tools/variation_calc.py: --sens-table 9 支持 9级显示 (4级默认兼容)
- devlog 2026-08-25 条目追加 9级制设计小节
代码落地 (set_factory_param/sens_level_from_config) 待 9级立项
2026-08-25 15:02:15 +08:00
wangfq 6483795079 docs(vd960Loop): 更正 devlog — flash sens[0]={512,400} 是 0x8A 自定义配置非坏数据
CMD_DBN_LOOP_SENS_LIST(0x8A) 支持灵敏度表读写 (main.c:946):
写 → memcpy(g_loop_sens_list.sens) → storage_dev() → para_store_init()
链路完整, 512/400 为用户自定义 SENS=0 档阈值(低灵敏度), 勿恢复出厂
2026-08-25 14:26:24 +08:00
wangfq af95660c68 fix(vd960Loop): init_vd_single 灵敏度从配置读, 不再硬编码 2
现场日志: 配置 SENS=3/2/1/0, 检测全用 sens_in:36 (SENS=2 表值)

根因: main→para_store_init() 同步 loop_SensLevel=配置, 但任务启动后
INIT_VDs()→init_vd_single() 硬编码 loop_SensLevel=2 覆盖配置 → 全通道 SENS=2

修复: init_vd_single 改为 sens_level_from_config(
  g_loop_cng_info.loop_cng[unit->loop_num].sensitvity)

另: flash 灵敏度表 sens[0]={512,400} 为坏数据(出厂应为{216,108}),
修复后 loop_3(SENS=0) 会撞上异常值, 需恢复出厂清配置区
2026-08-25 14:23:33 +08:00
wangfq bbf5492fe2 fix(vd960Loop): 灵敏度收敛 4 级制 + 运行中配置立即生效 (协议 V1.08)
根因: 设置不同灵敏度感应高度无区别
1. unpack_pkg_set_mcjq_param 未同步 loop_SensLevel → 改灵敏度需重启才生效
2. 9级制映射 sensitvity & 0x03 折叠 (默认7→3, 3→3, 8→0)
3. (测试陷阱) 金属块过大信号饱和, 建议小金属块测临界高度

修复:
- 提取 sens_level_from_config() 映射函数, 4级制 0~3 一一对应 (9级制后改)
- unpack_pkg_set_mcjq_param 补 loop_SensLevel 同步 (运行中立即生效)
- para_store_init 上电路径改用同一函数
- 协议 V1.08: Sensitivity 0~9级 → 4级制 0~3 (默认2)
- tests/test_sens_mapping.c gcc 隔离单测 PASS (0~3无折叠/兼容旧默认7→3)
2026-08-25 11:52:14 +08:00
wangfq fad99a550b fix(vd960Loop): 灵敏度阈值改用可配置灵敏度表 g_loop_sens_list
固定表 SensTable/SensTable_1 替换为 flash 可配置的
g_loop_sens_list.sens[loop_SensLevel].sens_in/sens_out
(进入/平坦性/离开三处 dlt_ORG 计算同步修改)
- 修复灵敏度值固定不可配置的问题
- 默认值由 set_factory_param 保持 {216,108,36,10}/{108,72,18,9}
2026-08-25 11:44:24 +08:00
wangfq 8151d5700e docs(vd960Loop): 自动分频(Auto Frequency Selection)技术分析与开发计划
- 现状盘点: set_flp_level/loopFreq_Level 已具备, 缺检测/决策/软切换
- 物理层: 四档频率表, 66/76nF 仅 7.3% 间隔(不均匀), 注入锁定才是真敌人
- 干扰检测: 差拍抖动/慢拍/冻结占比 → 干扰度 D (复用 §10 健康度)
- 时机策略: A上电/B运行中/C混合(推荐) 三方案 + 切换时序与安全约束
- 频率规划: 8+ 线圈共享 4 档的复用策略 + 邻道协调卡点
- 开发计划: 6 阶段 25+ 任务 (硬件标定→检测→决策→软切换→整合→现场)
- 5 个决策点待用户拍板 (方案/初始档位/切换代价/邻道协调/范围)
2026-08-25 11:37:19 +08:00
wangfq 2a267967b4 feat(vd960Loop): 环境健康度算法 §10 + drift_health.py 参考实现
- variation-analysis.md §10: 无车漂移监测 (零成本, 不改协议)
  - 口径修正: 绝对峰=1.5×窗口漂移(Origin 滞后半窗) → 用峰谷差
  - 600ms 采样系统性低估 ~12% (峰在窗口末采不到)
  - 冻结态失效(>4×dlt_ORG) → 双指标: 漂移速率 + 冻结占比
  - 冻结判定双判据: 跨窗跳变消失 + 峰谷差突变(>4×前4窗中位数)
- tools/drift_health.py: CSV 逐窗统计/冻结检测/JSON/自测
- devlog 2026-08-25 条目追加环境健康度小节
- 自测: 慢漂移 0.02%/s 识别准确, 冻结窗正确排除
2026-08-25 10:09:36 +08:00
wangfq e919ff477a docs(协议): variation 值域勘误 V1.07 — ±8388607 → −8388608 ~ +8388607
统一为 3B 补码标准值域 (-2^23 ~ +2^23-1), 对齐代码负向饱和边界 0x800000
- DLD960Loop_串口通信协议.md: variation 字段值域修正 + 修订记录 V1.07
- variation-analysis.md: 头部版本引用/§1/§5.1 表格/§5.6.3 口径小注同步
- devlog.md: 2026-08-25 条目追加勘误说明
代码无改动 (main.c 饱和边界本就正确)
2026-08-25 09:49:11 +08:00
wangfq 1ec4a39d92 docs(vd960Loop): variation ↔ ΔL/ΔL-L 物理对应分析 + 换算工具
- variation-analysis.md 新增 §9: CAPVD ∝ √L 推导, 精确/一阶/频域三口径公式
- 灵敏度档位 → ΔL/L 触发阈值表 (SensTable/65536 ×2, DLD154Pro 交叉验证)
- 电容档无关性: 33/43/66/76nF 共用同一换算表 (variation/Origin 中 C 消掉)
- tools/variation_calc.py: 正向/反推/3B LE 补码解析/灵敏度对照/交互 5 模式
- devlog 置顶 2026-08-25 条目
2026-08-25 08:39:37 +08:00
wangfq 572077031c docs(vd_960): V1.02.05 发布文档同步 — 产品手册/技术规格书 V1.02→V1.03 + 修订记录
产品手册 V1.03 (2026-08-21, 配套整机 V1.02.05):
- 头部文档版本/配套固件 → V1.03 / Loop 1.0 + DBN 1.02.05
- 核心特性补: 网络上报携带地感版本 (MQTT loop_version_query) + 联网稳定性加固
- §6 固件升级配套: DBN 1.02.04 → 1.02.05
- §7 故障排查: 配置保存建议升至 V1.02.05 及以上 (保留 V1.02.04 已修复历史说明)
- §8 相关文档表: MQTT 协议 V1.07 → V1.10 (修正滞后)
- 修订记录 V1.03 行

技术规格书 V1.03 (2026-08-21, 配套整机 V1.02.05):
- 头部文档版本/配套 → V1.03 / V1.02.05
- §5.1 协议矩阵标题 → V1.02.05 配套
- §10 版本配套矩阵 → V1.02.05 / DBN 1.02.05
- §10.1 三段式 FIRMWARE_VER 1.02.04 → 1.02.05 + 新增 UART2 0x4A 版本查询总线时序约束
- 修订记录 V1.03 行

README 文档索引: 产品手册/技术规格书 V1.02 → V1.03
2026-08-21 09:07:40 +08:00
wangfq 6e711e35ef release(vd960DBN): 固件版本 1.02.04 → 1.02.05 (2026-08-21)
涵盖 8-20 联网稳定性修复系列 + 8-21 MQTT 协议 V1.10 实现:
- SocketSend 0x11 退避 / SocketCreat 失败中止 (死循环闭环)
- OTA hex 提取兼容 json.dumps 空格
- OTA 刷写状态回 idle + ota_report 主动上报
- OTA 命令缓冲 union 合并 (RAM 90% 复位闭环)
- MQTT V1.10: dev_info_query/initialize 补 loop_ver/loop_hw_ver + loop_version_query 实时查询

cmcng.h 三段式一致: FIRMWARE_VER="1.02.05" + MAIN=1/SUB=2/SUBSUB=5
README 当前发布行 + 子项目版本表 → 1.02.05
CHANGELOG 新增 V1.02.05 条目 (配套矩阵: MQTT V1.07→V1.10)
devlog V4.8 条目补版本说明

产品手册/技术规格书留待打 tag 正式发布时同步
2026-08-21 08:55:01 +08:00
wangfq 181467b9e6 feat(vd960DBN): MQTT 协议 V1.10 固件实现 — 网络上报携带地感版本 (2026-08-21)
协议 V1.10 (3be9e50) 三处新增落地:
1. dev_info_query 响应 data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 未完成/失败为空串)
2. initialize 上报同样携带 (尽力, 可为空)
3. 新增 §4.25 loop_version_query: 0x4A 异步实时查询, 响应在 iot_verq_poll() 回包

固件:
- loop_uart_proto.h/c: 新增 LoopVerCache + g_lup_ver_cache (0x4A 缓存);
  lup_cache_from_info() 写缓存, lup_refresh_version_cache() 后台刷新 (通道空闲才发)
- iot_mqtt_srv.c: dev_info_query/initialize 加字段; loop_version_query 分支
  (暂存 msg_id/deadline 300ms, busy 拒绝重复查询); iot_verq_poll() 异步回包
  (RESPONSE_READY -> code=0 + 三字段 + 同步刷缓存; TIMEOUT -> code=5 保缓存值);
  后台刷新: 上电 iot_mqtt_init + OTA done (iot_ota_report_result ok=1) 置 dirty

关键设计: 版本来源=0x4A 实时查询 (非 OTA 元数据 version=目标版本);
缓存值语义 dev_info_query/initialize 同步回包不等异步; 与 TCP 共用 g_lup_cmd
单命令通道 (后发覆盖, 先发者超时 — 低频命令, 待板上验证时序)

单测: tests/test_iot_loop_ver.c — gcc 隔离 (extract+embed 6 函数 + mock)
44 断言全过: 版本帧解析(正常+4 错误路径) / 缓存写入+格式化(空串/NULL 防护) /
verq_poll 成功回包(code=0+三字段+topic+缓存刷新+通道释放) / 超时回包(code=5+缓存值) /
后台刷新(发起/忙不消费/响应消费/残留超时清理)

验证: check_c_balance.py 3 文件 OK; 待板上验证 UART2 0x4A 与 loop_data 共用总线时序
2026-08-21 08:26:11 +08:00
wangfq 3be9e504da docs(vd960DBN): MQTT 协议 V1.10 — 网络上报携带地感版本 (配合远程 OTA 版本核对)
需求: 能通过网络 OTA 地感固件后, 网络上报设备信息需带地感固件版本号,
供平台升级前后版本核对/归档。

现状基础 (复用, 零新协议原语):
- Loop 0x4A 版本查询已实现 (lup_build_get_version/lup_parse_version)
- TCP JSON 已有 loop_version_query 先例 (tcp_json_srv.c)
- MQTT 回包 topic 固定 dld960/{sn}/dev → 异步回包只需存 msg_id

协议 V1.10 变更:
- §4.2 dev_info_query 响应 + §5.1 initialize 上报: 新增 loop_ver/
  loop_hw_ver (地感固件/硬件版本, 主.次.次, 0x4A 缓存, 尽力携带可为空)
- 新增 §4.25 loop_version_query (srv→dev): 实时查询, 设备经 UART2
  0x4A 异步查询后回包 (loop_ver/loop_hw_ver/version_str), 超时回 code=5
- §3 命令表补 loop_version_query (对应串口 0x4A)
- 版本语义区分: soft_ver=整机 DBN 固件(主.次), loop_ver=地感 Loop 固件(主.次.次)
- 修订记录 V1.10; 设计稿/README/技术规格书同步; 工具 sample initialize 补字段
2026-08-20 22:18:08 +08:00
wangfq 51298693da fix(vd960DBN): OTA 刷写成功误判未启动 — 状态回 idle + 补 ota_report 主动上报
现场: 远程平台发起 OTA, 设备物理刷写成功 (70块全ACK, flash DONE),
但平台 ota_status 轮询只见 state=ready → 误判'刷写未启动'。

根因:
1. ota_flash_done 刷写成功后台侧状态置 READY (与协议状态机
   'FLASHING──成功──▶IDLE(清槽/保留)' 不符), 平台无法区分
   '待刷 ready' 与 '已刷完' → 持续 ready 判未启动
2. 固件漏实现协议 §5.5 ota_report 主动上报 (done/failed 重发 3次×5s),
   平台收不到成功信号, 只能靠 ota_status 轮询兜底

修复 (协议 V1.09 + 固件):
- 协议: 刷写成功状态明确回 idle (镜像保留, last_result=0, 可重刷);
  ota_report 升级为刷写结果主依据; 平台判定指引 (idle+size>0+last_result=0
  =成功; 不得以轮询未见 flashing 或持续 ready 判未启动)
- ota_srv.c: ota_flash_done → OTA_STATE_IDLE (镜像保留) + 上报 done;
  ota_flash_fail → 补 ota_report failed (保留 event_report 告警);
  ota_cmd_begin 兼容 idle+size/crc32 一致 → 直接回 ready (免下载重刷)
- iot_mqtt_srv.c: 新增 ota_report 上报状态机 (立即首发 + 5s×3 重发,
  同 msg_id/ts), 共享 _iot_pub_payload (event_report 复用, RAM 零新增)
- 单测: test_flash_flow/test_flash_ready_timeout 断言 ota_report 上报,
  state 断言 READY→IDLE; 新增 test_begin_reflash; 10/10 全过 + 22 py 断言
2026-08-20 18:56:21 +08:00
wangfq 1badba3893 fix(vd960DBN): 联网 SocketSend 0x11 死循环 — 发送退避 + SocketCreat 失败中止
现象: loop_data 606B 发送 0x11(ERR_MEM) → 立即重试10次 → 3次断连 →
重连 SocketCreat 0x1D(ISCONN) 仍继续 Connect → timeout 死循环永久失联

根因:
1. WCHNET_NUM_TCP_SEG=2 (瘦身改小): 重传队列占用即无发送缓冲 → 0x11 瞬时
2. iot_mqtt_send 失败处理过激: 无退避立即重试 + 3次就强制断连
3. WCHNET_CreateTcpMqttSocket: SocketCreat 失败(mStopIfError只打印)仍继续
   Connect → 无效 socket 等超时死循环; SocketId_TCP 未置 0xFF 沿用旧 id

修复:
- iot_mqtt_send: 0x11 200ms 退避重试(≤10次, 等SEG释放), 不立即断连
- 强制重连: 补 SocketId_TCP=0xFF
- WCHNET_CreateTcpMqttSocket: SocketCreat 失败置 0xFF + return 不 Connect,
  上层指数退避

验证: 语法 0 错误; 待板级确认重发自动恢复 + 重连退避
2026-08-20 18:16:31 +08:00
wangfq c305ca651f fix(vd960DBN): OTA 下载失败 — hex 提取兼容 json.dumps 空格
现象: DBNMQTTool 第一片 ota_data code=1 crc/gap, received=0, 重试全败

根因: 工具 json.dumps 默认带空格 ("data": "hex"), 设备端 strstr 找
"data":" 无空格格式 → 提取失败 → hexbuf 空 → 解码失败 code=1
(simple_parse_json 容错空格所以其他命令正常, 只有手工 strstr 踩坑)

修复:
- ota_srv.c/h: 新增 ota_json_extract_hex() 兼容有无空格 (扫描 data 键跳过空白)
- iot_mqtt_srv.c: ota_data 改用该函数, 删 strstr 手工提取 + hp 残留
- 单测 +test_json_extract_hex: 无空格/带空格(事故场景)/512hex/找不到 4 场景

验证: 单测 11/11 PASS; 语法 0 错误; 待板级重测下载
2026-08-20 14:13:06 +08:00
wangfq 2484a03326 fix(vd960DBN): 联网复位事故 — OTA 命令缓冲合并 union (RAM 90% 栈余量不足)
现象: SUBACK 后收平台 report_config (PUBLISH len=197) → HardFault →
NVIC_SystemReset (RST_REASON 0x10000000 = SFT, 非 IWDG) → 死循环复位

根因: RAM 90.02% + 6 个 OTA 分支独立 static resp[256~400] + hexbuf[513]
共 ~2.5KB BSS → 栈余量被挤 → iot_handle_publish 深调用链栈溢出
(局部栈改 static 是伪优化: BSS↑=栈余量↓; 正解是 union 复用减总量)

修复:
- iot_mqtt_srv.c: 6 resp + hexbuf 合并函数级 static union _ota_io (~2KB 省)
- ota_srv.c: _chunk_buf/_fs_frame 合并 union (~260B 省) + 删无用变量

验证: 语法 0 错误; gcc 隔离单测 10/10; 待板级确认 RAM 回 ~88% 不再复位
2026-08-20 14:04:00 +08:00
wangfq 0b51d84a9b perf(vd960DBN): OTA RAM 优化 — union 复用块缓冲 + 命令分支数组改 static
MRS 编译 RAM 90.02% (44248/48KB) 逼近历史 .bss 挤栈红线, 减负:
- ota_srv.c: _chunk_buf[256] + _fs_frame[254] 合并为 union (下载/校验与刷写帧
  互斥复用, 省 ~260B static); ota_send_9f 组帧直接用 union 缓冲 (省 254B 栈)
- iot_mqtt_srv.c: 6 个 ota_* 分支 resp[256~400] + ota_data hexbuf[513] 局部栈
  数组改 static (栈峰值 → BSS, 防运行时栈溢出; RAM 总量不变但运行时安全)

验证: gcc 隔离单测 9/9 全过; 语法 0 新增错误
2026-08-20 13:41:33 +08:00
wangfq 49736c341c feat(vd960DBN): Loop MCU 远程 OTA 固件实现 (MQTT V1.08, ROADMAP P1.4 ①)
先存后刷: MQTT 分片下载 → W25Qxx OTA 暂存区 (0x010000 512KB) → 全镜像 CRC32
复核 → 本地 0x9F ISP 透传刷写 Loop MCU (AT32F421)

- 新增 ota_srv.c/h: CRC32(ISO-HDLC 逐位零表)/OtaMeta 双备份(节流 4KB flush)/
  会话状态机(begin/data/end/abort/flash)/本地刷写状态机(非阻塞 tick: A5→A6→A7
  停等 ACK 1s×3)/安全窗口(有车拒绝 force 跳过)/失败 event_report ota_error
- iot_mqtt_srv.c: 6 个 ota_* 命令分发 + 会话静默(event_report 积压/offlog·快照
  落盘暂停, 结束补发) + IOT_EVT_OTA_ERROR + iot_any_car
- usart_biz.c: uart_srv OTA 分支 ACK 分发 (本地刷写→ota_flash_feed_ack, BLE 透传不变)
- peripheral_main.c: ota_init + ota_poll 挂主循环
- offlog: OTA_START 0x60/OTA_RESULT 0x61 审计事件
- tests/test_ota_srv.c: gcc 隔离单测 9 组全过 (mock NOR/UART2/时钟, 嵌入真实实现)
  抓到 2 个真 bug: ①A7 序号未递减(bootloader 等不到末包) ②重发时 retry 清零(超时永不失败)
- 语法检查: ota_srv/iot_mqtt_srv/usart_biz/offlog 0 新增错误 (基线 interrupt 假阳性除外)
- devlog 置顶条目 + 待板上验证清单
2026-08-20 12:09:13 +08:00
wangfq 6c1f276002 feat(DBNMQTTool): OTA 页签 + 协议单测 (MQTT V1.08 工具先行)
- protocol.py: OTA 模块 — 命令常量/ota_crc32(ISO-HDLC)/ota_split_bin(256B×96KB)/
  data_ota_* 构建器/状态·阶段·错误码描述
- main.py: OTA 页签 — bin 选择(≤96KB+CRC32 显示)/版本·force·slot 参数/
  下载(OtaDownloadThread: begin→data×N→end, 断点续传·缺片重定位·CRC 重发)/
  刷写(二次确认)/中止/状态查询; 进度条 + ota_report/ota_status 实时显示
- tests/test_ota_protocol.py: 22 断言 — CRC32 标准向量/分片边界/构建器/
  OtaDeviceSim 设备状态机(顺序·幂等·乱序·单片CRC·全镜像复核·续传)/完整下载流
- devlog 置顶条目
2026-08-20 11:53:50 +08:00
wangfq 2ccf0804a8 docs(vd_960): MQTT 协议并入 Loop 远程 OTA → V1.08 (ROADMAP P1.4 ①)
- DLD960_IoT_MQTT协议.md V1.07→V1.08: 命令表加 ota_begin/ota_data/ota_end/
  ota_abort/ota_flash/ota_status/ota_report; §4.19~4.24 命令详情; §5.5 ota_report;
  event_report 扩展 type=ota_error; §2.2 OTA 细分错误码 (err_code); 修订记录
- DLD960_MQTT_OTA协议.md V1.01 (按修改意见): Slot A/B 100KB / 维持 Loop 现状 /
  会话期间静默 / 非阻塞 tick 驱动
- README 协议索引 + 技术规格书 §5.1 协议矩阵同步 V1.08 (CRLF 保持)
2026-08-20 11:53:36 +08:00
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
73 changed files with 14903 additions and 1084 deletions
+8
View File
@@ -3,6 +3,9 @@
*.i
*.old
# 测试生成物 (extract_offlog_cases.py 输出, 嵌入源文件真实代码)
offlog_cases_embedded.c
# Object files
*.o
*.ko
@@ -89,3 +92,8 @@ wchreference/
__pycache__/
*.pyc
# gcc 单测编译产物 (tests/*.c 保留)
test_offlog
test_ble_offlog
test_snapshot
+210 -72
View File
@@ -1,72 +1,210 @@
# CHANGELOG — vd_960 (DLD960)
本文件记录 DLD960 整机(双 MCU 配套)的版本发布历史。
单侧固件的细粒度演进见 `vd960Loop/docs/devlog.md``vd960DBN/docs/devlog.md`
---
## V1.0.0 — 2026-07-16(首个正式发布
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960Loop 固件 (AT32F421) | 1.0 |
| vd960DBN 固件 (CH32V208, DLD960GA) | 1.0 |
| DLD960Loop 串口协议(MCU 间 0x7F | V1.05 |
| DLD960 串口通信协议(RS485/TTL | V1.01 |
| DLD960 TCP JSON 协议(:5960 | V1.01 |
| DLD960 IoT MQTT 协议 | V1.05 |
> ⚠ 自协议 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.01**
- 鉴权状态机 + 15 条命令;服务异常自动重启(3 条件 + 3 次限额 + 10min 冷却)
- event_report 客户端必答:5s 超时重发同 msg_id/原始 ts ×3
**IoT MQTT(协议 V1.05**
- 双主题 `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 帧双消费路径统一摄取,事件与缓存同源
**基础设施**
- 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`,随整机版本同步
- 协议文档版本:各文档内修订记录表独立演进,发布时在配套矩阵中锁定
# CHANGELOG — vd_960 (DLD960)
本文件记录 DLD960 整机(双 MCU 配套)的版本发布历史。
单侧固件的细粒度演进见 `vd960Loop/docs/devlog.md``vd960DBN/docs/devlog.md`
---
## vd960DBN V1.02.05 — 2026-08-21DBN 通信板单侧更新
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,可正常配套)。涵盖 2026-08-20 联网稳定性修复系列 + 2026-08-21 MQTT 协议 V1.10 实现。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.05** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(0x7F / 0x9F OTA | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
| DLD960 IoT MQTT 协议 | **V1.10** |
| DLD960 BLE 协议 | V1.02(不变) |
### 🆕 MQTT 协议 V1.10:网络上报携带地感版本(配合远程 OTA 版本核对
- `dev_info_query` 响应 / `initialize` 上报 data 新增 **`loop_ver`**(地感固件 `"主.次.次"`+ **`loop_hw_ver`**(地感硬件)——来自 Loop `0x4A` 查询缓存(上电自动查一次 + OTA done 后刷新),查询未完成/失败为空串
- 新增 §4.25 **`loop_version_query`** 命令:平台实时查询 Loop 版本(0x4A 异步 → 回包 `loop_ver`/`loop_hw_ver`/`version_str`),超时回 `code=5`
- 固件实现:`loop_uart_proto.c/h` 新增 `LoopVerCache` 版本缓存 + `lup_cache_from_info()`/`lup_refresh_version_cache()``iot_mqtt_srv.c` 新增 `iot_verq_poll()` 异步回包 + 后台刷新
- **单测 44 断言全过**(版本解析/缓存/异步回包时序,`tests/test_iot_loop_ver.c`
### 🔧 联网稳定性修复(2026-08-20 系列)
- **SocketSend 0x11 死循环**WCHNET `WCHNET_ERR_MEM(0x11)` 为瞬时发送缓冲不足(`WCHNET_NUM_TCP_SEG=2`)——改为 200ms 退避重试(≤10 次),不再 3 次强制断连;`SocketCreat` 失败置 `SocketId_TCP=0xFF` + 中止 Connect(不再 timeout 死循环
- **OTA 下载失败**`ota_json_extract_hex()` 兼容 `json.dumps` 空格(`"data": "..."`),单片 CRC 全错根因闭环
- **OTA 刷写状态语义**:刷写成功状态回 `idle`(镜像保留可重刷)+ 补 `ota_report done/failed` 主动上报(平台判定主依据)
- **联网复位事故**:OTA 命令缓冲合并 unionRAM 90% 栈余量不足 → HardFault 复位闭环)
### 📋 已知约束
- UART2 0x4A 版本查询与 `loop_data`/事件上报共用总线(低频命令,理论无冲突)——**待板上验证时序**
- OTA done 后缓存刷新存在 Loop 复位时间窗(查询可能超时,可接受;平台可用 `loop_version_query` 主动核对)
- BLE→Loop OTA 透传模式无自动退出(V1.02.03 遗留,升级后需断电重启
### 验证
- ✅ gcc 隔离单测:`test_iot_loop_ver.c` 44 例(V1.10+ 既有 ota/offlog/snapshot/mqtt 单测全过
- [ ] 待板级回归:MQTT 联网长稳、`loop_version_query` 实际回包、OTA 全流程
---
## vd960DBN V1.02.04 — 2026-08-19DBN 通信板单侧更新
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.04** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(0x7F / 0x9F OTA | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
| DLD960 IoT MQTT 协议 | V1.07(不变) |
| DLD960 BLE 协议 | V1.02(不变) |
### 🔧 SPI Flash 存储适配修复
- **识别去厂商代码**`W25Qxx` 宏去 `0XEF` 前缀、`SPI_Flash_ReadJEDEC_ID()` 删厂商校验、`storage_init` 低字节匹配——兼容其他厂家同容量型号(如 0x1A 厂商)
- **恢复 factory 配置写入**:解除 2026-08-13 止血(根因 8-17 已闭环),换新空片(W25Q128)后不再每次上电进出厂初始化,配置读写恢复正常
- **板级验证通过**2026-08-19):W25Q128 配置读写正常
---
## vd960DBN V1.02.03 — 2026-08-19DBN 通信板单侧更新)
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.03** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(MCU 间 0x7F / 0x9F OTA | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
| DLD960 IoT MQTT 协议 | V1.07(不变) |
| DLD960 BLE 协议 | V1.02(不变) |
### 🔧 BLE→Loop OTA 升级修复(0x9F 帧透传回 BLE)
- **现象**UART2 RX DMA 改造后,通过 BLE 给地感(Loop MCUOTA 升级失效
- **根因**DMA poll 帧解析 `lup_feed_byte()` 只认 0x7FLoop bootloader 回的 0x9F 响应帧(pre_ok/addr_ok/data ACK)全部被吞;OTA 为**停等协议**0xA7 WITH_BACK 每块必回 ACK),工具等不到 ACK 升级卡死
- **修复**:新增 `lup_feed_byte_ota()` 0x9F 帧状态机(SUM 校验无 XOR);`uart2_dma_poll``g_flag_counter_ota.flag` 切换 0x9F/0x7F 解析器;`uart_srv` OTA 分支 0x9F 帧透传回 BLE + 清 flagOTA 溢出阈值放宽 + 收帧 tick 归零
- **验证**:gcc 隔离单测 8 断言全过;**板级 BLE OTA 全流程实测通过**2026-08-19
- **已知约束**:OTA 模式无自动退出机制(`g_flag_counter_ota.flag` 置 1 后需断电重启恢复 0x7F 通信),维持现状
### 📦 脱机日志快照流 + hex 上报(2026-08-18 已含,协议 V1.03/V1.07
- TCP/MQTT `log_*` 命令支持 `stream=snapshot` 传感快照流(复用命令 + stream 字段,不新增命令码)
- `log_query` records 改 **hex 原始字节**上报(OfflogEvt 32B→64 hex / SnapRec 64B→128 hex2 条快照 406B < 800B 缓冲)
- MQTT 查询命令补齐:`ssc_net_query` / `iot_net_query` / `iot_topic_query`(协议命令表全对齐)
---
## vd960DBN V1.02.01 — 2026-08-17DBN 通信板单侧更新)
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议均未变更,可正常配套)。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.01** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(MCU 间 0x7F | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.02(不变) |
| 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/
+236
View File
@@ -8,6 +8,8 @@ DLD960 IoT MQTT 协议定义
import json
import time
import struct
import zlib
from typing import Optional, Any
from dataclasses import dataclass, field, asdict
@@ -61,6 +63,11 @@ CMD_LOOP_PARAM_SET = "loop_param_set"
CMD_LOOP_PARAM_QUERY = "loop_param_query"
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_EVENT_REPORT = "event_report"
@@ -71,6 +78,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_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}
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 +116,29 @@ FREQ_LEVELS = ["high", "mid_high", "mid_low", "low"]
OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"]
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 +249,119 @@ 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 {}
# ============================================================
# OTA 远程升级 (V1.08: Loop MCU 先存后刷, ROADMAP P1.4 ①)
# 协议依据《DLD960_IoT_MQTT协议.md》§4.19~4.24 / §5.5
# ============================================================
CMD_OTA_BEGIN = "ota_begin"
CMD_OTA_DATA = "ota_data"
CMD_OTA_END = "ota_end"
CMD_OTA_ABORT = "ota_abort"
CMD_OTA_FLASH = "ota_flash"
CMD_OTA_STATUS = "ota_status"
CMD_OTA_REPORT = "ota_report" # dev→srv 主动上报
OTA_TARGET_LOOP = "loop"
OTA_CHUNK_SIZE = 256 # 单片 256B(协议常量, 不接受协商)
OTA_MAX_SIZE = 96 * 1024 # 镜像上限 96KB (Slot 100KB - 4KB 边界余量)
OTA_SLOT_A = "a"
OTA_SLOT_B = "b"
OTA_STATE_DESC = {
"idle": "空闲(镜像保留,已刷写完成则 idle+size>0)",
"downloading": "下载中",
"ready": "校验通过(可刷写)",
"flashing": "刷写中",
"flash_failed": "刷写失败",
"aborted": "已中止",
}
OTA_STAGE_DESC = {
"begin": "会话开启",
"downloading": "分片落盘",
"ready": "校验通过",
"flashing": "刷写中",
"done": "刷写成功(Loop 已重启)",
"failed": "刷写失败",
}
OTA_ERROR_DESC = {
0x1001: "启动帧无响应",
0x1002: "地址帧错误",
0x1003: "数据块 ACK 超限",
0x1004: "全镜像校验失败",
0x1005: "安全窗口拒绝后强制失败",
}
def ota_crc32(data: bytes) -> int:
"""CRC-32/ISO-HDLC (协议 §4.19.1; zlib.crc32 即此算法, 设备查表法一致)"""
return zlib.crc32(data) & 0xFFFFFFFF
def ota_split_bin(data: bytes, chunk_size: int = OTA_CHUNK_SIZE) -> list[tuple[int, int, str]]:
"""bin → 分片列表 [(offset, crc32, hex_str), ...] (单片 256B, hex 512 字符)"""
if len(data) > OTA_MAX_SIZE:
raise ValueError(f"镜像 {len(data)}B 超上限 {OTA_MAX_SIZE}B (96KB)")
return [(off, ota_crc32(data[off:off + chunk_size]), data[off:off + chunk_size].hex())
for off in range(0, len(data), chunk_size)]
def data_ota_begin(size: int, crc32: int, version: str = "",
target: str = OTA_TARGET_LOOP, force: bool = False) -> dict:
"""ota_begin 请求 data (开启会话 / 断点续传定位)"""
return {"target": target, "size": size, "crc32": crc32,
"version": version, "force": force}
def data_ota_data(offset: int, crc32: int, data_hex: str,
target: str = OTA_TARGET_LOOP) -> dict:
"""ota_data 请求 data (分片下发, offset 256 对齐)"""
return {"target": target, "offset": offset, "crc32": crc32, "data": data_hex}
def data_ota_end(crc32: int, target: str = OTA_TARGET_LOOP) -> dict:
"""ota_end 请求 data (结束下载, 全镜像 CRC32 复核)"""
return {"target": target, "crc32": crc32}
def data_ota_abort(target: str = OTA_TARGET_LOOP) -> dict:
"""ota_abort 请求 data (中止会话, 释放暂存)"""
return {"target": target}
def data_ota_flash(slot: str = OTA_SLOT_A, force: bool = False,
target: str = OTA_TARGET_LOOP) -> dict:
"""ota_flash 请求 data (触发本地 ISP 刷写, 仅 ready 态)"""
return {"target": target, "slot": slot, "force": force}
# ============================================================
# 解析设备上报
# ============================================================
@@ -224,3 +372,91 @@ def parse_topic_dev_serial(topic: str) -> Optional[str]:
if len(parts) >= 2 and parts[0] == "dld960":
return parts[1]
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}
+266 -141
View File
@@ -1,141 +1,266 @@
# DBNMQTTool 开发日志
> DLD960 IoT MQTT 桌面工具 | Python 3.11+ | PySide6 | paho-mqtt v2 | 跨平台 (Windows/Linux/macOS)
>
> 定位: DLD960 车检器 MQTT 调试工具 — 设备管理、实时数据监控、模拟上报、主题订阅/发布
---
## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465`
### 1. 模拟上报 Tab
- **loop_data / event_report / heartbeat** 三类上报携带独立可编辑 JSON 编辑区
- **范例填充**:首次切换类型时自动填入协议定义的标准载荷
- 单次发送 + **周期上报**QSpinBox 可调间隔)
- 周期开关由 `QPushButton` toggle 控制
### 2. 协议Topic Tab
- 预加载双主题:`dld960/{sn}/srv`(下发)、`dld960/{sn}/dev`(上报)
- QListWidget 展示,双击自动填充到发布区
- 发布区可编辑 JSON 载荷,一键发布
- 主题列表受 SN 输入框驱动,实时更新
### 3. 自定义Topic Tab
- 自由输入 topic + JSON 载荷,QoS (0/1/2) 可调
- **订阅 / 取消订阅**,接收消息实时展示在 QPlainTextEdit
- `#` / `+` 通配符支持,`paho.mqtt.topic_matches_sub` 路由匹配
- 自定义订阅的消息自动分发到接收区,附带 topic 前缀
### 4. 新增依赖
- `paho.mqtt.client` 导入新增 `topic_matches_sub`
- UI 控件: QComboBox, QSpinBox, QPlainTextEdit
---
## 2026-07-06 — paho-mqtt v2 回调修复 `213c0dd`
### 问题
断开连接时报错:
```
TypeError: MqttClient._on_disconnect() missing 1 required positional argument: 'reason_code'
```
### 根因
paho-mqtt v2 默认 `callback_api_version=VERSION2``on_disconnect` 回调签名为 5 参数 `(client, userdata, flags, reason_code, properties)`,而 v1 为 3 参数 `(client, userdata, rc)`
### 修复
- `_on_disconnect` 改用 `*args` 兼容 v1/v2
- `Client` 构造显式指定 `callback_api_version=CallbackAPIVersion.VERSION2`
- `_on_connect` 保持 v2 签名不变
---
## 2026-07-06 — tkinter → PySide6 迁移 `e1bf3dc`
### 决策
放弃 tkinter,改用 PySide6 (Qt for Python),理由:
- 跨平台原生外观 (Fusion 风格)
- Qt 信号/槽机制替代 tkinter 回调,更易维护
- 组件丰富 (QGroupBox, QSplitter, QTabWidget 等)
### 改动
- `main.py` 全量重写为 PySide6 主窗口
- 布局: 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 引用丢失
# DBNMQTTool 开发日志
> DLD960 IoT MQTT 桌面工具 | Python 3.11+ | PySide6 | paho-mqtt v2 | 跨平台 (Windows/Linux/macOS)
>
> 定位: DLD960 车检器 MQTT 调试工具 — 设备管理、实时数据监控、模拟上报、主题订阅/发布
---
## 2026-08-20 — OTA 页签:Loop 远程 OTA 分片下载(协议 V1.08 工具先行)
### 背景
ROADMAP P1.4 ①:Loop MCU (AT32F421) 远程 OTA,先存后刷。协议设计稿 V1.01 已定(`docs/DLD960_MQTT_OTA协议.md`),并入 MQTT 协议 V1.08。按"工具先行,固件后到"惯例,先在 DBNMQTTool 实现 OTA 页签 + 协议单测,验证协议自洽后再动固件。
### 变更
- **protocol.py**:新增 OTA 模块——`CMD_OTA_*` 命令常量、`ota_crc32`CRC-32/ISO-HDLC = zlib.crc32,标准向量 0xCBF43926)、`ota_split_bin`256B/片 hex96KB 上限)、`data_ota_begin/data/end/abort/flash` 构建器、状态/阶段/错误码中文描述
- **main.py**:新增 **OTA 页签**——固件 bin 选择(≤96KB 校验 + 全镜像 CRC32 显示)、目标版本/force/slot 参数、①下载(begin→data×N→end`OtaDownloadThread` QThread 后台 + 响应队列,断点续传/缺片重定位/CRC 错重发)②刷写(ota_flash 二次确认)③中止 ④查询状态;进度条 + 状态日志(ota_report/ota_status 实时显示)
- **tests/test_ota_protocol.py**22 断言——CRC32 标准向量、分片边界(1B/256B/257B/超限)、构建器字段、**OtaDeviceSim 设备侧状态机**(顺序/幂等/乱序拒绝/单片 CRC/全镜像复核/断点续传)+ 完整下载流(正常/缺片补发)
### 验证
- `venv/bin/python -m unittest tests.test_ota_protocol`22 全过
- offscreen MainWindow 冒烟:OTA tab 构建正常、控件齐全、512B bin → 2 片 CRC 正确
- 协议规则自洽性验证:设备模拟器按文档规则(offset==received 顺序片 / offset<received 幂等 / offset>received 拒收 code=1)与工具分片流程对跑,断点续传与缺片重定位全通
### 已知约束
- 下载线程每片停等 ACK(~512 片 × RTT);量产平台可改窗口并发,协议层无需变更
- 固件未实现 ota_* 前,工具下发会收 `code=4 unsupported`——能力探测见协议 §4.19
---
## 2026-08-18 — log_query 响应改 hex 原始字节解析展示(协议 V1.07 修订)
### 背景
MQTT 快照流实测 `MQTTSerialize_publish failed`——JSON 化快照记录超 800B 发送缓冲。协议修订为**原始字节 hex 上报**(与 BLE 通道同语义),工具同步改为 hex 解析。
### 变更
- **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 一致
- `data_log_query` snapshot count 恢复 **2**hex 体积可控)
- **main.py**`_apply_log_query_data` 解析 `records[].hex`hex 长度 128=快照 / 64=事件);事件流展示 type 描述 + payload 可读化,快照流逐通道字段
### 验证
- 构造事件/快照二进制→hex→解析回断言全过(POR/SFT 复位位、coil payload、evt_retry、快照 4ch、负 variation 符号扩展)
- 2 条快照 hex 响应 payload = 406B < 800B
- offscreen UI:事件流(上电/复位、线圈事件、event 重发)+ 快照流(4ch/2ch)展示 + 翻页正常
---
## 2026-08-18 — 脱机日志翻页:上一页/下一页 + 自动填充起始序号
### 变更
- 脱机日志区按钮行新增 **◀ 上一页 / 下一页 ▶**(初始禁用,拉取成功后启用)
- **下一页**:起始序号 = 当前页最后一条 `seq` + 1(锚点法,不依赖请求条数,快照流 2 条/事件流 4 条均精确);空记录兜底按 `start + count` 步进
- **上一页**:起始序号 = `max(1, 当前起始 - 条数)`,边界保护
- 翻页后自动填充起始序号 SpinBox 并触发拉取;页锚点在 `_apply_log_query_data` 更新(`_log_page_last_seq`
### 验证
- offscreen 实测 6 场景全过:初始禁用 / 锚点=103 / 下一页→104 / 上一页→100 / 边界→1 / 空记录步进→504 / 快照流 2 条→12
---
## 2026-08-18 — 命令集对齐:禁用固件未实现的 MQTT 命令按钮
### 背景
实测发现 MQTT 通道 `ssc_net_query` / `iot_net_query` 返回 `code=4 unsupported command`——固件 MQTT 分发只实现 6 条命令,工具按协议文档命令表做了全按钮。
### 变更
- **固件同步补齐**vd960DBN V4.2):`ssc_net_query` / `iot_net_query` / `iot_topic_query` 三条查询命令已实现,工具对应查询按钮保持可用
- **工具禁用**固件仍未实现的命令按钮:`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)
### 验证
- offscreen 实例化:6 个按钮禁用 + tooltip 正确,查询类/验证密码/report_config/log_* 可用
---
## 2026-08-18 — 脱机日志支持快照流 (stream=snapshot, 协议 V1.07)
### 变更
- **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 中文描述)
- **main.py**:脱机日志区加"日志流"选择器(事件日志/传感快照);统计/拉取/清除三按钮按当前流发请求(log_clear 确认框提示阻塞时长 2.8s/45ms);`log_stat`/`log_query` 响应展示区分流——快照记录解析 `channels[]` 逐字段(freq_level/direction/freq_type/sens/cond/loop/car/freq/variation/misc
### 验证
- protocol 数据构建器断言 6 例全过(事件流不带 stream / 快照流 count≤2 / log_clear 流参数)
- `QT_QPA_PLATFORM=offscreen` 实例化 MainWindow 实测:`log_stat(snapshot)` capacity=48064 展示、`log_query(snapshot)` 2 条快照记录逐字段展示(含负 variation、misc_type 中文)、事件流展示回归无损、空记录边界
- 依赖:venv + PySide6 + paho-mqtt`venv/` 已加 .gitignore
### 配套
- 固件 vd960DBN `log_*` stream=snapshot 分支(同日实现,devlog V4.1
- 协议文档:TCP JSON V1.03 / IoT MQTT V1.07
---
## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465`
### 1. 模拟上报 Tab
- **loop_data / event_report / heartbeat** 三类上报携带独立可编辑 JSON 编辑区
- **范例填充**:首次切换类型时自动填入协议定义的标准载荷
- 单次发送 + **周期上报**QSpinBox 可调间隔)
- 周期开关由 `QPushButton` toggle 控制
### 2. 协议Topic Tab
- 预加载双主题:`dld960/{sn}/srv`(下发)、`dld960/{sn}/dev`(上报)
- QListWidget 展示,双击自动填充到发布区
- 发布区可编辑 JSON 载荷,一键发布
- 主题列表受 SN 输入框驱动,实时更新
### 3. 自定义Topic Tab
- 自由输入 topic + JSON 载荷,QoS (0/1/2) 可调
- **订阅 / 取消订阅**,接收消息实时展示在 QPlainTextEdit
- `#` / `+` 通配符支持,`paho.mqtt.topic_matches_sub` 路由匹配
- 自定义订阅的消息自动分发到接收区,附带 topic 前缀
### 4. 新增依赖
- `paho.mqtt.client` 导入新增 `topic_matches_sub`
- UI 控件: QComboBox, QSpinBox, QPlainTextEdit
---
## 2026-07-06 — paho-mqtt v2 回调修复 `213c0dd`
### 问题
断开连接时报错:
```
TypeError: MqttClient._on_disconnect() missing 1 required positional argument: 'reason_code'
```
### 根因
paho-mqtt v2 默认 `callback_api_version=VERSION2``on_disconnect` 回调签名为 5 参数 `(client, userdata, flags, reason_code, properties)`,而 v1 为 3 参数 `(client, userdata, rc)`
### 修复
- `_on_disconnect` 改用 `*args` 兼容 v1/v2
- `Client` 构造显式指定 `callback_api_version=CallbackAPIVersion.VERSION2`
- `_on_connect` 保持 v2 签名不变
---
## 2026-07-06 — tkinter → PySide6 迁移 `e1bf3dc`
### 决策
放弃 tkinter,改用 PySide6 (Qt for Python),理由:
- 跨平台原生外观 (Fusion 风格)
- Qt 信号/槽机制替代 tkinter 回调,更易维护
- 组件丰富 (QGroupBox, QSplitter, QTabWidget 等)
### 改动
- `main.py` 全量重写为 PySide6 主窗口
- 布局: 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
+528 -4
View File
@@ -7,6 +7,8 @@ DBN MQTT Tool — DLD960 IoT MQTT 设备管理工具
import sys
import os
import json
import queue
import time
from datetime import datetime
from typing import Optional
@@ -18,8 +20,9 @@ from PySide6.QtWidgets import (
QTabWidget, QTextEdit, QGroupBox, QGridLayout, QCheckBox,
QSplitter, QMessageBox, QHeaderView, QFrame,
QComboBox, QSpinBox, QPlainTextEdit,
QFileDialog, QProgressBar,
)
from PySide6.QtCore import Qt, QTimer, Signal, Slot
from PySide6.QtCore import Qt, QTimer, Signal, Slot, QThread
from PySide6.QtGui import QFont, QColor, QTextCursor
import paho.mqtt.client as mqtt
@@ -31,13 +34,136 @@ from dbn_mqtt_tool.protocol import (
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_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_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,
topic_down, topic_up,
# V1.08 OTA
CMD_OTA_BEGIN, CMD_OTA_DATA, CMD_OTA_END, CMD_OTA_ABORT,
CMD_OTA_FLASH, CMD_OTA_STATUS, CMD_OTA_REPORT,
OTA_TARGET_LOOP, OTA_CHUNK_SIZE, OTA_SLOT_A, OTA_SLOT_B,
OTA_STATE_DESC, OTA_STAGE_DESC, OTA_ERROR_DESC,
data_ota_begin, data_ota_data, data_ota_end, data_ota_abort, data_ota_flash,
ota_crc32, ota_split_bin,
)
# OTA 相关命令集合 (用于响应分发)
OTA_CMD_SET = {CMD_OTA_BEGIN, CMD_OTA_DATA, CMD_OTA_END, CMD_OTA_ABORT,
CMD_OTA_FLASH, CMD_OTA_STATUS}
class OtaDownloadThread(QThread):
"""OTA 分片下载线程 (协议 V1.08): ota_begin → ota_data×N → ota_end
断点续传: ota_begin 响应 offset 为设备已接收字节数, 从此处续传
重定位: ota_data code=1 时按 data.offset 调整 (缺片/乱序/CRC)
"""
progress = Signal(int, int) # sent, total
log = Signal(str)
finished_ok = Signal(str)
finished_err = Signal(str)
def __init__(self, mqtt_client: MqttClient, dev_serial: str, bin_data: bytes,
version: str, force: bool, resp_queue: "queue.Queue"):
super().__init__()
self._mqtt = mqtt_client
self._sn = dev_serial
self._bin = bin_data
self._version = version
self._force = force
self._resp_queue = resp_queue
self._stop = False
def stop(self):
self._stop = True
def _send(self, cmd: str, data: dict) -> int:
from dbn_mqtt_tool.protocol import get_topic_for_cmd
msg = build_request(cmd, data)
self._mqtt.publish(get_topic_for_cmd(cmd, self._sn), msg, qos=1)
return msg["msg_id"]
def _wait_resp(self, mid: int, cmd: str, timeout: float = 5.0) -> dict:
"""等待与 msg_id 匹配的响应 (resp_queue 由 _on_message 填充)"""
deadline = time.time() + timeout
while time.time() < deadline and not self._stop:
try:
c, payload = self._resp_queue.get(timeout=0.2)
except queue.Empty:
continue
if c == cmd and payload.get("msg_id") == mid:
return payload
raise TimeoutError(f"{cmd} 响应超时 (msg_id={mid})")
def run(self):
try:
size = len(self._bin)
total_crc = ota_crc32(self._bin)
chunks = ota_split_bin(self._bin)
self.log.emit(f"[OTA] 镜像 {size}B CRC32=0x{total_crc:08X}{len(chunks)} 片 @{OTA_CHUNK_SIZE}B")
# 1. ota_begin (断点续传定位)
mid = self._send(CMD_OTA_BEGIN, data_ota_begin(
size, total_crc, version=self._version, force=self._force))
resp = self._wait_resp(mid, CMD_OTA_BEGIN)
if resp.get("code") != 0:
raise RuntimeError(f"ota_begin 失败: code={resp.get('code')} {resp.get('msg')} "
f"(err_code={resp.get('data', {}).get('err_code', '-')})")
d = resp.get("data", {})
offset = int(d.get("offset", 0))
self.log.emit(f"[OTA] begin OK slot={d.get('slot', '?')} state={d.get('state', '?')} "
f"offset={offset} (断点续传点)")
# 2. 逐片下发 (停等: 每片等响应)
retry = 0
while offset < size and not self._stop:
idx = offset // OTA_CHUNK_SIZE
if idx >= len(chunks):
# 设备 received 溢出, 直接截断到 size
break
off, crc, hexs = chunks[idx]
mid = self._send(CMD_OTA_DATA, data_ota_data(off, crc, hexs))
resp = self._wait_resp(mid, CMD_OTA_DATA)
code = resp.get("code")
if code == 0:
offset = int(resp.get("data", {}).get("received", offset + OTA_CHUNK_SIZE))
retry = 0
self.progress.emit(min(offset, size), size)
elif code == 1:
# 缺片/乱序/CRC 失败 → 按设备指示重定位 (data.offset=received)
new_off = int(resp.get("data", {}).get("offset", offset))
if new_off == offset:
retry += 1
if retry > 3:
raise RuntimeError("分片连续失败 (>3 次), 建议 ota_abort 后重来")
else:
retry = 0
offset = new_off
else:
raise RuntimeError(f"ota_data@{off} 失败: code={code} {resp.get('msg')} "
f"(err_code={resp.get('data', {}).get('err_code', '-')})")
self.log.emit(f"[OTA] data@{off} → received={offset}")
if self._stop:
self.log.emit("[OTA] 已停止")
return
self.progress.emit(size, size)
# 3. ota_end (全镜像 CRC32 复核)
mid = self._send(CMD_OTA_END, data_ota_end(total_crc))
resp = self._wait_resp(mid, CMD_OTA_END, timeout=10)
if resp.get("code") != 0:
raise RuntimeError(f"ota_end 失败: code={resp.get('code')} {resp.get('msg')} "
f"crc_ok={resp.get('data', {}).get('crc_ok', '-')}")
self.log.emit("[OTA] end OK crc_ok=true → state=ready, 可触发 ota_flash")
self.finished_ok.emit("下载+校验完成, 可触发刷写 (ota_flash)")
except Exception as e:
self.log.emit(f"[OTA] ✗ {e}")
self.finished_err.emit(str(e))
class MainWindow(QMainWindow):
_mqtt_status = Signal(bool, str)
_mqtt_msg = Signal(str, str, object)
@@ -88,6 +214,7 @@ class MainWindow(QMainWindow):
self._notebook.addTab(self._build_config_tab(), "参数配置")
self._notebook.addTab(self._build_data_tab(), "实时数据")
self._notebook.addTab(self._build_log_tab(), "日志")
self._notebook.addTab(self._build_ota_tab(), "OTA")
self._notebook.addTab(self._build_simulate_tab(), "模拟上报")
self._notebook.addTab(self._build_proto_topic_tab(), "协议Topic")
self._notebook.addTab(self._build_custom_topic_tab(), "自定义Topic")
@@ -194,6 +321,8 @@ class MainWindow(QMainWindow):
b1.clicked.connect(self._query_ssc_net)
b2 = QPushButton("设置")
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(b2)
btn_row.addStretch()
@@ -222,6 +351,8 @@ class MainWindow(QMainWindow):
b3.clicked.connect(self._query_iot_net)
b4 = QPushButton("设置")
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(b4)
btn_row2.addStretch()
@@ -249,6 +380,8 @@ class MainWindow(QMainWindow):
b5.clicked.connect(self._query_iot_topic)
b6 = QPushButton("设置")
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(b6)
btn_row3.addStretch()
@@ -276,6 +409,9 @@ class MainWindow(QMainWindow):
]:
btn = QPushButton(label)
btn.clicked.connect(slot)
if label != "验证密码":
btn.setEnabled(False)
btn.setToolTip(f"MQTT 固件暂未实现 {label},请用 TCP JSON 或蓝牙通道")
btn_row4.addWidget(btn)
btn_row4.addStretch()
g4_layout.addLayout(btn_row4, 1, 0, 1, 4)
@@ -326,6 +462,61 @@ class MainWindow(QMainWindow):
g5_layout.addLayout(btn_row5, 2, 0, 1, 6)
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()
return w
@@ -362,6 +553,192 @@ class MainWindow(QMainWindow):
layout.addLayout(layout2)
return w
def _build_ota_tab(self) -> QWidget:
"""OTA 升级页签 (协议 V1.08: Loop 远程 OTA, 先存后刷)"""
w = QWidget()
layout = QVBoxLayout(w)
# -- 固件选择 --
row1 = QHBoxLayout()
row1.addWidget(QLabel("固件 bin:"))
self._ota_file = QLineEdit()
self._ota_file.setReadOnly(True)
self._ota_file.setPlaceholderText("选择 Loop 固件 bin (≤96KB, 0x08003400 起 APP 映像)")
row1.addWidget(self._ota_file, 1)
btn_file = QPushButton("选择…")
btn_file.clicked.connect(self._ota_choose_file)
row1.addWidget(btn_file)
layout.addLayout(row1)
# -- 参数 --
row2 = QHBoxLayout()
row2.addWidget(QLabel("目标版本:"))
self._ota_version = QLineEdit("1.1.0")
self._ota_version.setMaximumWidth(110)
row2.addWidget(self._ota_version)
self._ota_force = QCheckBox("force")
self._ota_force.setToolTip("强制: 覆盖现有镜像 / 跳过安全检查 (高风险)")
row2.addWidget(self._ota_force)
row2.addWidget(QLabel("slot:"))
self._ota_slot = QComboBox()
self._ota_slot.addItems([OTA_SLOT_A, OTA_SLOT_B])
self._ota_slot.setMaximumWidth(60)
row2.addWidget(self._ota_slot)
self._ota_info = QLabel("未选择固件")
row2.addWidget(self._ota_info, 1)
layout.addLayout(row2)
# -- 操作按钮 --
row3 = QHBoxLayout()
b_begin = QPushButton("① 下载 (begin/data/end)")
b_begin.setToolTip("开启会话→分片下发→全镜像校验; 已下载过则断点续传")
b_begin.clicked.connect(self._ota_start_download)
row3.addWidget(b_begin)
b_flash = QPushButton("② 刷写 (ota_flash)")
b_flash.setToolTip("仅 state=ready 可触发; 安全检查 (有车拒绝) 后异步执行")
b_flash.clicked.connect(self._ota_send_flash)
row3.addWidget(b_flash)
b_abort = QPushButton("中止 (ota_abort)")
b_abort.clicked.connect(self._ota_send_abort)
row3.addWidget(b_abort)
b_status = QPushButton("查询状态 (ota_status)")
b_status.clicked.connect(self._ota_send_status)
row3.addWidget(b_status)
b_clear = QPushButton("清空日志")
b_clear.clicked.connect(lambda: self._ota_log_text.clear())
row3.addWidget(b_clear)
layout.addLayout(row3)
# -- 进度 --
self._ota_progress = QProgressBar()
self._ota_progress.setRange(0, 100)
self._ota_progress.setValue(0)
layout.addWidget(self._ota_progress)
# -- 状态日志 --
self._ota_log_text = QPlainTextEdit()
self._ota_log_text.setReadOnly(True)
self._ota_log_text.setFont(QFont("Consolas", 9))
self._ota_log_text.setMaximumBlockCount(2000)
layout.addWidget(self._ota_log_text, 1)
# -- 响应队列 (下载线程 ← _on_message) --
self._ota_resp_queue: "queue.Queue" = queue.Queue()
self._ota_thread: Optional[OtaDownloadThread] = None
return w
# ================================================================
# OTA 升级 (V1.08)
# ================================================================
def _ota_choose_file(self):
path, _ = QFileDialog.getOpenFileName(self, "选择固件 bin", "",
"固件文件 (*.bin);;所有文件 (*)")
if path:
self._ota_file.setText(path)
try:
with open(path, "rb") as f:
data = f.read()
if len(data) > 96 * 1024:
QMessageBox.warning(self, "固件过大", f"{len(data)}B 超上限 96KB (Slot 100KB-4KB)")
crc = ota_crc32(data)
self._ota_info.setText(f"{len(data)}B · CRC32=0x{crc:08X}")
except OSError as e:
QMessageBox.critical(self, "读取失败", str(e))
def _ota_load_bin(self) -> Optional[bytes]:
path = self._ota_file.text()
if not path:
QMessageBox.warning(self, "提示", "请先选择固件 bin")
return None
try:
data = open(path, "rb").read()
except OSError as e:
QMessageBox.critical(self, "读取失败", str(e))
return None
if len(data) > 96 * 1024:
QMessageBox.warning(self, "固件过大", f"{len(data)}B 超上限 96KB")
return None
return data
def _ota_log(self, text: str):
self._ota_log_text.appendPlainText(f"[{datetime.now().strftime('%H:%M:%S')}] {text}")
def _ota_start_download(self):
sn = self._require_dev()
if not sn:
return
if self._ota_thread and self._ota_thread.isRunning():
QMessageBox.warning(self, "提示", "OTA 下载已在运行, 先中止")
return
data = self._ota_load_bin()
if data is None:
return
self._ota_log(f"开始 OTA 下载: sn={sn} size={len(data)}B")
self._ota_progress.setValue(0)
self._ota_thread = OtaDownloadThread(
self._mqtt, sn, data,
version=self._ota_version.text().strip() or "0.0.0",
force=self._ota_force.isChecked(),
resp_queue=self._ota_resp_queue,
)
self._ota_thread.progress.connect(self._ota_on_progress)
self._ota_thread.log.connect(self._ota_log)
self._ota_thread.finished_ok.connect(
lambda m: (self._ota_log(f"{m}"),
QMessageBox.information(self, "OTA", m)))
self._ota_thread.finished_err.connect(
lambda m: QMessageBox.critical(self, "OTA 失败", m))
self._ota_thread.start()
def _ota_on_progress(self, sent: int, total: int):
pct = int(sent * 100 / total) if total else 0
self._ota_progress.setValue(pct)
self._ota_progress.setFormat(f"{sent}/{total} B ({pct}%)")
def _ota_send_flash(self):
sn = self._require_dev()
if not sn:
return
confirm = QMessageBox.question(
self, "确认刷写",
"触发 ota_flash 将把暂存镜像刷入 Loop MCU (约 6~8s 窗口, 期间检测中断)。\n"
"请确认现场无车压线圈。继续?",
QMessageBox.Yes | QMessageBox.No)
if confirm != QMessageBox.Yes:
return
self._send_cmd(CMD_OTA_FLASH, data_ota_flash(
slot=self._ota_slot.currentText(), force=self._ota_force.isChecked()))
def _ota_send_abort(self):
if self._ota_thread and self._ota_thread.isRunning():
self._ota_thread.stop()
self._send_cmd(CMD_OTA_ABORT, data_ota_abort())
def _ota_send_status(self):
self._send_cmd(CMD_OTA_STATUS)
def _ota_show_status(self, data: dict):
"""显示 ota_status / ota_report 状态"""
line = []
state = data.get("state", "")
stage = data.get("stage", "")
prog = data.get("progress", {})
if state:
line.append(f"state={state}({OTA_STATE_DESC.get(state, '?')})")
if stage:
line.append(f"stage={stage}({OTA_STAGE_DESC.get(stage, '?')})")
if prog:
line.append(f"progress={prog.get('sent', 0)}/{prog.get('total', 0)}B")
if data.get("received") is not None:
line.append(f"received={data['received']}B")
if data.get("last_result"):
line.append(f"last_result={data['last_result']}")
if data.get("last_error"):
err = OTA_ERROR_DESC.get(data["last_error"], data["last_error"])
line.append(f"last_error={err}")
self._ota_log("状态: " + (" | ".join(line) if line else json.dumps(data, ensure_ascii=False)))
# ================================================================
# 模拟设备上报
# ================================================================
@@ -504,6 +881,8 @@ class MainWindow(QMainWindow):
"model": "DLD960",
"hard_ver": "1.0",
"soft_ver": "1.0",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"extra_info": {
"code": "869756049404948",
"csq": "21",
@@ -873,7 +1252,21 @@ class MainWindow(QMainWindow):
self._show_json({cmd: data})
elif cmd == CMD_REPORT_CONFIG:
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"脱机日志已清除 (审计留痕, 需重新统计确认)")
elif cmd in OTA_CMD_SET:
# OTA 响应 → 喂下载线程响应队列 + 状态显示
self._ota_resp_queue.put((cmd, payload))
self._ota_show_status(payload.get("data", {}))
else:
if cmd in OTA_CMD_SET:
self._ota_resp_queue.put((cmd, payload))
self._ota_log(f"{cmd} 失败 code={code} {pmsg} "
f"(err_code={payload.get('data', {}).get('err_code', '-')})")
self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
elif cmd == CMD_LOOP_DATA:
@@ -909,6 +1302,12 @@ class MainWindow(QMainWindow):
self._log_recv(topic, payload, f"initialize sn={dev_sn} model={model}")
self._show_json({"online": dev_sn, "model": model, "extra": extra})
elif cmd == CMD_OTA_REPORT:
# V1.08: OTA 进度/结果主动上报 (无 code 字段)
data = payload.get("data", {})
self._ota_show_status(data)
self._log_recv(topic, payload, f"ota_report stage={data.get('stage', '?')}")
# 旧协议 / 无 cmd 字段 的消息(如 Initialize
elif "Method" in payload:
self._log_recv(topic, payload, f"Method={payload.get('Method', '?')}")
@@ -967,7 +1366,7 @@ class MainWindow(QMainWindow):
if not sn:
return
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:
self._mqtt.send_command(sn, cmd)
except Exception:
@@ -1078,6 +1477,131 @@ class MainWindow(QMainWindow):
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 更新
# ================================================================
+316
View File
@@ -0,0 +1,316 @@
"""
DLD960 IoT MQTT OTA 协议单测 (V1.08)
验证对象: dbn_mqtt_tool/protocol.py 的 OTA 模块
- ota_crc32: CRC-32/ISO-HDLC 标准向量
- ota_split_bin: 分片边界 (256B 协议常量, 96KB 上限)
- data_ota_*: 命令构建器字段
- OtaDeviceSim: 设备侧接收状态机模拟 (协议 §4.19~4.21 规则)
- 完整下载流: 正常/断点续传/缺片重定位/CRC 错重发/重复片幂等
运行: venv/bin/python -m unittest tests.test_ota_protocol -v
"""
import os
import sys
import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dbn_mqtt_tool.protocol import (
ota_crc32, ota_split_bin, OTA_CHUNK_SIZE, OTA_MAX_SIZE,
data_ota_begin, data_ota_data, data_ota_end, data_ota_abort, data_ota_flash,
OTA_TARGET_LOOP, OTA_SLOT_A,
)
class OtaDeviceSim:
"""模拟固件 OTA 接收状态机 (协议 §4.19 ota_begin / §4.20 ota_data / §4.21 ota_end)
规则 (与协议文档一致):
- 顺序片 (offset == received): 单片 CRC32 校验 → 写暂存 → received += 256
- 重复片 (offset < received): 幂等回 code=0, 不重写
- 缺片 (offset > received): code=1, data.offset=received (要求续传)
- 单片 CRC 失败: code=1 (平台重发本片)
- ota_end: received!=size → code=1; 全镜像 CRC 复核 → ready
"""
def __init__(self, bin_data: bytes):
self.bin = bin_data
self.size = len(bin_data)
self.state = "idle"
self.received = 0
self.storage = bytearray(self.size)
def _base(self, offset: int) -> dict:
return {"target": OTA_TARGET_LOOP, "slot": OTA_SLOT_A,
"offset": offset, "received": self.received,
"size": self.size, "crc32": ota_crc32(self.bin),
"state": self.state}
def ota_begin(self, req: dict) -> dict:
# 镜像一致且已 ready → 直接返回可刷写
if self.state == "ready" and req["size"] == self.size \
and req["crc32"] == ota_crc32(self.bin):
return {"code": 0, "data": self._base(self.size)}
# downloading 中断 → 返回已接收偏移 (断点续传)
if self.state == "downloading":
return {"code": 0, "data": self._base(self.received)}
self.state = "downloading"
self.received = 0
self.storage = bytearray(self.size)
return {"code": 0, "data": self._base(0)}
def ota_data(self, req: dict) -> dict:
offset, crc, hexs = req["offset"], req["crc32"], req["data"]
data = bytes.fromhex(hexs)
if offset == self.received:
if ota_crc32(data) != crc:
return {"code": 1, "err_code": 1, "msg": "crc mismatch",
"data": self._base(self.received)}
self.storage[offset:offset + len(data)] = data
self.received = min(offset + len(data), self.size)
return {"code": 0, "data": self._base(offset)}
if offset < self.received:
return {"code": 0, "data": self._base(offset)} # 幂等
return {"code": 1, "err_code": 1, "msg": "gap/out-of-order",
"data": self._base(self.received)}
def ota_end(self, req: dict) -> dict:
if self.received != self.size:
return {"code": 1, "data": self._base(self.received)}
if ota_crc32(bytes(self.storage)) != req["crc32"]:
return {"code": 5, "err_code": 5, "data": {"crc_ok": False}}
self.state = "ready"
return {"code": 0, "data": {"crc_ok": True}}
class TestOtaCrc32(unittest.TestCase):
def test_standard_vector(self):
"""CRC-32/ISO-HDLC 标准校验向量: crc32('123456789') = 0xCBF43926"""
self.assertEqual(ota_crc32(b"123456789"), 0xCBF43926)
def test_empty(self):
self.assertEqual(ota_crc32(b""), 0x00000000)
def test_zlib_alias(self):
import zlib
for data in (b"", b"a", os.urandom(300)):
self.assertEqual(ota_crc32(data), zlib.crc32(data) & 0xFFFFFFFF)
class TestOtaSplitBin(unittest.TestCase):
def test_small_single_chunk(self):
data = b"\x00\x01\x02\x03"
chunks = ota_split_bin(data)
self.assertEqual(len(chunks), 1)
off, crc, hexs = chunks[0]
self.assertEqual(off, 0)
self.assertEqual(hexs, data.hex())
self.assertEqual(len(hexs), 8) # 4B → 8 hex
self.assertEqual(crc, ota_crc32(data))
def test_exact_chunk(self):
data = bytes(range(256))
chunks = ota_split_bin(data)
self.assertEqual(len(chunks), 1)
self.assertEqual(len(chunks[0][2]), 512) # 256B → 512 hex
def test_two_chunks(self):
data = bytes(range(256)) + b"\x00" # 257B → 2 片
chunks = ota_split_bin(data)
self.assertEqual(len(chunks), 2)
self.assertEqual(chunks[0][0], 0)
self.assertEqual(chunks[1][0], 256)
self.assertEqual(len(chunks[1][2]), 2) # 末片 1B
def test_offsets_256_aligned(self):
data = os.urandom(5000)
for off, _, _ in ota_split_bin(data):
self.assertEqual(off % OTA_CHUNK_SIZE, 0)
def test_roundtrip_reconstruct(self):
data = os.urandom(4097)
merged = b"".join(bytes.fromhex(hexs) for _, _, hexs in ota_split_bin(data))
self.assertEqual(merged, data)
def test_over_max_size(self):
with self.assertRaises(ValueError):
ota_split_bin(os.urandom(OTA_MAX_SIZE + 1))
class TestOtaBuilders(unittest.TestCase):
def test_begin(self):
d = data_ota_begin(46864, 0x12345678, version="1.1.0")
self.assertEqual(d, {"target": "loop", "size": 46864, "crc32": 0x12345678,
"version": "1.1.0", "force": False})
def test_begin_force(self):
d = data_ota_begin(100, 1, force=True)
self.assertTrue(d["force"])
def test_data(self):
d = data_ota_data(0, 0x9E3779B9, "abcd")
self.assertEqual(d, {"target": "loop", "offset": 0,
"crc32": 0x9E3779B9, "data": "abcd"})
def test_end_abort_flash(self):
self.assertEqual(data_ota_end(0x11223344),
{"target": "loop", "crc32": 0x11223344})
self.assertEqual(data_ota_abort(), {"target": "loop"})
self.assertEqual(data_ota_flash(slot="b", force=True),
{"target": "loop", "slot": "b", "force": True})
class TestDeviceSim(unittest.TestCase):
"""设备侧规则: 顺序/幂等/乱序/CRC/全镜像复核"""
def setUp(self):
self.bin_data = bytes(range(256)) * 20 # 5120B = 20 片
self.dev = OtaDeviceSim(self.bin_data)
def test_normal_flow(self):
dev = self.dev
chunks = ota_split_bin(self.bin_data)
resp = dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
self.assertEqual(resp["code"], 0)
self.assertEqual(resp["data"]["offset"], 0)
for off, crc, hexs in chunks:
resp = dev.ota_data(data_ota_data(off, crc, hexs))
self.assertEqual(resp["code"], 0, f"片@{off} 应成功")
self.assertEqual(dev.received, len(self.bin_data))
resp = dev.ota_end(data_ota_end(ota_crc32(self.bin_data)))
self.assertEqual(resp["code"], 0)
self.assertTrue(resp["data"]["crc_ok"])
self.assertEqual(dev.state, "ready")
self.assertEqual(bytes(dev.storage), self.bin_data)
def test_resume_after_begin(self):
"""断点续传: downloading 中断后 begin 返回已接收偏移"""
dev = self.dev
chunks = ota_split_bin(self.bin_data)
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
for off, crc, hexs in chunks[:7]: # 只收 7 片
dev.ota_data(data_ota_data(off, crc, hexs))
self.assertEqual(dev.received, 7 * OTA_CHUNK_SIZE)
# 重新 begin → 续传点
resp = dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
self.assertEqual(resp["data"]["offset"], 7 * OTA_CHUNK_SIZE)
# 从续传点继续
for off, crc, hexs in chunks[7:]:
dev.ota_data(data_ota_data(off, crc, hexs))
self.assertEqual(dev.received, len(self.bin_data))
resp = dev.ota_end(data_ota_end(ota_crc32(self.bin_data)))
self.assertTrue(resp["data"]["crc_ok"])
def test_dup_chunk_idempotent(self):
"""重复片幂等: 已收片重发 → code=0, received 不变"""
dev = self.dev
chunks = ota_split_bin(self.bin_data)
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
off0, crc0, hexs0 = chunks[0]
dev.ota_data(data_ota_data(off0, crc0, hexs0))
r1 = dev.ota_data(data_ota_data(off0, crc0, hexs0)) # 重复
self.assertEqual(r1["code"], 0)
self.assertEqual(r1["data"]["received"], OTA_CHUNK_SIZE)
def test_gap_rejected(self):
"""缺片: offset > received → code=1, data.offset=received"""
dev = self.dev
chunks = ota_split_bin(self.bin_data)
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
off2, crc2, hexs2 = chunks[2] # 跳过 0,1
resp = dev.ota_data(data_ota_data(off2, crc2, hexs2))
self.assertEqual(resp["code"], 1)
self.assertEqual(resp["data"]["offset"], 0) # 要求从 0 续传
def test_chunk_crc_error(self):
"""单片 CRC 错 → code=1, 不落盘"""
dev = self.dev
chunks = ota_split_bin(self.bin_data)
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
off0, _, hexs0 = chunks[0]
resp = dev.ota_data(data_ota_data(off0, 0xDEADBEEF, hexs0)) # 错 crc
self.assertEqual(resp["code"], 1)
self.assertEqual(dev.received, 0) # 未落盘
# 重发正确片 → 成功
resp = dev.ota_data(data_ota_data(off0, ota_crc32(self.bin_data[:256]), hexs0))
self.assertEqual(resp["code"], 0)
def test_end_incomplete(self):
"""ota_end 时 received != size → code=1"""
dev = self.dev
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
resp = dev.ota_end(data_ota_end(ota_crc32(self.bin_data)))
self.assertEqual(resp["code"], 1)
self.assertEqual(resp["data"]["offset"], 0)
def test_end_crc_fail(self):
"""全镜像 CRC 复核失败 → code=5 crc_ok=false"""
dev = self.dev
chunks = ota_split_bin(self.bin_data)
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
for off, crc, hexs in chunks:
dev.ota_data(data_ota_data(off, crc, hexs))
resp = dev.ota_end(data_ota_end(0x00000000)) # 错的全镜像 crc
self.assertEqual(resp["code"], 5)
self.assertFalse(resp["data"]["crc_ok"])
class TestDownloadFlow(unittest.TestCase):
"""工具侧下载流程 (等价 OtaDownloadThread 核心逻辑, 无 Qt 依赖)"""
def _run_flow(self, bin_data: bytes, simulate_gap: bool = False):
dev = OtaDeviceSim(bin_data)
chunks = ota_split_bin(bin_data)
total_crc = ota_crc32(bin_data)
# ota_begin
resp = dev.ota_begin(data_ota_begin(len(bin_data), total_crc))
assert resp["code"] == 0
offset = resp["data"]["offset"]
sent_offsets = []
gap_done = False
while offset < len(bin_data):
idx = offset // OTA_CHUNK_SIZE
off, crc, hexs = chunks[idx]
if simulate_gap and not gap_done and off == 256:
# 模拟一次丢包: 跳过该片直接发下一片 → 设备回 gap → 重定位
gap_done = True
resp = dev.ota_data(data_ota_data(chunks[idx + 1][0],
chunks[idx + 1][1],
chunks[idx + 1][2]))
self.assertEqual(resp["code"], 1)
offset = resp["data"]["offset"] # 重定位
continue
resp = dev.ota_data(data_ota_data(off, crc, hexs))
if resp["code"] == 0:
offset = resp["data"]["received"]
elif resp["code"] == 1:
offset = resp["data"]["offset"] # 重定位
else:
self.fail(f"意外 code={resp['code']}")
sent_offsets.append(off)
# ota_end
resp = dev.ota_end(data_ota_end(total_crc))
self.assertEqual(resp["code"], 0)
self.assertTrue(resp["data"]["crc_ok"])
self.assertEqual(dev.state, "ready")
self.assertEqual(bytes(dev.storage), bin_data)
return sent_offsets
def test_flow_normal(self):
bin_data = os.urandom(3000)
self._run_flow(bin_data)
def test_flow_with_gap_retransmit(self):
"""缺片重传: 丢 1 片后设备要求重定位, 工具补发, 最终全量一致"""
bin_data = os.urandom(3000)
sent = self._run_flow(bin_data, simulate_gap=True)
# 所有片最终都被发送过 (含补发)
self.assertIn(256, sent)
if __name__ == "__main__":
unittest.main(verbosity=2)
+94 -86
View File
@@ -1,86 +1,94 @@
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
> 当前发布版本**V1.0.0**2026-07-16)· 变更历史见 [CHANGELOG.md](CHANGELOG.md)
## 产品概述
DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、RS485、蓝牙(小程序配置 + OTA),面向停车场出入口控制、流量计数、会车控制等场景。
产品型号:**DLD960GA**
## 硬件架构(双 MCU
```
┌─────────────────────────────────────────┐
线圈1~4 ──────────► AT32F421 (vd960Loop) │
(TMR3 输入捕获) │ · 四路线圈驱动/频率采样 │
继电器1~4 ◄───────│ · 车辆检测算法(IIR/斜率限幅/基线跟踪) │
└───────────────┬─────────────────────────┘
│ UART 0x7F 协议 @192000
┌───────────────▼─────────────────────────┐
蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │
网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │
RS485 ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │
│ · 参数/日志外部存储 │
└─────────────────────────────────────────┘
```
| 芯片 | 型号 | 主频 | Flash | SRAM | 职责 |
|------|------|------|-------|------|------|
| 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 |
| 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz | 128KB | 64KB | 蓝牙、网口、RS485、外部存储 |
- 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。
- 支持 OTACH32V208 经蓝牙小程序 OTAAT32F421 经 CH32V208 串口透传 ISP 升级。
- 地感配置存储于 AT32F421,可经 小程序↔BLE↔CH32V208↔UART↔AT32F421 链路读写。
详见 [docs/DLD960硬件资源.md](docs/DLD960硬件资源.md)。
## 子项目
| 子项目 | 目录 | MCU | 固件版本 | 功能 |
|--------|------|-----|---------|------|
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
| vd960DBN | `vd960DBN/` | CH32V208 | 1.0 | 蓝牙管理、TCP/IPWCHNET)、MQTT、RS485、串口桥接 |
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
## 协议文档(docs/
| 文档 | 版本 | 适用层 | 说明 |
|------|------|--------|------|
| [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 |
| [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 RS485/TTL | 设备管理、参数配置、数据上报 |
| [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 平台必答、设备时钟同步 |
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
## 开发文档
| 文档 | 说明 |
|------|------|
| [vd960Loop/docs/devlog.md](vd960Loop/docs/devlog.md) | 检测算法开发日志(IIR、斜率限幅、基线冻结、时间量单位演进) |
| [vd960DBN/docs/devlog.md](vd960DBN/docs/devlog.md) | 通信固件开发日志(WCHNET、MQTT 稳定性、event_report 可靠上报 |
| [vd960Loop/docs/acceptance-standard.md](vd960Loop/docs/acceptance-standard.md) | 检测算法验收标准 |
| [vd960Loop/docs/variation-analysis.md](vd960Loop/docs/variation-analysis.md) | variation 上报量分析报告(基线 vs 当前值三时钟关系) |
| [vd960DBN/docs/incidents/](vd960DBN/docs/incidents/) | 现场事故分析归档 |
## 配套工具
| 工具 | 目录 | 说明 |
|------|------|------|
| DBNetClient | `DBNetClient/` | TCP JSON 协议桌面测试工具(Python + tkinter,零第三方依赖) |
| DBNMQTTool | `DBNMQTTool/` | IoT MQTT 协议测试工具(Python + PySide6 + paho-mqtt,双主题 UI |
## 开发环境
- **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 串口
## 版本发布流程
1. 确认 `vd960Loop``vd960DBN` 固件按同一协议版本联调通过;
2. 更新两侧 `cmcng.h``FIRMWARE_VER`
3. 更新 `CHANGELOG.md` 与各 devlog 修订记录;
4. 打 tag`vX.Y.Z`)推送 Gitea,并在 Gitea Releases 挂发布说明与固件产物(hex/bin)。
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
> 当前发布:**vd960DBN V1.02.05**2026-08-21,DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md)
## 产品概述
DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、蓝牙(小程序配置 + OTA)、4G 扩展(RFU,面向停车场出入口控制、流量计数、会车控制等场景。
产品型号:**DLD960GA**
## 硬件架构(双 MCU
```
┌─────────────────────────────────────────┐
线圈1~4 ──────────► AT32F421 (vd960Loop) │
(TMR3 输入捕获) │ · 四路线圈驱动/频率采样 │
继电器1~4 ◄───────│ · 车辆检测算法(IIR/斜率限幅/基线跟踪) │
└───────────────┬─────────────────────────┘
│ UART 0x7F 协议 @192000
┌───────────────▼─────────────────────────┐
蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │
网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │
4G(RFU) ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │
│ · 参数/日志外部存储 │
└─────────────────────────────────────────┘
```
| 芯片 | 型号 | 主频 | Flash | SRAM | 职责 |
|------|------|------|-------|------|------|
| 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 |
| 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz | 128KB | 64KB | 蓝牙、网口、双路 TTL 串口(内部互联 + 4G 扩展)、外部存储 |
- 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。
- 支持 OTACH32V208 经蓝牙小程序 OTAAT32F421 经 CH32V208 串口透传 ISP 升级。
- 地感配置存储于 AT32F421,可经 小程序↔BLE↔CH32V208↔UART↔AT32F421 链路读写。
详见 [docs/DLD960硬件资源.md](docs/DLD960硬件资源.md)。
## 子项目
| 子项目 | 目录 | MCU | 固件版本 | 功能 |
|--------|------|-----|---------|------|
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
| vd960DBN | `vd960DBN/` | CH32V208 | 1.02.05 | 蓝牙管理、TCP/IPWCHNET)、MQTT、串口桥接、4G 扩展(RFU |
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
## 产品文档(docs/
| 文档 | 版本 | 说明 |
|------|------|------|
| [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.03 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 |
| [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.03 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 |
## 协议文档(docs/
| 文档 | 版本 | 适用层 | 说明 |
|------|------|--------|------|
| [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 客户端必答 + 脱机日志(事件/快照流) |
| [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.13 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initializeextra_info 含 imei/iccid)、event_report 平台必答、设备时钟同步、脱机日志(事件/快照流)、Loop 远程 OTA(ota_* 命令,先存后刷)、**4G 通道适配(方案 C:Air780 协议转换,标准 JSON 命令面 + linkV1.12** |
| [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.03 | 蓝牙 BLE | 帧格式 + 分包 + 配置命令全表(0x09~0x24/0x31/0x8A/0x92/0xC5,未实现如实标注)+ 脱机日志(OFFLOG_STAT/QUERY/CLEAR+ 传感快照(SNAP_STAT/QUERY/CLEAR+ 透传(0x7F/0x9F |
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
## 开发文档
| 文档 | 说明 |
|------|------|
| [vd960Loop/docs/devlog.md](vd960Loop/docs/devlog.md) | 检测算法开发日志(IIR、斜率限幅、基线冻结、时间量单位演进) |
| [vd960DBN/docs/devlog.md](vd960DBN/docs/devlog.md) | 通信固件开发日志(WCHNET、MQTT 稳定性、event_report 可靠上报) |
| [vd960Loop/docs/acceptance-standard.md](vd960Loop/docs/acceptance-standard.md) | 检测算法验收标准 |
| [vd960Loop/docs/variation-analysis.md](vd960Loop/docs/variation-analysis.md) | variation 上报量分析报告(基线 vs 当前值三时钟关系 |
| [vd960DBN/docs/incidents/](vd960DBN/docs/incidents/) | 现场事故分析归档 |
## 配套工具
| 工具 | 目录 | 说明 |
|------|------|------|
| DBNetClient | `DBNetClient/` | TCP JSON 协议桌面测试工具(Python + tkinter,零第三方依赖) |
| DBNMQTTool | `DBNMQTTool/` | IoT MQTT 协议测试工具(Python + PySide6 + paho-mqtt,双主题 UI |
## 开发环境
- **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 串口
## 版本发布流程
1. 确认 `vd960Loop``vd960DBN` 固件按同一协议版本联调通过;
2. 更新两侧 `cmcng.h``FIRMWARE_VER`
3. 更新 `CHANGELOG.md` 与各 devlog 修订记录;
4. 打 tag`vX.Y.Z`)推送 Gitea,并在 Gitea Releases 挂发布说明与固件产物(hex/bin)。
+28 -7
View File
@@ -147,7 +147,7 @@ A2 00 51 00: 第0级灵敏度,0x00A2为进入 1620x0051为离开 81
* AutoMode: 是否处于自动模式(自动调频),0表示不启用,默认0。
* Sensitivity 低四位表示灵敏度,0~9级,值越大,灵敏度越高,默认7;高四位表示高低频,其中,\[4,5\]表示线圈的高低频。
* Sensitivity 低四位表示灵敏度序号,**4级制 0~3**(0 最低,3 最高),默认 2;9级制灵敏度后续版本实现。高四位表示高低频,其中,\[4,5\]表示线圈的高低频。
* Loop\_Delay: 地感延时时间,0~200, 0.1s为一级,延时时间范围:\[0, 20秒\];
@@ -176,10 +176,28 @@ A2 00 51 00: 第0级灵敏度,0x00A2为进入 1620x0051为离开 81
返回:
| | Header | AutoMode+Amount + Data\[Sensitivity, Loop_Delay, Output_Mode, Exist_Mode, Direction_Mode, freq1,  freq2, freq3\] |
| | Header | AutoMode + Amount + Data\[Sensitivity, Loop_Delay, Output_Mode, Exist_Mode, Direction_Mode, freq1, freq2, freq3\] + **LOOP_MEASURE_BASE(3B LE)** |
| --- | --- | --- |
| **Value(Hex)** | 00 xx 64 | XX |
| **Length** | 3 Byte | (2 + 8 \* Amount) Byte |
| **Value(Hex)** | 00 xx 64 | XX |
| **Length** | 3 Byte | (2 + 8 \* Amount + **3**) Byte |
**V1.09 起响应尾部追加 3B LE `LOOP_MEASURE_BASE`variation 归一化基数,本产品 = 131072 = 2^17**
| 偏移(相对 Data 起点) | 字节 | 内容 |
| --- | --- | --- |
| 0 | 1 | AutoModesmart_mode |
| 1 | 1 | 线圈数量 Amount |
| 2 .. 2+8×Amount1 | 8×Amount | 每路线圈参数(Sensitivity, Loop_Delay, Output_Mode, Exist_Mode, Direction_Mode, freq1, freq2, freq3 |
| 2+8×Amount .. +2 | 3 | **LOOP_MEASURE_BASE** 小端(低字节在前)。本产品固定 `00 00 02` = 0x00020000 = **131072** |
**用途(第三方评估换算)**:上报变化量 variation 是**周期域计数值**`variation = Origin CAPVD`,CAPVD ∝ √L 而非 L),归一化必须除本基数:
```
Δf/f ≈ variation / LOOP_MEASURE_BASE
ΔL/L ≈ -2 × variation / LOOP_MEASURE_BASE (系数 2 来自 CAPVD ∝ √L 展开,阈值区间残差 <0.2%)
```
> ⚠ **各产品 MEASUREMENT_BASE 不同**AT32@120MHz = 131072CH59x@60MHz = 524288),接收端/第三方应用**禁止写死任一型号常量**,一律从 0x64 响应读取本基数,否则所有百分比放大/缩小 4 倍。
## 3.07 设备主动上报传感信息(0xC0)
@@ -212,10 +230,10 @@ SensData:传感数据
| 字段 | 字节数 | 内容 | 详情 |
| --- | --- | --- | --- |
| 配置 1 | 1 | freq\_level(2bit), Direction(1bit)freq\_type(1bit), sens(4bit) | 线圈 1 配置freq\_level:高低频,两个比特位,00 表示高频(33nF),01表示中高(43nF), 10表示中低(66nF), 11表示低频(76nF)。Direction: 0 表示触发,1 表示方向判别,freq\_type: 0 表示初始频率,1 表示当前实时频率。sens:低四位表示当前的灵敏度等级 |
| 配置 1 | 1 | freq\_level(2bit), Direction(1bit)freq\_type(1bit), sens(4bit) | 线圈 1 配置freq\_level:高低频,两个比特位,00 表示高频(33nF),01表示中高(43nF), 10表示中低(66nF), 11表示低频(76nF)。Direction: 0 表示触发,1 表示方向判别,freq\_type: 0 表示初始频率,1 表示当前实时频率。sens:低四位表示当前的灵敏度等级(**4级制 0~3,值越大越灵敏**) |
| 线圈评估条件 | 1 | condition(4bit), loop\_state(1bit),<br>car\_state(1bit),<br>report\_msic(2bit) | loop\_condition 正常(0)有效,环境状态条件(环境状态评估值,值越大,干扰越大),高四位有效。低四位中 ,其中第3位表示线圈状态,0 表示正常,1 表示线圈断开;第 2 位表示有无车,0 表示无车,1 表示有车;低两位表示**杂项类型**,0b00表示时间量,0b01表示线圈断开次数;0b10表示车流量数;0b11表示继电器输出次数 |
| 频率 1 | 3 | frequent | 低字节在前,无符号 |
| 变化量 1 | 3 | variation | **有符号** 3 字节,低字节在前(小端),补码表示。定义 `variation = loop_Origin loop_CAPVD`(基线 − 当前值)。**正值**表示当前值低于基线(车辆/金属进入,检测裕量方向);**负值**表示当前值高于基线(反向漂移/异常抬升)。值域 ±8388607(±2²³−1),发送端已做饱和限幅,不会回绕。<br>**接收端解析**:读入 3 字节后须做符号扩展——`v = b0\|(b1<<8)\|(b2<<16); if (v & 0x800000) v \|= 0xFF000000;` |
| 变化量 1 | 3 | variation | **有符号** 3 字节,低字节在前(小端),补码表示。定义 `variation = loop_Origin loop_CAPVD`(基线 − 当前值)。**正值**表示当前值低于基线(车辆/金属进入,检测裕量方向);**负值**表示当前值高于基线(反向漂移/异常抬升)。值域 8388608 ~ +8388607(−2²³ ~ +2²³−1,3B 补码标准值域),发送端已做饱和限幅,不会回绕。<br>**接收端解析**:读入 3 字节后须做符号扩展——`v = b0\|(b1<<8)\|(b2<<16); if (v & 0x800000) v \|= 0xFF000000;` |
| 杂项 | 4 | in\_out\_passtime/cut\_amount/flow\_amount | **杂项类型**,可能是时间量,也可能是线圈断开次数、车流量数,说明:<br>1、时间量有两种类型,通过时间或车间距,**50ms 为单位**(固件 50ms tickTMR15 5ms×10 分频)。通过时间是从有车开始到车辆离开的时间(上报时car\_state 为0);车间距是 从上一次车辆离开到有车时候的时间(上报时car\_state 为1)。非0有效。<br>2、线圈断开次数。<br>3、车流量数。<br>4、继电器输出次数。 |
eg:
@@ -260,4 +278,7 @@ FE 38 校验字节(XOR=0xFE, SUM=0x38
| V1.03 | 2026-07-02 | 0x8A 响应格式 + LEN 计算修正 | wangfq |
| V1.04 | 2026-07-02 | 主动上报增加继电器输出次数 类型 | wangfq |
| V1.05 | 2026-07-14 | 多线圈上报(0x0C)变化量 variation 由 2 字节无符号扩展为 **3 字节有符号**(补码, 小端),定义 `variation = Origin CAPVD`(正=车/裕量, 负=反向漂移);每单元 11→12 字节,Len 47→51。接收端须做符号扩展。**Loop 固件与 DBN 固件须同版本发布。** | wangfq |
| V1.06 | 2026-07-15 | 修正杂项时间量单位说明:5ms → **50ms**(对齐固件 50ms tick 实现,TMR15 5ms×10 分频) | wangfq |
| V1.06 | 2026-07-15 | 修正杂项时间量单位说明:5ms → **50ms**(对齐固件 50ms tick 实现,TMR15 5ms×10 分频) | wangfq |
| V1.07 | 2026-08-25 | variation 值域表述勘误:`±8388607`**8388608 ~ +8388607**(对齐代码负向饱和边界 0x800000,3B 补码标准值域) | wangfq |
| V1.08 | 2026-08-25 | 灵敏度收敛为 **4级制 0~3**(0 最低,3 最高,默认 2):修复 9级制映射 `&0x03` 折叠(7→3、8→0 错乱)导致"改灵敏度感应高度无区别";运行中配置灵敏度立即生效(不再依赖重启)。9级制后续版本实现 | wangfq |
| V1.09 | 2026-08-27 | 0x64 读参响应尾部追加 **3B LE `LOOP_MEASURE_BASE`**variation 归一化基数 131072 = 2^17,与固件 MEASUREMENT_BASE 同步);新增换算公式 `ΔL/L ≈ -2×variation/LOOP_MEASURE_BASE``Δf/f ≈ variation/LOOP_MEASURE_BASE`(跨产品基数对齐,防第三方评估 ×4/÷4 陷阱) | wangfq |
+486
View File
@@ -0,0 +1,486 @@
# DLD960 BLE 通信协议
> 版本: V1.032026-08-31,命令总表补齐既有配置命令 0x09~0xC5 + 透传通道;未实现命令如实标注)
> 适用: vd960DBNCH32V208WCH BLE 协议栈)
> 用途: 小程序/APP 经蓝牙对 vd960DBN 进行**配置管理、参数查询、脱机日志/传感快照读取**(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
---
## 1 帧格式
```
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
- 校验实现:`check_pkg()` / `compute_ckb()`dbn_ble_srv.c
### 1.1 魔数(Magic
| 魔数 | 名称 | 用途 |
|------|------|------|
| `0x8F` | MAGIC_BYTE_DBN_DEFAULT | 设备配置/查询/日志/快照命令族(本协议主体) |
| `0x7F` | MAGIC_BYTE_DBN_TRANSPARENT | 透传:显示屏协议、地感协议、探头协议等 → 原样转发 UART2(Loop MCU 口) |
| `0x9F` | MAGIC_BYTE_DBN_SUB_OTA | 透传子设备 OTA 升级(g_flag_counter_ota.flag 时进入透传模式) |
### 1.2 分包(请求与响应)
**请求分包**unpack_packs0x13/0x15/0x1C/0x1D/0x22 等长请求):header 字节高 4 位 = 总包数,低 4 位 = 序号(从 1 递增);首包初始化重组缓冲,中间包校验 `amount/cmd/seq 连续`,末包 `amount == seq` 触发处理。校验失败丢弃整包。
**响应分包**set_response_to_notify):单包 data 上限**随协商 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 命令总表
| 命令码 | 名称 | 方向 | 实现状态 |
|--------|------|------|---------|
| `0x09` | UPDATE_DEV_SERIAL | APP→设备 | ✅ §3.1 |
| `0x10` | GET_DEV_INFO | APP→设备 | ✅ §3.2 |
| `0x11` | SET_LSSC_NET | APP→设备 | ✅ §3.3 |
| `0x12` | GET_LSSC_NET | APP→设备 | ✅ §3.4 |
| `0x13` | SET_IOT_NET | APP→设备 | ✅ §3.5 |
| `0x14` | GET_IOT_NET | APP→设备 | ✅ §3.6 |
| `0x15` | SET_IOT_TOPIC | APP→设备 | ✅ §3.7 |
| `0x16` | GET_IOT_TOPIC | APP→设备 | ✅ §3.8 |
| `0x17` | SET_SUB_TRAFFIC_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x18` | GET_SUB_TRAFFIC_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x1C` | CHECK_PASS | APP→设备 | ✅ §4.1 |
| `0x1D` | MODIFY_PASS | APP→设备 | ✅ §4.2 |
| `0x1E` | SET_FACTORY | APP→设备 | ✅ §4.3 |
| `0x1F` | RESET_DEV | APP→设备 | ✅ §4.4 |
| `0x20` | SET_NOTIFY | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x21` | SET_NOTIFY_LOOP | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x22` | SET_SUB_CODE | APP→设备 | ✅ §4.5 |
| `0x23` | SET_CJQ_PARAM | APP→设备 | ⚠ 空壳(仅回 [0],参数未处理)§4.6 |
| `0x24` | GET_CJQ_PARAM | APP→设备 | ⚠ 空响应(无数据)§4.7 |
| `0x25` | OFFLOG_STAT | APP→设备 | ✅ §6 |
| `0x26` | OFFLOG_QUERY | APP→设备 | ✅ §7 |
| `0x27` | OFFLOG_CLEAR | APP→设备 | ✅ §8 |
| `0x28` | SNAP_STAT | APP→设备 | ✅ §9 |
| `0x29` | SNAP_QUERY | APP→设备 | ✅ §9 |
| `0x2A` | SNAP_CLEAR | APP→设备 | ✅ §9 |
| `0x31` | RW_UART_BAUD | APP→设备 | ✅ §4.8 |
| `0x87` | LOOP_SAMPLE_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x88` | LOOP_BALANCE_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x89` | LOOP_RELEASE_PLANB | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x8A` | LOOP_SENS_LIST | APP→设备 | ⚠ 空响应(无数据)§4.9 |
| `0x92` | SET_CJQ_FACTORY | APP→设备 | ✅ §4.10 |
| `0xC0` | SUB_SENS_REPORT | 设备→APP | ⚠ 未实现(report_sens_acs 仅声明无定义;0xC5 使能后无上报动作) |
| `0xC5` | SENS_ACS_ENABLE | APP→设备 | ✅(设置生效,无响应)§4.11 |
> 与 MQTT / TCP JSON 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同(V1.02 起)。
---
## 3 设备配置命令
### 3.1 更改设备序列码 `UPDATE_DEV_SERIAL` (0x09)
**请求 data6B):** 新序列码(6 字节,g_dev_number 更新源为 `alter_dev_serila()`)。
**响应 data1B):** `status``0x01`=成功。
### 3.2 查询设备信息 `GET_DEV_INFO` (0x10)
**请求 data** 无(`Len=1`)。
**响应 data19 + N 字节):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 6 | `mac` | 设备 MAC 地址(gMacAddr |
| 6 | 1 | `hw_ver_main` | 硬件版本主 |
| 7 | 1 | `hw_ver_sub` | 硬件版本次 |
| 8 | 1 | `fw_ver_main` | 固件版本主 |
| 9 | 1 | `fw_ver_sub` | 固件版本次 |
| 10 | 1 | `model_len` | 型号字符串长度 N |
| 11 | N | `model` | 产品型号(PRODUCT_MODELASCII |
| 11+N | 6 | `dev_number` | 设备编号(6B |
| 17+N | 1 | `sub_code_lo` | 子功能码低字节(当前恒 0) |
| 18+N | 1 | `sub_code_hi` | 子功能码高字节(当前恒 0) |
| 19+N | 1 | `bus1_amount` | 总线1 设备数(当前恒 0) |
| 20+N | 1 | `bus2_amount` | 总线2 设备数(当前恒 0) |
> `sub_code` / `bus_amount` 字段代码中为占位(注释掉的旧逻辑),恒 0。
### 3.3 设置 SSC 网络配置 `SET_LSSC_NET` (0x11)
**请求 data30B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 4 | `lip` | 本机 IP |
| 4 | 4 | `subnet_mask` | 子网掩码 |
| 8 | 4 | `route_ip` | 网关 |
| 12 | 4 | `lssc_ip` | 中心 IP |
| 16 | 4 | `dns` | DNS(代码未读入,保留位) |
| 20 | 2 | `port_ssc_tcp` | SSC TCP 端口(小端) |
| 22 | 2 | `port_ssc_udp` | SSC UDP 端口(小端) |
| 24 | 2 | `port_ssc_udp_message` | SSC UDP 消息端口(小端) |
| 26 | 2 | `port_dev_tcp` | 设备 TCP 端口(小端) |
| 28 | 2 | `port_dev_udp` | 设备 UDP 端口(小端) |
**响应 data1B):** `status``0x00`=成功(配置写入 Flash)。
### 3.4 查询 SSC 网络配置 `GET_LSSC_NET` (0x12)
**请求 data** 无。
**响应 data30B):** 与请求布局一致:`lip(4)+sub(4)+gw(4)+lssc_ip(4)+dns(4)+` 5×端口(2B 小端)。注意响应含 `dns`(读 local_net_cfg.dns)。
### 3.5 设置 IoT 网络配置 `SET_IOT_NET` (0x13)
**请求 data** 字符串数组,`0x00` 分隔(支持分包,见 §1.2):
```
<host>\0<port(ascii)>\0<client_id>\0<username>\0<password>\0
```
- `host`MQTT 服务器地址
- `port`:MQTT 端口(ASCII 十进制,代码逐字符换算)
- `client_id`ClientID**单字节 `0x20`(空格)表示使用本地序列号**
- `username` / `password`MQTT 鉴权
**响应 data1B):** `status``0x00`=成功(写入 Flash)。
### 3.6 查询 IoT 网络配置 `GET_IOT_NET` (0x14)
**请求 data** 无。
**响应 data** 字符串数组,`0x00` 分隔(同 §3.5 布局):`<remote_addr>\0<port(ascii)>\0<client_id>\0<username>\0<password>\0`
### 3.7 设置设备 Topic `SET_IOT_TOPIC` (0x15)
**请求 data** 字符串数组,`0x00` 分隔(支持分包):
```
<clientid_enable+0x30>\0<topic_pub>\0<topic_sub>\0
```
- `clientid_enable`ASCII 数值,解析时减 `0x30` 还原
**响应 data1B):** `status``0x00`=成功(写入 Flash)。
### 3.8 查询设备 Topic `GET_IOT_TOPIC` (0x16)
**请求 data** 无。
**响应 data**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `clientid_enable` | `g_iot_topic.clientid_enable + 0x30` |
| 1 | 1 | `rsvd` | 保留(0 |
| 2 | N | `topic_pub` | 发布主题(`\0` 结尾) |
| 2+N+1 | M | `topic_sub` | 订阅主题(`\0` 结尾) |
---
## 4 安全与控制命令
### 4.1 验证密码 `CHECK_PASS` (0x1C)
**请求 data6B):** 密码明文(ASCII)。
**响应 data1B):** `status``0x00`=通过;`0x01`=失败(或长度≠6)。
### 4.2 修改密码 `MODIFY_PASS` (0x1D)
**请求 data12B):** 旧密码(6B+ 新密码(6B)。
**响应 data1B):** `status``0x00`=成功(旧密码校验通过并写入);`0x01`=失败(长度≠12 或旧密码错误)。
> 实现位置:`unpack_packs()`switch 无 case,属分包收尾处理),V1.03 文档补录。
### 4.3 设备出厂初始化 `SET_FACTORY` (0x1E)
**请求 data** 无。
**响应 data1B):** `status``0x00`(先回包后执行 `factory_dev_info()` 写默认参数)。⚠ 未整机复位(代码 TODO:仅复位蓝牙以外部分)。
### 4.4 设备复位 `RESET_DEV` (0x1F)
**请求 data** 无。**无响应**(直接 `NVIC_SystemReset()`)。
### 4.5 设置子功能码 `SET_SUB_CODE` (0x22)
**请求 data2B):** `code_set`(小端,bit 定义如下)。**无响应**(生效并持久化 `update_sub_code_enable()`)。
| bit | 子功能 | bit | 子功能 |
|-----|--------|-----|--------|
| 0 | net_enableSSC 网络) | 6 | radar_enable(雷达) |
| 1 | iot_enableIoT MQTT | 9 | laser_enable(激光) |
| 2 | custom_enable(自定义) | 15 | lora_enableLoRa |
| 3 | loop_enable(地感) | | |
| 4 | dgdus_enable(地感采集器) | | |
| 5 | wbdus_enable(称重采集器) | | |
### 4.6 设置车检器参数 `SET_CJQ_PARAM` (0x23)
**⚠ 空壳:** 代码中参数解析被注释(`unpack_pkg_set_cjq_param` 未调用),仅返回 `[0x00]` 伪成功。**参数实际未写入。**
### 4.7 读取车检器参数 `GET_CJQ_PARAM` (0x24)
**⚠ 空响应:** 仅构造响应帧,data 长度 = 0。**无有效数据返回。**
### 4.8 读写 UART 波特率 `RW_UART_BAUD` (0x31)
**请求 data** `rw(1B)` + 数据(`rw≠0` 时):`uart_num(1B) + baud(3B 小端)`
- `rw=0`:读。
**读响应 data10B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `rw` | 0(读) |
| 1 | 1 | `status` | 0x01 |
| 2 | 1 | `idle_timeout` | BLE 空闲超时(g_max_counter_bt_min |
| 3 | 3 | `baud_1` | UART1 波特率(3B 小端) |
| 6 | 1 | `idx_2` | 0x02(第二组标识) |
| 7 | 3 | `baud_2` | 第二组波特率(3B 小端) |
- `rw≠0`:写。`alter_dev_baud(&pkg[5])` + 更新 `g_storage_uart_num/baud`
**写响应 data9B 语义):** `rw(1B) + status(1B) + baud(3B 小端) + reserved(4B)`
> ⚠ **固件实际多发 1B**(baud 低字节重复写入,共 10B):接收端以前 3B(offset 2..4)为准。
### 4.9 读取 Loop 灵敏度列表 `LOOP_SENS_LIST` (0x8A)
**⚠ 空响应:** case 仅 `memset` 无组包。**无有效数据返回。**
### 4.10 车检器出厂初始化 `SET_CJQ_FACTORY` (0x92)
与 §4.3 `SET_FACTORY`0x1E)合并处理:响应 `[0x00]` + 执行 `factory_dev_info()`
### 4.11 传感主动上报使能 `SENS_ACS_ENABLE` (0xC5)
**请求 data3B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `enable` | 0=关闭;非0=开启 |
| 1 | 1 | `interval` | 上报间隔系数(实际 = 值 × 0x7F,单位 tick 待确认) |
| 2 | 1 | `timeout_min` | 超时分钟数(timeout_counter = 值 × 60 × 100 tick |
**无响应。** ⚠ 即使使能,`report_sens_acs()` 无定义无调用,**当前无 0xC0 BLE 主动上报**(上报走 MQTT/TCP 通道)。
---
## 5 透传通道
### 5.1 通用透传 (0x7F)
Magic=`0x7F` 的整帧(含校验)→ `manage_dbn_ble_transparent()`**原样转发 UART2**(Loop MCU 口)。用于显示屏协议、地感协议、探头协议等。响应分包时 header 的 Addr/Sub 字节 = pkg_seq(见 set_response_to_notify 单包分支)。
### 5.2 子设备 OTA 透传 (0x9F)
`g_flag_counter_ota.flag != 0` 时所有 BLE 接收直接走透传 → UART2。用于 Loop MCU OTA0x9F A5/A6/A7 帧透传,协议见《DLD960Loop 串口通信协议》OTA 章节)。
---
## 6 查询日志统计 `OFFLOG_STAT` (0x25)
**请求 data** 无(`Len=1`,仅 cmd 字节)。
**响应 data19B,全小端):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用(Flash 初始化失败) |
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
| 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| 7 | 4 | `capacity` | 容量上限(**随存储芯片动态**W25Q32=16256 / W25Q64=32640 / W25Q128=65408 / W25Q256=130944 |
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
| 15 | 4 | `seq_last` | 最新一条全局序号 |
示例(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
```
---
## 7 分页拉取日志 `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 原始结构(小端,见 §10) |
记录顺序 = 逻辑序号升序(与 `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
```
---
## 8 清除日志 `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` 继续递增(序号不复位)。
---
## 9 传感快照日志(0x28/0x29/0x2AV1.02 新增)
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
### 9.1 查询快照统计 `SNAP_STAT` (0x28)
**请求 data** 无(`Len=1`)。
**响应 data19B,全小端):** 与 §6 `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` | 最新一条全局序号 |
### 9.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 原始结构(小端,见 §9.4) |
记录顺序 = 逻辑序号升序。QUERY 响应最大 130BMTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
### 9.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` 继续递增(序号不复位)。
### 9.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`。未同步段仅相对时间。
---
## 10 记录格式(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)。
---
## 11 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| 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 |
| V1.03 | 2026-08-31 | 命令总表补齐既有配置命令(0x09~0x24、0x31、0x8A、0x92、0xC5+ 透传通道(0x7F/0x9F);对照 dbn_ble_srv.c 如实标注未实现命令(0x17/0x18/0x20/0x21/0x87/0x88/0x89 无 case0x23/0x24/0x8A 空壳;0xC0 上报未实现) |
+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) |
+819 -4
View File
@@ -2,6 +2,7 @@
> 基于《DLD960 串口通信协议》V1.01,将设备管理、参数配置、数据上报映射到 MQTT 协议。
> 交互格式:JSON。
> 版本:V1.142026-09-10,新增 §6.9 4G 配置同步(BLE → DBN → Air780);0x8F 定义为本机侧私有帧,帧长上限 260B)
---
@@ -96,6 +97,19 @@ dld960/{dev_serial}/{direction}
| 5 | 内部错误 |
| 6 | 数据超长 |
### OTA 细分错误码(V1.08
顶层 `code` 保持通用语义,`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 |
## 2.3 设备时钟同步(`ts` 语义)
设备无 RTC/SNTP 时间源,上电后本地时钟为**上电秒数**(从 0 递增)。为使上行数据带真实 Unix 时间:
@@ -110,6 +124,12 @@ dld960/{dev_serial}/{direction}
- 校准前(含首个 `initialize`),`ts` 为上电秒数;平台应能容忍并可按数量级区分。
- 设备重启后需重新校准(无掉电保持)。平台在每次设备 `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 命令详表
@@ -131,10 +151,25 @@ dld960/{dev_serial}/{direction}
| `loop_param_set` | 设置车检器多路参数 | srv→dev | 0x63 |
| `loop_param_query` | 读取车检器多路参数 | srv→dev | 0x64 |
| `report_config` | 设置主动上报 | srv→dev | 0xC5 |
| `log_stat` | 查询脱机日志统计(事件/快照流) | srv→dev | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | srv→dev | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | srv→dev | — |
| `ota_begin` | 开启 OTA 会话 / 断点续传定位 | srv→dev | — |
| `ota_data` | OTA 分片下发(256B/片,单片 CRC32 | srv→dev | — |
| `ota_end` | 结束 OTA 下载,全镜像 CRC32 复核 | srv→dev | — |
| `ota_abort` | 中止 OTA 会话,释放暂存 | srv→dev | — |
| `ota_flash` | 触发本地 ISP 刷写(仅 ready 态) | srv→dev | — |
| `ota_status` | 查询 OTA 状态(含进度) | srv→dev | — |
| `loop_version_query` | 实时查询地感 Loop MCU 版本(V1.10 | srv→dev | 0x4A |
| `frame_cmd` | 4G 通道原始帧透传下发(hex 封装,可选兜底) | srv→dev | — |
| `initialize` | 设备上电初始化登陆 | dev→srv | — |
| `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 |
| `event_report` | 事件上报(**平台须应答**,见 §5.3 | dev→srv | — |
| `ota_report` | OTA 进度/结果主动上报 | dev→srv | — |
| `heartbeat` | 设备心跳 | dev→srv | — |
| `frame_report` | 4G 通道原始帧透传上报(hex 封装,可选兜底) | dev→srv | — |
> 4G 通道(方案 C,见 §6):上行由 Air780 解析 0x7F 帧并转换为**标准 JSON 命令**loop_data / event_report / initialize / heartbeat,与有线通道一致),平台无感。`frame_cmd` / `frame_report` 保留为**可选兜底**Air780 未实现转换的命令 / 未识别帧透传),有线通道不使用。
---
@@ -196,6 +231,8 @@ dld960/{dev_serial}/{direction}
"dev_serial": "A1B2C3D4E5F6",
"hard_ver": "1.1",
"soft_ver": "1.1",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"model": "DLD960",
"product_code": "960001",
"sub_code": {
@@ -215,14 +252,18 @@ dld960/{dev_serial}/{direction}
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `dev_serial` | string | 12 位十六进制序列码 |
| `hard_ver` | string | 硬件版本,格式 `"主.次"` |
| `soft_ver` | string | 软件版本,格式 `"主.次"` |
| `hard_ver` | string | 硬件版本(整机),格式 `"主.次"` |
| `soft_ver` | string | 软件版本(整机 DBN MCU 固件),格式 `"主.次"` |
| `loop_ver` | string | **地感 Loop MCU 固件版本**V1.10),格式 `"主.次.次"`(如 `1.2.3`);来自 Loop 0x4A 查询缓存,**查询未完成/失败则为空字符串** |
| `loop_hw_ver` | string | **地感 Loop MCU 硬件版本**V1.10),格式 `"主.次.次"`;同上,可为空 |
| `model` | string | 产品型号,1~10 字符 |
| `product_code` | string | 产品编码,6 位数字字符串 |
| `sub_code.net` | bool | 网络功能是否启用 |
| `sub_code.iot` | bool | IoT/MQTT 功能是否启用 |
| `bus.bus1~4` | uint8 | 各总线探头数 |
> **loop_ver 语义(V1.10**`loop_ver`/`loop_hw_ver` 是**缓存值**(设备上电后自动经 Loop `0x4A` 查询一次,OTA 刷写成功后刷新);MQTT 命令处理为同步回包,不等 0x4A 异步响应,故查询未完成/失败时回空。平台需最新版本时用 `loop_version_query`(§4.25)实时查询。
## 4.3 设置 SSC 网络配置 `ssc_net_set`
> Topic: `dld960/{sn}/srv`
@@ -639,6 +680,448 @@ 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**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
---
## 4.19 开启 OTA 会话 `ota_begin`
> Topic: `dld960/{sn}/srv`
> 依据《DLD960_MQTT_OTA协议.md》设计稿 V1.01ROADMAP P1.4 ①:Loop MCU 远程 OTA,先存后刷)。
> 能力探测:老固件(V1.07 及以下)无 `ota_*` 命令,收到回 `code=4`;平台下发前用 `ota_status` 或 `dev_info_query.soft_ver` 判断。
**请求:**
```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 = 96KBSlot 数据区 100KB 预留 4KB 边界余量) |
| `crc32` | uint32 | 全镜像 CRC32(十进制,算法见 §4.19.1 |
| `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, err_code=4`(防重复刷写,可 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"
}
}
```
### 4.19.1 CRC32 算法(必须双方一致)
标准 **CRC-32/ISO-HDLC**poly `0x04C11DB7`reflected `0xEDB88320`),init `0xFFFFFFFF`refin/refout truexorout `0xFFFFFFFF`。平台侧 Python `zlib.crc32()` / `binascii.crc32()` 即此算法;设备侧查表法。全镜像 CRC = offset 0 ~ size-1 连续;单片 CRC = 仅该 256B。
---
## 4.20 OTA 分片下发 `ota_data`
> Topic: `dld960/{sn}/srv`
> 单片大小 **256B** 是协议常量,不接受协商;hex 512 字符 + JSON 外壳 ≈ 640B < 设备接收缓冲 1024B。
**请求:**
```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 校验 → 写 W25Qxx 暂存(256B 页对齐)→ `received += 256`(≥size 截断为 size)→ `code=0`
2. `offset < received`(重复片,平台重发)→ **幂等回 `code=0`**,不重写
3. `offset > received`(缺片/乱序)→ `code=1, err_code=1` + `data.offset=received`(指示平台从该处续传;协议不要求乱序重组)
4. 单片 CRC 失败 → `code=1, err_code=1`(平台重发本片;连续失败平台可 `ota_abort`
5. 会话未开始 / 状态非 downloading → `code=3, err_code=2`
6. `data` 非 512 hex / offset 非 256 对齐 / 超 size → `code=1` 参数错误
**响应:** 标准成功/失败 + data 回显 `{offset, received}`
---
## 4.21 结束下载 `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, err_code=5, data={crc_ok:false}`
---
## 4.22 中止会话 `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 兜底)。响应标准成功。
---
## 4.23 触发刷写 `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, err_code=3`(有车,拒绝;平台提示"车辆离开后重试")。刷写期间会阻断检测与继电器控制 → 平台建议低峰执行
2. 元数据非 ready → `code=3`(先 `ota_end` 完成校验)
3. 通过 → 立即回 `code=0`(异步),进入刷写:
- 写 offlog 事件日志:`固件升级开始`target/slot/version/size
- **暂停事件上报与脱机日志(会话期间)**:MQTT `event_report` 暂停发送(入队积压,16 深溢出丢最旧,会话结束恢复后补发);offlog/快照落盘暂停("升级开始"日志在暂停前写入、"升级结果"在恢复后补记)
- 维持 Loop 现状:复位进 bootloader 后的 GPIO/继电器状态与现网 BLE OTA 升级一致,不额外干预
-`9F 01 00 01 A5 A7` 启动帧 → Loop APP 写 flag 复位 → bootloader 回 pre_ok
- 发 A6 地址帧(`0x08003400` 4 字节大端)→ addr_ok
- 从 W25Qxx 暂存读镜像,按 ≤254B/块发 A7(**非阻塞 tick 驱动**:每轮主循环发送 1~2 块并检查 ACK,绝不阻塞主循环,保证刷写窗口内 MQTT PINGREQ/心跳/IWDG 喂狗正常);停等 ACK,1s 超时重发 ×3
- 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP
4. 进度经 `ota_report` 上行(§5.5);失败重试 ×3 仍失败 → 元数据 `state=flash_failed` + `event_report{type:ota_error}` 告警(平台必答)
5. **刷写结果(设备侧,V1.09 明确)**
- 成功(末块 ACK)→ 元数据 `state=idle`**镜像保留**`size`/`crc32`/`version`/`slot` 不变,`last_result=0``flash_cnt+1`)→ 立即上报 `ota_report{stage:done}`(重发 3 次×5s,同 `msg_id`/`ts`)→ 恢复 `event_report` 发送与 offlog/快照落盘
- 失败 → 元数据 `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}`done/failed 同重发策略)
- **状态语义**:刷写完成后 DBN 侧状态回 `idle`("本轮刷写已结束"),与"下载完成待刷(`ready`)"严格区分——**平台不得把 `state=ready` 判为"刷写未启动"**:本地刷写 70 块(17224B)实测 <1s,平台轮询间隔可能错过 `flashing` 中间态
**响应:** `code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report` / `ota_status` 为准。
---
## 4.24 查询状态 `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`**`idle``size>0` = 已刷写完成(镜像保留可重刷)** |
| `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 ──成功──▶ IDLE(镜像保留, last_result=0)
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
```
**平台判定指引(V1.09):**
- **主依据**`ota_report` 主动上报(`stage=done` 成功 / `stage=failed` 失败,§5.5)——`ota_flash` 后平台应优先等该信号
- **兜底**`ota_status` 查询——`state=idle 且 size>0 且 last_result=0` = 刷写成功(镜像保留,可重刷);`state=flash_failed` = 失败;`state=ready` = 下载完成待刷(**不是**"刷写未启动"
- 刷写窗口极短(<1s),轮询可能捕捉不到 `flashing` 中间态,不得以此判失败
---
## 4.25 查询地感版本 `loop_version_query`
> Topic: `dld960/{sn}/srv`
> 用途:实时查询地感 Loop MCU 固件/硬件版本(V1.10,配合远程 OTA 做升级前后版本核对)。
**请求:**
```json
{ "msg_id": 407, "cmd": "loop_version_query", "ts": 1719000007 }
```
**设备行为(异步):**
1. 经 UART2 向 Loop MCU 发 `0x4A` 查询帧(`7F 00 01 4A ...`,见 `DLD960Loop_串口通信协议.md` §3.01
2. 收到 Loop 响应后解析(Soft/Hard 各三段)→ 更新本地缓存 → 立即回包
3. Loop 无响应/超时 → 回 `code=5``loop_ver` 保持缓存值(可为空)
**响应:**
```json
{
"msg_id": 407,
"cmd": "loop_version_query",
"ts": 1719000008,
"code": 0,
"msg": "success",
"data": {
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"version_str": "V1.2.3 (HW:1.0.0)"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `loop_ver` | string | 地感固件版本,格式 `"主.次.次"`Soft_Main.Sub.SSub |
| `loop_hw_ver` | string | 地感硬件版本,格式 `"主.次.次"`Hard_Main.Sub.SSub |
| `version_str` | string | 人类可读串,`"V{soft} (HW:{hard})"`Loop 无响应时可能为空 |
> **时序与缓存**:设备上电后自动查询一次并缓存;`ota_report stage=done`Loop 复位跑新固件)后自动刷新缓存。平台升级流程建议:`ota_flash` 前 `loop_version_query` 记录旧版本 → 刷写完成后再次查询核对新版本是否生效。
---
# 5 设备主动上报
## 5.1 设备上电登陆信息 `initialize`
@@ -655,8 +1138,12 @@ dld960/{dev_serial}/{direction}
"model": "DLD960",
"hard_ver": "1.0",
"soft_ver": "1.0",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"extra_info": {
"code": "869756049404948",
"imei": "860012345678901",
"iccid": "89860012345678901234",
"csq": "21",
"location": "113.9237976,022.6400375",
}
@@ -668,12 +1155,18 @@ dld960/{dev_serial}/{direction}
|------|------|------|
| `data.dev_serial` | string | 设备序列码 |
| `data.model` | string | 产品型号 |
| `data.hard_ver` | string | 硬件版本 |
| `data.soft_ver` | string | 固件版本 |
| `data.hard_ver` | string | 硬件版本(整机) |
| `data.soft_ver` | string | 固件版本(整机 DBN MCU |
| `data.loop_ver` | string | **地感 Loop MCU 固件版本**V1.10),格式 `"主.次.次"`;来自 0x4A 缓存,查询未完成/失败则为空字符串 |
| `data.loop_hw_ver` | string | **地感 Loop MCU 硬件版本**V1.10),格式 `"主.次.次"`;同上可为空 |
| `data.extra_info.code` | string | 可选,设备代码如IMSI/ICCID |
| `data.extra_info.imei` | string | 可选,4G 模块 IMEIV1.13;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,见 §6) |
| `data.extra_info.iccid` | string | 可选,流量卡 ICCIDV1.13;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,见 §6) |
| `data.extra_info.csq` | string | 可选,当前信号强度 |
| `data.extra_info.location` | string | 可选,经纬度(经度,纬度) |
> **V1.10 说明**`loop_ver`/`loop_hw_ver` 为**尽力携带**(上电后异步 0x4A 查询,initialize 发出时可能未就绪 → 回空);平台核对版本请用 `loop_version_query`(§4.25)。
## 5.2 线圈传感数据 `loop_data`
@@ -808,6 +1301,7 @@ dld960/{dev_serial}/{direction}
| `car_leave` | 车辆离开 | 通过时间 (×50ms) |
| `loop_cut` | 线圈断开 | 0 |
| `loop_restore` | 线圈恢复 | 断开持续时长 (×50ms) |
| `ota_error` | OTA 刷写失败告警(重试×3 仍失败,V1.08) | 0x1001=启动帧无响应 / 0x1002=地址帧错误 / 0x1003=数据块 ACK 超限 / 0x1004=全镜像校验失败 / 0x1005=安全窗口拒绝后强制失败 |
**平台应答(srv topic,收到后必须立即回复):**
@@ -881,6 +1375,317 @@ dld960/{dev_serial}/{direction}
| `net_status` | bool | 以太网连接状态 |
| `iot_status` | bool | MQTT 连接状态 |
## 5.5 OTA 进度/结果上报 `ota_report`
> Topic: `dld960/{sn}/dev` · QoS 1
> 用途:OTA 会话与刷写进度/结果主动推送(V1.08)。进度可丢,结果可经 `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 字段 | 说明 |
|-----------|------|
| `target` | 目标:`loop`(当前支持) |
| `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` 同策略)。
**平台判定(V1.09 明确)**`ota_report` 是刷写结果的**主依据**——收到 `stage=done` 判成功、`stage=failed` 判失败,无需再轮询 `ota_status`。设备 `ota_flash` 响应 `code=0` 仅表示已启动(异步),**不得以"轮询 `ota_status` 未见 `flashing`"或"状态持续为 `ready`"判"刷写未启动"**——本地刷写 <1s 完成,轮询大概率错过中间态;兜底判定见 §4.24。
**会话期间静默**V1.08):OTA 会话期间(`ota_begin` ~ 结束)设备暂停 `event_report` 发送(入队积压,结束后补发)与 offlog/快照落盘("升级开始"日志暂停前写入、"升级结果"恢复后补记),保证刷写窗口内 MQTT 保活与 IWDG 喂狗不受影响。
---
# 6 4G 通道适配(方案 CAir780 协议转换,V1.12 修订)
> 适用场景:vd960DBN 有线网络失效时,经 **Air8781P 整板(Air780EPM 4G 模组,LuatOS vd960Air 工程)** 兜底上报。
> **方案 C**Air780 解析 0x7F 帧并转换为**标准 JSON 命令**(与有线通道一致),平台无感。
> 修订说明:V1.11 曾定方案 B(原始帧 hex 透传 frame_report/frame_cmd),V1.12 改为方案 Cframe_* 降级为可选兜底。
## 6.1 通道架构
```
上行: vd960Loop --0x7F帧(UART2)--> vd960DBN --原样转发(UART1)--> Air780 --解析转JSON--> MQTT(loop_data/event_report/响应)
下行: 平台 --标准JSON命令--> Air780 --转换0x7F帧--> vd960DBN --0x7F帧(UART2)--> vd960Loop
```
- **vd960DBN**UART2↔UART1 双向转发(魔数分流:`0x7F` 帧 → 转发 UART2vd960Loop);`0x8F` 帧 → DBN 本地处理)。列入 vd960DBN 开发计划,**固件未实现**。⚠ 转发必须**不丢帧**(沿检测依赖完整 0xC0 帧流)。
- **Air780**0x7F 帧解析(Lua 状态机)+ **协议转换**0x7F 帧 ↔ 标准 JSON,§6.3+ MQTT(标准 JSON 命令面)
- **平台**:标准 JSON 解析,与有线通道一致(**零新增依赖**)
## 6.2 命令面(4G 通道 = 标准 JSON)
| 通道 | 上行(dev→srv | 下行(srv→dev |
|------|----------------|----------------|
| 有线(ETH MQTT | initialize / loop_data / event_report / heartbeat / ota_report(标准 JSON | 标准 JSON 命令全表(dev_serial_set / ssc_net_* / iot_net_* / loop_param_* / report_config / log_* / ota_* / loop_version_query |
| 4GAir780 转换) | **标准 JSON**initialize / loop_data / event_report / heartbeatAir780 从 0x7F 帧生成)+ 可选 frame_report | **标准 JSON**Air780 转换为 0x7F 帧下发)+ 可选 frame_cmd |
- 4G 通道上行 JSON 结构与有线通道**完全一致**(平台按同一解析逻辑处理)
- 4G 通道 `initialize``extra_info` 填真实值:`imei` / `iccid` / `csq`(V1.13;有线通道无 4G 模块时省略或空串)
- `frame_report` / `frame_cmd` 保留为**可选兜底**(§6.8):Air780 未实现转换的命令 / 未识别帧,平台可直接发/收原始帧
## 6.3 Air780 协议转换职责(0x7F 帧 ↔ 标准 JSON)
**上行(0x7F → JSON):**
| 0x7F 帧 | 转换目标 | 说明 |
|---------|---------|------|
| 0xC0 传感上报 | `loop_data` | 4 路通道字段映射(§5.2),携带 link(§6.7 |
| 0xC0 car_state 沿 | `event_report` | 进出车事件(§6.4 |
| 0xC0 loop_state 沿 | `event_report` | 线圈断开/恢复(loop_cut / loop_restore,§6.4 |
| 0x09~0x1F 配置响应 | 对应命令响应 | code / msg / data(按 §4 各命令响应结构) |
| 0x63 / 0x64 车检器参数响应 | `loop_param_set` / `loop_param_query` 响应 | 多路参数结构(§4.13 / §4.14 |
| 0x4A 版本响应 | `loop_version_query` 响应 / `initialize.loop_ver` | 地感版本(§4.25 |
**下行(JSON → 0x7F):**
| 平台 JSON 命令 | 转换 0x7F 帧 | 说明 |
|---------------|-------------|------|
| loop_param_set / loop_param_query | 0x63 / 0x64 | 车检器多路参数 |
| loop_version_query | 0x4A | 地感版本查询 |
| (其余需转发的命令) | 对应 0x7F 命令 | 按《DLD960Loop_串口通信协议》 |
## 6.4 事件上报(仅线圈事件,V1.12 明确)
- **4G 通道事件面 = 仅线圈事件**car_enter / car_leave / loop_cut / loop_restore,源自 0xC0 帧 car_state / loop_state 沿)
- **不含 DBN 内部网络事件**iot_connect / iot_ready / iot_reconn 等——Air780 无法感知 DBN 内部状态,**平台勿依赖 4G 通道获取网络事件**)
- `event_report` 语义与有线通道**完全一致**(V1.04 机制):
- 平台必答(回显 msg_id + code=0)→ 出队
- 5s 超时重发,同 msg_id / 原始 ts,最多 3 次;耗尽挂起
- 16 深环形队列,溢出丢最旧;多事件合并一条 publish
- 跨重连保持同 msg_id(平台按 (sn, msg_id) 去重)
- Air780 以 Lua 复刻 DBN `iot_evt_*` 逻辑(沿检测 + ACK 状态机 + 重发定时器),可行性已评估(2026-08-31):逻辑块全部可映射 Luatable 队列 / sys.timer / mqtt 回调 / json),无硬障碍
## 6.5 命令响应链路
- 平台 JSON 命令 → Air780 转 0x7F → vd960DBN → vd960Loop → 响应帧 → vd960DBN → Air780 → JSON 回包
- Air780 维护**单命令状态机**(暂存 msg_id + 超时回 code=5,与 DBN `g_lup_cmd` 同模式)
- 链路 4 跳:响应超时建议与 DBN 命令超时一致(当前 300ms~1s 量级,待板级确认)
## 6.6 4G 通道不支持的命令(网络配置类)
| cmd | 说明 |
|-----|------|
| `ssc_net_set` / `ssc_net_query` | SSC 有线网络配置(4G 不适用) |
| `iot_net_set` / `iot_net_query` | IoT 有线网络配置(4G 不适用) |
| `iot_topic_set` / `iot_topic_query` | Topic 配置(4G 主题由 Air780 配置,同步链路见 §6.9 |
> 经 4G 通道下发以上命令:设备回 `code=4 unsupported`。
>
> 补充(§6.9):这些配置**仅能经 BLE 写入 DBN**。DBN 作为权威源,经 UART1 把配置同步给 4G 通道;
> Air780 **不接受**平台经 4G 下发配置(否则会改掉自己正在使用的连接参数,形成第二权威源)。
## 6.7 link 对象(4G 特有字段)
| 字段 | 来源 | 说明 |
|------|------|------|
| `imei` | mobile.imei() | 4G 模块 IMEI,设备唯一标识 |
| `iccid` | mobile.iccid() | **流量卡卡号**,物联网卡管理识别用(卡商未写入 → 空串) |
| `imsi` | mobile.imsi() | IMSI(部分卡返回空) |
| `msisdn` | mobile.msisdn() | 手机号(物联网卡通常拿不到 → 空串) |
| `csq` | mobile.csq() | 信号强度 0-3131 最强,99/255 无信号) |
| `net` | 固定 "4G" | 网络制式(预留扩展) |
## 6.8 平台侧要求
1. **标准 JSON 解析**:与有线通道一致(**零新增依赖**);依 `link.net` 或报文形态识别 4G 通道
2. **设备唯一标识**`dev_serial` 与有线通道同一序列号(Topic 族一致,平台认同一台设备);`link.imei` / `link.iccid` 辅助 4G 设备/流量卡管理
3. **时钟校准**Air780 上线发 `initialize`(JSON + link)后,平台照常下发 `report_config` 校准 `ts`(§2.3
4. **事件面约束**:4G 通道仅报线圈事件,不报 DBN 内部网络事件(§6.4)
5. **可选兜底**:若启用 `frame_report` / `frame_cmd`(Air780 未识别帧 / 未实现转换命令),平台需按《DLD960Loop_串口通信协议》解析/组帧
## 6.9 4G 配置同步(BLE → DBN → Air780V1.14 新增)
### 6.9.1 背景与权威源
4G 通道的 MQTT 连接参数(服务器地址、端口、ClientID、账号、密码、发布/订阅主题)由**蓝牙小程序经 BLE 写入 DBN**。§6.6 已规定这些配置**不允许经 4G 通道下发**(回 `code=4`),因此 **BLE 是网络配置的唯一入口**。若 DBN 不同步给 Air780,Air780 只能使用自身固件内的默认值,**平台侧改配置无效**。
配置权威源 = **DBN 侧 flash**`IOT_NET_INFO` / `IOT_Topic` 结构,`cfig_flash.c` 持久化)。
| 角色 | 职责 |
|------|------|
| 蓝牙小程序 | **唯一配置入口**,经 BLE 写 DBN |
| DBN | **唯一权威源**:持久化 + 应答拉取 + 主动推送 |
| Air780 | 配置**消费者**:fskv 缓存仅用于加速启动;**唯一合法写入者 = 收到的 DBN 配置帧**(平台下发、产线预置均不得写入,否则产生第二权威源) |
### 6.9.2 帧格式
配置同步复用 **0x8F 本机侧私有帧**(与 BLE 侧 `MAGIC_BYTE_DBN_DEFAULT` 同值),沿用《DLD960Loop 串口通信协议》帧布局:
| 偏移 | 字段 | 说明 |
|------|------|------|
| 0 | `0x8F` | 本机侧私有帧魔数 |
| 1 | `Addr` | 设备地址 |
| 2 | `LEN` | `1 + DATA 长度`(最小 1 |
| 3 | `CMD` | 见 §6.9.3 |
| 4.. | `DATA` | 载荷,见 §6.9.4 |
| -2 | `XOR` | 自 `Addr` 起算(**不含**魔数) |
| -1 | `SUM` | 自 `Addr` 起算(**不含**魔数) |
帧总长 = `LEN + 5``XOR` / `SUM` 覆盖 = `Addr``2 + LEN` 字节。
**UART1 魔数语义(链路两端一致)**
| 魔数 | 处理 |
|------|------|
| `0x7F` | 业务帧:转发 UART2Loop |
| `0x8F` | **本机侧私有帧**:链路两端各自本地消费,**均不转发** |
| 其他 | 丢弃,并回找最近魔数重新同步(resync) |
**⚠ 帧长上限(两侧必须同步扩容)**
| 帧类型 | 帧长上限 | 说明 |
|--------|----------|------|
| `0x7F` Loop 业务帧 | **70 B**`LUP_MAX_PKG_LEN`,不变) | 与 Loop 协议隔离,维持原纪律 |
| `0x8F` 本机侧同步帧 | **260 B**`LEN` ≤ 255) | 覆盖最坏载荷(见 §6.9.4.3),**DBN 帧缓冲需由 70 B 扩至 260 BAir780 parser 缓冲同步 ≥ 260 B** |
> 注:`LEN` 为 1 字节,理论上限 255,故同步帧最大 260 B255 + 5)。B 端(Air780)解析器与 A 端(DBN)装配器**必须使用同一上限**,否则超长帧会被判 `LEN` 非法丢弃。
### 6.9.3 命令
复用 BLE 侧既有命令码(`dbn_ble_srv.h`),保证**同一命令在 BLE 通道与 UART1 同步通道语义一致**:
| CMD | 名称 | 方向 | 说明 |
|-----|------|------|------|
| `0x14` | `GET_IOT_NET` | 双向 | →:请求 net 配置(DATA 空);←:应答 net 配置(DATA 见 §6.9.4.1 |
| `0x16` | `GET_IOT_TOPIC` | 双向 | →:请求 topic 配置(DATA 空);←:应答 topic 配置(DATA 见 §6.9.4.2 |
**应答方向统一使用 `GET_*` 命令码**:载荷由 DBN 侧既有构造器 `set_response_iot_net()` / `set_response_iot_topic()` 生成,与 **BLE 读响应逐字节同源**(零新增序列化代码,避免出现第二套口径)。DBN 主动推送时同样使用 `GET_*` + 对应载荷(语义 = “当前权威值”)。
### 6.9.4 载荷
`0x00` 分隔字符串序列;字符串按 `strlen` **紧凑发送**(不补足结构体定长)。
#### 6.9.4.1 netCMD `0x14`
| 序号 | 字段 | 类型 | 最大长度 | 说明 |
|------|------|------|----------|------|
| 1 | `remote_addr` | 字符串 | 63 | 服务器域名或 IP |
| 2 | `mqtt_port` | ASCII 十进制 | 5 | 如 `1883` |
| 3 | `client_id` | 字符串 | 63 | 空 = 使用本机序列号 |
| 4 | `username` | 字符串 | 63 | |
| 5 | `password` | 字符串 | 31 | 末字段**无**结尾 `0x00` |
#### 6.9.4.2 topicCMD `0x16`
| 序号 | 字段 | 类型 | 最大长度 | 说明 |
|------|------|------|----------|------|
| 1 | `clientid_enable` | ASCII `'0'` / `'1'` | 1 | `1` = 启用自定义 ClientID |
| 2 | `topic_pub` | 字符串 | 63 | 上报主题 |
| 3 | `topic_sub` | 字符串 | 63 | 下发主题 |
#### 6.9.4.3 长度核算
| 载荷 | 典型长度 | 最坏长度 | 结论 |
|------|----------|----------|------|
| net | ≈ 59 B | 63+1+5+1+63+1+63+1+31 = **229 B** | 单帧可传(需 260 B 帧上限 **+ 缓冲扩容**,见下注) |
| topic | ≈ 51 B | 1+1+63+1+63 = **129 B** | 单帧可传 |
**必须拆成两条独立记录**`net` / `topic`):既与 BLE 两个命令的粒度对齐,也使 Air780 侧 fskv 单值(**≤ 255 B**`luat_fskv_set` 限制)安全:net 229 B < 255 B ✓,topic 129 B ✓。
Air780 侧建议**直接存原始载荷字节串(不转 JSON)**,加载时按 `0x00` 拆分 —— 与 DBN 侧构造器逐字节同源。
> ⚠️ **承载与缓冲前置条件(2026-09-10 补充)**229 B 经 BLE **分包**确可送达 —— 分包头编码于 `pkg[1]` 高/低 4 位(总包数 / 序号),每包 dat ≤ 94 B(MTU ≥ 103),4 位限制下最多 15 包。
> 但 DBN 侧 `g_buf_ble_response.dat` / `tmp_ble_buf` 仅 **132 B** 且 `set_response_iot_net()` / `set_response_iot_topic()` **无长度钳制** ⇒ 现版本载荷一旦 > 131 B 即**越界写**(可操作门槛:端口 4 位时 `host + clientid + username + password` 字符数 > 124)。
> 故本节的 229 B 能力**以完成 §6.9.9 前置改造项 1–4 为前提**,否则属纸面能力。
### 6.9.5 交互流程
**① 拉取(主:兜底一切不一致)**
```
Air780 上电 / 链路建立:
① fskv.init() → 失败则降级为纯拉取模式(功能不受影响)
② 读 fskv 缓存 → 有则立即用缓存发起 MQTT 连接(不必等 DBN 就绪)
③ 链路建立后 → 发 0x8F CMD=0x14 / 0x16DATA 空)
④ DBN 应答 → 与缓存比对:相同则不动;不同则写 fskv + 断开重连重订阅
```
**② 推送(辅:免重启立即生效)**
```
BLE 写 SET_IOT_NET(0x13) / SET_IOT_TOPIC(0x15) / UPDATE_DEV_SERIAL(0x09) 成功
→ DBN 持久化后,若 UART1 链路可用,立即发 0x8F CMD=0x14 / 0x16 + 权威载荷
→ Air780 比对,变化则写 fskv + 重连重订阅
```
DBN **不维护“待同步”状态位**:推送时链路不可用则不重试、不记账,等 Air780 下次拉取即可(无状态设计,避免状态机与真实链路状态不一致)。
**③ 约束**
- Air780 收到配置后 **先落 fskv 再重连**(避免掉电后反复以旧参数重连)
- fskv **仅在值实际变化时写入**littlefs 擦写磨损,4 KB 擦写块)
- 重连流程:断开 → 以新参数连接 → 重订阅 `topic_sub` → 恢复上报
### 6.9.6 其他触发同步的 BLE 命令
| BLE 命令 | 值 | 是否必须同步 | 说明 |
|----------|-----|--------------|------|
| `CMD_DBN_UPDATE_DEV_SERIAL``0x09` | 设备序列号 | **必须** | Air780 的 Topic 族与上报 JSON 的 `dev_serial` 均由序列号派生。序列号变更 → Topic 族变更,Air780 需重算并重连 |
| `CMD_DBN_SET_SUB_CODE``0x22` | `iot_enable` 位 | **必须** | IoT 通道总使能;DBN 有线 WCHNET MQTT 与 Air780 4G 通道均受此位控制。不同步 → DBN 已停发而 Air780 仍在连接(白耗流量),平台侧表现为“在线无数据” |
| `CMD_DBN_RW_UART_BAUD``0x31` | UART1 波特率 | **禁止在线修改** | 该命令可改任意串口波特率,含 UART1(4G 链路)。在线改动而两侧未同步 = **链路永久失联,只能拆机重刷**。建议固件侧对 `uart_num == 1` 直接拒绝 |
### 6.9.7 明确不同步的配置
| 配置 | 原因 |
|------|------|
| `local_net_cfg`(有线 IP/网关/掩码 + 各端口)、`net_center_info``lssc_ip` / `tcp_port`) | 有线以太网通道专用,与 4G 模组无关 |
| `CMD_DBN_SET_CJQ_*`(车检器参数)、`CMD_DBN_LOOP_*` | 经 UART2 下发给 Loop,方向与 4G 无关 |
| `CMD_DBN_SET_SUB_CODE``iot_enable` 外的位 | 局部外设使能(网口 / 雷达 / 激光 / LoRa 等) |
| `CMD_DBN_OFFLOG_*` / `CMD_DBN_SNAP_*` | 本地 flash 读写;4G 侧仅作数据通道(§4 `log_*``stream=snapshot` 已覆盖) |
| `CMD_DBN_CHECK_PASS` / `CMD_DBN_MODIFY_PASS` | 蓝牙访问密码,纯本地 |
| `CMD_DBN_SET_FACTORY` / `CMD_DBN_RESET_DEV` | 本地动作;若涉及重启,链路断开重连由 Air780 自行处理 |
| 所有 `GET_*`(读操作) | 无同步语义 |
**不随载荷同步的字段**`IOT_NET_INFO.mode`(IP / DNS 标志)。实测该字段**无 BLE 写入路径**、恒为 `0``IOT_Addr_IP_Mode`),当前仅 DBN 有线 MQTT 路径读取。Air780 侧按主机串自行判别地址类型(LuatOS `socket.connect(host, port)` 对 IP 与域名均兼容),无需同步。
### 6.9.8 异常处理
| 场景 | 行为 |
|------|------|
| 0x8F 帧校验失败 | 丢弃 + 回找最近魔数 resync;**不回错**(配置链路静默重试成本低) |
| `LEN` 非法 / 帧超上限 | 丢弃并重新同步(`LEN < 1``LEN + 5 > 260` |
| Air780 请求时 DBN 尚无有效配置 | DBN 回**出厂默认值**(`cfig_flash.c` 初始化值),保证 Air780 有明确行为而非空值 |
| fskv 挂载失败(首刷 / 分区异常) | `fskv.init()` 返回 false → **降级为纯拉取模式**,不阻塞业务;缓存丢失不影响正确性 |
| 推送时 UART1 链路不可用 | 不重试、不记状态;等 Air780 下次拉取 |
| Air780 重连失败(服务器不可达 / 鉴权失败) | 按既有重连策略退避重试;MQTT 失败**不影响** UART1 链路与配置同步 |
| 载荷含 `0x00` 的字段(如密码本身含 `0x00`) | 不支持;字段均为可见 ASCII 串 |
> **已知缺口(待办)**DBN 自身经**有线 WCHNET 直连 MQTT** 的路径(`peripheral_main.c`**仅实现 IP 模式,域名模式分支为空**。故配置为域名时,有线通道 MQTT 不工作(4G 通道不受影响)。是否补齐由产品决定。
### 6.9.9 实施状态
| 侧 | 项目 | 状态 |
|----|------|------|
| DBN | UART1 通道 + 0x8F 本机侧私有帧接入本机指令处理器(不转发) | **已实现**2026-09-10 |
| DBN | UART1 帧缓冲 70 B → 260 B 扩容 | 待实现 |
| DBN | ⚠️ **前置**`g_buf_ble_response.dat` / `tmp_ble_buf` 132 B → ≥ 260 B(与蓝牙小程序/MRS 工程**同步**扩容) | 待实现 |
| DBN | ⚠️ **前置**`set_response_iot_net/topic()` 补长度钳制(对齐 `set_response_buf()` @`dbn_ble_srv.c:288` | 待实现 |
| DBN | ⚠️ **前置**`unpack_packs()` 接收侧累加加钳制(`dat_len + (_len-1) > MAX_BLE_DAT_BUF_LEN` 即丢包清缓冲,防 `uint8_t` 回绕) | 待实现 |
| DBN | ⚠️ **前置**`unpack_packs()` 用起 `len` 形参(现全程未引用,memcpy 长度全取自 `pkg[2]` | 待实现 |
| DBN | 0x8F 配置请求应答 + BLE 写成功后主动推送 | 待实现 |
| DBN | `CMD_DBN_RW_UART_BAUD` 拒绝 `uart_num == 1` | 待实现 |
| Air780 | `frame_parser.lua` 支持 0x8F(本地消费,不转发)| 待实现 |
| Air780 | parser 缓冲 ≥ 260 B | 待实现 |
| Air780 | fskv 缓存 + 启动即连 + 拉取校准 + 变化重连重订阅 | 待实现 |
| Air780 | 配置读取由 require 期 `local` 改为重连时重读 | 待实现 |
---
# 修订记录
@@ -893,4 +1698,14 @@ dld960/{dev_serial}/{direction}
| V1.03 | 2026-07-09 | 增加设备上电初始化指令 | 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.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 |
| V1.15 | 2026-09-10 | §6.9 补充 **BLE 分包承载与缓冲边界**:核实 BLE 链路**分包机制存在且收发双向对称**(分包头编码于 `pkg[1]` 高/低 4 位 = 总包数/序号;每包 dat ≤ 94 B @MTU ≥ 103;4 位 ⇒ 最多 15 包)⇒ 229 B 载荷**确可送达**,故「载荷 > 131 B 即越界写 `g_buf_ble_response.dat` / `tmp_ble_buf`(132 B,构造器无钳制)」属**可达缺陷**而非纸面;§6.9.9 新增 4 条**实现前置改造项**(缓冲扩 ≥260 B / 构造器补钳制 / 接收侧累加钳制防 `uint8_t` 回绕 / `unpack_packs` 用起 `len` 形参);§6.9.4.3 补承载前置条件说明 | wangfq |
| V1.14 | 2026-09-10 | 新增 **§6.9 4G 配置同步(BLE → DBN → Air780**:明确配置权威源为 DBN flashAir780 侧 fskv 仅作启动缓存,**唯一写入者 = DBN 配置帧**,避免第二权威源);复用 **0x8F 本机侧私有帧**(与 BLE 同魔数 `MAGIC_BYTE_DBN_DEFAULT`,链路两端各自本地消费、均不转发),命令复用 `GET_IOT_NET(0x14)` / `GET_IOT_TOPIC(0x16)`,载荷与 BLE 读响应**逐字节同源**(复用 `set_response_iot_net()` / `set_response_iot_topic()`,零新增序列化)、拆 net/topic 两条(最坏 229B / 129B,均 ≤ fskv 单值 255B);**0x8F 帧长上限独立定为 260B**(Loop 业务帧 70B 上限不变,两侧帧缓冲需同步扩容);同步触发点补 `UPDATE_DEV_SERIAL(0x09)` / `SET_SUB_CODE``iot_enable` 位 / **禁改 UART1 波特率**V1.11 的 **0x7D 帧配置同步方案作废**`iot_net_info.mode` 实测无 BLE 写入路径、恒为 IP 模式,故不随载荷同步 | wangfq |
| V1.13 | 2026-08-31 | `initialize``extra_info` 增加可选字段 **`imei`** / **`iccid`**4G 模块 IMEI / 流量卡 ICCID;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,§6.2 说明) | wangfq |
| V1.12 | 2026-08-31 | **4G 通道适配修订:方案 B(hex 透传)改为方案 C(Air780 协议转换)**——Air780 解析 0x7F 帧并转换为**标准 JSON 命令**loop_data / event_report / initialize / heartbeat,与有线通道一致),平台零改动;V1.11 的 frame_report / frame_cmd 降级为**可选兜底**4G 事件面 = **仅线圈事件**car_enter/car_leave/loop_cut/loop_restore,不含 DBN 内部网络事件);命令响应链路 Air780 单命令状态机(超时回 code=5);link 对象保留;Air780 以 Lua 复刻 iot_event_report 逻辑(沿检测 + ACK + 5s×3 重发 + 16 深队列 + 跨重连同 msg_id),可行性已评估(2026-08-31 | wangfq |
| V1.11 | 2026-08-31 | **4G 通道适配(方案 B:原始帧透传 + hex 封装)**:新增 `frame_report`dev→srv,§6.3/ `frame_cmd`srv→dev,§6.4)两命令(4G 通道专用);4G 通道**不使用**有线标准 JSON 业务命令(loop_data/event_report),不适用网络配置类命令(ssc_net_*/iot_net_*/iot_topic_*4G 通道下发回 code=4);上行附加 `link` 对象(IMEI/ICCID/IMSI/MSISDN/CSQ,§6.6);链路层数据面 0x7F 帧字节流透传(魔数分流在 vd960DBN 侧),0x7D 帧仅 DBN↔Air780 配置同步/握手;平台双通道区分解析 + 新增《DLD960Loop_串口通信协议》解析依赖(§6.7);实施主体:Air8781PAir780EPMvd960Air 工程,vd960DBN UART1 通道列入开发计划(固件未实现) | wangfq |
| V1.10 | 2026-08-20 | **网络上报携带地感版本**(配合远程 OTA 升级前后版本核对):`dev_info_query` 响应 + `initialize` 上报新增 `loop_ver`/`loop_hw_ver`(地感 Loop MCU 固件/硬件版本,格式 `"主.次.次"`,来自 0x4A 查询缓存,尽力携带可为空);新增命令 `loop_version_query`(§4.25,srv→dev 实时查询,设备经 UART2 0x4A 异步查询后回包,含 `loop_ver`/`loop_hw_ver`/`version_str`);版本语义:`soft_ver`=整机 DBN 固件(`主.次`),`loop_ver`=地感 Loop 固件(`主.次.次`),二者区分 | wangfq |
| V1.09 | 2026-08-20 | **OTA 刷写结果判定修复**(现场:刷写物理成功但平台误判"刷写未启动"):① 刷写成功后台侧状态回 `idle`(镜像保留:`size`/`crc32`/`version` 不变,`last_result=0`,可重刷),与"下载完成待刷 `ready`"严格区分;② `ota_report` 升级为刷写结果**主依据**`stage=done`/`failed` 设备必报,重发 3 次×5s),`ota_status` 仅兜底;③ 明确平台判定指引——`idle+size>0+last_result=0`=成功,不得以轮询未见 `flashing` 或状态持续 `ready` 判"刷写未启动"(本地刷写 <1s,轮询大概率错过中间态);④ `ota_begin` 兼容 `idle+size/crc32 一致` → 免下载直接可刷(重刷) | wangfq |
| V1.08 | 2026-08-20 | 增加 **Loop MCU 远程 OTA**ROADMAP P1.4 ①,先存后刷):命令 `ota_begin` / `ota_data` / `ota_end` / `ota_abort` / `ota_flash` / `ota_status`srv→dev+ `ota_report`dev→srv);单片 256B + 单片/全镜像 CRC32ISO-HDLC,§4.19.1);断点续传(`ota_begin` 返回 offset);Slot A/B 双槽回滚(镜像 ≤96KB);`ota_flash` 安全窗口检查 + 非阻塞 tick 驱动刷写(刷写窗口内 MQTT 保活/IWDG 不受影响);会话期间暂停 `event_report` 发送与脱机日志落盘;`event_report` 扩展 `type=ota_error` 失败告警;§2.2 补 OTA 细分错误码(err_code);老固件兼容(`ota_*` 回 code=4 | wangfq |
+516
View File
@@ -0,0 +1,516 @@
# 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` 为准)。
**刷写结果(V1.09 明确):**
- 成功(末块 ACK)→ 元数据 `state=idle`**镜像保留**`size`/`crc32`/`version`/`slot` 不变,`last_result=0``flash_cnt+1`)→ 上报 `ota_report{stage:done}`(重发 3 次×5s)→ 恢复 event_report/offlog 落盘
- 失败 → `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}`
- **状态语义**:刷写完成后回 `idle`("本轮刷写已结束"),与"下载完成待刷 `ready`"严格区分——平台**不得把 `state=ready` 判为"刷写未启动"**:本地刷写 <1s,轮询大概率错过 `flashing` 中间态;平台判定以 `ota_report done/failed` 为主依据,`ota_status` 兜底(`idle+size>0+last_result=0`=成功)
## 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 ──成功──▶ IDLE(镜像保留, last_result=0)
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
```
**平台判定(V1.09**:刷写结果以 `ota_report``stage=done`/`failed`)为主依据;`ota_status` 兜底——`state=idle 且 size>0 且 last_result=0` = 成功(镜像保留可重刷);`state=ready` = 待刷(**不是**"刷写未启动");轮询可能错过 `flashing` 中间态,不得以此判失败。
---
# 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 同策略)。
**平台判定(V1.09 明确)**`ota_report` 是刷写结果**主依据**——`stage=done` 判成功、`stage=failed` 判失败,无需轮询 `ota_status``ota_flash` 响应 `code=0` 仅表示已启动(异步),**不得以"轮询未见 `flashing`"或"状态持续 `ready`"判"刷写未启动"**(本地刷写 <1s,轮询大概率错过中间态);兜底判定见 §5.6。
## 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 约定 + 修订记录;**V1.10 补充版本核对**:`dev_info_query`/`initialize``loop_ver`/`loop_hw_ver`,新增 `loop_version_query`(§4.25)供平台升级前后核对地感版本 |
| `DLD960_TCP_JSON协议.md` | 本版**不扩展**ROADMAP 先走 MQTT);后续按"复用命令 + stream/字段"模式补(与 log_* 扩展同套路)。注:TCP 侧 `loop_version_query` 代码已有(`tcp_json_srv.c`),文档待同步 |
| `DLD960_BLE协议.md` | 不变(BLE OTA 维持流式透传现状;本地刷写状态机与 BLE 透传共用 `lup_feed_byte_ota` |
| `README.md` / `DLD960_技术规格书.md` | 协议矩阵同步 V1.10 |
**固件代码落点(协议拍板后动):**
- `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_query` | 读取车检器多路参数 | 是 | 0x64 |
| `report_config` | 设置主动上报 | 是 | 0xC5 |
| `log_stat` | 查询脱机日志统计(事件/快照流) | 是 | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | 是 | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | 是 | — |
| 主动推送 | | | |
| `loop_data` | 线圈传感数据(设备→客户端) | — | 0xC0 |
| `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 设备主动推送
主动推送帧与请求-响应共用同一条 TCP 连接,由设备在任意时刻发出。
@@ -457,3 +560,5 @@ Client Device (DLD960)
|------|----------|----------|--------|
| 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.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 |
+187
View File
@@ -0,0 +1,187 @@
# DLD960 四通道车辆检测器 产品手册
> Product Manual · 产品型号:**DLD960GA**
> 文档版本 V1.04 · 2026-09-02 · 适用固件:整机 V1.02.05Loop V1.03 + DBN 1.02.05
---
## 1. 产品简介
DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖四条车道/四个检测断面。采用双 MCU 架构:检测核心专注线圈信号的实时处理,通信核心提供蓝牙、以太网、4G 多种接入方式,可直连道闸控制器,也可对接云平台做流量统计与远程运维。
**典型应用场景**
- 停车场出入口道闸联动(存在检测、防砸车压线检测)
- 分车道流量计数(进出计数、车间距/通过时间统计)
- 单通道会车系统的车辆存在/方向输入
- 传感数据上云(MQTT 接入业务平台)
**核心特性**
- 四路线圈独立检测,10ms 采样周期,进入确认 + 斜率限幅双重抗干扰
- 四档灵敏度、四档工作频率,支持智能选频抑制相邻线圈串扰
- 基线自适应跟踪:温漂、雨天频率漂移自动补偿,不误报不漏检
- 四个双路继电器:存在/脉冲输出、方向判别、输出延时逐路可配
- 蓝牙小程序现场配置,双 MCU 均可无线升级(OTA/ISP 透传)
- 以太网双协议:TCP JSON(局域网对接)+ MQTT(云平台),关键事件确认送达
- 通信可靠性加固:串口硬件 DMA 接收 + 固件内存优化,长时间运行稳定不丢帧、不异常重启
- BLE OTA 升级链路加固:检测 MCU 升级响应帧(0x9F 协议)完整透传回小程序,停等 ACK 不再丢失,升级成功率提升
- 网络上报携带地感版本:云平台可实时核对地感固件/硬件版本(MQTT `loop_version_query`,配合远程 OTA 升级前后版本核对)
- 联网稳定性加固:MQTT 发送退避与断连重连修复,长时间联网不掉线、不死循环
## 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 呼吸;各通道车检灯自检慢闪,该路绿灯闪至**本路基准稳定**才停(约 2~5 s,最长 5 s 兜底),随后无车全灭进入检测状态。若某路车检灯常亮且现场无车,参见 §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 呼吸闪烁;各通道绿灯自检慢闪至基准稳定(约 2~5 s,最长 5 s 兜底)后进入检测状态(上电 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.05。
>
> ⚠ **检测 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.05 及以上版本 |
| 事件重复上报 | 平台未按协议应答 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.10 | 云平台对接开发(含 Loop 远程 OTA 与地感版本查询) |
| 《DLD960 BLE 协议》V1.02 | 蓝牙小程序/APP 对接开发(脱机日志与传感快照读写) |
| 《环路车辆检测器验收标准》 | 采购/部署验收 |
---
## 修订记录
| 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------|
| V1.04 | 2026-09-02 | Loop V1.03 单侧更新:绿灯上电自检说明改为闪至基准稳定才停(约 2~5 s,最长 5 s 兜底),上电流程/故障排查同步 | wangfq |
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:核心特性补网络上报携带地感版本 + 联网稳定性加固;相关文档表 MQTT 协议 V1.07→V1.10(修正滞后);故障排查配置保存建议升至 V1.02.05 及以上 | wangfq |
| 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 |
+194
View File
@@ -0,0 +1,194 @@
# DLD960 四通道车辆检测器 技术规格书
> Technical Specification · 产品型号:**DLD960GA**
> 文档版本 V1.04 · 2026-09-02 · 配套整机发布 V1.02.05Loop V1.03 单侧更新)
---
## 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% 稳定性窗校验 | 温漂/换线圈后自动重建基线;慢速进车不误吸收 |
| 上电稳定期 | 基准稳定判定(Loop V1.03 起):连续 2 窗(100 样本/窗≈1 sOrigin 均值漂移 ≤0.1% + 最少 128 样本(≈1.3 s),硬兜底 500 样本(≈5 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.05 配套)
| 协议 | 版本 | 通道 | 要点 |
|------|------|------|------|
| 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.10 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分);Loop 远程 OTAota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status + ota_report256B/片 CRC32,先存后刷断点续传,Slot A/B 双槽回滚) |
| 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.05 配套版本 |
|------|-------------------|
| vd960Loop 固件 | 1.0(不变) |
| vd960DBN 固件 | 1.02.05 |
### 10.1 已知约束(V1.02.01
- 固件版本三段式:`FIRMWARE_VER="1.02.05"`MAIN=1 / SUB=2 / SUBSUB=5);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 已知约束,升级完成后重启设备)。
- **UART2 0x4A 版本查询与 `loop_data`/事件上报共用总线**:低频命令理论无冲突,时序待板上验证(V1.02.05)。
---
## 修订记录
| 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------|
| V1.04 | 2026-09-02 | Loop V1.03 单侧更新:上电稳定期由固定 128 样本改为基准判稳(连续 2 窗 Origin 漂移 ≤0.1% + 5 s 硬兜底),绿灯自检闪至基准稳定才停止 | wangfq |
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:协议矩阵标题/版本配套矩阵/固件三段式版本同步;新增已知约束 UART2 0x4A 版本查询总线时序;MQTT V1.10 地感版本上报与 loop_version_query 说明 | wangfq |
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出;存储适配(SPI 识别去厂商代码 + factory 写入恢复);2026-08-20 协议矩阵 MQTT 同步 V1.08Loop 远程 OTA 协议设计先行,固件待实现,Slot A/B 100KB | 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)
{
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();
NVIC_SystemReset();
PRINT("CFG: magic mismatch - factory defaults written\n");
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;
@@ -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);
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.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);
// g_sub_code_enable.laser_enable = (g_sub_code_enable.code_set >> 9) & 0x01;
@@ -16,6 +16,8 @@
#include "ch32v20x_it.h"
#include "eth_driver.h"
#include "CONFIG.h"
#include "debug.h"
#include "fault_diag.h"
/*********************************************************************
* 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
*
@@ -47,6 +83,16 @@ void NMI_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();
while(1)
{
@@ -10,7 +10,9 @@
#include "storage.h"
#include <string.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
@@ -29,6 +31,12 @@ extern uint8_t loop1_FLAG_CUT;
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->flag = 0;
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;
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->magic = 0;
@@ -65,7 +79,7 @@ void clear_buf_dbn_ble_all(Buf_DBN_BLE * buf)
buf->flag = 0;
buf->magic = 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_offset = 0;
buf->pkg_amount = 0;
@@ -181,6 +195,25 @@ static uint8_t set_net_info_to_ready(uint8_t * dat_dst)
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)
{ // config iot_net
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));
i += strlen(iot_net_info.password);
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;
@@ -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));
i += strlen(g_iot_topic.topic_sub);
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->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 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);
response_dst->magic = magic;
response_dst->cmd = cmd;
uint8_t _amount = dat_len / MAX_BLE_DAT_RESPONSE_LEN;
if((dat_len % MAX_BLE_DAT_RESPONSE_LEN) > 0)
uint16_t _chunk = ble_notify_chunk_max();
uint8_t _amount = dat_len / _chunk;
if((dat_len % _chunk) > 0)
{
_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->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_seq = response_ori->pkg_seq;
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;
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);
} 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:
{
} break;
@@ -893,6 +1122,7 @@ void manage_dbn_ble_transparent(uint8_t *pkg, uint8_t len)
void poll_dbn_ble(void)
{
static uint8_t _last_ntf_flag = 0;
uint8_t i = 0;
uint8_t magic = 0;
if(g_ble_rcv_buf.flag)
@@ -935,8 +1165,24 @@ void poll_dbn_ble(void)
}
if(g_notify_buftemp.flag == 0)
{
// report_sens_acs();
g_flag_notify_temp = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
static uint8_t _last_resp_flag = 0;
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>
#define PRODUCT_MODEL "DLD960GA"
#define FIRMWARE_VER "1.0"
#define FIRMWARE_VER "1.02.05"
#define HARDWARE_VER "1.0"
#define FIRMWARE_VER_MAIN 1
#define FIRMWARE_VER_SUB 1
#define FIRMWARE_VER_SUB 2
#define FIRMWARE_VER_SUBSUB 5
#define HARDWARE_VER_MAIN 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_TOUCHUAN_DISABLE_TIME 15
#define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min
#define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min
typedef enum
{
DDType_DNT900 = 1, //1 ???????? DNT900
DDType_WireDUS, //2???ù?¨???????? DG
DDType_DNT900 = 1, //1 区位有线 DNT900
DDType_WireDUS, //2超声波车位有线 DG
DDType_4G_1,
DDType_WireDus_Wb, //4 ???ù?¨???????? ?ò??
DDType_LoraAreaMain, //5 Lora ?????÷?ú
DDType_LoraAreaSub, //6 Lora ???????ú
DDType_LoraLotMain, //7 Lora ?????÷?ú
DDType_LoraLotSub, //8 Lora ???????ú
DDType_LoraBroadcast, //9 Lora ????????
DDType_LoraRelay, //10 Lora ?????? ???? DLR101
DDType_Relay, //11 ?????????? ?¨??RS485-2 ??·?
DDType_4G_IOT, //12 4G ??±¨??????????????MQTT
DDType_Wire_Collect_IOT, //13 ????????±¨??????????????MQTT
DDType_Wire_Display_IOT, //14 ????????·?????????MQTT
DDType_Radar_Lot, //15 ?×??????
DDType_WireDus_Wb, //4 超声波车位有线 万泊
DDType_LoraAreaMain, //5 Lora 区位主机
DDType_LoraAreaSub, //6 Lora 区位从机
DDType_LoraLotMain, //7 Lora 车位主机
DDType_LoraLotSub, //8 Lora 车位从机
DDType_LoraBroadcast, //9 Lora 广播模式
DDType_LoraRelay, //10 Lora 中继, 使用 DLR101
DDType_Relay, //11 中继模式, 通过RS485-2 收发
DDType_4G_IOT, //12 4G 上报传感数据,使用MQTT
DDType_Wire_Collect_IOT, //13 有线,上报传感数据,使用MQTT
DDType_Wire_Display_IOT, //14 有线显示发布,使用MQTT
DDType_Radar_Lot, //15 雷达车位
DDType_DLD950 = 20, //DLD950
DDType_DBN101 = 21,
DDType_DNT910 = 22, //DNT910,DNT920
DDType_DNT920 = 23,
DDType_DLD950V4 = 24,
DDType_DLD950V4 = 24, // DLD960
}DG_Device_Type;
typedef enum
@@ -67,12 +68,12 @@ typedef struct _SUB_CODE_ENABLE_
uint8_t net_enable;
uint8_t iot_enable;
uint8_t custom_enable;
uint8_t loop_enable; // ?÷??????
uint8_t loop_enable; // 流量计数
uint8_t dgdus_enable;
uint8_t wbdus_enable;
uint8_t radar_enable;
uint8_t laser_enable; // lora?¤???×??????
uint8_t lora_enable; // Lora ????
uint8_t laser_enable; // lora激光雷达车位
uint8_t lora_enable; // Lora 车位
//uint8_t input_enable;
} Sub_Code_Enable;
@@ -91,21 +92,12 @@ extern Pkg_Uart g_pkg_uart_1;
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_
{
uint8_t flag; // if need to report
uint8_t enable; // 是否使能
uint8_t enable; // 使能
uint8_t send_flag;
uint8_t obj_amount;
uint8_t cmd;
@@ -162,4 +154,16 @@ void uart_srv(void);
void UART2_SendString(uint8_t *buf, uint16_t len);
void UART1_SendString(uint8_t *buf, uint16_t len);
/* ---- 4G 通道: UART1 <-> Air780 (协议 §6.1 纯字节流透传) ---- */
void uart1_dma_init(void); /* 初始化 (DEBUG!=0 时为空实现) */
void uart1_dma_poll(void);
/* DBN(CH32V208GBU6) 私有指令分发器 (dbn_ble_srv.c) —— 0x8F 帧, 4G 下行本地指令复用同一入口 */
void manage_dbn_ble_default(uint8_t *pkg, uint8_t len); /* 主循环轮询: 消费 DMA 缓冲 -> 分流 */
extern uint32_t g_uart1_fwd_to_air; /* 上行转发帧数 (UART2->UART1) */
extern uint32_t g_uart1_fwd_to_loop; /* 下行转发帧数 (UART1->UART2, 0x7F) */
extern uint32_t g_uart1_local_cnt; /* 下行本地处理帧数 (0x8F) */
extern uint32_t g_uart1_badchk; /* 下行校验失败帧数 */
extern uint32_t g_uart1_drop; /* 下行 DMA 溢出丢弃次数 */
#endif /* INCLUDE_CMCNG_H_ */
@@ -49,6 +49,13 @@
#define CMD_DBN_SET_CJQ_PARAM 0x23 // 设置 车检器参数
#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
@@ -73,7 +80,8 @@
#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_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_
{
@@ -91,7 +99,7 @@ typedef struct _BUF_DBN_BLE_
uint8_t dat_offset;
uint8_t pkg_amount;
uint8_t pkg_seq;
uint8_t dat[MAX_BLE_BUF_LEN];
uint8_t dat[MAX_BLE_DAT_BUF_LEN];
} Buf_DBN_BLE;
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_HEARTBEAT_MS 60000 // 心跳上报周期 (60s)
#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 最大长度
/*===========================================================================
@@ -52,5 +52,10 @@ void iot_mqtt_poll(void); // 主循环轮询 (状态机 + 心跳 +
void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理
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_report_ota_error(uint32_t err); // OTA 刷写失败告警入队 (V1.08, 走 event_report 闭环)
void iot_ota_report_result(uint8_t ok, uint32_t err); // OTA 刷写结果上报 ota_report (V1.09: ok=1 done / ok=0 failed)
uint8_t iot_any_car(void); // 任一通道有车 (ota_flash 安全窗口检查)
void iot_watchdog_init(void); // 初始化硬件 IWDG (无条件, 主循环卡死兜底)
void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用)
#endif /* __IOT_MQTT_SRV_H__ */
@@ -7,7 +7,7 @@
* @brief DLD960Loop 串口通信协议 (7F) — CH32V208 ↔ DLD960Loop MCU
*
* 帧格式: 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)之间的接口。
* 网络接口(TCP JSON/5960)通过本模块向地感MCU下发命令并获取数据。
@@ -137,6 +137,17 @@ typedef struct {
char version_str[32]; // 格式化字符串
} LUP_VersionInfo;
/* 版本缓存 (V1.10) — 0x4A 查询结果缓存, 供 MQTT dev_info_query/initialize 带缓存值 */
typedef struct {
uint8_t valid; /* 1=缓存有效 (查询成功过) */
uint8_t hard_main;
uint8_t hard_sub;
uint8_t hard_ssub;
uint8_t soft_main;
uint8_t soft_sub;
uint8_t soft_ssub;
} LoopVerCache;
/* 命令状态机 */
typedef enum {
LUP_STATE_IDLE = 0, // 空闲
@@ -160,6 +171,7 @@ typedef struct {
* Global State
*===========================================================================*/
extern LUP_CmdTracker g_lup_cmd;
extern LoopVerCache g_lup_ver_cache; /* V1.10: 0x4A 版本缓存 */
/*===========================================================================
* Public API
@@ -194,6 +206,10 @@ void lup_cmd_done(void);
int lup_cmd_check_timeout(void);
void lup_cmd_on_response(const uint8_t *pkg, uint16_t len);
/* --- 版本缓存 (V1.10) --- */
void lup_cache_from_info(const LUP_VersionInfo *info); /* 解析结果写入缓存 */
void lup_refresh_version_cache(void); /* 后台刷新: 命令通道空闲时发 0x4A */
/* --- High-level Commands (send + wait handled by state machine) --- */
void lup_send_get_version(void);
void lup_send_reset(void);
@@ -212,7 +228,11 @@ typedef enum {
LUP_FRAME_STATE_HEADER, // 接收 Header (Addr, LEN, CMD)
LUP_FRAME_STATE_VALUE, // 接收 Value
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;
typedef struct {
@@ -228,6 +248,11 @@ extern LUP_FrameParser g_lup_parser;
/* 喂一个字节给帧解析器,返回 1 表示帧接收完成 */
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);
uint16_t lup_frame_len(void);
@@ -36,7 +36,7 @@ extern "C" {
/* 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_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 */
@@ -47,6 +47,12 @@ extern "C" {
#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
#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 */
@@ -84,7 +90,10 @@ extern "C" {
/* If you want to achieve a higher transmission speed,
* try to increase RECE_BUF_LEN to (WCHNET_TCP_MSS*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 */
@@ -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_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 */
@@ -0,0 +1,173 @@
/**
******************************************************************************
* @file offlog.h
* @author wfq
* @version V1.0
* @date 2026-08-04
* @brief DBN 脱机事件日志 (W25Qxx 外部 SPI NOR, 环形)
*
* 目的: 解决"平台重复上线/异常重启但现场无日志"的取证问题。
* 区分「设备真复位」vs「MQTT 断连重连」(两者都会导致平台重收 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_OTA_START = 0x60, /* 固件升级开始 (V1.08); payload[0]=slot, payload[1]=target, payload[2..5]=size BE */
OFFLOG_EVT_OTA_RESULT = 0x61, /* 固件升级结果 (V1.08); payload[0..3]=result code BE (0=成功, 非0=OTA_ERR_*) */
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);
void offlog_ota_start(uint8_t slot, uint32_t size); /* 固件升级开始 (V1.08, 会话静默前写入) */
void offlog_ota_result(uint32_t code); /* 固件升级结果 (V1.08, 恢复后补记) */
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,147 @@
/**
******************************************************************************
* @file ota_srv.h
* @author wangfq
* @version V1.0
* @date 2026-08-20
* @brief DLD960 Loop MCU 远程 OTA 服务 (MQTT 协议 V1.08, ROADMAP P1.4 ①)
*
* 先存后刷: MQTT 分片下载 → W25Qxx 暂存区 (0x010000 起 512KB) →
* 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU
*
* 协议依据: docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5
* 设计稿: docs/DLD960_MQTT_OTA协议.md V1.01
******************************************************************************
*/
#ifndef __OTA_SRV_H__
#define __OTA_SRV_H__
#include <stdint.h>
/*===========================================================================
* 分区常量 (与 offlog.h: OFFLOG_PARAM_SIZE=64KB + OFFLOG_OTA_SIZE=512KB)
* OTA 区 = 0x010000 ~ 0x090000 (512KB)
* 布局: 元数据扇区(4KB) + Slot A(100KB) + Slot B(100KB) + 预留(308KB)
*===========================================================================*/
#define OTA_AREA_BASE 0x10000UL /* 0x010000 (参数区 64KB 后) */
#define OTA_AREA_SIZE 0x80000UL /* 512KB */
#define OTA_META_BASE (OTA_AREA_BASE + 0x0000UL) /* 元数据主备份 */
#define OTA_META_BACKUP (OTA_AREA_BASE + 0x0200UL) /* 元数据备份 (同扇区) */
#define OTA_META_SECTOR (OTA_AREA_BASE / 4096UL) /* 元数据所在扇区号 */
#define OTA_SLOT_A_BASE (OTA_AREA_BASE + 0x1000UL) /* 0x011000 */
#define OTA_SLOT_B_BASE (OTA_AREA_BASE + 0x1A000UL) /* 0x02A000 */
#define OTA_SLOT_SIZE (100UL * 1024UL) /* 100KB */
#define OTA_MAX_IMAGE (96UL * 1024UL) /* 镜像上限 96KB (留 4KB 余量) */
/* 下载单片 (协议常量, 256B 页对齐) */
#define OTA_CHUNK_SIZE 256
/* 刷写块 (0x9F LEN 为 uint8 → DATA ≤ 254B; 取 248B) */
#define OTA_FLASH_BLOCK 248
/*===========================================================================
* 会话状态
*===========================================================================*/
enum {
OTA_STATE_IDLE = 0, /* 空闲 */
OTA_STATE_DOWNLOADING, /* 下载中 */
OTA_STATE_READY, /* 校验通过, 可刷写 */
OTA_STATE_FLASHING, /* 刷写中 */
OTA_STATE_FLASH_FAILED, /* 刷写失败 (保留镜像可重试) */
OTA_STATE_ABORTED, /* 已中止 */
};
/* 刷写子状态 (非阻塞 tick 驱动) */
enum {
OTA_FLASH_IDLE = 0,
OTA_FLASH_START, /* 发 A5 启动帧 */
OTA_FLASH_WAIT_READY, /* 等 pre_ok */
OTA_FLASH_ADDR, /* 发 A6 地址帧 */
OTA_FLASH_WAIT_ADDR, /* 等 addr_ok */
OTA_FLASH_DATA, /* 发 A7 数据块 */
OTA_FLASH_WAIT_ACK, /* 等 data ACK */
};
/* OTA 错误码 (event_report type=ota_error 的 value, 协议 §5.3) */
#define OTA_ERR_READY_TIMEOUT 0x1001 /* 启动帧无响应 (pre_ok 超时) */
#define OTA_ERR_ADDR_FAIL 0x1002 /* 地址帧错误 (addr_err / 超时) */
#define OTA_ERR_DATA_ACK_LIMIT 0x1003 /* 数据块 ACK 超限 (重试×3 耗尽) */
#define OTA_ERR_CRC_FAIL 0x1004 /* 全镜像校验失败 */
#define OTA_ERR_FORCE_FAIL 0x1005 /* 安全窗口拒绝后强制失败 */
/*===========================================================================
* 元数据结构 (56B, 写 64B 槽)
*===========================================================================*/
typedef struct {
uint32_t magic; /* OTA_MAGIC 'DLD9' */
uint32_t state; /* OTA_STATE_* */
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; /* 上次刷写结果: 0=无/成功, 非0=错误码 */
uint32_t begin_ts; /* 会话开始 (同步后 Unix 秒, 0=未同步) */
char version[16]; /* 目标固件版本 (审计) */
uint32_t flash_cnt; /* 刷写尝试次数 */
uint32_t rsv2;
} OtaMeta; /* 56B */
#define OTA_MAGIC 0x39444C44UL /* 'DLD9' */
/*===========================================================================
* API
*===========================================================================*/
/* 初始化: 读元数据 (双备份校验), 刷写状态机复位 */
void ota_init(void);
/* 主循环轮询: 刷写状态机 (非阻塞 tick 驱动, 绝不阻塞主循环) */
void ota_poll(void);
/* OTA 会话期间静默标志: downloading/flashing 时置位
→ event_report 暂停发送 (队列积压) + offlog/快照落盘暂停 */
uint8_t ota_silent_active(void);
/* 当前会话元数据 (只读, MQTT 响应组装用) */
const OtaMeta *ota_meta(void);
/* 状态字符串 (JSON state 字段) */
const char *ota_state_str(uint8_t state);
/*===========================================================================
* MQTT 命令入口 (iot_mqtt_srv.c 调用, 主循环上下文)
* 返回 0=成功, 非0=错误码 (1=参数 3=忙 5=内部; data.err_code 用 OTA_ERR_*)
*===========================================================================*/
int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force);
int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex);
int ota_cmd_end(uint32_t crc32);
int ota_cmd_abort(void);
int ota_cmd_flash(uint8_t slot, uint8_t force);
/* 分片落盘用: hex → bin (n 字节), 成功 0 */
int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n);
/* 从 MQTT JSON 中提取 ota_data 的 data 字段 hex 值 (兼容 "data":"..." 与 "data": "...")
返回 hex 字符串长度, 0=未找到/为空 */
int ota_json_extract_hex(const char *json, char *out, int out_len);
/* CRC-32/ISO-HDLC (逐位实现, 零查表 RAM) */
uint32_t ota_crc32(const uint8_t *data, uint32_t len);
/*===========================================================================
* 本地刷写与 UART2 联动 (usart_biz.c 使用)
*===========================================================================*/
/* 1 = 本地刷写进行中: uart_srv OTA 分支把 0x9F ACK 交给 ota_flash_feed_ack,
而非透传回 BLE (extern 定义于 ota_srv.c) */
extern uint8_t g_ota_flash_active;
/* usart_srv 收到 bootloader 0x9F ACK 帧时调用 (主循环上下文) */
void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len);
/* 刷写进度 (已送字节 / 总量, ota_report 用) */
uint32_t ota_flash_sent(void);
uint32_t ota_flash_total(void);
#endif /* __OTA_SRV_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_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);
uint8_t SPI_Flash_ReadJEDEC_ID(void); /* 0x9F JEDEC device id (W25Q32=0x16...) */
#endif
@@ -17,7 +17,8 @@
* Protocol Constants
*===========================================================================*/
#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_RECV_BUF_LEN (TCP_JSON_MAX_FRAME * 2)
#define TCP_JSON_MAX_PWD_RETRY 3 // Max password retries before lockout
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,7 @@
#include "CONFIG.h"
#include "loop_uart_proto.h"
#include "cmcng.h"
#include "fault_diag.h"
#include <string.h>
/*===========================================================================
@@ -508,6 +509,31 @@ void lup_send_get_version(void)
lup_cmd_begin(LUP_CMD_GET_VERSION, 200);
}
/*===========================================================================
* (V1.10) 0x4A , MQTT dev_info_query/initialize
*===========================================================================*/
LoopVerCache g_lup_ver_cache; /* BSS 清零: valid=0 */
void lup_cache_from_info(const LUP_VersionInfo *info)
{
if (!info) return;
g_lup_ver_cache.valid = 1;
g_lup_ver_cache.hard_main = info->hard_main;
g_lup_ver_cache.hard_sub = info->hard_sub;
g_lup_ver_cache.hard_ssub = info->hard_ssub;
g_lup_ver_cache.soft_main = info->soft_main;
g_lup_ver_cache.soft_sub = info->soft_sub;
g_lup_ver_cache.soft_ssub = info->soft_ssub;
}
/* 后台刷新: 仅命令通道空闲时发起 (g_lup_cmd 全局唯一, 与 TCP/MQTT 命令共用
; , ) */
void lup_refresh_version_cache(void)
{
if (g_lup_cmd.state != LUP_STATE_IDLE) return;
lup_send_get_version();
}
void lup_send_reset(void)
{
uint8_t buf[LUP_MAX_PKG_LEN];
@@ -654,6 +680,110 @@ int lup_feed_byte(uint8_t byte)
// 不应该到达这里,调用方应在 COMPLETE 后 reset
lup_frame_reset();
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;
@@ -681,14 +811,24 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
int csr;
uint8_t i;
if (len < 6) return;
FAULT_MARKER(MK_LUP_FRAME_IN);
// Debug: print raw
PRINT("LUP Rx:");
for (i = 0; i < len; i++) {
PRINT(" %02X", pkg[i]);
if (len < 6) { FAULT_MARKER(MK_LUP_FRAME_OUT); return; }
// Debug: print raw (2026-08-13: 缓冲一次性输出 — 逐字节 PRINT 关中断 ~350us/字节
// 会屏蔽 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 ---
csr = lup_verify_checksum(pkg, len);
@@ -712,4 +852,5 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
// Try matching with pending command
lup_cmd_on_response(pkg, len);
FAULT_MARKER(MK_LUP_FRAME_OUT);
}
@@ -19,6 +19,7 @@
#include "simple_json.h"
#include "tcp_json_srv.h"
#include "iot_mqtt_srv.h"
#include "offlog.h"
uint32_t slen;
@@ -64,7 +65,7 @@ uint8_t MyBuf[RECE_BUF_LEN];
#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_password[32] = {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/异常
_dev_time_base_unix = unix_ts;
_dev_time_base_tick = mstick();
offlog_time_anchor(unix_ts); // 事件日志: 时钟同步锚点 (boot_seq↔unix 回算)
PRINT("TIME: synced unix=%lu\n", (unsigned long)unix_ts);
}
@@ -120,7 +122,7 @@ uint32_t dev_time_now(void)
void mStopIfError(u8 iError)
{
if (iError == WCHNET_ERR_SUCCESS) return;
printf("Error: 0x%02X\r\n", (u16)iError);
PRINT("Error: 0x%02X\r\n", (u16)iError); /* 原裸 printf: DEBUG=0 时会去写未被初始化的 USART1 */
}
void mqtt_connect(void)
@@ -344,6 +346,13 @@ void WCHNET_CreateTcpMqttSocket(void)
{
PRINT("WCHNET_MQTT_SocketCreate %d,desIP:%d.%d.%d.%d, des_port:%d, srcport:%d\r\n", SocketId_TCP, RemoteIP[0],RemoteIP[1],RemoteIP[2],RemoteIP[3], iot_net_info.mqtt_port, TmpSocketInf.SourPort);
}
if (i != WCHNET_ERR_SUCCESS) {
/* 2026-08-20: SocketCreat 失败 (0x1D ISCONN 等, 旧 socket 未清干净)
, 退 Connect */
PRINT("MQTT: SocketCreat failed 0x%02X, abort connect\r\n", i);
SocketId_TCP = 0xFF;
return;
}
mStopIfError(i);
i = WCHNET_SocketConnect(SocketId_TCP); //make a TCP connection
if(i == WCHNET_ERR_SUCCESS)
@@ -578,28 +587,20 @@ uint8_t i;
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 (intstat & SINT_STAT_RECV) {
mqtt_data_manage(socketid);
}
if (intstat & SINT_STAT_CONNECT) {
#if KEEPLIVE_ENABLE
WCHNET_SocketSetKeepLive(socketid, ENABLE);
#endif
WCHNET_ModifyRecvBuf(socketid, (uint32_t)SocketRecvBuf[socketid], RECE_BUF_LEN);
PRINT("TCP Connect Success (MQTT)\n");
mqtt_connect();
PRINT("TCP Connect Success (MQTT IoT)\n");
}
if (intstat & SINT_STAT_DISCONNECT) {
PRINT("TCP Disconnect (MQTT)\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;
if (intstat & SINT_STAT_DISCONNECT || intstat & SINT_STAT_TIM_OUT) {
PRINT("TCP Disconnect/Timeout (MQTT IoT)\n");
}
iot_mqtt_handle_sock_int(socketid, intstat);
return;
}
@@ -0,0 +1,529 @@
/**
******************************************************************************
* @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_ota_start(uint8_t slot, uint32_t size)
{
uint8_t p[6];
p[0] = slot;
p[1] = 0; /* target: 0=loop */
p[2] = (uint8_t)(size >> 24);
p[3] = (uint8_t)(size >> 16);
p[4] = (uint8_t)(size >> 8);
p[5] = (uint8_t)size;
offlog_evt(OFFLOG_EVT_OTA_START, p, 6);
}
void offlog_ota_result(uint32_t code)
{
uint8_t p[4];
p[0] = (uint8_t)(code >> 24);
p[1] = (uint8_t)(code >> 16);
p[2] = (uint8_t)(code >> 8);
p[3] = (uint8_t)code;
offlog_evt(OFFLOG_EVT_OTA_RESULT, p, 4);
}
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_OTA_START: return "ota_start";
case OFFLOG_EVT_OTA_RESULT: return "ota_result";
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;
}
@@ -0,0 +1,633 @@
/**
******************************************************************************
* @file ota_srv.c
* @author wangfq
* @version V1.0
* @date 2026-08-20
* @brief DLD960 Loop MCU OTA (MQTT V1.08, ROADMAP P1.4 )
*
* :
* 1 : MQTT (256B/, + CRC32) W25Qxx
* 2 : ota_end CRC32 state=ready
* 3 : ota_flash 0x9F ISP (A5/A6/A7 ),
* tick ota_poll(), MQTT
*
* : downloading/flashing event_report (,
* ) + offlog/ ("升级开始",
* "升级结果")
*
* : docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5
* 稿: docs/DLD960_MQTT_OTA协议.md V1.01
******************************************************************************
*/
#include "CONFIG.h"
#include "cmcng.h"
#include "ota_srv.h"
#include "offlog.h"
#include "storage.h"
#include "loop_uart_proto.h"
#include <string.h>
/*===========================================================================
*
*===========================================================================*/
static OtaMeta _meta; /* 当前会话元数据 (RAM 副本) */
static uint32_t _slot_base; /* 当前 Slot 数据区基址 */
static uint32_t _meta_watermark; /* 上次落盘元数据时的 received (节流) */
static uint8_t _init_done; /* ota_init 完成 */
/* 刷写状态机 (非阻塞 tick 驱动) */
static uint8_t _fs; /* OTA_FLASH_* */
static uint8_t _fs_retry; /* 当前步重试计数 */
static uint32_t _fs_start_ms; /* 当前步起始时刻 (超时判断) */
static uint16_t _fs_sub; /* 当前块序号 (首=总块数, 末=1) */
static uint32_t _fs_sent; /* 已送 Loop 字节数 */
uint8_t g_ota_flash_active = 0; /* 本地刷写进行中 → uart_srv 把 ACK 交给 ota_srv */
/* 块缓冲 (static: 栈紧张教训; union: 下载片 256B 与 A7 帧 254B 互斥复用, 省 ~260B RAM) */
static union {
uint8_t chunk[OTA_CHUNK_SIZE]; /* 下载片缓冲 / 校验读缓冲 */
uint8_t frame[5 + OTA_FLASH_BLOCK + 1]; /* A7 帧: 9F+SubL+SubH+LEN+CMD+DATA+CHECK */
} _ota_buf;
/*===========================================================================
* CRC-32/ISO-HDLC ( §4.19.1; , RAM)
* poly=0x04C11DB7 (reflected 0xEDB88320), init=0xFFFFFFFF, xorout=0xFFFFFFFF
* : crc32("123456789") = 0xCBF43926
*===========================================================================*/
/* 增量更新: 调用方初始 crc=0xFFFFFFFF, 分块续算, 最后 ^0xFFFFFFFF
( 96KB , RAM) */
static uint32_t ota_crc32_update(uint32_t crc, const uint8_t *data, uint32_t len)
{
uint32_t i;
uint8_t b;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (b = 0; b < 8; b++) {
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
}
return crc;
}
uint32_t ota_crc32(const uint8_t *data, uint32_t len)
{
return ota_crc32_update(0xFFFFFFFFu, data, len) ^ 0xFFFFFFFFu;
}
/*===========================================================================
* hex
*===========================================================================*/
static uint8_t ota_hex_val(char c)
{
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10);
if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10);
return 0xFF;
}
int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n)
{
uint32_t i;
for (i = 0; i < n; i++) {
uint8_t hi = ota_hex_val(hex[i * 2]);
uint8_t lo = ota_hex_val(hex[i * 2 + 1]);
if (hi == 0xFF || lo == 0xFF) return -1;
out[i] = (uint8_t)((hi << 4) | lo);
}
return 0;
}
/* 从 MQTT JSON 提取 ota_data 的 data 字段 hex 值。
2026-08-20 : json.dumps ("data": "hex"),
strstr("\"data\":\"") CRC
"data" /, */
int ota_json_extract_hex(const char *json, char *out, int out_len)
{
const char *p = json;
int n = 0;
if (!json || !out || out_len <= 0) return 0;
out[0] = '\0';
while (*p) {
if (strncmp(p, "\"data\"", 6) == 0) {
const char *q = p + 6;
while (*q == ' ' || *q == '\t') q++;
if (*q == ':') {
q++;
while (*q == ' ' || *q == '\t') q++;
if (*q == '"') {
q++; /* 跳过起始引号 */
while (*q && *q != '"' && n < out_len - 1) {
out[n++] = *q;
q++;
}
out[n] = '\0';
return n;
}
}
}
p++;
}
return 0;
}
/*===========================================================================
* (, 0x010000~0x0103FF)
* : (45ms) + NoCheck +
* : magic , ; IDLE
*===========================================================================*/
static void ota_meta_write(const OtaMeta *m)
{
uint8_t buf[64];
memset(buf, 0xFF, sizeof(buf));
memcpy(buf, m, sizeof(OtaMeta));
SPI_Flash_Erase_Sector((uint32_t)OTA_META_SECTOR);
SPI_Flash_Write_NoCheck(buf, OTA_META_BASE, 64);
SPI_Flash_Write_NoCheck(buf, OTA_META_BACKUP, 64);
PRINT("OTA: meta write state=%lu received=%lu\n",
(unsigned long)m->state, (unsigned long)m->received);
}
static int ota_meta_read(OtaMeta *m)
{
uint8_t buf[64];
memset(buf, 0xFF, sizeof(buf));
SPI_Flash_Read(buf, OTA_META_BASE, 64);
memcpy(m, buf, sizeof(OtaMeta));
if (m->magic == OTA_MAGIC) return 0;
memset(buf, 0xFF, sizeof(buf));
SPI_Flash_Read(buf, OTA_META_BACKUP, 64);
memcpy(m, buf, sizeof(OtaMeta));
if (m->magic == OTA_MAGIC) return 0;
return -1;
}
/* 节流落盘: 每累计 4KB 或关键节点写元数据 (擦 45ms 不能每片做) */
static void ota_meta_flush(void)
{
if (_meta.received - _meta_watermark >= 4096u) {
ota_meta_write(&_meta);
_meta_watermark = _meta.received;
}
}
/*===========================================================================
*
*===========================================================================*/
const char *ota_state_str(uint8_t state)
{
static const char *names[] = {
"idle", "downloading", "ready", "flashing", "flash_failed", "aborted"
};
return (state < 6) ? names[state] : "idle";
}
uint8_t ota_silent_active(void)
{
return (_meta.state == OTA_STATE_DOWNLOADING || _meta.state == OTA_STATE_FLASHING);
}
const OtaMeta *ota_meta(void)
{
return &_meta;
}
/*===========================================================================
*
*===========================================================================*/
void ota_init(void)
{
OtaMeta m;
memset(&m, 0, sizeof(m));
if (ota_meta_read(&m) == 0) {
/* 恢复上次会话状态 (掉电/复位后) */
_meta = m;
if (_meta.state == OTA_STATE_DOWNLOADING) {
/* 下载中断: 保持 downloading, 平台 ota_begin 时续传 */
PRINT("OTA: resume downloading received=%lu\n", (unsigned long)_meta.received);
} else if (_meta.state == OTA_STATE_FLASHING) {
/* 刷写中断 (掉电): 置失败态, 保留镜像可重试 */
_meta.state = OTA_STATE_FLASH_FAILED;
_meta.last_result = OTA_ERR_READY_TIMEOUT;
PRINT("OTA: flash interrupted on boot, mark failed\n");
ota_meta_write(&_meta);
}
/* ready/aborted/flash_failed/idle: 原样保留 */
} else {
memset(&_meta, 0, sizeof(_meta));
_meta.magic = OTA_MAGIC;
_meta.state = OTA_STATE_IDLE;
_meta.slot = 0;
PRINT("OTA: fresh meta (idle)\n");
}
_slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE;
_meta_watermark = _meta.received;
_fs = OTA_FLASH_IDLE;
_init_done = 1;
PRINT("OTA: init done state=%s slot=%d received=%lu\n",
ota_state_str(_meta.state), _meta.slot, (unsigned long)_meta.received);
}
/*===========================================================================
* ota_begin ( / )
* : 0= (data.offset = _meta.received size), 0=
*===========================================================================*/
int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force)
{
(void)force; /* 版本冲突判断暂不校验 (bootloader 不校验版本), 预留 */
if (size == 0 || size > OTA_MAX_IMAGE) {
return 1; /* 参数错误 */
}
/* 已有镜像且 size+crc32 一致 → 续传或直接 ready */
if (_meta.magic == OTA_MAGIC && _meta.size == size && _meta.crc32 == crc32) {
_slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE;
if (_meta.state == OTA_STATE_READY) {
/* 镜像已就绪, 平台可直接 ota_flash */
return 0; /* data.offset = size */
}
if (_meta.state == OTA_STATE_IDLE) {
/* V1.09: 刷写完成后的 idle 且镜像一致 → 直接回 ready, 免下载可重刷 */
_meta.state = OTA_STATE_READY;
ota_meta_write(&_meta);
PRINT("OTA: begin idle+same image → ready (reflash)\n");
return 0; /* data.offset = size */
}
if (_meta.state == OTA_STATE_DOWNLOADING) {
_meta.state = OTA_STATE_DOWNLOADING;
PRINT("OTA: begin resume received=%lu\n", (unsigned long)_meta.received);
return 0; /* data.offset = received */
}
/* flash_failed / aborted: 重新走下载 */
}
/* 新会话: 始终用 Slot A (B 保留上一版镜像做回滚) */
_slot_base = OTA_SLOT_A_BASE;
memset(&_meta, 0, sizeof(_meta));
_meta.magic = OTA_MAGIC;
_meta.state = OTA_STATE_DOWNLOADING;
_meta.target = 0;
_meta.slot = 0;
_meta.size = size;
_meta.crc32 = crc32;
_meta.received = 0;
_meta.last_result = 0;
_meta.begin_ts = 0;
memset(_meta.version, 0, sizeof(_meta.version));
if (version) strncpy(_meta.version, version, sizeof(_meta.version) - 1);
_meta.flash_cnt = 0;
_meta_watermark = 0;
/* 先擦目标 Slot 数据区 (100KB = 25 扇区 ~1.1s, 主循环上下文可接受 < IWDG 4s) */
{
uint32_t sec;
for (sec = 0; sec < OTA_SLOT_SIZE / 4096UL; sec++) {
SPI_Flash_Erase_Sector((uint32_t)(_slot_base / 4096UL) + sec);
}
}
ota_meta_write(&_meta);
PRINT("OTA: begin new session size=%lu crc32=0x%08lX slot=%d\n",
(unsigned long)size, (unsigned long)crc32, _meta.slot);
return 0;
}
/*===========================================================================
* ota_data (, 256B/)
* : 0= (), 1=/CRC/ (err_code ), 3=
*===========================================================================*/
int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex)
{
uint32_t clen;
if (_meta.state != OTA_STATE_DOWNLOADING) {
return 3; /* 会话状态不允许 (err_code=2 由调用方映射) */
}
if (offset % OTA_CHUNK_SIZE != 0 || offset >= _meta.size) {
return 1; /* 参数错误 */
}
if (offset < _meta.received) {
return 0; /* 重复片: 幂等 */
}
if (offset > _meta.received) {
return 1; /* 乱序缺片: 调用方用 data.offset=received 指示续传 */
}
clen = _meta.size - offset;
if (clen > OTA_CHUNK_SIZE) clen = OTA_CHUNK_SIZE;
if (ota_hex_decode(hex, _ota_buf.chunk, clen) != 0) {
return 1; /* hex 格式错误 */
}
if (ota_crc32(_ota_buf.chunk, clen) != crc32) {
return 1; /* 单片 CRC 失败 */
}
/* 落盘: SPI_Flash_Write 自带"目标区非 0xFF 则读-改-写擦扇区" (256B 页对齐) */
SPI_Flash_Write(_ota_buf.chunk, _slot_base + offset, (uint16_t)clen);
_meta.received = offset + clen;
ota_meta_flush();
PRINT("OTA: data@%lu crc_ok received=%lu\n",
(unsigned long)offset, (unsigned long)_meta.received);
return 0;
}
/*===========================================================================
* ota_end ( CRC32 )
*===========================================================================*/
int ota_cmd_end(uint32_t crc32)
{
if (_meta.state != OTA_STATE_DOWNLOADING) {
return 3;
}
if (_meta.received != _meta.size) {
return 1; /* 不完整: data.offset=received 续传 */
}
if (crc32 != _meta.crc32) {
return 1; /* 声明值不一致 */
}
/* 读回暂存区计算全镜像 CRC32 (分块增量, 96KB ≈ 40ms @120MHz, 主循环上下文) */
{
uint32_t total = 0xFFFFFFFFu;
uint32_t i = 0;
while (i < _meta.size) {
uint32_t n = _meta.size - i;
if (n > OTA_CHUNK_SIZE) n = OTA_CHUNK_SIZE;
SPI_Flash_Read(_ota_buf.chunk, _slot_base + i, (uint16_t)n);
total = ota_crc32_update(total, _ota_buf.chunk, n);
i += n;
}
total ^= 0xFFFFFFFFu;
if (total != _meta.crc32) {
_meta.state = OTA_STATE_DOWNLOADING; /* 保留已下载数据, 可重发错片 */
PRINT("OTA: end CRC mismatch 0x%08lX != 0x%08lX\n",
(unsigned long)total, (unsigned long)_meta.crc32);
return 5; /* 内部错误 + data.crc_ok=false */
}
}
_meta.state = OTA_STATE_READY;
_meta.last_result = 0;
ota_meta_write(&_meta);
PRINT("OTA: end OK crc_ok → ready (slot %d)\n", _meta.slot);
return 0;
}
/*===========================================================================
* ota_abort
*===========================================================================*/
int ota_cmd_abort(void)
{
if (_meta.state == OTA_STATE_IDLE) {
return 0; /* 无会话: 幂等 */
}
_meta.state = OTA_STATE_ABORTED;
ota_meta_write(&_meta);
PRINT("OTA: abort → aborted\n");
return 0;
}
/*===========================================================================
* ota_flash ( ISP , + )
* : 4 (err_code=3); force
*===========================================================================*/
int ota_cmd_flash(uint8_t slot, uint8_t force)
{
uint8_t any_car;
extern uint8_t iot_any_car(void);
if (_meta.state != OTA_STATE_READY) {
return 3; /* 非 ready: 先 ota_end */
}
if (slot != 0 && slot != 1) {
return 1;
}
if (slot != _meta.slot) {
/* 指定了非当前 ready 槽: 读该槽元数据? 简化: 仅允许当前槽 */
return 1;
}
/* 与 BLE OTA 透传互斥: g_flag_counter_ota.flag=1 且非本地刷写 → 拒绝 */
if (g_flag_counter_ota.flag != 0 && !g_ota_flash_active) {
return 3;
}
/* 安全窗口: 有车压线圈拒绝 (force 跳过) */
any_car = iot_any_car();
if (!force && any_car) {
PRINT("OTA: flash rejected, car on loop\n");
return 3;
}
/* 启动刷写 (异步): offlog 开始事件先写 (会话静默前) */
offlog_ota_start(_meta.slot, _meta.size);
_meta.state = OTA_STATE_FLASHING;
_meta.flash_cnt++;
_meta.last_result = 0;
ota_meta_write(&_meta);
_fs = OTA_FLASH_START;
_fs_retry = 0;
_fs_sent = 0;
_fs_sub = 0;
g_ota_flash_active = 1;
g_flag_counter_ota.flag = 1; /* UART2 切 0x9F 解析 */
g_flag_counter_ota.tick = 0;
PRINT("OTA: flash start slot=%d size=%lu (force=%d)\n",
_meta.slot, (unsigned long)_meta.size, force);
return 0;
}
/*===========================================================================
* ( tick , ota_poll )
*
* A5 pre_ok A6 addr_ok A7 ×N (=1)
* 1s ×3; state=flash_failed + event_report ota_error
*===========================================================================*/
/* 0x9F 帧发送: 9F SubL SubH LEN CMD DATA... CHECK(SUM 不含 magic) */
static void ota_send_9f(uint8_t sub_l, uint8_t sub_h, uint8_t cmd,
const uint8_t *data, uint16_t datalen)
{
uint16_t i;
uint8_t sum = 0;
_ota_buf.frame[0] = 0x9F;
_ota_buf.frame[1] = sub_l;
_ota_buf.frame[2] = sub_h;
_ota_buf.frame[3] = (uint8_t)(1 + datalen); /* LEN 含 CMD */
_ota_buf.frame[4] = cmd;
if (data && datalen) memcpy(&_ota_buf.frame[5], data, datalen);
for (i = 1; i < 5 + datalen; i++) sum += _ota_buf.frame[i]; /* SubL..DATA */
_ota_buf.frame[5 + datalen] = sum;
UART2_SendString(_ota_buf.frame, (uint16_t)(5 + datalen + 1));
}
/* 刷写失败收尾 (V1.09): 状态落盘 + ota_report failed + event_report 告警 + 恢复 0x7F 模式 */
static void ota_flash_fail(uint32_t err)
{
PRINT("OTA: flash FAILED err=0x%lX\n", (unsigned long)err);
_meta.state = OTA_STATE_FLASH_FAILED;
_meta.last_result = err;
ota_meta_write(&_meta);
offlog_ota_result(err);
/* 恢复 UART2 0x7F 模式 */
g_ota_flash_active = 0;
g_flag_counter_ota.flag = 0;
lup_frame_reset();
_fs = OTA_FLASH_IDLE;
/* ota_report stage=failed (平台判定主依据, V1.09) */
extern void iot_ota_report_result(uint8_t ok, uint32_t err);
iot_ota_report_result(0, err);
/* event_report ota_error 告警 (平台必答, 由 iot_mqtt_srv 发送) */
extern void iot_evt_report_ota_error(uint32_t err);
iot_evt_report_ota_error(err);
}
/* 刷写成功收尾 (V1.09): 状态回 idle 但镜像保留 (可重刷, last_result=0),
ota_report stage=done (), 0x7F */
static void ota_flash_done(void)
{
PRINT("OTA: flash DONE sent=%lu\n", (unsigned long)_fs_sent);
_meta.state = OTA_STATE_IDLE; /* V1.09: idle+size>0 = 已刷写完成 (区别于 ready=待刷) */
_meta.last_result = 0;
ota_meta_write(&_meta);
offlog_ota_result(0);
g_ota_flash_active = 0;
g_flag_counter_ota.flag = 0;
lup_frame_reset();
_fs = OTA_FLASH_IDLE;
/* ota_report stage=done (平台判定主依据, V1.09) */
extern void iot_ota_report_result(uint8_t ok, uint32_t err);
iot_ota_report_result(1, 0);
}
/* UART2 收到 bootloader 0x9F ACK 帧 (uart_srv OTA 分支调用, 主循环上下文)
* ACK : 9F SubL SubH 02 CMD Status CHECK
* pre_ok = 9F 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 ack= 9F SubL SubH 02 A7 00 CK | data err = 9F SubL SubH 02 A7 01 CK */
void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len)
{
uint8_t cmd, status;
if (len < 7 || pkg[0] != 0x9F) return;
if (pkg[3] != 0x02) return; /* ACK 帧 LEN=2 */
cmd = pkg[4];
status = pkg[5];
PRINT("OTA: ack cmd=0x%02X status=%d sub=%u\n", cmd, status, pkg[1]);
switch (_fs) {
case OTA_FLASH_WAIT_READY:
if (cmd == 0xA5 && status == 0x00) {
_fs = OTA_FLASH_ADDR;
_fs_retry = 0;
}
break;
case OTA_FLASH_WAIT_ADDR:
if (cmd == 0xA6 && status == 0x00) {
/* 地址 OK → 进入数据阶段: 首块序号 = 总块数 (末块=1) */
uint32_t blocks = (_meta.size + OTA_FLASH_BLOCK - 1) / OTA_FLASH_BLOCK;
_fs_sub = (uint16_t)(blocks > 0xFFFF ? 0xFFFF : blocks);
_fs_sent = 0;
_fs = OTA_FLASH_DATA;
_fs_retry = 0;
} else if (cmd == 0xA6 && status != 0x00) {
ota_flash_fail(OTA_ERR_ADDR_FAIL);
}
break;
case OTA_FLASH_WAIT_ACK:
if (cmd == 0xA7) {
if (status == 0x00) {
_fs_sent += OTA_FLASH_BLOCK;
if (_fs_sent >= _meta.size) _fs_sent = _meta.size;
_fs_retry = 0;
if (_fs_sub <= 1) {
/* 末块已 ACK → 完成 (bootloader 清 flag 复位跑新 APP) */
ota_flash_done();
} else {
_fs_sub--; /* 序号递减: 首=总块数, 末=1 */
_fs = OTA_FLASH_DATA;
}
} else {
/* 数据块错误: 重发同块 (序号不变) */
_fs_retry++;
if (_fs_retry > 3) {
ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT);
} else {
_fs = OTA_FLASH_DATA;
}
}
}
break;
default:
break;
}
}
uint32_t ota_flash_sent(void) { return _fs_sent; }
uint32_t ota_flash_total(void) { return _meta.size; }
/* 主循环轮询: 推进刷写状态机 (每轮最多发 1~2 块, 绝不阻塞) */
void ota_poll(void)
{
uint32_t now;
if (!_init_done || _fs == OTA_FLASH_IDLE) return;
now = mstick();
if (_fs == OTA_FLASH_START) {
/* 发 A5 启动帧: 9F 01 00 01 A5 A7 → Loop APP 写 flag 复位 → bootloader 回 pre_ok */
static const uint8_t start_frame[6] = {0x9F, 0x01, 0x00, 0x01, 0xA5, 0xA7};
UART2_SendString((uint8_t *)start_frame, 6);
_fs = OTA_FLASH_WAIT_READY;
_fs_start_ms = now;
/* 重试计数保持 (首发由 ota_cmd_flash 置 0; 重发不清零, 否则超时重试永不失败) */
PRINT("OTA: sent A5 start\n");
return;
}
if (_fs == OTA_FLASH_WAIT_READY) {
if (now - _fs_start_ms > 1000u) {
_fs_retry++;
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_READY_TIMEOUT); return; }
_fs = OTA_FLASH_START; /* 重发启动帧 */
}
return;
}
if (_fs == OTA_FLASH_ADDR) {
/* 发 A6 地址帧: 9F 01 00 05 A6 08 00 34 00 SUM (0x08003400 大端) */
uint8_t addr[4] = {0x08, 0x00, 0x34, 0x00}; /* APP_START_ADDR */
ota_send_9f(0x01, 0x00, 0xA6, addr, 4);
_fs = OTA_FLASH_WAIT_ADDR;
_fs_start_ms = now;
/* 重试计数保持 (ACK 成功路径在 ota_flash_feed_ack 清 0) */
return;
}
if (_fs == OTA_FLASH_WAIT_ADDR) {
if (now - _fs_start_ms > 1000u) {
_fs_retry++;
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_ADDR_FAIL); return; }
_fs = OTA_FLASH_ADDR; /* 重发地址帧 */
}
return;
}
if (_fs == OTA_FLASH_DATA) {
/* 从 W25Qxx 读一块 → 组 A7 帧 → 发 (停等 ACK) */
uint32_t read_off = _fs_sent;
uint32_t remain = _meta.size - read_off;
uint16_t datalen = (remain > OTA_FLASH_BLOCK) ? OTA_FLASH_BLOCK : (uint16_t)remain;
SPI_Flash_Read(_ota_buf.frame + 5, _slot_base + read_off, datalen);
ota_send_9f((uint8_t)(_fs_sub & 0xFF), (uint8_t)(_fs_sub >> 8), 0xA7,
_ota_buf.frame + 5, datalen);
_fs = OTA_FLASH_WAIT_ACK;
_fs_start_ms = now;
PRINT("OTA: sent A7 sub=%u off=%lu datalen=%u\n",
_fs_sub, (unsigned long)read_off, datalen);
return;
}
if (_fs == OTA_FLASH_WAIT_ACK) {
if (now - _fs_start_ms > 1000u) {
_fs_retry++;
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT); return; }
_fs = OTA_FLASH_DATA; /* 重发同块 */
}
return;
}
}
@@ -147,6 +147,11 @@ static uint8_t attDeviceName[GAP_DEVICE_NAME_LEN] = "DLD960GA"; // "Simple Perip
static peripheralConnItem_t peripheralConnList;
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
*/
@@ -679,13 +684,30 @@ static void peripheralStateNotificationCB(gapRole_States_t newState, gapRoleEven
*/
static void performPeriodicTask(void)
{
// uint8_t notiData[SIMPLEPROFILE_CHAR4_LEN] = {0x88};
// peripheralChar4Notify(notiData, SIMPLEPROFILE_CHAR4_LEN);
/* 2026-08-17 cleanup: removed _dbg_cnt 500ms debug heartbeat print */
if(g_flag_notify_temp){
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);
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)
{
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))
{
PRINT("Too large noti\n");
PRINT("Too large noti, len:%d, peripheralMTU:%d\n", len, peripheralMTU);
return;
}
noti.len = len;
@@ -713,8 +738,22 @@ static void peripheralChar4Notify(uint8_t *pValue, uint16_t len)
tmos_memcpy(noti.pValue, pValue, noti.len);
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);
}
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,13 @@
#include "eth_driver.h"
#include "net_srv.h"
#include "storage.h"
#include "offlog.h"
#include "snapshot.h"
#include "tcp_json_srv.h"
#include "loop_uart_proto.h"
#include "iot_mqtt_srv.h"
#include "ota_srv.h"
#include "fault_diag.h"
/*********************************************************************
* GLOBAL TYPEDEFS
@@ -229,6 +233,9 @@ void Main_Circulation(void)
uint32_t _counter = 0;
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();
if(g_net_state.flag < 2)
@@ -265,13 +272,18 @@ void Main_Circulation(void)
WCHNET_HandleGlobalInt();
}
FAULT_MARKER(MK_UART_SRV_IN);
uart_srv();
FAULT_MARKER(MK_UART_SRV_OUT);
uart1_dma_poll(); /* 4G 通道下行收帧 (协议 §6.1): 置于 fault marker 之外, 不改变 MK_UART_SRV 语义 */
ota_poll(); /* OTA 刷写状态机 (V1.08): 非阻塞 tick 驱动, 先消费 UART2 ACK 再推进 */
snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */
FAULT_MARKER(MK_POLL_BLE_IN);
poll_dbn_ble();
if (g_sub_code_enable.iot_enable) {
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 {
tcp_json_push_sensor(); // Push 0xC0 sensor data to TCP JSON client
tcp_json_poll();
@@ -298,12 +310,49 @@ int main(void)
// USART_Printf_Init( 115200 );
USART_Printf_Init( 256000 );
#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("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);
storage_init();
PRINT("INIT: storage ok\n");
offlog_init();
PRINT("INIT: offlog ok\n");
ota_init(); /* OTA 元数据恢复 (V1.08): 下载中断续传 / 刷写中断标记失败 */
PRINT("INIT: ota 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();
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("net version:%x\n", WCHNET_GetVer());
@@ -311,6 +360,24 @@ int main(void)
HAL_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();
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 */
#define W25Q80 0XEF13
#define W25Q16 0XEF14
#define W25Q32 0XEF15
#define W25Q64 0XEF16 //64Mbit, 8MByte
#define W25Q128 0XEF17
#define W25Q80 0X13 // device ID only, no vendor code
#define W25Q16 0X14
#define W25Q32 0X15
#define W25Q64 0X16 //64Mbit, 8MByte
#define W25Q128 0X17
/* Winbond SPIFalsh Instruction List */
#define W25X_WriteEnable 0x06
@@ -90,13 +90,13 @@ void SPI_Flash_Init(void)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
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_SetBits(GPIOA, GPIO_Pin_4);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
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_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_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
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_CPHA = SPI_CPHA_2Edge;
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_CRCPolynomial = 7;
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
*
@@ -481,7 +508,7 @@ void storage_init(void)
SPI_Flash_Init();
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 W25Q16: PRINT("W25Q16 OK!\r\n"); break;
@@ -18,6 +18,9 @@
#include "loop_uart_proto.h"
#include "simple_json.h"
#include "storage.h"
#include "offlog.h"
#include "snapshot.h"
#include "fault_diag.h"
#include <string.h>
#include <stdlib.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);
}
/* 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) {
lup_send_get_version();
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_write", handle_loop_sens_write, 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]))
@@ -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)
{
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",
g_json_socket_listen, g_json_auth_state, g_report_cfg.enable);
#endif
// Check: socket active, authed, report enabled
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_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;
memset(&sr, 0, sizeof(sr));
@@ -829,7 +967,7 @@ static void json_sensor_callback(const uint8_t *pkg, uint16_t len)
return;
}
char data_json[2048];
char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
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;
if (recv_len > space) recv_len = space;
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);
g_json_last_comm_ts = mstick(); // 有数据交互
memcpy(g_json_recv_buf + g_json_recv_len, tmp_buf, (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))) {
PRINT("JSON recv: %s\n", frame);
json_process_frame(socketid, frame);
@@ -1102,7 +1240,7 @@ static void json_check_pending(void) {
memset(&pg, 0, sizeof(pg));
int ret = lup_parse_param_get(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &pg);
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) {
json_send_ok(g_json_pending.socket, g_json_pending.msg_id,
g_json_pending.cmd, data_json);
@@ -1319,7 +1457,7 @@ void tcp_json_push_sensor(void) {
return;
}
char data_json[2048];
char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
if (out) {
@@ -8,14 +8,352 @@
#include "config.h"
#include "cmcng.h"
#include "debug.h" /* DEBUG 宏: 4G 版本以 -DDEBUG=0 编译, 此时 printf 不占用 USART1 */
#include "loop_uart_proto.h"
#include "dbn_ble_srv.h" /* MAGIC_BYTE_DBN_DEFAULT(0x8F): DBN 私有指令魔数 */
#include <string.h>
#include "dbn_ble_srv.h"
#include "ota_srv.h"
void USART1_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;
}
}
}
/*==================== UART1 <-> Air780 4G 通道 (协议 §6.1) ====================
* : 4G vd960DBN "字节流透传",
* : Loop 0x7F (UART2) ----> UART1 --> Air780 JSON
* : Air780 (UART1) ----> 0x7F: UART2( vd960Loop)
* 0x8F: DBN(CH32V208GBU6)
* ,
*
* :
* 1) lup_verify_checksum() Air780 loop_state 沿,
* =
* 2) DMA ( UART2 A ): PRINT / BLE / SPI
* RXNE -> -> "不丢帧"
* 3) g_lup_parser: UART2(Loop) ,
* ()
*
* ( Loop/BLE ):
* [7F][Addr][LEN][CMD][Value: LEN-1B][XOR][SUM] = LEN + 5
* Addr..Value ( magic), lup_verify_checksum()
*
* :
* (DEBUG != 0): printf USART1/PB6, ->
* 4G (DEBUG == 0): -DDEBUG=0 (PB6/PB7 )
* -DUART1_AIR780_EN=1/0
*===========================================================================*/
#ifndef UART1_AIR780_EN
#define UART1_AIR780_EN (DEBUG == 0)
#endif
#define UART1_MAGIC_LOOP LUP_MAGIC /* 0x7F: 转发给 vd960Loop */
#define UART1_MAGIC_LOCAL MAGIC_BYTE_DBN_DEFAULT /* 0x8F: DBN 私有指令(自身配置读写), 不转发 */
#define UART1_DMA_BUF_LEN 512
#if (UART1_AIR780_EN)
static uint8_t uart1_dma_buf[UART1_DMA_BUF_LEN] __attribute__((aligned(4)));
static uint16_t uart1_dma_last = 0; /* 主循环消费位置 (DMA 写指针由硬件维护) */
/* 下行帧装配器 (独立实例, 不与 g_lup_parser 共享) */
static uint8_t uart1_rx_frame[LUP_MAX_PKG_LEN];
static uint16_t uart1_rx_idx = 0; /* 已收字节数 */
static uint16_t uart1_rx_need = 0; /* 本帧总长, 0 = 尚未定长 */
/* 通道计数: 现场核对"上行转发数 == Air780 实收数", 比翻日志可靠 */
uint32_t g_uart1_fwd_to_air = 0; /* 上行: UART2 -> UART1 转发帧数 */
uint32_t g_uart1_fwd_to_loop = 0; /* 下行: UART1 -> UART2 转发帧数 */
uint32_t g_uart1_local_cnt = 0; /* 下行: DBN 私有指令帧数 (0x8F) */
uint32_t g_uart1_badchk = 0; /* 下行: 校验失败帧数 */
uint32_t g_uart1_drop = 0; /* 下行: DMA 溢出丢弃次数 */
/* ---- 下行分流 ---- */
static void uart1_dispatch_frame(uint8_t *pkg, uint16_t len)
{
if (pkg[0] == UART1_MAGIC_LOOP) {
/* 0x7F -> 原样转发给 vd960Loop */
g_uart1_fwd_to_loop++;
UART2_SendString(pkg, len);
} else {
/* 0x8F -> DBN(CH32V208GBU6) 私有指令: 只作用在自身读写配置上, 不转发给 Loop/Air780。
* BLE manage_dbn_ble_default()
* (pkg[1]=Addr, pkg[2]=LEN, pkg[3]=CMD, pkg[4..]=Value),
* OTA : ( flash), OTA */
g_uart1_local_cnt++;
if (g_flag_counter_ota.flag == 0) {
manage_dbn_ble_default(pkg, (uint8_t)len);
}
}
}
/* ---- 坏帧回找魔数: 把 [1..len) 里第一个魔数起的残余搬到头部继续装配 ----
* "单帧损坏 -> 后续帧全部失步" */
static void uart1_resync(const uint8_t *buf, uint16_t len)
{
uint16_t i;
for (i = 1; i < len; i++) {
if (buf[i] == UART1_MAGIC_LOOP || buf[i] == UART1_MAGIC_LOCAL) {
uint16_t k, n = 0;
for (k = i; k < len; k++) {
uart1_rx_frame[n++] = buf[k];
}
uart1_rx_idx = n;
uart1_rx_need = 0;
if (n >= 3) {
uint8_t lf = uart1_rx_frame[2];
uint16_t need = (uint16_t)lf + 5;
if (lf >= 1 && need <= LUP_MAX_PKG_LEN) {
uart1_rx_need = need;
} else {
uart1_rx_idx = 0; /* LEN 非法: 残余作废 */
}
}
return;
}
}
/* 残余中没有魔数 -> 全部丢弃 */
}
/* ---- 定长收满 -> 校验 -> 分流 ---- */
static void uart1_frame_ready(void)
{
uint16_t len = uart1_rx_idx;
uart1_rx_idx = 0;
uart1_rx_need = 0;
if (lup_verify_checksum(uart1_rx_frame, len) == 0) {
uart1_dispatch_frame(uart1_rx_frame, len);
} else {
g_uart1_badchk++;
uart1_resync(uart1_rx_frame, len);
}
}
/* ---- UART1 字节流装配 ---- */
static void uart1_feed_byte(uint8_t b)
{
uint8_t guard = 0;
if (uart1_rx_idx == 0) {
if (b == UART1_MAGIC_LOOP || b == UART1_MAGIC_LOCAL) {
uart1_rx_frame[0] = b;
uart1_rx_idx = 1;
}
return; /* 非魔数字节: 帧间空闲, 直接丢 */
}
if (uart1_rx_idx >= LUP_MAX_PKG_LEN) { /* 防御: 正常流程到不了 */
uart1_rx_idx = 0;
uart1_rx_need = 0;
return;
}
uart1_rx_frame[uart1_rx_idx++] = b;
if (uart1_rx_idx == 3) { /* [7F][Addr][LEN] 到手 -> 定长 */
uint8_t lf = uart1_rx_frame[2];
uint16_t need = (uint16_t)lf + 5;
if (lf < 1 || need > LUP_MAX_PKG_LEN) {
uart1_rx_idx = 0; /* LEN 非法 -> 重新找魔数 */
uart1_rx_need = 0;
return;
}
uart1_rx_need = need;
}
/* 校验失败后 resync 可能让残余已够长, 故用 while (guard 防跑飞) */
while (uart1_rx_need != 0 && uart1_rx_idx >= uart1_rx_need && guard++ < 8) {
uart1_frame_ready();
}
}
void uart1_dma_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0};
DMA_InitTypeDef DMA_InitStructure;
/* PB6 = USART1_TX / PB7 = USART1_RX (USART1 重映射) */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; /* Tx */
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; /* Rx */
GPIO_Init(GPIOB, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200; /* 与 Air780 侧 uart1 一致 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
/* RX 用 DMA1_Ch5 循环缓冲 (UART2 占用 Ch6/Ch7, 通道不冲突) */
DMA_DeInit(DMA1_Channel5);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DATAR);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart1_dma_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = UART1_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_Channel5, &DMA_InitStructure);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
DMA_Cmd(DMA1_Channel5, ENABLE);
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE); /* DMA 接管, 逐字节中断不再需要 */
uart1_dma_last = 0;
uart1_rx_idx = 0;
uart1_rx_need = 0;
}
/* 主循环每轮调用: 消费 DMA 环形缓冲新字节 -> 装配 -> 分流 */
void uart1_dma_poll(void)
{
uint16_t cur = (uint16_t)(UART1_DMA_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel5));
uint16_t n = (uint16_t)((cur + UART1_DMA_BUF_LEN - uart1_dma_last) & (UART1_DMA_BUF_LEN - 1));
if (n == 0) return;
/* 溢出保护: 未消费超过半缓冲 -> 已被 DMA 覆盖, 复位装配器重同步 */
if (n > (UART1_DMA_BUF_LEN / 2)) {
g_uart1_drop++;
uart1_rx_idx = 0;
uart1_rx_need = 0;
uart1_dma_last = cur;
return;
}
while (n--) {
uint8_t b = uart1_dma_buf[uart1_dma_last];
uart1_dma_last = (uint16_t)((uart1_dma_last + 1) & (UART1_DMA_BUF_LEN - 1));
uart1_feed_byte(b);
}
}
#else /* !UART1_AIR780_EN: 调试版 printf 占用 USART1 -> 空实现, 保证链接一致 */
void uart1_dma_init(void) { }
void uart1_dma_poll(void) { }
uint32_t g_uart1_fwd_to_air = 0;
uint32_t g_uart1_fwd_to_loop = 0;
uint32_t g_uart1_local_cnt = 0;
uint32_t g_uart1_badchk = 0;
uint32_t g_uart1_drop = 0;
#endif /* UART1_AIR780_EN */
void uart_init(void){
GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0};
@@ -44,8 +382,7 @@ void uart_init(void){
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART2, &USART_InitStructure);
// USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
/* RXNE 中断由 uart2_dma_init 关闭 (DMA 接管, 2026-08-17) */
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
@@ -55,6 +392,9 @@ void uart_init(void){
USART_Cmd(USART2, ENABLE);
uart2_dma_init(); /* DMA 循环接收接管 (2026-08-17) */
uart1_dma_init(); /* 4G 通道 UART1↔Air780 初始化 (协议 §6.1); DEBUG!=0 时为空实现 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
@@ -90,27 +430,9 @@ void USART1_IRQHandler(void)
*/
void USART2_IRQHandler(void)
{
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
{
uint8_t _dat = USART_ReceiveData(USART2);
// 喂给帧解析器
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;
}
}
/* 2026-08-17: RXNE 中断已关 (DMA 接管), 本 handler 不再触发。
+ IDLE : ,
USART_IT_IDLE ( SR + DR , ) */
}
@@ -151,6 +473,8 @@ void uart_srv(void)
uint8_t i;
uint8_t _report_flag = 0;
uart2_dma_poll(); /* DMA 环形缓冲 → 帧解析 (2026-08-17) */
// 检查命令超时
lup_cmd_check_timeout();
@@ -165,6 +489,14 @@ void uart_srv(void)
// 其他: 校验后匹配挂起命令
lup_process_frame(g_pkg_uart_2.pkg, g_pkg_uart_2.offset);
/* --- 4G 通道上行转发 (协议 §6.1) ---
Air780; InitPkgUart
verify : Air780 loop_state 沿, = */
if (lup_verify_checksum(g_pkg_uart_2.pkg, g_pkg_uart_2.offset) == 0) {
g_uart1_fwd_to_air++;
UART1_SendString(g_pkg_uart_2.pkg, g_pkg_uart_2.offset);
}
// --- 传感器上报 (0xC0) 分流 ---
// 回调已处理 TCP 推送,此处仅处理 BLE 转发
if(cmd == LUP_CMD_SENSOR_REPORT)
@@ -172,43 +504,65 @@ void uart_srv(void)
if(g_dbn_ble_state_acs_enable.flag != 0){
// BLE ACS 已连接 → 改 Magic 为 0x8F 发给 BLE
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++){
PRINT(" %02X", g_pkg_uart_2.pkg[i]);
}
PRINT("\n");
#endif
if(_report_flag){
// 传感器帧保留在 pkg 中供上层 (tcp_json_srv) 处理
// 不 InitPkgUart — 由 tcp_json_push_sensor 消费后清理
}
}
else {
// 非 0x7F 魔法字节
PRINT("Rcv_len:%d,dat: %s\n", g_pkg_uart_2.offset, g_pkg_uart_2.pkg);
// 非 0x7F 魔法字节 — hex 打印 (原 %s 会把二进制当字符串 → 乱码)
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{
// OTA 模式 — 忽略 Loop MCU 数据
/* OTA 模式: Loop bootloader 回的 0x9F ACK 帧 (pre_ok/addr_ok/data ACK) 分发:
- OTA (g_ota_flash_active, V1.08) ota_srv
- BLE (, V1.02.03) BLE
(0xA7 WITH_BACK) ACK */
if(g_ota_flash_active){
ota_flash_feed_ack(g_pkg_uart_2.pkg, g_pkg_uart_2.offset);
}
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);
}
/* OTA 模式同样清 flag — 残留帧会在 OTA 结束后被误处理 */
InitPkgUart(&g_pkg_uart_2);
}
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{
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);
PROVIDE( _sbss = .);
*(.sbss*)
*(.sbss*)
*(.gnu.linkonce.sb.*)
*(.bss*)
*(.gnu.linkonce.b.*)
*(.gnu.linkonce.b.*)
*(COMMON*)
. = ALIGN(4);
PROVIDE( _ebss = .);
} >RAM AT>FLASH
/* 故障诊断现场: 复位不清零 (startup 只清 .bss, 不碰 .noinit) */
.noinit (NOLOAD) :
{
. = ALIGN(4);
*(.noinit*)
. = ALIGN(4);
} >RAM
PROVIDE( _end = _ebss);
PROVIDE( end = . );
+10 -1
View File
@@ -46,7 +46,16 @@ void USART_Printf_Init(uint32_t baudrate);
void SDI_Printf_Enable(void);
#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
#define PRINT(X...)
#endif
+2030 -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)")
+5
View File
@@ -0,0 +1,5 @@
/* mock CONFIG.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
#ifndef __MOCK_CONFIG_H__
#define __MOCK_CONFIG_H__
#endif
+22
View File
@@ -0,0 +1,22 @@
/* mock cmcng.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
#ifndef __MOCK_CMCNG_H__
#define __MOCK_CMCNG_H__
#include <stdint.h>
#include <stdio.h>
typedef struct {
uint32_t flag;
uint32_t timeout;
uint32_t tick;
} Flag_Counter;
extern Flag_Counter g_flag_counter_key;
extern Flag_Counter g_flag_counter_ota;
uint32_t mstick(void);
void UART2_SendString(uint8_t *buf, uint16_t len);
#define PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
#endif
+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;
}
+463
View File
@@ -0,0 +1,463 @@
/*
* test_iot_loop_ver.c V1.10 : 0x4A + + MQTT
*
* :
* 1. lup_parse_version: 0x4A (/)
* 2. lup_cache_from_info / iot_loop_ver_str / iot_loop_hw_ver_str:
* 3. iot_verq_poll :
* - pending + RESPONSE_READY code=0 + +
* - pending + TIMEOUT code=5
* - : dirty=1 + IDLE 0x4A ( pending)
* - : !dirty + RESPONSE_READY +
* - : TIMEOUT IDLE
*
* : gcc -o test_iot_loop_ver test_iot_loop_ver.c && ./test_iot_loop_ver
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
/* ---- mock: 最小依赖 ---- */
#define LUP_MAGIC 0x7F
#define LUP_CMD_GET_VERSION 0x4A
#define LUP_MAX_PKG_LEN 70
#define IOT_MQTT_TOPIC_MAX_LEN 64
typedef struct {
uint8_t status;
uint8_t hard_main, hard_sub, hard_ssub;
uint8_t soft_main, soft_sub, soft_ssub;
char version_str[32];
} LUP_VersionInfo;
typedef struct {
uint8_t valid;
uint8_t hard_main, hard_sub, hard_ssub;
uint8_t soft_main, soft_sub, soft_ssub;
} LoopVerCache;
typedef enum {
LUP_STATE_IDLE = 0,
LUP_STATE_WAIT_RESPONSE,
LUP_STATE_RESPONSE_READY,
LUP_STATE_TIMEOUT
} LUP_CmdState;
typedef struct {
uint8_t pending_cmd;
uint32_t send_tick;
uint32_t timeout_ms;
LUP_CmdState state;
uint8_t resp_buf[LUP_MAX_PKG_LEN];
uint16_t resp_len;
} LUP_CmdTracker;
/* ---- mock 全局 ---- */
static LUP_CmdTracker g_lup_cmd;
static LoopVerCache g_lup_ver_cache;
static char g_iot_dev_serial[16] = "A1B2C3D4E5F6";
/* ---- mock 时间 ---- */
static uint32_t g_mock_ms = 100000;
static uint32_t mstick(void) { return g_mock_ms; }
static uint32_t dev_time_now(void) { return 1719000008UL; }
/* ---- mock 发布捕获 ---- */
static char g_pub_topic[64];
static char g_pub_payload[512];
static uint16_t g_pub_len;
static int g_pub_count;
static int g_getver_calls;
static int iot_mqtt_publish(const char *topic, const char *payload,
uint16_t payload_len, uint8_t qos)
{
(void)qos;
g_pub_count++;
snprintf(g_pub_topic, sizeof(g_pub_topic), "%s", topic ? topic : "");
if (payload_len < sizeof(g_pub_payload)) {
memcpy(g_pub_payload, payload, payload_len);
g_pub_payload[payload_len] = '\0';
} else {
memcpy(g_pub_payload, payload, sizeof(g_pub_payload) - 1);
g_pub_payload[sizeof(g_pub_payload) - 1] = '\0';
}
g_pub_len = payload_len;
return 0;
}
/* mock lup_send_get_version: 模拟 lup_cmd_begin 的状态推进 */
static void lup_send_get_version(void)
{
g_getver_calls++;
g_lup_cmd.pending_cmd = LUP_CMD_GET_VERSION;
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE;
g_lup_cmd.resp_len = 0;
memset(g_lup_cmd.resp_buf, 0, sizeof(g_lup_cmd.resp_buf));
}
/* mock lup_cmd_done: 释放命令通道 */
static void lup_cmd_done(void)
{
g_lup_cmd.pending_cmd = 0;
g_lup_cmd.state = LUP_STATE_IDLE;
g_lup_cmd.resp_len = 0;
}
#define PRINT(...) printf(__VA_ARGS__)
/* ---- 被测静态状态 (从 iot_mqtt_srv.c 提取的变量, 单测转为可访问全局) ---- */
static uint8_t _iot_verq_pending;
static uint32_t _iot_verq_msg_id;
static uint32_t _iot_verq_deadline;
static uint8_t _iot_ver_cache_dirty;
/* ================= 嵌入被测函数 (从真实源码提取) ================= */
int lup_parse_version(const uint8_t *pkg, uint16_t len, LUP_VersionInfo *info)
{
if (len < 10) return -1; // 1+3+7+2 = 13 min
// pkg layout: [7F][Addr=0][LEN=8][CMD=0x4A][Status...7bytes][XOR][SUM]
if (pkg[0] != LUP_MAGIC || pkg[3] != LUP_CMD_GET_VERSION) return -2;
if (pkg[2] < 7) return -3; // LEN should be >= 7
const uint8_t *val = pkg + 4; // skip magic + header(3)
info->status = val[0];
info->hard_main = val[1];
info->hard_sub = val[2];
info->hard_ssub = val[3];
info->soft_main = val[4];
info->soft_sub = val[5];
info->soft_ssub = val[6];
return 0;
}
void lup_cache_from_info(const LUP_VersionInfo *info)
{
if (!info) return;
g_lup_ver_cache.valid = 1;
g_lup_ver_cache.hard_main = info->hard_main;
g_lup_ver_cache.hard_sub = info->hard_sub;
g_lup_ver_cache.hard_ssub = info->hard_ssub;
g_lup_ver_cache.soft_main = info->soft_main;
g_lup_ver_cache.soft_sub = info->soft_sub;
g_lup_ver_cache.soft_ssub = info->soft_ssub;
}
static void iot_loop_ver_str(char *out, uint16_t out_len)
{
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
snprintf(out, out_len, "%d.%d.%d",
g_lup_ver_cache.soft_main, g_lup_ver_cache.soft_sub, g_lup_ver_cache.soft_ssub);
}
static void iot_loop_hw_ver_str(char *out, uint16_t out_len)
{
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
snprintf(out, out_len, "%d.%d.%d",
g_lup_ver_cache.hard_main, g_lup_ver_cache.hard_sub, g_lup_ver_cache.hard_ssub);
}
static void iot_verq_publish(const char *payload, uint16_t len)
{
char topic[IOT_MQTT_TOPIC_MAX_LEN];
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
iot_mqtt_publish(topic, payload, len, 1);
}
static void iot_verq_poll(void)
{
if (_iot_verq_pending) {
/* 命令查询在途: 等 0x4A 响应或超时 */
if (g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
LUP_VersionInfo info;
char resp[320];
char ver_str[16], hw_str[16], vs_str[64];
int ok = 0;
memset(&info, 0, sizeof(info));
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
lup_cache_from_info(&info); /* 同步刷新缓存 */
snprintf(ver_str, sizeof(ver_str), "%d.%d.%d",
info.soft_main, info.soft_sub, info.soft_ssub);
snprintf(hw_str, sizeof(hw_str), "%d.%d.%d",
info.hard_main, info.hard_sub, info.hard_ssub);
snprintf(vs_str, sizeof(vs_str), "V%s (HW:%s)", ver_str, hw_str);
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":0,"
"\"msg\":\"success\","
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"%s\"}}",
_iot_verq_msg_id, dev_time_now(), ver_str, hw_str, vs_str);
ok = 1;
} else {
char lv[16], lhv[16];
iot_loop_ver_str(lv, sizeof(lv));
iot_loop_hw_ver_str(lhv, sizeof(lhv));
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
"\"msg\":\"parse error\","
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
_iot_verq_msg_id, dev_time_now(), lv, lhv);
}
lup_cmd_done();
_iot_verq_pending = 0;
iot_verq_publish(resp, strlen(resp));
PRINT("IOT: loop_version_query resp %s\n", ok ? "ok" : "parse error");
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT || mstick() > _iot_verq_deadline) {
char resp[320];
char lv[16], lhv[16];
iot_loop_ver_str(lv, sizeof(lv));
iot_loop_hw_ver_str(lhv, sizeof(lhv));
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
"\"msg\":\"no response\","
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
_iot_verq_msg_id, dev_time_now(), lv, lhv);
lup_cmd_done();
_iot_verq_pending = 0;
iot_verq_publish(resp, strlen(resp));
PRINT("IOT: loop_version_query timeout\n");
}
/* 其它 state (WAIT_RESPONSE) 继续等 */
} else {
/* 后台缓存刷新: 上电 / OTA done 后置 dirty, 命令通道空闲时发 0x4A */
if (_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_IDLE) {
lup_send_get_version();
_iot_ver_cache_dirty = 0;
PRINT("IOT: loop version cache refresh query\n");
} else if (!_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
/* 后台查询响应 (无命令 pending) → 更新缓存 + 释放通道 */
LUP_VersionInfo info;
memset(&info, 0, sizeof(info));
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
lup_cache_from_info(&info);
PRINT("IOT: loop version cache refreshed %d.%d.%d (HW:%d.%d.%d)\n",
info.soft_main, info.soft_sub, info.soft_ssub,
info.hard_main, info.hard_sub, info.hard_ssub);
}
lup_cmd_done();
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT) {
lup_cmd_done(); /* 清残留超时态 (后台查询失败或被其它命令覆盖) */
}
}
}
/* ================= 测试工具 ================= */
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, msg) do { \
if (cond) { g_pass++; printf(" PASS: %s\n", msg); } \
else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \
} while (0)
static void reset_state(void)
{
memset(&g_lup_cmd, 0, sizeof(g_lup_cmd));
memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache));
memset(&g_pub_payload, 0, sizeof(g_pub_payload));
g_pub_count = 0;
g_pub_len = 0;
g_getver_calls = 0;
_iot_verq_pending = 0;
_iot_verq_msg_id = 0;
_iot_verq_deadline = 0;
_iot_ver_cache_dirty = 0;
g_mock_ms = 100000;
}
/* 构造 0x4A 响应帧: 7F 00 LEN 4A [status][hard_m][hard_s][hard_ss][soft_m][soft_s][soft_ss] [xor][sum]
(checksum lup_parse_version , 0) */
static void build_version_frame(uint8_t *pkg, uint16_t *len,
uint8_t status, uint8_t hm, uint8_t hs, uint8_t hss,
uint8_t sm, uint8_t ss, uint8_t sss)
{
pkg[0] = LUP_MAGIC;
pkg[1] = 0x00;
pkg[2] = 0x08;
pkg[3] = LUP_CMD_GET_VERSION;
pkg[4] = status;
pkg[5] = hm; pkg[6] = hs; pkg[7] = hss;
pkg[8] = sm; pkg[9] = ss; pkg[10] = sss;
pkg[11] = 0; pkg[12] = 0; /* XOR/SUM 不校验 */
*len = 13;
}
static void test_parse_version(void)
{
printf("[1] lup_parse_version\n");
uint8_t pkg[16];
uint16_t len;
LUP_VersionInfo info;
build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3);
CHECK(lup_parse_version(pkg, len, &info) == 0, "valid frame returns 0");
CHECK(info.status == 0x00, "status=0");
CHECK(info.hard_main == 1 && info.hard_sub == 0 && info.hard_ssub == 0, "hard=1.0.0");
CHECK(info.soft_main == 1 && info.soft_sub == 2 && info.soft_ssub == 3, "soft=1.2.3");
/* 错误路径 */
CHECK(lup_parse_version(pkg, 9, &info) == -1, "len<10 -> -1");
pkg[0] = 0x00;
CHECK(lup_parse_version(pkg, len, &info) == -2, "bad magic -> -2");
pkg[0] = LUP_MAGIC;
pkg[3] = 0x55;
CHECK(lup_parse_version(pkg, len, &info) == -2, "bad cmd -> -2");
pkg[3] = LUP_CMD_GET_VERSION;
pkg[2] = 0x05;
CHECK(lup_parse_version(pkg, len, &info) == -3, "LEN<7 -> -3");
}
static void test_cache(void)
{
printf("[2] 版本缓存\n");
LUP_VersionInfo info;
char out[24];
memset(&info, 0, sizeof(info));
info.hard_main = 1; info.hard_sub = 0; info.hard_ssub = 0;
info.soft_main = 1; info.soft_sub = 2; info.soft_ssub = 3;
lup_cache_from_info(&info);
CHECK(g_lup_ver_cache.valid == 1, "cache valid after write");
CHECK(g_lup_ver_cache.soft_main == 1 && g_lup_ver_cache.soft_ssub == 3, "cache soft fields");
iot_loop_ver_str(out, sizeof(out));
CHECK(strcmp(out, "1.2.3") == 0, "loop_ver str = 1.2.3");
iot_loop_hw_ver_str(out, sizeof(out));
CHECK(strcmp(out, "1.0.0") == 0, "loop_hw_ver str = 1.0.0");
/* 无效缓存 → 空串 */
memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache));
iot_loop_ver_str(out, sizeof(out));
CHECK(strcmp(out, "") == 0, "invalid cache -> empty str");
iot_loop_hw_ver_str(out, sizeof(out));
CHECK(strcmp(out, "") == 0, "invalid hw cache -> empty str");
/* NULL/0 长度防护 */
iot_loop_ver_str(NULL, 0);
CHECK(1, "NULL guard no crash");
}
static void test_verq_success(void)
{
printf("[3] verq_poll: 命令成功回包\n");
reset_state();
uint8_t pkg[16];
uint16_t len;
build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3);
memcpy(g_lup_cmd.resp_buf, pkg, len);
g_lup_cmd.resp_len = len;
g_lup_cmd.state = LUP_STATE_RESPONSE_READY;
_iot_verq_pending = 1;
_iot_verq_msg_id = 407;
_iot_verq_deadline = g_mock_ms + 300;
iot_verq_poll();
CHECK(g_pub_count == 1, "publish once");
CHECK(strstr(g_pub_payload, "\"msg_id\":407") != NULL, "msg_id echoed");
CHECK(strstr(g_pub_payload, "\"code\":0") != NULL, "code=0");
CHECK(strstr(g_pub_payload, "\"loop_ver\":\"1.2.3\"") != NULL, "loop_ver=1.2.3");
CHECK(strstr(g_pub_payload, "\"loop_hw_ver\":\"1.0.0\"") != NULL, "loop_hw_ver=1.0.0");
CHECK(strstr(g_pub_payload, "\"version_str\":\"V1.2.3 (HW:1.0.0)\"") != NULL, "version_str format");
CHECK(strcmp(g_pub_topic, "dld960/A1B2C3D4E5F6/dev") == 0, "resp topic");
CHECK(_iot_verq_pending == 0, "pending cleared");
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd channel released");
CHECK(g_lup_ver_cache.valid == 1, "cache refreshed by command resp");
}
static void test_verq_timeout(void)
{
printf("[4] verq_poll: 命令超时\n");
reset_state();
/* 缓存先有效, 超时回包应携带缓存值 */
g_lup_ver_cache.valid = 1;
g_lup_ver_cache.soft_main = 9; g_lup_ver_cache.soft_sub = 9; g_lup_ver_cache.soft_ssub = 9;
g_lup_ver_cache.hard_main = 8; g_lup_ver_cache.hard_sub = 8; g_lup_ver_cache.hard_ssub = 8;
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE; /* 还挂着 */
_iot_verq_pending = 1;
_iot_verq_msg_id = 408;
_iot_verq_deadline = g_mock_ms + 300;
g_mock_ms += 400; /* 超过 deadline */
iot_verq_poll();
CHECK(g_pub_count == 1, "timeout publish once");
CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "code=5");
CHECK(strstr(g_pub_payload, "\"msg\":\"no response\"") != NULL, "msg=no response");
CHECK(strstr(g_pub_payload, "\"loop_ver\":\"9.9.9\"") != NULL, "cache value kept in resp");
CHECK(_iot_verq_pending == 0, "pending cleared");
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd released after timeout");
/* 直接 TIMEOUT 态 */
reset_state();
g_lup_cmd.state = LUP_STATE_TIMEOUT;
_iot_verq_pending = 1;
_iot_verq_msg_id = 409;
_iot_verq_deadline = g_mock_ms + 300;
iot_verq_poll();
CHECK(g_pub_count == 1, "TIMEOUT state publish once");
CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "TIMEOUT state code=5");
}
static void test_bg_refresh(void)
{
printf("[5] verq_poll: 后台缓存刷新\n");
reset_state();
/* 5a. dirty=1 + IDLE → 发起查询, 不设 pending */
_iot_ver_cache_dirty = 1;
g_lup_cmd.state = LUP_STATE_IDLE;
iot_verq_poll();
CHECK(g_getver_calls == 1, "bg query issued");
CHECK(_iot_ver_cache_dirty == 0, "dirty cleared");
CHECK(_iot_verq_pending == 0, "no command pending set");
CHECK(g_pub_count == 0, "no publish for bg query");
/* 5b. 通道忙 (WAIT_RESPONSE) 时 dirty 不消费 */
{
int base = g_getver_calls;
_iot_ver_cache_dirty = 1;
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE;
iot_verq_poll();
CHECK(g_getver_calls == base, "busy channel -> no query");
CHECK(_iot_ver_cache_dirty == 1, "dirty kept while busy");
}
/* 5c. 后台响应消费: !dirty + RESPONSE_READY → 更新缓存 + 释放, 不回包 */
reset_state();
uint8_t pkg[16];
uint16_t len;
build_version_frame(pkg, &len, 0x00, 2,0,0, 3,4,5);
memcpy(g_lup_cmd.resp_buf, pkg, len);
g_lup_cmd.resp_len = len;
g_lup_cmd.state = LUP_STATE_RESPONSE_READY;
_iot_ver_cache_dirty = 0;
iot_verq_poll();
CHECK(g_pub_count == 0, "bg resp no publish");
CHECK(g_lup_ver_cache.valid == 1, "bg resp cache valid");
CHECK(g_lup_ver_cache.soft_main == 3 && g_lup_ver_cache.soft_ssub == 5, "bg resp soft=3.4.5");
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "bg resp channel released");
/* 5d. 残留超时清理 */
reset_state();
g_lup_cmd.state = LUP_STATE_TIMEOUT;
iot_verq_poll();
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "stale timeout cleaned");
}
int main(void)
{
printf("=== test_iot_loop_ver: V1.10 版本缓存 + 异步回包 ===\n");
reset_state();
test_parse_version();
test_cache();
test_verq_success();
test_verq_timeout();
test_bg_refresh();
printf("\nRESULT: %d passed, %d failed\n", g_pass, g_fail);
return g_fail ? 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;
}
+547
View File
@@ -0,0 +1,547 @@
/*
* ota_srv.c (gcc, WCH )
*
* : mock CONFIG.h/cmcng.h + mock (NOR flash / UART2 /
* / offlog ) #include ../APP/ota_srv.c (,
* static ) + +
*
* :
* gcc -I tests/ota_mock -I APP/include -I APP -o /tmp/test_ota_srv \
* tests/test_ota_srv.c
* : /tmp/test_ota_srv
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include "cmcng.h" /* mock: Flag_Counter / PRINT / mstick / UART2_SendString */
/* ============================================================================
* mock
* ============================================================================*/
Flag_Counter g_flag_counter_key = {0, 0, 0};
Flag_Counter g_flag_counter_ota = {0, 0, 0};
static uint32_t _mock_ms = 0;
uint32_t mstick(void) { return _mock_ms; }
static void mock_advance(uint32_t ms) { _mock_ms += ms; }
/* UART2 发送记录: 保存最近一帧 */
static uint8_t _tx_buf[512];
static uint16_t _tx_len = 0;
static uint16_t _tx_count = 0;
void UART2_SendString(uint8_t *buf, uint16_t len)
{
_tx_len = (len <= sizeof(_tx_buf)) ? len : (uint16_t)sizeof(_tx_buf);
memcpy(_tx_buf, buf, _tx_len);
_tx_count++;
}
static void tx_reset(void) { _tx_len = 0; _tx_count = 0; }
/* ---- mock NOR flash (W25Q32 语义: 写=AND, 擦=0xFF) ---- */
#define MOCK_FLASH_SIZE (4 * 1024 * 1024)
static uint8_t flash[MOCK_FLASH_SIZE];
static void flash_init(void) { memset(flash, 0xFF, sizeof(flash)); }
void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size)
{
assert(ReadAddr + size <= MOCK_FLASH_SIZE);
memcpy(pBuffer, &flash[ReadAddr], size);
}
void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size)
{
uint16_t i;
assert(WriteAddr + size <= MOCK_FLASH_SIZE);
for (i = 0; i < size; i++) flash[WriteAddr + i] &= pBuffer[i];
}
void SPI_Flash_Erase_Sector(uint32_t Dst_Addr)
{
assert(Dst_Addr * 4096UL + 4096UL <= MOCK_FLASH_SIZE);
memset(&flash[Dst_Addr * 4096UL], 0xFF, 4096);
}
/* 与 storage.c 语义一致: 目标区非 0xFF → 擦扇区 → 读-改-写保留同扇区其他数据 */
void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size)
{
uint32_t secpos, secoff, secremain;
uint16_t i;
while (size > 0) {
secpos = WriteAddr / 4096;
secoff = WriteAddr % 4096;
secremain = 4096 - secoff;
if (size <= secremain) secremain = size;
/* 目标区是否全 0xFF */
{
uint8_t need_erase = 0;
for (i = 0; i < secremain; i++) {
if (flash[secpos * 4096 + secoff + i] != 0xFF) { need_erase = 1; break; }
}
if (need_erase) {
uint8_t sector[4096];
memcpy(sector, &flash[secpos * 4096], 4096);
for (i = 0; i < secremain; i++) sector[secoff + i] = pBuffer[i];
memset(&flash[secpos * 4096], 0xFF, 4096);
for (i = 0; i < 4096; i++) flash[secpos * 4096 + i] &= sector[i];
} else {
for (i = 0; i < secremain; i++) flash[WriteAddr + i] &= pBuffer[i];
}
}
pBuffer += secremain;
WriteAddr += (uint32_t)secremain;
size -= (uint16_t)secremain;
}
}
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* W25Q32 */
/* ---- mock offlog (记录最近 OTA 事件) ---- */
static uint32_t _ota_start_cnt = 0, _ota_result_cnt = 0;
static uint32_t _last_ota_result = 0;
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) { (void)type; (void)payload; (void)len; }
void offlog_ota_start(uint8_t slot, uint32_t size) { _ota_start_cnt++; (void)slot; (void)size; }
void offlog_ota_result(uint32_t code) { _ota_result_cnt++; _last_ota_result = code; }
void offlog_boot(uint32_t r) { (void)r; }
/* ---- mock loop_uart_proto ---- */
void lup_frame_reset(void) { }
/* ---- mock iot_mqtt_srv ---- */
static uint8_t _mock_any_car = 0;
static uint32_t _ota_err_report = 0, _ota_err_cnt = 0;
static uint8_t _ota_rep_ok = 0xFF;
static uint32_t _ota_rep_err = 0, _ota_rep_cnt = 0;
uint8_t iot_any_car(void) { return _mock_any_car; }
void iot_evt_report_ota_error(uint32_t err) { _ota_err_report = err; _ota_err_cnt++; }
/* V1.09: ota_report 结果上报 mock (ok=1 done, ok=0 failed) */
void iot_ota_report_result(uint8_t ok, uint32_t err) { _ota_rep_ok = ok; _ota_rep_err = err; _ota_rep_cnt++; }
/* ============================================================================
* ota_srv.c (, static )
* ============================================================================*/
#include "../APP/ota_srv.c"
/* ============================================================================
*
* ============================================================================*/
#define CHK(cond) do { if (!(cond)) { \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; } } while (0)
/* 构造测试镜像 (5120B = 20 片) */
static void make_image(uint8_t *buf, uint32_t size)
{
uint32_t i;
for (i = 0; i < size; i++) buf[i] = (uint8_t)(i * 7 + 3);
}
static int test_crc32(void)
{
/* CRC-32/ISO-HDLC 标准向量 */
CHK(ota_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL);
CHK(ota_crc32(NULL, 0) == 0x00000000UL);
printf("PASS test_crc32\n");
return 0;
}
static int test_hex_decode(void)
{
uint8_t out[4];
CHK(ota_hex_decode("0102aBff", out, 4) == 0);
CHK(out[0] == 0x01 && out[1] == 0x02 && out[2] == 0xAB && out[3] == 0xFF);
CHK(ota_hex_decode("01z2", out, 2) != 0); /* 非法字符 */
CHK(ota_hex_decode("010", out, 2) != 0); /* 长度不足 */
printf("PASS test_hex_decode\n");
return 0;
}
static int test_json_extract_hex(void)
{
char out[520];
/* 无空格 (紧凑格式) */
CHK(ota_json_extract_hex(
"{\"msg_id\":402,\"cmd\":\"ota_data\",\"ts\":1,\"data\":{\"target\":\"loop\",\"offset\":0,\"crc32\":1,\"data\":\"0102aBff\"}}",
out, sizeof(out)) == 8);
CHK(strcmp(out, "0102aBff") == 0);
/* 带空格 (json.dumps 默认) — 2026-08-20 事故场景 */
CHK(ota_json_extract_hex(
"{\"msg_id\": 402, \"cmd\": \"ota_data\", \"ts\": 1, \"data\": {\"target\": \"loop\", \"offset\": 0, \"crc32\": 1, \"data\": \"0102aBff\"}}",
out, sizeof(out)) == 8);
CHK(strcmp(out, "0102aBff") == 0);
/* 512 hex 满片 */
{
char json[700];
char hex[513];
int i, n;
for (i = 0; i < 256; i++) sprintf(hex + i * 2, "%02x", i);
snprintf(json, sizeof(json),
"{\"msg_id\":1,\"cmd\":\"ota_data\",\"data\":{\"data\":\"%s\"}}", hex);
n = ota_json_extract_hex(json, out, sizeof(out));
CHK(n == 512);
CHK(strcmp(out, hex) == 0);
}
/* 找不到 data 字段 */
CHK(ota_json_extract_hex("{\"cmd\":\"ota_status\"}", out, sizeof(out)) == 0);
CHK(out[0] == '\0');
printf("PASS test_json_extract_hex\n");
return 0;
}
static int test_meta_restore(void)
{
flash_init();
tx_reset();
ota_init();
CHK(_meta.state == OTA_STATE_IDLE);
/* 写一份 downloading 元数据 → 重新 init → 恢复续传状态 */
_meta.magic = OTA_MAGIC;
_meta.state = OTA_STATE_DOWNLOADING;
_meta.size = 512;
_meta.received = 256;
ota_meta_write(&_meta);
ota_init();
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 256);
printf("PASS test_meta_restore\n");
return 0;
}
static int test_session_flow(void)
{
uint8_t img[5120];
uint32_t i;
make_image(img, sizeof(img));
flash_init();
tx_reset();
ota_init();
/* begin */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 0);
/* 20 片下发 */
for (i = 0; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
CHK(_meta.received == sizeof(img));
/* end → ready */
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
/* 暂存区与镜像一致 (读回比对) */
{
uint8_t back[5120];
SPI_Flash_Read(back, OTA_SLOT_A_BASE, sizeof(back));
CHK(memcmp(back, img, sizeof(img)) == 0);
}
printf("PASS test_session_flow\n");
return 0;
}
static int test_begin_resume(void)
{
uint8_t img[5120];
uint32_t i;
make_image(img, sizeof(img));
flash_init();
ota_init();
/* 下载 16 片 (4096B, 触发元数据节流 flush) 后"断电重启" */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
for (i = 0; i < 16 * 256; i += 256) {
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, 256), hex) == 0);
}
CHK(_meta.received == 16 * 256); /* 4096B, 已 flush */
/* 重启 */
ota_init();
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 16 * 256); /* 节流点精确续传 */
/* 重新 begin → 续传点 */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
CHK(_meta.received == 16 * 256);
/* 继续下载剩余 → ready */
for (i = 16 * 256; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
printf("PASS test_begin_resume\n");
return 0;
}
static int test_dup_and_gap(void)
{
uint8_t img[1024];
uint8_t chunk[256];
char hex[513];
uint32_t j;
make_image(img, sizeof(img));
flash_init();
ota_init();
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
memcpy(chunk, &img[0], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0);
/* 重复片幂等 */
CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0);
CHK(_meta.received == 256);
/* 乱序缺片 (跳 256 直接发 512) → 拒绝 */
memcpy(chunk, &img[512], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(512, ota_crc32(chunk, 256), hex) == 1);
CHK(_meta.received == 256);
/* 单片 CRC 错 → 拒绝 */
memcpy(chunk, &img[256], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(256, 0xDEADBEEFUL, hex) == 1);
CHK(_meta.received == 256);
printf("PASS test_dup_and_gap\n");
return 0;
}
/* 模拟 bootloader: 根据收到的 A7 块回 data ACK */
static void flash_step_until_ack(uint32_t *blocks_done, uint32_t total_blocks)
{
/* ota_poll 推进一帧发送 + 喂 ACK, 直到收到末块 */
uint32_t guard = 0;
while (*blocks_done < total_blocks && guard++ < 10000) {
ota_poll(); /* 发 A7 块 */
CHK(_tx_len >= 7 && _tx_buf[0] == 0x9F);
CHK(_tx_buf[4] == 0xA7);
/* 构造 data ACK: 9F SubL SubH 02 A7 00 CHECK */
{
uint8_t ack[7];
uint8_t sum = 0;
ack[0] = 0x9F; ack[1] = _tx_buf[1]; ack[2] = _tx_buf[2];
ack[3] = 0x02; ack[4] = 0xA7; ack[5] = 0x00;
sum = (uint8_t)(ack[1] + ack[2] + ack[3] + ack[4] + ack[5]);
ack[6] = sum;
ota_flash_feed_ack(ack, 7);
}
(*blocks_done)++;
}
CHK(*blocks_done == total_blocks);
}
static int test_flash_flow(void)
{
uint8_t img[5120];
make_image(img, sizeof(img));
flash_init();
tx_reset();
_ota_err_cnt = 0;
_ota_rep_cnt = 0;
_ota_rep_ok = 0xFF;
ota_init();
/* 下载 → ready */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
{
uint32_t i;
for (i = 0; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
}
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
/* flash: 无车 → 启动 */
_mock_any_car = 0;
tx_reset();
CHK(ota_cmd_flash(0, 0) == 0);
CHK(_meta.state == OTA_STATE_FLASHING);
CHK(g_ota_flash_active == 1);
CHK(g_flag_counter_ota.flag == 1);
CHK(_ota_start_cnt >= 1); /* offlog 升级开始已写 */
/* START → 发 A5 启动帧 */
ota_poll();
CHK(_tx_len == 6 && _tx_buf[0] == 0x9F && _tx_buf[4] == 0xA5 && _tx_buf[5] == 0xA7);
/* pre_ok → ADDR */
{
uint8_t pre_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8};
ota_flash_feed_ack(pre_ok, 7);
}
ota_poll();
/* A6 地址帧: 9F 01 00 05 A6 08 00 34 00 CHECK */
CHK(_tx_len == 10 && _tx_buf[4] == 0xA6);
CHK(_tx_buf[5] == 0x08 && _tx_buf[6] == 0x00 && _tx_buf[7] == 0x34 && _tx_buf[8] == 0x00);
/* 校验和: SubL+SubH+LEN+CMD+ADDR = 01+00+05+A6+08+00+34+00 */
CHK(_tx_buf[9] == (uint8_t)(0x01 + 0x00 + 0x05 + 0xA6 + 0x08 + 0x00 + 0x34 + 0x00));
/* addr_ok → DATA 阶段 (总块数 = ceil(5120/248) = 21) */
{
uint8_t addr_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9};
ota_flash_feed_ack(addr_ok, 7);
}
CHK(_fs_sub == 21); /* 首块序号 = 总块数 */
/* 逐块 A7 + ACK 直到完成 */
flash_step_until_ack(&(uint32_t){0}, 21);
/* 完成后 (V1.09): 状态回 idle (镜像保留可重刷), 恢复 0x7F 模式, 结果已记,
ota_report stage=done */
CHK(_meta.state == OTA_STATE_IDLE);
CHK(_meta.last_result == 0);
CHK(g_ota_flash_active == 0);
CHK(g_flag_counter_ota.flag == 0);
CHK(_ota_result_cnt >= 1);
CHK(_last_ota_result == 0);
CHK(_ota_err_cnt == 0); /* 无失败告警 */
CHK(_ota_rep_cnt == 1); /* ota_report done 上报一次 */
CHK(_ota_rep_ok == 1);
CHK(_ota_rep_err == 0);
printf("PASS test_flash_flow (21 blocks)\n");
return 0;
}
static int test_flash_reject_car(void)
{
flash_init();
ota_init();
/* 直接构造 ready 态 */
_meta.state = OTA_STATE_READY;
_meta.size = 512;
_mock_any_car = 1;
CHK(ota_cmd_flash(0, 0) == 3); /* 有车 → 拒绝 */
CHK(g_ota_flash_active == 0);
_mock_any_car = 1;
CHK(ota_cmd_flash(0, 1) == 0); /* force → 跳过 */
CHK(g_ota_flash_active == 1);
printf("PASS test_flash_reject_car\n");
return 0;
}
static int test_flash_ready_timeout(void)
{
flash_init();
tx_reset();
_ota_err_cnt = 0;
_ota_rep_cnt = 0;
_ota_rep_ok = 0xFF;
ota_init();
_meta.state = OTA_STATE_READY;
_meta.size = 512;
_mock_any_car = 0;
CHK(ota_cmd_flash(0, 0) == 0);
ota_poll(); /* 发 A5 (首发) */
/* 不喂 ACK: 首发后 3 次超时重发 + 第 4 个超时周期 → 失败
: 1(START) A5(2) 2(START) A5(3) 3(START) A5(4) 4(fail) */
{
int k;
for (k = 0; k < 7; k++) { mock_advance(1001); ota_poll(); }
}
CHK(_meta.state == OTA_STATE_FLASH_FAILED);
CHK(_meta.last_result == OTA_ERR_READY_TIMEOUT);
CHK(_ota_err_cnt == 1); /* event_report ota_error 告警 */
CHK(_ota_err_report == OTA_ERR_READY_TIMEOUT);
CHK(g_ota_flash_active == 0);
CHK(g_flag_counter_ota.flag == 0);
CHK(_ota_result_cnt >= 1);
CHK(_last_ota_result == OTA_ERR_READY_TIMEOUT);
CHK(_ota_rep_cnt == 1); /* ota_report failed 上报一次 */
CHK(_ota_rep_ok == 0);
CHK(_ota_rep_err == OTA_ERR_READY_TIMEOUT);
printf("PASS test_flash_ready_timeout\n");
return 0;
}
/* V1.09: 刷写完成后 idle+镜像一致 → ota_begin 直接回 ready (免下载重刷) */
static int test_begin_reflash(void)
{
uint8_t img[5120];
make_image(img, sizeof(img));
flash_init();
tx_reset();
ota_init();
/* 下载 → ready */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
{
uint32_t i;
for (i = 0; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
}
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
/* 模拟刷写完成 → idle (镜像保留) */
_meta.state = OTA_STATE_IDLE;
CHK(_meta.size == sizeof(img));
/* 重刷: begin 同镜像 → 直接 ready, offset=size (免下载) */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
CHK(_meta.state == OTA_STATE_READY);
CHK(_meta.received == sizeof(img));
/* 不同镜像 → 新会话重下 */
CHK(ota_cmd_begin(sizeof(img) + 256, ota_crc32(img, sizeof(img)), "1.2.0", 0) == 0);
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 0);
printf("PASS test_begin_reflash\n");
return 0;
}
int main(void)
{
int fails = 0;
fails += test_crc32();
fails += test_hex_decode();
fails += test_json_extract_hex();
fails += test_meta_restore();
fails += test_session_flow();
fails += test_begin_resume();
fails += test_dup_and_gap();
fails += test_flash_flow();
fails += test_flash_reject_car();
fails += test_flash_ready_timeout();
fails += test_begin_reflash();
if (fails == 0) {
printf("\nALL PASS\n");
return 0;
}
printf("\n%d TEST(S) FAILED\n", fails);
return 1;
}
+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;
}
+245
View File
@@ -0,0 +1,245 @@
# vd960Loop 自动分频(Auto Frequency Selection)技术分析与开发计划
> 分析日期:2026-08-25
> 场景:高速公路,每条车道 4 个线圈(前后串行),相邻车道也有线圈
> 目标:检测到干扰时,四通道各自独立调节 4 档电容频率(33/43/66/76nF)错开干扰源
---
## 0. 现状盘点
| 能力 | 状态 | 说明 |
|------|------|------|
| 硬件切换 | ✅ 已具备 | 每通道 2 个 GPIOFLPA1/2…FLPD1/2),2bit 编码 4 档电容 |
| 基础软件 | ✅ 已具备 | `set_flp_level(loop_num, freq_level)` 已实现(storage.c:117 |
| 配置字段 | ✅ 已具备 | `Loop_Cng_Unit.loopFreq_Level`(2bit),flash 持久化,协议字段 freq_level 已定义 |
| 频率测量 | ✅ 已具备 | CAPVD ∝ 周期,上报实际频率(main.c 转换公式) |
| 干扰检测 | ❌ 缺失 | 无"自动判断有干扰"的逻辑 |
| 切换决策 | ❌ 缺失 | 无"切哪档、何时切"的逻辑 |
| 切换时序 | ❌ 缺失 | 切档会打断检测,无软切换/基线重建流程 |
| 频率规划 | ❌ 缺失 | 无多线圈错频分配策略 |
**结论:硬件和基础软件齐了,缺的是"自动"三件套:检测 → 决策 → 软切换。**
---
## 1. 物理层分析:四档频率与错频裕量
### 1.1 四档频率(L₀≈59μH 时)
| 档位 | 电容 | 频率 | 相对上一档间隔 |
|------|------|------|---------------|
| 00 高频 | 33nF | ~114.1 kHz | — |
| 01 中高 | 43nF | ~100.0 kHz | 14.1 kHz14.1% |
| 10 中低 | 66nF | ~80.7 kHz | 19.3 kHz19.3% |
| 11 低频 | 76nF | ~75.2 kHz | **5.5 kHz7.3%** ⚠ |
**关键发现:四档电容间隔不均匀(+30%/+53%/+15%),66nF 与 76nF 之间频率间隔只有 7.3%**。f ∝ 1/√C,这个比例与线圈 L 无关(恒为 √(76/66)=1.073)。若按行业惯例"相邻线圈频率差 ≥10%"要求,**66/76 这一对不达标**。
### 1.2 但真正的敌人不是"差拍大不大",而是"注入锁定"
错频抑制串扰的物理机制分两层:
1. **异频耦合 → 差拍**:本道 f1、邻道 f2,互感耦合产生 |f1f2| 差拍。本道 10ms tick 采样,奈奎斯特 50Hz
- 差拍 >50Hz(如 5.5kHz):欠采样 → 随机高频抖动 → **IIR(τ≈32ms)天然压住**,无害
- 差拍 <50Hz(近频):慢拍,可穿透 IIR → 有害
- 差拍 →0(同频):注入锁定,最危险
2. **同频/近频 → 注入锁定**:f1≈f2 时振荡器互相牵引,频率被拉偏、相位锁定,CAPVD 出现慢周期扰动,直接污染检测。
**结论:错频目标不是"差拍尽量大",而是"避免同档/近档导致注入锁定"。** 只要相邻线圈不同档(≥1 档间隔,即 ≥7.3% 频率差),差拍欠采样抖动可被滤波抑制。**7.3% 的最小档间隔对避免注入锁定足够,但余量偏小**,规划时优先避免 66/76 相邻组合。
### 1.3 串扰方向性(衔接上一轮分析)
- 错频配置下串扰为差拍抖动(正负交替,无稳定极性)
- 单极性串扰只在同频/近频耦合时出现(错误配置)
- 负向串扰进滑动平均会污染基线(真实危害)
---
## 2. 信号层:干扰检测算法("怎么知道有干扰")
### 2.1 干扰特征(候选)
| 特征 | 检测量 | 灵敏度 | 可靠性 |
|------|--------|--------|--------|
| **差拍抖动** | 无车时窗口内 variation 峰谷差/σ(复用 §10 健康度) | 高(差拍泄漏直接可见) | 高 |
| **注入锁定慢拍** | CAPVD 出现 0.5~50Hz 周期性扰动(相邻样本差分谱) | 中 | 高(最隐蔽也最危险) |
| **基线冻结频率** | 无车时 dev ≥ 4×dlt_ORG 的次数/占比(§10 冻结占比) | 中 | 高 |
| **误触发率** | 无车误触发计数(单位时间) | 低(需要时间累积) | 中(最终后果指标) |
### 2.2 检测窗口约束(关键)
- **干扰检测必须在"本道无车"窗口做**——有车时 CAPVD 大变化是正常信号,不能当干扰
- 但邻道车经过时差拍增强(邻道线圈被车扰动)→ 检测窗口应**包含"本道无车 + 邻道有车"时段**(无需知道邻道状态,本道无车时抖动超阈值即视为干扰)
- 复用 §10 健康度框架:无车窗口峰谷差 > 阈值 × 连续 N 窗 → 判定"干扰存在"
### 2.3 干扰度评分(量化)
```
干扰度 D = α·(抖动能量超标度) + β·(冻结占比) + γ·(误触发率归一化)
D > D_HIGH → 需切换
D > D_MID → 可疑,加强监测
```
阈值需现场标定("待现场验证"),初值建议:无车窗口峰谷差 > 2× 正常噪声底(即 §10 健康度的漂移速率超基线 2 倍)。
---
## 3. 调节时机策略(核心决策点)
### 方案 A:上电自适应(一次性探测)
```
上电 → 各通道稳定期 → 分频探测(每档测 ~5s 噪声底)→ 选最优组合 → 固化 → 正常检测
```
| 维度 | 评价 |
|------|------|
| 实现 | 简单,无运行中切换风险 |
| 探测时长 | 4 通道 × 4 档 × 5s = 80s(交错并行可压到 ~20-30s|
| 缺点 1 | 探测期无检测功能(几十秒)|
| 缺点 2 | **邻道可能未同时上电**,探测结果基于不完整邻居信息 |
| 缺点 3 | 现场后期新增设备(护栏、新线圈、改造)不响应 |
### 方案 B:运行中自适应(动态切换)
```
运行中持续监测干扰度 D → 超阈值 → 软切换(无车时执行)→ 切换后评估(改善则保持,恶化则回滚)
```
| 维度 | 评价 |
|------|------|
| 实现 | 复杂,需软切换 + 回滚 |
| 响应 | 实时适应现场变化 |
| 风险 | 切换期间通道短暂不可用;切换震荡(需防抖:限次、评估、回滚)|
| 硬约束 | **必须有车才能切**(防丢检测);切换后重建基线 |
### 方案 C:混合策略(推荐)
```
上电探测建立初始分配(快速) + 运行中事件触发复测(慢速、保守) + 回滚保护
```
- 上电:扫描选档(几十秒,覆盖"邻道未上电"风险用保守选档:选次优但间隔安全的档)
- 运行中:干扰度 D 超阈值 或 定时(如每小时)复测 → 软切换 + 评估回滚
- 切换频率限制:1 小时内最多 N 次(防震荡)
**切换时机细节(任何方案都需要):**
```
前置条件: 该通道无车(VD_FLAG=0) 且 无脉冲输出中 且 非安全复位中
执行序列: 挂起检测 → GPIO 切档 → 等待振荡稳定(~100ms, 实测标定) → 重建基线(稳定期 128样本≈1.3s) → 恢复检测
安全约束: 切换期间继电器保持原状态; 四通道交错切换(防同时切换电源跌落)
```
> 切换期间该通道约 1.5s 不可用——**这是方案取舍的核心代价,需用户确认可接受**。
---
## 4. 频率规划:4 档怎么分给 8+ 个线圈
### 4.1 冲突定义
场景:每车道 4 线圈(前后)+ 相邻车道 4 线圈。冲突组合:
- **同车道前后相邻**:线圈间距 ~几米,磁耦合强
- **左右相邻车道最近线圈**:跨车道间距,耦合中等
- 斜向/隔一档:耦合弱,可复用
### 4.2 复用策略(蜂窝式频率规划)
```
同车道 4 线圈: 档位 1-2-3-4 顺序错开(每线圈独立档)
相邻车道错相位: 车道 2 用 2-3-4-1 循环 → 左右最近线圈档位差 ≥1
优先避免: 66/76(7.3% 间隔)相邻组合;同档复用距离 ≥2 个线圈位
```
### 4.3 关键矛盾:车检器不知道邻道的档位
无通信前提下可选:
| 方案 | 做法 | 评价 |
|------|------|------|
| **现场配置基准**(推荐起步) | 协议/拨码设置每通道初始档位,各车道按约定错相位 | 简单可靠,依赖施工规范;为后续自动调整提供基准 |
| **盲探测避让** | 扫描各档时测抖动能量,避开"差拍落入危险区"的档 | 不需邻道信息,但只能"避开坏的"不能"选最优" |
| **差拍频率反推** | 从抖动频谱估计 f_beat,反推邻道档位 | 理论可行,工程复杂(欠采样谱估计),暂缓 |
**建议:初始档位现场配置(保证基本错频)+ 自动扫描避让(动态优化)+ 复用规则(防同档相邻)。**
---
## 5. 工程约束与风险清单
| 项 | 约束/风险 | 对策 |
|----|----------|------|
| 检测中断 | 切换 ~1.5s 不可用 | 无车才切;交错切换;切换评估回滚 |
| 安全 | 有车时切档丢检测 → 砸车风险 | **硬约束:VD_FLAG=0 才可切**;继电器保持 |
| 看门狗 | 探测/切换流程不能阻塞主循环 | 全部状态机化,单步 ≤50ms |
| 基线 | 换档后 CAPVD 跳变 | 重建基线 + 稳定期(现有机制复用)|
| 灵敏度 | 换档后阈值变化? | **归一化红利:SensTable 是相对量(×Origin/65536),换档后 Origin 变了但相对阈值不变,灵敏度自动保持** ✓ |
| 持久化 | 新档位掉电丢失 | 写回 flash loopFreq_Level |
| 切换震荡 | 频繁切换 | 限次(1h≤N)、评估回滚、Hysteresis |
| 电源 | 多通道同时切电容冲击 | 交错切换 |
| 协议联动 | 运行中档位变化后台未知 | 新增自动分频状态上报(含当前档位/干扰度/切换事件)|
| 测试 | 无真实串扰环境难验证 | 实验室双线圈对测 + 工装注入模拟干扰 + 现场实测 |
---
## 6. 开发计划
### 阶段 0:硬件标定(前置,依赖实验室/现场)
- [ ] 0.1 实测四档频率、切换 GPIO 到频率稳定的时间(示波器)
- [ ] 0.2 标定振荡稳定等待时间(切档 → CAPVD 有效)
- [ ] 0.3 两线圈对测:同档/相邻档/隔档的差拍形态与注入锁定距离
- [ ] 0.4 标定干扰检测阈值初值(正常噪声底、抖动超标倍率)
- 输出:参数表(写回代码宏)
### 阶段 1:干扰检测模块(先会"看病",不切换)
- [ ] 1.1 新建 `freq_monitor.c/h`:无车窗口抖动能量统计(复用 §10 峰谷差逻辑)
- [ ] 1.2 注入锁定慢拍检测(相邻样本差分谱,0.5~50Hz 能量)
- [ ] 1.3 干扰度评分 D(抖动 + 冻结占比 + 误触发)
- [ ] 1.4 检测状态机:正常/可疑/干扰(带防抖、去程)
- [ ] 1.5 协议上报:干扰状态字段(可选,先本地 PRINT)
- [ ] 1.6 gcc 隔离单测(模拟差拍/锁定/车辆信号,验证不误报)
- 验证:模拟数据注入,检测准确率;实车现场对比人工判断
### 阶段 2:选档决策(freq_planner
- [ ] 2.1 候选档评估:每档噪声底 + 避让规则(差拍危险区)+ 复用约束(同车道不重复)
- [ ] 2.2 决策状态机:保持/切换/回滚(带 Hysteresis + 限次)
- [ ] 2.3 持久化:新档位写 flash
- [ ] 2.4 单测:给定干扰特征序列 → 期望档位序列
### 阶段 3:软切换执行(freq_switch
- [ ] 3.1 切档状态机:挂起检测 → GPIO 切档 → 等待稳定 → 重建基线 → 恢复
- [ ] 3.2 与 vd1_task 集成(切档期间跳过检测、继电器保持)
- [ ] 3.3 四通道交错切换调度
- [ ] 3.4 切换前后 CAPVD 突变防护(切档瞬间不进入 IIR)
- [ ] 3.5 单测:状态机全路径 + 异常分支(切换中有车进入 → 中止/回滚)
### 阶段 4:自适应策略整合(混合方案)
- [ ] 4.1 上电分频探测流程(4 通道交错扫描,~30s)
- [ ] 4.2 运行中事件触发复测(干扰度超阈值)
- [ ] 4.3 定时复测(如 1h,可配置)
- [ ] 4.4 切换评估:切后 5 分钟干扰度对比,恶化回滚
- [ ] 4.5 切换限次 + 防震荡
### 阶段 5:协议/后台联动 + 现场验证
- [ ] 5.1 协议新增:自动分频状态(当前档位/干扰度/切换事件),版本 bump
- [ ] 5.2 vd960DBN 解析同步(若走 0xC0 传感帧或独立命令)
- [ ] 5.3 现场测试方案:双车道对测、邻道干扰注入、长期稳定性(一周)
- [ ] 5.4 devlog/产品文档同步
---
## 7. 需要用户拍板的决策点
1. **切换时机**:方案 A(仅上电)/ B(仅运行中)/ C(混合,推荐)?
2. **初始档位来源**:现场配置基准(拨码/协议)vs 全自动扫描?(建议:配置基准 + 自动避让)
3. **切换代价**:每通道 ~1.5s 检测中断,可接受?(有车不切,只影响无车时段,实际影响很小)
4. **邻道协调**:相邻车道是否也是自家车检器?能否约定统一的错相位配置规范?
5. **范围**:v1 只做"干扰检测 + 手动/半自动提示"(后台建议换档),v2 再做全自动?还是一次到位?
+288
View File
@@ -4,6 +4,294 @@
---
## 2026-09-02 — V1.03:上电自检基准判稳同步 V4B V4.23(版本号 V1.03
> **固件版本:V1.03**cmcng.hFIRMWARE_VER="1.03" + MAIN=1/SUB=3SSUB=1 保持)。devlog 修订。
### 背景(源自 DLD154V4B 现场反馈,V4B V4.23 已实测通过)
绿灯上电自检快闪:原逻辑固定 128 样本(≈1.3 s)即判稳,**与基准值是否真稳定无关**;
`stable_cnt` 只在 `init_vd_single` 清零,**有限存在超时 / 安全复位(LC_Reset=1)重学阶段不清零**
残留计数会让二次稳定期几乎瞬间判稳(绿灯自检形同虚设)。V4B V4.23 修复后本仓四路同步。
### 改动
- `TaskLoop.h`:新增 `STABLE_ORIGIN_PPT 1`(窗口 Origin 均值漂移 ≤ Origin×0.1%/
`STABLE_SETTLE_WINDOWS 2`(连续 2 窗判稳)/ `STABLE_MAX_SAMPLES 500`(≈5 s 硬兜底);
`Loop154_Unit` 新增 `settle_cnt`
- `TaskLoop.c`
- 稳定期:每窗(100 样本≈1 s)完成时比较 Origin 均值漂移,≤0.1% → `settle_cnt++` 否则清零;
判稳 = 最少 128 样本 + 连续 2 窗稳定,或 500 样本硬兜底;判稳退出时 `stable_cnt/settle_cnt` 清零
- `init_vd_single` / 有限存在超时 / 安全复位超时(重学路径)统一清零自检计数 → 二次稳定期从零开始
- 绿灯 `poll_green_led` 条件不变:该路绿灯闪至**本路基准稳定**才停(四路独立)
### 验证
- `tests/test_stable_settle.c`:常量 → 200 样本判稳;首窗尖峰 → 400(扰动后需 2 个干净窗口);
持续漂移 → 500 硬兜底;重学复位后 → 200(防呆:非瞬间判稳)ALL PASS
### 待现场验证
上电绿灯自检时长(干净启动 ≈2 s / 有扰动 3~4 s / 干扰大 5 s 兜底)与四路独立闪烁观感。
---
## 2026-09-02 — V1.02:基线跟踪双向对称保护同步 V4B V4.20(版本号 V1.02
> **固件版本:V1.02**cmcng.hFIRMWARE_VER="1.02" + MAIN=1/SUB=2SSUB=1 保持)。devlog 修订。
### 背景(现场问题,源自 DLD154V4B 同步)
特殊形状铁块(磁导率主导)靠近线圈 → variation 为**负**CAPVD 高于 Origin):
- 原基线跟踪**单边保护**(只挡 `dev > +4×dlt`),负 variation 时基线照常更新 → **Origin 被污染抬高**
- 铁块离开时 CAPVD 回落 → 跨过抬高的进入线 → **假进入** → 进入后 Origin 冻结 → 释放线抬高 → **锁死不释放**
- DLD154V4B V4.20 已修复并现场验证(三次铁块测试 0 锁死),本仓同步
### 改动(TaskLoop.c,四路 unit 结构同步)
```
原逻辑: dev < +4×dlt → 更新基线 ← 单边, 负variation漏网
修复后: |dev| < 4×dlt → 更新基线 ← 对称窗口 [-4×dlt, +4×dlt]
dev < 0 (正variation,疑似车) → 冻结 + FREEZE_TIMEOUT 超时兜底更新(保留)
dev > 0 (负variation,铁块) → 只冻结, 永不超时更新Origin ← 核心
```
- 对称窗口内正常跟踪时补 `loop_freeze_ref = 0`(残留清零,与 V4B 一致)
- 权衡:长时间负温漂基线不跟随(安全方向,正车检测仍正常)
- 逻辑与 DLD154V4B V4.20 `test_neg_variation.c` 6 项回归单测覆盖一致
### 版本
- cmcng.hFIRMWARE_VER "1.01"→"1.02"FIRMWARE_VER_SUB 1→2
- 同步仓:DLD154Pro5f7082c)、DLD110SV4fd6ee79)同期完成
---
## 2026-08-28 — 看门狗升级:双保险喂狗模式 + 超时对齐 V4B(1.64s)
### 背景
与 DLD154V4B(同款 AT32F421)看门狗实现对照后发现三个问题:
| 问题 | 说明 |
|------|------|
| **直喂模式缺 ISR 活性检测** | 原实现主循环每 10ms 无条件 `wdt_feed()`——TMR15 ISR 死掉但主循环活着时,看门狗照样被喂,系统"假活"(时序状态机全停但永不复位) |
| **超时 3s 偏长** | `div=64, rld=1875` → 3s;V4B 已用 1.64s,复位自愈更快 |
| **注释矛盾** | `main.c` 注释写"重载值=4687 超时≈3秒"4687 是 7.5s 的算法),实际 1875→3s |
### 改动(对齐 DLD154V4B V2.10
```c
// TaskLoop.h
#define WDT_COUNTER_LIMIT 100 // 喂狗周期 500ms (TMR15 5ms × 100)
#define WDT_DIV 2 // IWDT 分频枚举值: 2 = WDT_CLK_DIV_16 (实际分频 16)
#define WDT_RLD 4095 // 12bit 重装载 → 超时 ≈ 1.64s
```
| 文件 | 变更 |
|------|------|
| `src/main.c` | `wdt_init()``WDT_CLK_DIV_64/1875``WDT_CLK_DIV_16/4095`(超时 3s→1.64s);`wdt_feed` 定义删除(移至 TaskLoop.c);注释修正 |
| `src/TaskLoop.c` | +`g_wdg_counter` + `poll_wdg()`500ms 才 reload);TMR15 ISR 5ms 递增;调用点 `wdt_feed()``poll_wdg()` |
| `inc/TaskLoop.h` | +WDT 宏 + `poll_wdg` 声明 + `g_wdg_counter` extern(删 `wdt_feed` |
### 验证
- 隔离测试(host gcc mock 3bit 寄存器语义)3 项断言全过:分频 16/重载 4095/超时 1638ms > 500ms 喂狗周期、10s 喂狗 20 次(500ms 精确)、ISR 死不喂狗
- 双保险语义:ISR 死 → 计数不涨 → 永不喂狗;主循环死 → poll_wdg 不执行 → 超时复位
- 待现场验证:LSI 实际频率偏差对复位时间的影响
### ⚠ 踩坑提醒(V4B 同源事故)
`wdt_divider_set()` 参数是**枚举值**不是分频数值(`WDT_CLK_DIV_16=0x02`,寄存器只取低 3 位)——V4B 曾把 `WDT_DIV=16` 当数值传入 → 实际分频 4 → 超时 410ms < 500ms 喂狗周期 → **不断复位重启**。本次 vd960Loop 直接沿用枚举写法,避免同坑。
---
## 2026-08-27 — LOOP_MEASURE_BASE 归一化基数上报(协议 V1.09)
### 背景
跨产品 MEASUREMENT_BASE 对齐(2026-08-26 DLD110S ×4 事故:第三方小程序把 ΔL/L 报成 8.85%,真实 2.21%)。variation 是周期域计数值(`variation = Origin CAPVD`,CAPVD ∝ √L),归一化必须除**设备自身**的 MEASUREMENT_BASE。960Loop 是产品线中唯一未上报基数的型号——0x64 读参响应裸发参数,第三方平台无从换算(写死 131072 恰好蒙对,写 524288 就 ÷4 失真)。
### 改动
| 文件 | 内容 |
|------|------|
| `docs/DLD960Loop_串口通信协议.md`vd_960 仓库) | **V1.09**0x64 响应尾部追加 3B LE `LOOP_MEASURE_BASE`(响应 34B→37B)+ 换算公式 + 修订记录 |
| `inc/cmcng.h` | `#define LOOP_MEASURE_BASE 131072L`(注释标明与 TaskLoop.h MEASUREMENT_BASE 同步) |
| `src/main.c` | `CMD_DBN_GET_MCJQ_PARAM` (0x64) handler for 循环后追加 3B LE(小端,响应 2+8×4+3=37B |
### 换算公式(写入协议文档)
```
Δf/f ≈ variation / LOOP_MEASURE_BASE (131072, AT32@120MHz)
ΔL/L ≈ -2 × variation / LOOP_MEASURE_BASE (系数 2 来自 CAPVD ∝ √L 展开)
```
### 设计决策
- 常量放 **0x64 查询响应**(低频),不进 0xC0 高频帧(每线圈 12B 已贴边,加字段触发分包)
- 向后兼容:老客户端只读前 34B 不受影响;新客户端读 37B 拿基数
- ⚠ 联动待办:DBN 侧对外(BLE/TCP/MQTT)暴露基数未落地——平台走网络协议评估时仍需加字段
### 验证
- ✅ `check_c_balance.py` 语法平衡 OK;编码保持 UTF-8+CRLF
- [ ] 板级验证:0x64 响应长度 37B、尾部 3B = `00 00 02`131072 LE
---
## 2026-08-25 — 灵敏度 4 级制修复(协议 V1.08)
### 背景
现场用金属块测感应高度,设置不同灵敏度档位**感应高度无区别**。排查发现三个根因:
| # | 根因 | 位置 |
|---|------|------|
| 1 | **运行中配置命令不同步检测单元**`unpack_pkg_set_mcjq_param()` 只更新 `g_loop_cng_info.loop_cng[].sensitvity` + 写 flash**没有同步 `g_loop_states.loop_unit[].loop_SensLevel`** → 改灵敏度不重启不生效(对比上电路径 `para_store_init()` 有同步) | main.c:576 |
| 2 | **9级制映射 `& 0x03` 折叠**:协议 0~9 级(默认 7),`sensitvity & 0x03` 只取低 2 位 → 7→3、**3→3(从默认 7 改到 3 无效)**、8→0(反向折叠) | storage.c:184 |
| 3 | 测试陷阱:金属块过大/过近信号饱和,4 档阈值都在饱和区 → 感应高度差异不可见(建议小金属块/测临界距离) | 测试方法 |
### 修复(方案 B4 级制)
- 提取映射函数 `sens_level_from_config()`storage.c/h):**4级制 0~3 一一对应**,9 级制后续实现时只改这一处
- `unpack_pkg_set_mcjq_param()``loop_SensLevel` 同步 → 运行中配置立即生效
- `para_store_init()` 上电路径改用同一函数(消除双处映射漂移)
- 协议 V1.08Sensitivity 语义 0~9 级 → **4 级制 0~3**(默认 2
- 新增 `tests/test_sens_mapping.c` gcc 隔离单测(mock flash.h/FreeRTOS.h/task.h + FLP GPIO 宏):验证 0~3 无折叠、兼容旧默认 7→3、SensTable 单调
### 改动
| 文件 | 内容 |
|------|------|
| `src/storage.c` | 新增 `sens_level_from_config()``para_store_init` 改用 |
| `src/main.c` | `unpack_pkg_set_mcjq_param` 补 loop_SensLevel 同步(运行中生效) |
| `inc/storage.h` | sensitvity 注释 4 级制;函数声明 |
| `tests/test_sens_mapping.c` | 新增映射单测(gcc PASS |
| `tests/mock/` | flash.h/FreeRTOS.h/task.h 隔离测试 mock |
| `docs/DLD960Loop_串口通信协议.md` | V1.08:灵敏度 4 级制 |
### 验证
gcc 单测 PASS0→0, 1→1, 2→2, 3→3 无折叠;`7&0x0F → 3` 兼容旧默认。实机待金属块复测(建议小金属块测临界感应高度)。
### 9 级制灵敏度表设计(2026-08-25)
用户给行业典型范围:**0.01% ~ 5.00% ΔL/L**(跨度 546 倍)。9 级等比公比 2.198,方向 0=最低、8=最高(与 4 级一致)。
| SENS | sens_in | ΔL/L 进入 | sens_out | ΔL/L 离开 | out/in |
|------|---------|-----------|----------|-----------|--------|
| 0 | 1638 | 5.00% | 819 | 2.50% | 50% |
| 1 | 745 | 2.27% | 373 | 1.14% | 50% |
| 2 | 339 | 1.03% | 170 | 0.52% | 50% |
| 3 | 154 | 0.47% | 100 | 0.31% | 65% |
| 4 | 70 | 0.21% | 46 | 0.14% | 65% |
| 5 | 32 | 0.098% | 21 | 0.064% | 65% |
| 6 | 15 | 0.046% | 14 | 0.043% | 90% |
| 7 | 7 | 0.021% | 6 | 0.018% | 90% |
| 8 | 3 | 0.01% | 3 | 0.01% | 100% |
设计要点:
- 滞回渐变 50%→65%→90%/100%:高档 out 低于噪声底会不释放(对齐 DLD154Pro 最高档 86% 经验)
- 4 级→9 级迁移(就近):0→3、1→4、2→5、3→7(端点不再保留,需现场复测)
- 工程警示:SENS=8 dlt≈6 counts 贴噪声底(仅低噪声环境);SENS=0 dlt≈3275(只认重型车)
- 溢出检查:Origin×1638 = 214M < 2³² ✓
落地:
| 文件 | 内容 |
|------|------|
| `docs/variation-analysis.md` | §9.3.1 9 级制表(设计稿) |
| `tools/variation_calc.py` | `--sens-table 9` 支持 9 级显示(4 级默认兼容) |
验证:工具 9 级端点 0.0092%/4.9988% ≈ 0.01%/5.00% ✓。代码落地(set_factory_param/sens_level_from_config)待 9 级立项。
### 补充根因(同日):INIT_VDs 硬编码覆盖配置 + flash 坏数据
现场串口日志(`Read_Cng_Store`)显示:配置 4 通道 SENS=3/2/1/0,但检测时 `Car_In` 全部打印 `sens_in:36`(SENS=2 表值)——4 通道检测全用 SENS=2。定位两个根因:
**① `init_vd_single()` 硬编码 `loop_SensLevel = 2`TaskLoop.c:111**
时序:`main → para_store_init()`(同步 loop_SensLevel=配置)→ 任务启动 → `INIT_VDs()``init_vd_single()` **把 loop_SensLevel 重置为 2**,覆盖配置值 → 全通道 SENS=2。
修复:`init_vd_single` 改为从配置读——`sens_level_from_config(g_loop_cng_info.loop_cng[unit->loop_num].sensitvity)`unit->loop_num 由 INIT_VDs 先设置)。
**② flash 灵敏度表 sens[0]={512,400} 是 0x8A 配置值(更正:非坏数据)**
解析 `Read_Cng_Store` flash 数据(0x40 起小端 2B):`{512,400},{108,72},{36,18},{10,9}`。**512/400 是 `CMD_DBN_LOOP_SENS_LIST (0x8A)` 配置的自定义 SENS=0 档阈值**main.c:946:写 → `memcpy(g_loop_sens_list.sens)``storage_dev()` 持久化 → `para_store_init()` 重读),链路完整。值大 = 阈值高 = 更不灵敏(0.78% vs 出厂 0.33% Δf/f),属于用户自定义,**勿恢复出厂(会清掉配置)**。修复 ① 后 loop_3SENS=0)将使用 512/400 配置值,符合预期。
---
## 2026-08-25 — variation ↔ ΔL/ΔL-L 物理对应分析与工具
### 背景
后台需要把上报的 `variation`(CAPVD 计数域)换算成物理电感变化量,用于标定灵敏度、分析车辆信号强度。
### 核心推导
CAPVD ∝ 周期 T = 2π√(LC),即 **CAPVD ∝ √L 而非 L**
```
variation/Origin = 1 √(1 + ΔL/L₀) 精确
ΔL/L₀ ≈ 2 × variation/Origin 一阶(残差 <0.2%
Δf/f ≈ variation/Origin 频域对应
```
- 系数 2 来源于 √(1+x) ≈ 1+x/2 展开
- 符号:车辆进入 ΔL<0 → variation 恒为正,与 V1.05 协议语义一致
- **电容档无关性**Origin 与 CAPVD 均 ∝ √Cvariation/Origin 中 C 消掉 → 33/43/66/76nF 四档共用同一换算表,现场换档不换标定
### 灵敏度档位 → ΔL/L 触发阈值(SensTable {216,108,36,10}
| SENS | Δf/f 进入 (=SensTable/65536) | ΔL/L 进入 ≈ 2× |
|------|------------------------------|-----------------|
| 0 低 | 0.330% | 0.659% |
| 1 中 | 0.165% | 0.330% |
| 2 高 | 0.055% | 0.110% |
| 3 最高 | 0.015% | 0.031% |
交叉验证:DLD154Pro 技术文档 sens_in {108,54,28,14} 标注 ΔL/L = 0.330%/0.165%/0.085%/0.043%,恰为 SensTable/65536 的 2 倍——产品线口径一致。
### 改动
| 文件 | 内容 |
|------|------|
| `docs/variation-analysis.md` | 新增 §9(物理链路/公式/档位表/电容无关性/示例/注意事项) |
| `tools/variation_calc.py` | 新增换算工具:正向(freq+cap+variation+origin→L₀,ΔL,ΔL/L)、反推、3B LE 补码解析(V1.05)、灵敏度对照、交互模式 |
### 验证
工具四模式实测通过,正反算自洽(ΔL/L=0.33% ↔ variation=216 @ Origin=131072)。
### 协议文字勘误(协议 V1.07)
用户指出 §5.6.3 记录的口径差异应直接改协议:原文 `±8388607` 不对称,负向边界少了 1。统一为 **8388608 ~ +8388607**3B 补码标准值域 −2²³ ~ +2²³−1,对齐代码负向饱和边界 0x800000),后台按文档实现时不会对 0x800000 边界犯嘀咕。
| 文件 | 变更 |
|------|------|
| `DLD960Loop_串口通信协议.md` | variation 字段值域文字修正 + 修订记录 V1.07 |
| `variation-analysis.md` | 头部版本引用 → V1.07;§1 值域;§5.1 表格;§5.6.3 口径小注改写 |
代码无改动(`main.c` 饱和边界本就是 8388608 ~ +8388607)。
### 环境健康度算法(§10
用户提出零成本环境健康度方案:无车时 variation 锯齿的峰 = 漂移信号,后台逐窗统计画健康度曲线。模拟验证后修正两处口径并落地:
1. **绝对峰 ≠ 窗口漂移**:Origin 更新到窗口均值(滞后半窗 2.5s),稳态绝对峰 = **1.5×** 窗口漂移 → 改用**峰谷差**(max−min,误差 <1%
2. **600ms 采样系统性低估 ~12%**:峰恒在窗口末(500 tick),末采样点 480 tick 永远错过 → 单边偏差,趋势可用,绝对标定 ×1.14
3. **冻结态失效**:漂移 > 4×dlt_ORG(中档 0.13%/s)→ 锯齿消失,速率口径失效 → 双指标:`漂移速率 = 峰谷差/Origin/5s` + `冻结占比`
4. 冻结判定双判据:跨窗跳变消失(|jump|≈0)+ 峰谷差突变(>4×前 4 窗中位数),最后一窗也可判
| 文件 | 内容 |
|------|------|
| `docs/variation-analysis.md` | 新增 §10(口径修正表/双指标/冻结判定/工具用法) |
| `tools/drift_health.py` | 新增参考实现:CSV 输入 + 逐窗统计 + 冻结检测 + JSON 输出 + 自测 |
自测:前 6 窗慢漂移 0.02%/s 全正常,后 2 窗快漂移正确标冻结,平均速率/冻结占比准确。
---
## 2026-07-14 — variation 上报量 2B→3B 有符号 (协议 V1.05)
### 背景
+216 -4
View File
@@ -2,7 +2,7 @@
> 分析对象:`uart_report_packet_loop_acs()` 中上报的 **变化量 variation**
> 涉及文件:`src/main.c`(组包)、`src/TaskLoop.c`(基线/IIR 更新)、`inc/TaskLoop.h`(参数宏)
> 协议版本:与《DLD960Loop_串口通信协议.md》**V1.05** 同步(2026-07-14variation 无符号 2B → **有符号 3B**,见 §5
> 协议版本:与《DLD960Loop_串口通信协议.md》**V1.07** 同步(2026-07-14 V1.05variation 无符号 2B → **有符号 3B**,见 §52026-08-25 V1.07:值域表述勘误统一为 **8388608 ~ +8388607**
> 结论层次:物理层 → 信号层 → 数据层 → 上报调度层
---
@@ -38,7 +38,7 @@ g_pkg_uart_report.pkg[i++] = (uint8_t)((variation >> 16) & 0xFF);
```
- **带符号带方向**:正 = 检测裕量方向(与旧版有车时的数值语义连续),负 = 反向漂移
- 3 字节小端补码,值域 ±2²³−1,发送端 **饱和限幅,不会回绕**
- 3 字节小端补码,值域 2²³ ~ +2²³−1,发送端 **饱和限幅,不会回绕**
- 旧版(≤V1.04)为 2B 无符号绝对值,缺陷与变更动机见 **§5**
---
@@ -136,7 +136,7 @@ variation: | | | | ← 锯齿波!
|---|---|---|
| 计算式 | `\|Origin CAPVD\|` 取绝对值 | `Origin CAPVD` 带符号 |
| 编码 | 2 字节无符号,小端 | **3 字节有符号补码**,小端 |
| 值域 | 0~65535**溢出回绕** | ±8388607**发送端饱和限幅** |
| 值域 | 0~65535**溢出回绕** | 8388608 ~ +8388607**发送端饱和限幅** |
| 方向信息 | 无 | 正 = 车/金属进入,负 = 反向漂移 |
| 每通道单元 | 11 字节 | **12 字节** |
| 帧 Len / 总长(4 路无分包) | 0x2F=47 / 52B | **0x33=51 / 56B**(仍 < `BUFF_STACK_SIZE=64`,缓冲安全) |
@@ -182,7 +182,7 @@ if (v & 0x800000) v |= 0xFF000000; // bit23 = 1 → 负数,符号扩展
1. **Loop 与 DBN 固件必须同版本发布**。单元步长 11↔12 死绑定,任一端用旧固件,4 通道数据整体错位。**且帧级 XOR/SUM 校验依然通过**(校验的是整帧,不感知单元边界)——这是**静默数据损坏**,不会报错,只会得到一堆"看起来合理"的错值。
2. 上位机若想兼容新旧固件,可用 **Len 字节区分**4 路无分包时 Len=0x2F(47) 为旧格式(2B 无符号),Len=0x33(51) 为新格式(3B 有符号),按帧动态选解析器。
3. 口径小注:协议文字写值域 ±8388607,代码负向饱和到 −8388608(0x800000)。解析端符号扩展后无歧义,且实际物理量远到不了该量级(CAPVD 全量程才 ~1310723B 有 **~64 倍余量**),不构成问题,仅作记录。
3. 口径说明(V1.07 已勘误):协议文字与代码统一为 **8388608 ~ +8388607**0x800000 是合法负边界)。解析端符号扩展后无歧义实际物理量远到不了该量级(CAPVD 全量程才 ~1310723B 有 **~64 倍余量**),边界仅作记录。
### 5.7 对分析工作的直接增益
@@ -218,3 +218,215 @@ if (v & 0x800000) v |= 0xFF000000; // bit23 = 1 → 负数,符号扩展
- 将 variation 改为上报 **斜率 / 趋势** 而非瞬时差,削掉锯齿混叠
- ~~补 variation 饱和保护~~ → V1.05 已完成
- **随包上报当前基线状态(跟踪 / 冻结 / 有车)**,让后台区分锯齿谷与真空闲、冻结大值与真车
---
## 9. variation ↔ ΔL / ΔL-L 物理对应(2026-08-25 补充)
> 配套工具:`tools/variation_calc.py`(正向/反推/3B 补码解析/灵敏度对照,纯 stdlib)
### 9.1 物理链路:CAPVD 正比于 √L,不是 L
`main.c` 频率转换公式反推:
```c
freq = sclk_freq × input_div × LPCNT / CAPVD → CAPVD ∝ 1/freq = T(周期)
```
LC 振荡器周期 `T = 2π√(LC)`,因此:
```
CAPVD = A·√(LC) (A = 系统归一化常数,含时钟/分频/MEASUREMENT_BASE=131072
```
**CAPVD 与周期成正比,与 √L 成正比**——这是 variation 换算 ΔL 的关键前提(平方根关系,不是线性)。
### 9.2 换算公式
设基线电感 L₀,有车后电感 L₀+ΔL:
```
Origin = A·√(L₀·C)
CAPVD = Origin·√(1 + ΔL/L₀)
variation = Origin CAPVD = Origin·[1 √(1 + ΔL/L₀)]
```
| 口径 | 公式 | 适用 |
|------|------|------|
| **精确** | `ΔL/L₀ = (1 variation/Origin)² 1 = (variation/Origin)² 2(variation/Origin)` | 大信号(如大车全覆盖) |
| **一阶近似** | `ΔL/L₀ ≈ 2 × variation/Origin` | 检测阈值区间,残差 <0.2% |
| 频域对应 | `Δf/f ≈ variation/Origin`CAPVD ∝ 1/f) | 与验收标准 §3 频偏口径衔接 |
符号语义:车辆进入 → 涡流 → ΔL<0 → CAPVD<Origin → **variation 恒为正**,与 V1.05 协议一致;负值 = 反向漂移 / 基线污染。
**一阶近似的误差上界**:二阶残差 ≈ (variation/Origin)²,最低灵敏度档 0.33% 时仅 0.0011%,相对误差 <0.2%——系数 2 直接可用。
### 9.3 灵敏度档位 → ΔL/L 触发阈值
进入条件 `CAPVD < Origin dlt_ORG`,进入瞬间 `variation ≈ dlt_ORG = Origin×SensTable/65536`,逐档对应:
| SENS | 进入表 | 离开表 | Δf/f 进入 (=vr) | **ΔL/L 进入 ≈ 2×vr** | ΔL/L 离开 |
|------|--------|--------|-----------------|----------------------|-----------|
| 0 低 | 216 | 108 | 0.3296% | **0.659%** | 0.330% |
| 1 中 | 108 | 72 | 0.1648% | **0.330%** | 0.220% |
| 2 高 | 36 | 18 | 0.0549% | **0.110%** | 0.055% |
| 3 最高 | 10 | 9 | 0.0153% | **0.031%** | 0.027% |
> 交叉验证:DLD154Pro 技术文档 sens_in {108,54,28,14} 标注 ΔL/L = 0.330%/0.165%/0.085%/0.043%,恰为 SensTable/65536 的 2 倍——产品线已统一使用 `ΔL/L ≈ 2×SensTable/65536` 口径。
### 9.3.1 9 级制灵敏度表(设计稿 2026-08-25)
行业典型范围:**0.01% ~ 5.00% ΔL/L**(跨度 546 倍,9 级等比公比 2.198)。方向:SENS **0=最低灵敏度、8=最高灵敏度**(值越大越灵敏,与 4 级一致)。
**sens_in(车来触发)**
| SENS | sens_in | Δf/f | **ΔL/L** | 典型用途 |
|------|---------|------|----------|----------|
| 8(最高) | 3 | 0.005% | **0.01%** | 摩托车/低噪声极限档 |
| 7 | 7 | 0.011% | 0.021% | 摩托车 |
| 6 | 15 | 0.023% | 0.046% | 小型车/电动车 |
| 5 | 32 | 0.049% | 0.098% | 轿车 |
| 4 | 70 | 0.107% | 0.214% | 轿车/SUV |
| 3 | 154 | 0.235% | 0.470% | 常规 |
| 2 | 339 | 0.517% | 1.034% | 大车优先 |
| 1 | 745 | 1.137% | 2.273% | 大车/货车 |
| 0(最低) | 1638 | 2.500% | **5.00%** | 只认重型车/强干扰环境 |
**sens_out(车走释放,滞回渐变 50%→65%→90%/100%**
| SENS | sens_out | out/in | ΔL/L 离开 |
|------|----------|--------|-----------|
| 0 | 819 | 50% | 2.50% |
| 1 | 373 | 50% | 1.14% |
| 2 | 170 | 50% | 0.52% |
| 3 | 100 | 65% | 0.31% |
| 4 | 46 | 65% | 0.14% |
| 5 | 21 | 65% | 0.064% |
| 6 | 14 | 90% | 0.043% |
| 7 | 6 | 90% | 0.018% |
| 8 | 3 | 100% | 0.01% |
- **滞回渐变原因**:高档 sens_in 很小,out 若按 50% 会低于噪声底 → 车走不释放。6/7/8 档收窄到 90%/100%(对齐 DLD154Pro 最高档 out/in=86% 经验)
- **4 级→9 级迁移**(就近):4级 SENS 0→9级3、1→4、2→5、3→7(4 级端点不再保留,迁移后阈值小幅变化,需现场复测)
- **工程警示**SENS=80.01%dlt≈6 counts 贴着噪声底,仅限低噪声环境;SENS=0(5%)dlt≈3275,只认重型车
- **乘法溢出检查**Origin(131072)×1638 = 214M < 2³² ✓
- 代码落地:`set_factory_param` total=9 填上表;`sens_level_from_config``sensitvity & 0x0F`(0~8 直接索引);协议 sensitvity 0~8
### 9.4 电容档位无关性(33/43/66/76nF
四档电容只改绝对工作频率 f₀ = 1/(2π√(L₀C))
| C 档 | 频率 @ L₀≈59μH |
|------|----------------|
| 33nF | ~114 kHz |
| 43nF | ~100 kHz |
| 66nF | ~81 kHz |
| 76nF | ~75 kHz |
**Origin 与 CAPVD 均 ∝ √C,比值 variation/Origin 中 C 被消掉**
```
variation/Origin = 1 √(1 + ΔL/L₀) ← 与 C 无关
```
**结论:四档电容共用同一套 ΔL/L 换算表,现场换档不换标定。** 这是 LPCNT = MEASUREMENT_BASE/Xn 自适应归一化的直接收益。
### 9.5 绝对电感与 ΔL 完整换算
```
① 绝对电感: L₀ = 1/(4π²·f₀²·C) ← f₀ 用上报频率(3B),C 用当前档位电容
② 相对变化: ΔL/L₀ ≈ 2 × variation/Origin
③ 绝对变化: ΔL = L₀ × ΔL/L₀
```
示例(C=43nF, f₀=100kHz → L₀≈58.9μHSENS=1 触发,variation=216, Origin=131072):
```
variation/Origin = 0.1648%
ΔL/L₀ ≈ 0.330%
ΔL ≈ 58.9μH × 0.330% ≈ 0.194 μH
```
### 9.6 使用注意事项(后台/上位机口径)
1. **必须归一化**variation 是 CAPVD 计数域(~131072 量级),直接拿绝对值当 ΔL 会差 4~5 个数量级。所有换算先除 Origin。
2. **只有有车冻结期的 variation 干净**:有车时 Origin 冻结,variation 忠实反映信号强度,此刻换算 ΔL/L 最可信;无车时 Origin 5s 阶跃 → variation 呈锯齿,锯齿峰 = 5s 漂移量,不能当车辆信号。
3. **基线重锁识别**:冻结超时(10s 稳定)后 Origin 强制 = CAPVDvariation 突降归零——后台应识别为"基线重锁"事件,勿当车辆离开。
4. **文档口径 2 倍陷阱**SensTable/65536 直接读是 Δf/f0.33%),标成 ΔL/L 是 0.66%。验收/规格文档必须注明口径(推荐统一用 ΔL/L)。
5. **绝对 ΔL 依赖 C 档信息**:协议帧未带 C 档位,后台需按现场配置查表,或由 f₀ 与档位映射反推(f₀ 落在哪档频率区间即哪档)。
### 9.7 工具用法速查
```bash
cd tools
python3 variation_calc.py --freq 100 --cap 43 --variation 216 --origin 131072 # 正向
python3 variation_calc.py --freq 100 --cap 43 --dl-rel 0.33 # 反推
python3 variation_calc.py --parse-var F6FFFF # 3B LE 补码解析
python3 variation_calc.py --sens-table # 四档对照
python3 variation_calc.py --interactive # 交互
```
---
## 10. 环境健康度算法:无车漂移监测(2026-08-25 补充)
> 配套工具:`tools/drift_health.py`。零成本方案——不改协议、不改固件,直接用现有 `variation` 锯齿。
### 10.1 思路
无车时 Origin 每 5s 阶跃更新(窗口均值),CAPVD 缓慢漂移 → variation 呈锯齿。**锯齿就是漂移信号**:峰谷差 = 窗口内漂移量,后台逐窗口统计即可画环境健康度曲线(湿度突变、变频器干扰加剧提前可见)。
### 10.2 正确口径(踩坑修正)
| 口径 | 关系 | 结论 |
|------|------|------|
| 绝对峰 \|variation\| | = **1.5×** 窗口漂移量 | ❌ 高估 50%——Origin 更新到**窗口均值**(滞后半窗 2.5s),峰含半窗累积 |
| **峰谷差**(窗口内 max−min) | ≈ 窗口漂移量(误差 <1%) | ✅ 正确 |
| 600ms 采样(8 点/窗) | 峰谷差**系统性低估 ~12%** | ⚠ 峰恒在窗口末(500 tick),末采样点 480 tick 永远错过它;实测 11%~14% |
**推荐公式:**
```
漂移速率(%/s) = (窗口内 max min) / Origin / 5s × 100
```
- 方向保留:V1.05 带符号,正向锯齿(CAPVD 下降/频率上升)= 正速率,反向为负
- 采样低估 ~12% 为单边已知偏差:趋势曲线看相对变化可直接接受;要绝对标定乘 1.14
### 10.3 失效场景:冻结态(必须双指标)
`dev ≥ 4×dlt_ORG` 基线冻结 → **锯齿消失**,variation 跨窗口单调累积不归零,"每窗口取峰"变成取历史累积,速率口径失效:
| 灵敏度档 | 冻结门槛(窗口漂移) | 对应速率 |
|---------|-------------------|---------|
| 0 低 | 1.32% / 5s | 0.26%/s |
| 1 中 | 0.66% / 5s | **0.13%/s** |
| 3 最高 | 0.06% / 5s | 0.012%/s |
变频器启动、湿度突变轻松超中档 0.13%/s → 冻结。**冻结本身 + 累积 variation 就是更强的恶化信号**,健康度曲线用双指标:
```
① 漂移速率 = 峰谷差 / Origin / 5s ← 慢漂移场景(温/湿度),慢且稳
② 冻结占比 = 冻结 tick / 总 tick ← 快干扰场景(变频器),触发即预警
```
**冻结判定**(后台可用跨窗回落检测):正常锯齿窗口边界 variation **跳回谷值**,跳变幅度 ≈ 峰谷差;冻结时 Origin 不动,窗口边界无跳变、variation 继续同向爬升。
```
jump = variation[下窗起点] variation[本窗终点]
正常: |jump| ≈ 峰谷差(窗口边界跳回谷值)
冻结: |jump| ≈ 0Origin 不动, 连续爬升)
```
### 10.4 工具用法
```bash
cd tools
python3 drift_health.py --selftest # 模拟数据自测
python3 drift_health.py --csv drift.csv --window 500 # CSV: tick,origin,variation
python3 drift_health.py --csv drift.csv --window 500 --json # JSON 输出
```
### 10.5 与 §8"改上报斜率"的关系
当前零成本方案精度受上报窗限制(600ms 采样 vs 固件 10ms tick,斜率分辨率差 60 倍)。§8 的固件侧斜率字段是**远期升级**:先跑通后台算法验证指标价值,确认值得后,再由固件直接上报 dCAPVD/dt 消除采样混叠与相位滞后。
+4
View File
@@ -0,0 +1,4 @@
/* mock FreeRTOS.h — gcc 隔离测试用空壳 */
#ifndef MOCK_FREERTOS_H
#define MOCK_FREERTOS_H
#endif
+15
View File
@@ -0,0 +1,15 @@
/* mock flash.h — gcc 隔离测试用 (storage.c 依赖的 flash API + 宏) */
#ifndef MOCK_FLASH_H
#define MOCK_FLASH_H
#include <stdint.h>
#define DLD_BUFEER_SIZE 100
#define DLD_FLASH_ADDRESS_START (0x08000000 + 1024 * 63 + 512)
#define DLD_FLASH_MAX_SIZE (0x08000000 + 1024 * 64)
#define MAX_Store_Size DLD_BUFEER_SIZE
void flash_write(uint32_t addr, uint16_t *buf, uint16_t len);
void flash_read(uint32_t addr, uint16_t *buf, uint16_t len);
#endif
+4
View File
@@ -0,0 +1,4 @@
/* mock task.h — gcc 隔离测试用空壳 (TaskHandle_t 由 TaskLoop.h 自行定义) */
#ifndef MOCK_TASK_H
#define MOCK_TASK_H
#endif
+87
View File
@@ -0,0 +1,87 @@
/*
* test_sens_mapping.c gcc (vd960Loop)
*
* : storage.c sens_level_from_config() (4 0~3 )
* : 2026-08-25 "设置不同灵敏度感应高度无区别"
* - 1: loop_SensLevel (main.c unpack_pkg_set_mcjq_param)
* - 2: 9 &0x03 (73, 80 ) 4
* : 9, 0~3
*
* :
* gcc -I tests/mock -I utilities/at32f421_freertos_demo/inc \
* tests/test_sens_mapping.c -o /tmp/test_sens_mapping && /tmp/test_sens_mapping
*/
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* ---- 被测试对象: include storage.c 使 static 逻辑同编译单元可见 ---- */
/* 硬件 GPIO 宏 mock (storage.c 的 set_flp_level 使用, 空实现) */
#define FLPA1_HIGH do { } while (0)
#define FLPA1_LOW do { } while (0)
#define FLPA2_HIGH do { } while (0)
#define FLPA2_LOW do { } while (0)
#define FLPB1_HIGH do { } while (0)
#define FLPB1_LOW do { } while (0)
#define FLPB2_HIGH do { } while (0)
#define FLPB2_LOW do { } while (0)
#define FLPC1_HIGH do { } while (0)
#define FLPC1_LOW do { } while (0)
#define FLPC2_HIGH do { } while (0)
#define FLPC2_LOW do { } while (0)
#define FLPD1_HIGH do { } while (0)
#define FLPD1_LOW do { } while (0)
#define FLPD2_HIGH do { } while (0)
#define FLPD2_LOW do { } while (0)
/* storage.c 引用的系统函数 (消除隐式声明 warning) */
void nvic_system_reset(void);
void delay_ms(uint32_t ms);
#include "../utilities/at32f421_freertos_demo/src/storage.c"
/* ---- 外部符号 stub ---- */
#include "flash.h"
void flash_write(uint32_t addr, uint16_t *buf, uint16_t len) { (void)addr; (void)buf; (void)len; }
void flash_read(uint32_t addr, uint16_t *buf, uint16_t len) { (void)addr; (void)buf; (void)len; }
void nvic_system_reset(void) { abort(); }
void delay_ms(uint32_t ms) { (void)ms; }
const uint16_t SensTable[4] = {216, 108, 36, 10};
const uint16_t SensTable_1[4] = {108, 72, 18, 9};
Loop154_States g_loop_states; /* TaskLoop.c 的全局, 此处提供定义 */
int main(void)
{
/* 4级制: 0~3 一一对应, 无折叠 */
assert(sens_level_from_config(0) == 0);
assert(sens_level_from_config(1) == 1);
assert(sens_level_from_config(2) == 2);
assert(sens_level_from_config(3) == 3);
/* 配置命令链路: pkg 低4位 & 0x0F 后传入 (main.c unpack 语义) */
/* 上位机发 0x00~0x03 → 档位 0~3 */
assert(sens_level_from_config(0x00 & 0x0F) == 0);
assert(sens_level_from_config(0x01 & 0x0F) == 1);
assert(sens_level_from_config(0x02 & 0x0F) == 2);
assert(sens_level_from_config(0x03 & 0x0F) == 3);
/* 兼容旧默认 7 (旧 9 级制默认): 7&0x0F=7 → &0x03=3 最高档, 行为连续 */
assert(sens_level_from_config(0x07 & 0x0F) == 3);
/* 出厂默认 loops_cng_default[0]=2 → SENS=2 (高灵敏度档) */
assert(sens_level_from_config(2) == 2);
/* 灵敏度表逐档单调: sens_in 递减 = 档位越高阈值越低 (越灵敏) */
assert(SensTable[0] > SensTable[1]);
assert(SensTable[1] > SensTable[2]);
assert(SensTable[2] > SensTable[3]);
printf("PASS: sens_level_from_config 4级映射无折叠\n");
printf(" SENS 0→%d, 1→%d, 2→%d, 3→%d\n",
sens_level_from_config(0), sens_level_from_config(1),
sens_level_from_config(2), sens_level_from_config(3));
printf(" SensTable: {216,108,36,10} 单调递减 ✓\n");
return 0;
}
+183
View File
@@ -0,0 +1,183 @@
/*
* test_stable_settle.c gcc (vd960Loop V1.03)
*
* : TaskLoop.c vd1_task_per_channel()
* ( DLD154V4B V4.23, 2026-09-02 )
*
* :
* 绿 "基准值真正稳定":
* - (1001s) Origin , Origin×0.1% settle++
* - = 128 + 2 ; 500 (5s)
* - 退/init_vd_single//(LC_Reset=1)
* ("残留计数 1 样本瞬间判稳")
*
* :
* gcc tests/test_stable_settle.c -o /tmp/test_stable_settle && /tmp/test_stable_settle
*/
#include <assert.h>
#include <stdio.h>
#include <stdint.h>
/* ---- 与 TaskLoop.h / TaskLoop.c 一致的宏与结构 (隔离副本) ---- */
#define STABLE_SAMPLES 128 // 稳定期最少样本数 (128×10ms≈1.3s)
#define STABLE_ORIGIN_PPT 1 // 基准稳定判据: 窗口均值漂移 ≤ Origin×0.1%
#define STABLE_SETTLE_WINDOWS 2 // 连续 2 窗(100样本/窗≈1s)漂移达标 → 判稳
#define STABLE_MAX_SAMPLES 500 // 硬兜底 ≈5s
typedef struct {
uint32_t loop_Origin;
uint32_t loop_Value;
uint32_t loop_CAPVD;
uint32_t loop_ORG_SUM;
uint16_t loop_ORG_CNT;
uint16_t stable_cnt;
uint16_t settle_cnt;
uint8_t loop_stable;
} Unit;
/* ---- 与 TaskLoop.c update_moving_average 同逻辑副本 ---- */
static uint8_t update_moving_average(uint32_t *p_sum, uint16_t *p_cnt,
uint32_t *p_origin, uint32_t new_value,
uint16_t window)
{
*p_sum += new_value;
(*p_cnt)++;
if (*p_cnt >= window) {
*p_origin = *p_sum / window;
*p_cnt = 0;
*p_sum = 0;
return 1;
}
return 0;
}
/* init_vd_single 等价 (含 V1.03 清零) */
static void unit_init(Unit *u, uint32_t origin)
{
u->loop_Origin = origin;
u->loop_Value = 0;
u->loop_CAPVD = 0;
u->loop_ORG_SUM = 0;
u->loop_ORG_CNT = 0;
u->stable_cnt = 0;
u->settle_cnt = 0;
u->loop_stable = 0;
}
/* vd1_task_per_channel() 稳定期分支的精确副本 (V1.03)
* : Origin==0 return ( Origin ) */
static void stable_step(Unit *u, uint32_t value)
{
uint32_t _prev_origin = u->loop_Origin;
uint8_t _win_done;
if (u->loop_Origin == 0) return;
u->loop_CAPVD = value;
_win_done = update_moving_average(&u->loop_ORG_SUM, &u->loop_ORG_CNT,
&u->loop_Origin, u->loop_CAPVD, 100);
u->stable_cnt++;
if (_win_done) {
uint32_t _drift = (u->loop_Origin > _prev_origin)
? (u->loop_Origin - _prev_origin)
: (_prev_origin - u->loop_Origin);
uint32_t _band = (uint32_t)(u->loop_Origin * STABLE_ORIGIN_PPT / 1000);
if (_drift <= _band) {
if (u->settle_cnt < 0xFFFF) u->settle_cnt++;
} else {
u->settle_cnt = 0;
}
}
if ((u->stable_cnt >= STABLE_SAMPLES &&
u->settle_cnt >= STABLE_SETTLE_WINDOWS) ||
u->stable_cnt >= STABLE_MAX_SAMPLES) {
u->loop_stable = 1;
u->stable_cnt = 0;
u->settle_cnt = 0;
}
}
/* 打样本直至判稳, 返回判稳时的累计样本数; 上限 5000 防死循环 */
static int run_until_stable(Unit *u, uint32_t (*gen)(Unit *u, int i))
{
int i;
for (i = 1; i <= 5000; i++) {
stable_step(u, gen(u, i));
if (u->loop_stable) return i;
}
return -1;
}
static uint32_t gen_const(Unit *u, int i) { (void)u; (void)i; return 100000UL; }
static uint32_t gen_spike(Unit *u, int i) { (void)u; return (i == 50) ? 200000UL : 100000UL; }
static uint32_t gen_drift(Unit *u, int i) { (void)u; return 100000UL + 2UL * (uint32_t)i; }
static int expect_stable(int n, int exp, const char *tag)
{
printf("%-24s 判稳样本=%d (期望 %d)%s\n", tag, n, exp,
(n == exp) ? "" : " <-- MISMATCH");
return n == exp;
}
int main(void)
{
Unit u;
int n, fail = 0;
/* 1. 常量基准: 窗1@100 settle=1, 窗2@200 settle=2 → 200 判稳 */
unit_init(&u, 100000UL);
n = run_until_stable(&u, gen_const);
fail += !expect_stable(n, 200, "1. 常量值");
/* 2. 首窗尖峰(第50样本 200000): 窗1均值101000 漂移>0.1% 清零;
* 2100000 1010001000 >band ; 3/4 0
* settle=1@300, settle=2@400 400 (2) */
unit_init(&u, 100000UL);
n = run_until_stable(&u, gen_spike);
fail += !expect_stable(n, 400, "2. 首窗尖峰");
/* 3. 持续漂移(每样本+2): 每窗漂移200 > band(≈100) → 永远不达标,
* 500 */
unit_init(&u, 100000UL);
n = run_until_stable(&u, gen_drift);
fail += !expect_stable(n, 500, "3. 持续漂移(硬兜底)");
/* 4. 二次稳定期防呆: 判稳后模拟 有限存在/安全复位 重学路径
* (loop_stable=0 + , C2/C3 ),
* 200 , 1 */
unit_init(&u, 100000UL);
n = run_until_stable(&u, gen_const);
if (n == 200) {
u.loop_stable = 0; /* 重学复位 (同 LC_Reset=1 路径) */
u.stable_cnt = 0;
u.settle_cnt = 0;
u.loop_ORG_CNT = 0;
u.loop_ORG_SUM = 0;
n = run_until_stable(&u, gen_const);
fail += !expect_stable(n, 200, "4. 二次稳定期(重学复位)");
} else {
printf("4. 二次稳定期 前置失败 (首轮 n=%d)\n", n);
fail++;
}
/* 5. Origin==0 边界 (vd1 入口 return, 稳定期不应推进) */
unit_init(&u, 0UL);
stable_step(&u, 100000UL);
if (u.stable_cnt != 0 || u.settle_cnt != 0) {
printf("5. Origin=0 守卫 FAIL (stable_cnt=%u settle_cnt=%u)\n",
u.stable_cnt, u.settle_cnt);
fail++;
} else {
printf("%-24s OK (不推进自检计数)\n", "5. Origin=0 守卫");
}
if (fail == 0) {
printf("ALL PASS (test_stable_settle)\n");
return 0;
}
printf("FAILED (%d)\n", fail);
return 1;
}
+177
View File
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
drift_health.py vd960Loop 无车漂移环境健康度分析零成本
原理docs/variation-analysis.md §10:
无车时 Origin 5s 阶跃更新窗口均值 variation 呈锯齿
锯齿峰谷差 = 窗口内漂移量 绝对峰 = 1.5×窗口漂移, Origin 滞后半窗
漂移速率(%/s) = 峰谷差 / Origin / 窗口秒数 × 100
输入: CSV (tick,origin,variation 三列) --selftest 模拟数据
输出: 逐窗口统计 + 汇总平均速率/方向/冻结占比
注意:
- 600ms 上报采样会使峰谷差系统性低估 ~12%峰恒在窗口末, 末采样点错过
- 漂移 > 4×dlt_ORG 时基线冻结, 锯齿消失 冻结占比是第二指标
- 冻结判定: 跨窗回落检测 (正常窗口边界回落峰谷差; 冻结继续同向爬升)
用法:
python3 drift_health.py --selftest
python3 drift_health.py --csv drift.csv --window 500
python3 drift_health.py --csv drift.csv --window 500 --json
"""
import argparse
import csv
import json
import math
import random
import sys
WINDOW_DEFAULT = 500 # tick, 5s @10ms
TICK_S_DEFAULT = 0.01 # 10ms
def analyze(variation, origins, window, tick_s):
"""逐窗口统计。返回 (窗口列表, 汇总字典)"""
import statistics
n = len(variation)
nw = n // window
rows = []
pkpk_hist = []
for k in range(nw):
a = k * window
b = a + window
seg = variation[a:b]
oseg = origins[a:b]
vmax = max(seg)
vmin = min(seg)
pkpk = vmax - vmin # 峰谷差 = 窗口漂移量
origin = sum(oseg) / len(oseg)
# 跨窗回落: jump = 下窗起点 - 本窗终点
jump = None
if k + 1 < nw:
jump = variation[b] - variation[b - 1]
# 冻结判定 (双判据):
# ① 跨窗跳变消失: 正常 |jump| ≈ 峰谷差, 冻结 |jump| ≈ 0
# ② 峰谷差突变: 当前窗 pkpk > 4× 前 4 窗中位数 (冻结=漂移持续累积)
# 判据②让最后一窗(无 jump)也能判冻结, 实时滚动同样适用
baseline = statistics.median(pkpk_hist[-4:]) if pkpk_hist else 0.0
frozen = False
if pkpk > 0:
if jump is not None and abs(jump) < 0.3 * pkpk:
frozen = True
if baseline > 0 and pkpk > 4 * baseline:
frozen = True
pkpk_hist.append(pkpk)
# 速率 %/s (按峰谷差)
rate = pkpk / origin / (window * tick_s) * 100.0 if origin > 0 else 0.0
rows.append({
"window": k,
"max": round(vmax, 4),
"min": round(vmin, 4),
"pkpk": round(pkpk, 4),
"origin": round(origin, 1),
"rate_pct_per_s": round(rate, 4),
"jump": round(jump, 4) if jump is not None else None,
"frozen": frozen,
})
if not rows:
return [], {}
rates = [r["rate_pct_per_s"] for r in rows if not r["frozen"]]
frozen_cnt = sum(1 for r in rows if r["frozen"])
summary = {
"windows": len(rows),
"frozen_ratio_pct": round(frozen_cnt / len(rows) * 100, 1),
"avg_rate_pct_per_s": round(sum(rates) / len(rates), 4) if rates else None,
"max_rate_pct_per_s": round(max(rates), 4) if rates else None,
"last_direction": "正(频率上升/CAPVD下降)" if rows[-1]["pkpk"] > 0 and rows[-1]["max"] >= abs(rows[-1]["min"]) else "负(频率下降/CAPVD上升)",
}
return rows, summary
def gen_selftest(window=WINDOW_DEFAULT, tick_s=TICK_S_DEFAULT):
"""生成模拟数据: 前 6 窗慢漂移(0.02%/s, 正常锯齿), 后 2 窗快漂移(0.5%/s, 冻结)
归一化坐标: Origin1.0, 漂移为相对比例"""
r_slow = 2e-6 # 0.02%/s → 窗口漂移 0.1%
r_fast = 5e-5 # 0.5%/s → 窗口漂移 2.5%, 远超 4×dlt_ORG → 冻结
n = 8 * window
capvd = []
for i in range(n):
if i < 6 * window:
capvd.append(1.0 + i * r_slow)
else:
capvd.append(1.0 + 6 * window * r_slow + (i - 6 * window) * r_fast)
# Origin: 窗口 k 期间 = 窗口 k-1 均值 (k>=1); 冻结窗(6,7) 锁在窗口 5 均值
origin = [0.0] * n
base6 = sum(capvd[5 * window:6 * window]) / window
for i in range(n):
k = i // window
if k == 0:
origin[i] = capvd[0]
elif k < 6:
origin[i] = sum(capvd[(k - 1) * window:k * window]) / window
else:
origin[i] = base6
variation = [origin[i] - capvd[i] for i in range(n)]
return variation, origin
def fmt_table(rows, summary):
lines = []
lines.append(f"{'窗口':<5}{'max':>10}{'min':>10}{'峰谷差':>10}{'Origin':>12}{'速率%/s':>10}{'跨窗jump':>10} 状态")
for r in rows:
state = "冻结" if r["frozen"] else "正常"
jump = f"{r['jump']:+.4f}" if r["jump"] is not None else " -"
lines.append(f"{r['window']:<5}{r['max']:>10.4f}{r['min']:>10.4f}{r['pkpk']:>10.4f}"
f"{r['origin']:>12.1f}{r['rate_pct_per_s']:>10.4f}{jump:>10} {state}")
lines.append("-" * 66)
lines.append(f"平均速率: {summary['avg_rate_pct_per_s']} %/s "
f"最大速率: {summary['max_rate_pct_per_s']} %/s "
f"冻结占比: {summary['frozen_ratio_pct']}% "
f"末窗方向: {summary['last_direction']}")
return "\n".join(lines)
def main():
p = argparse.ArgumentParser(
description="vd960Loop 无车漂移环境健康度分析(零成本, 用现有 variation 锯齿)",
formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
p.add_argument("--csv", help="CSV 文件, 表头含 tick,origin,variation")
p.add_argument("--window", type=int, default=WINDOW_DEFAULT, help=f"窗口 tick (默认 {WINDOW_DEFAULT}=5s)")
p.add_argument("--tick-s", type=float, default=TICK_S_DEFAULT, help=f"tick 秒数 (默认 {TICK_S_DEFAULT}=10ms)")
p.add_argument("--json", action="store_true", help="JSON 输出")
p.add_argument("--selftest", action="store_true", help="模拟数据自测")
args = p.parse_args()
if args.selftest:
variation, origin = gen_selftest(args.window, args.tick_s)
rows, summary = analyze(variation, origin, args.window, args.tick_s)
if args.json:
print(json.dumps({"rows": rows, "summary": summary}, ensure_ascii=False, indent=2))
else:
print("模拟: 前 6 窗慢漂移 r=0.0002/tick (0.1%/窗 → 0.02%/s), 后 2 窗快漂移 r=0.002 (冻结)")
print(fmt_table(rows, summary))
return
if not args.csv:
p.error("需要 --csv 或 --selftest")
variations, origins = [], []
with open(args.csv, newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
variations.append(float(row["variation"]))
origins.append(float(row["origin"]))
if len(variations) < args.window:
p.error(f"数据不足一个窗口 ({len(variations)} < {args.window})")
rows, summary = analyze(variations, origins, args.window, args.tick_s)
if args.json:
print(json.dumps({"rows": rows, "summary": summary}, ensure_ascii=False, indent=2))
else:
print(fmt_table(rows, summary))
if __name__ == "__main__":
sys.exit(main())
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
variation_calc.py vd960Loop variation ΔL / ΔL-L 换算工具
物理关系推导见 docs/variation-analysis.md §9:
CAPVD T = 2π·(LC) 周期域, 归一化到 MEASUREMENT_BASE131072
variation = Origin CAPVD V1.05 3B 有符号补码, =金属进入方向
variation/Origin = 1 (1 + ΔL/L0) 精确
ΔL/L0 2 × variation/Origin 一阶, |ΔL/L| 1 , 残差 <0.2%
Δf/f variation/Origin 频域对应: CAPVD 1/f
L0 = 1/(4π²·f0²·C) 绝对电感, f0=上报频率, C=档位电容
电容档位无关性: Origin CAPVD C, variation/Origin C 消掉,
四档电容(33/43/66/76nF)共用同一套 ΔL/L 换算表
用法示例:
# 正向: 频率+电容+variation+origin → L0, ΔL, ΔL/L
python3 variation_calc.py --freq 100 --cap 43 --variation 216 --origin 131072
# 反推: 给定 ΔL/L 看需要多大 variation
python3 variation_calc.py --freq 100 --cap 43 --dl-rel 0.33
# 解析协议 3B LE 有符号补码 (V1.05 variation 字段)
python3 variation_calc.py --parse-var F6FFFF
# 灵敏度档位对照表
python3 variation_calc.py --sens-table
# 交互模式
python3 variation_calc.py --interactive
"""
import argparse
import math
import sys
# ---------------------------------------------------------------- 常量
CAP_OPTIONS = (33, 43, 66, 76) # 线圈内部电容四档 (nF)
ORIGIN_DEFAULT = 131072 # MEASUREMENT_BASE = 2^17
SENS_TABLE = {0: (216, 108), 1: (108, 72), 2: (36, 18), 3: (10, 9)}
SENS_TABLE_9 = { # 9级制设计稿 (0=最低, 8=最高, 0.01%~5.00% ΔL/L)
0: (1638, 819), 1: (745, 373), 2: (339, 170), 3: (154, 100), 4: (70, 46),
5: (32, 21), 6: (15, 14), 7: (7, 6), 8: (3, 3),
}
SENS_NAMES = {0: "0(低)", 1: "1(中)", 2: "2(高)", 3: "3(最高)"}
SENS_NAMES_9 = {0: "0(最低)", 1: "1", 2: "2", 3: "3", 4: "4",
5: "5", 6: "6", 7: "7", 8: "8(最高)"}
# ---------------------------------------------------------------- 核心计算
def inductance(freq_hz, cap_nf):
"""绝对电感 L0 = 1/(4π²·f²·C), 返回 μH"""
c = cap_nf * 1e-9
return 1.0 / (4.0 * math.pi * math.pi * freq_hz * freq_hz * c) * 1e6
def variation_ratio(variation, origin):
"""variation/Origin (有符号)"""
return variation / origin
def delta_ll(vr):
"""ΔL/L: (精确, 一阶), 返回相对值(负=电感减小)"""
exact = vr * vr - 2.0 * vr
approx = -2.0 * vr
return exact, approx
def variation_from_dl_rel(dl_rel_pct, origin):
"""反推: 给定 |ΔL/L|(%) → 需要的 variation (计数域)"""
r = dl_rel_pct / 100.0 # ΔL/L = r
vr = 1.0 - math.sqrt(1.0 - r) # 精确
return vr * origin, vr
def sens_hit(vr):
"""当前 variation/Origin 落在哪档进入阈值 (SensTable/65536)"""
for sens in (0, 1, 2, 3):
thr = SENS_TABLE[sens][0] / 65536.0
if abs(vr) >= thr:
return sens, thr
return None, None
def parse_var_3byte(hex_str):
"""V1.05 协议 variation 字段: 3B LE 有符号补码 → int32
F6FF FF 10; 000001 65536
"""
h = hex_str.strip()
if len(h) != 6:
raise ValueError("需要 6 位 hex (3 字节 LE), 如 F6FFFF")
b0 = int(h[0:2], 16)
b1 = int(h[2:4], 16)
b2 = int(h[4:6], 16)
v = b0 | (b1 << 8) | (b2 << 16)
if v & 0x800000:
v |= 0xFF000000
if v >= 0x80000000:
v -= 0x100000000
return v
def fmt_sens_table(sens_count=4):
"""打印灵敏度档位对照表 (4级或9级)"""
table = SENS_TABLE if sens_count == 4 else SENS_TABLE_9
names = SENS_NAMES if sens_count == 4 else SENS_NAMES_9
lines = [f"灵敏度档位 → ΔL/L 触发阈值 ({sens_count}级制, 一阶 ΔL/L≈−2×vr):"]
lines.append(f"{'SENS':<10}{'进入表':<8}{'离开表':<8}{'Δf/f进入':<12}{'ΔL/L进入':<12}{'ΔL/L离开'}")
for sens in sorted(table):
sin, sout = table[sens]
vr_in = sin / 65536.0
vr_out = sout / 65536.0
lines.append(f"{names[sens]:<10}{sin:<8}{sout:<8}"
f"{vr_in * 100:>7.4f}%{'':<4}{-2 * vr_in * 100:>8.4f}%{'':<4}{-2 * vr_out * 100:>8.4f}%")
return "\n".join(lines)
def forward(freq, cap, variation, origin):
"""正向计算并打印"""
f_hz = freq * 1000.0 if freq < 1e6 else freq
l0 = inductance(f_hz, cap)
vr = variation_ratio(variation, origin)
exact, approx = delta_ll(vr)
dl = l0 * exact
sens, thr = sens_hit(vr)
out = []
out.append("=" * 62)
out.append("正向换算: 频率+电容+variation+Origin")
out.append("=" * 62)
out.append(f" 上报频率 f0 : {f_hz / 1000.0:>10.3f} kHz")
out.append(f" 电容档 C : {cap:>10d} nF")
out.append(f" 绝对电感 L0 : {l0:>10.3f} μH (L=1/(4π²f²C))")
out.append(f" Origin : {origin:>10d} (CAPVD 基线)")
out.append(f" variation : {variation:>10d} (3B 有符号)")
out.append(f" variation/Origin : {vr * 100:>9.4f} % (≈ Δf/f)")
out.append("-" * 62)
out.append(f" ΔL/L 一阶 : {-2 * vr * 100:>9.4f} % (≈ 2×vr)")
out.append(f" ΔL/L 精确 : {exact * 100:>9.4f} % (vr²−2vr)")
out.append(f" ΔL (绝对) : {dl:>9.4f} μH")
out.append("-" * 62)
if sens is not None:
out.append(f" ≥ SENS={SENS_NAMES[sens]} 进入阈值 (vr_thr={thr * 100:.4f}%)")
else:
out.append(f" 低于全部进入阈值 (最小档 SENS=3: {10 / 65536 * 100:.4f}%)")
out.append("=" * 62)
return "\n".join(out)
def reverse(freq, cap, dl_rel_pct, origin):
"""反推: 给定 |ΔL/L|% → variation"""
f_hz = freq * 1000.0 if freq < 1e6 else freq
l0 = inductance(f_hz, cap)
variation, vr = variation_from_dl_rel(dl_rel_pct, origin)
out = []
out.append("=" * 62)
out.append(f"反推: ΔL/L = {dl_rel_pct}% 需要的 variation")
out.append("=" * 62)
out.append(f" L0 = {l0:.3f} μH (f0={f_hz / 1000:.3f}kHz, C={cap}nF)")
out.append(f" ΔL = {l0 * (-dl_rel_pct / 100.0):.4f} μH")
out.append(f" variation/Origin = {vr * 100:.4f} %")
out.append(f" variation = {variation:.0f} (Origin={origin})")
out.append(f" 一阶估算 = {origin * dl_rel_pct / 200.0:.0f} (vr≈r/2)")
out.append("=" * 62)
return "\n".join(out)
def interactive():
print("vd960Loop variation 换算工具 — 交互模式 (Ctrl+C 退出)")
cap = int(input("电容档 (33/43/66/76 nF): "))
if cap not in CAP_OPTIONS:
print(f"警告: {cap}nF 不在标准档位 {CAP_OPTIONS}")
freq = float(input("上报频率 (kHz, 如 100): "))
origin = int(input(f"Origin (默认 {ORIGIN_DEFAULT}): ") or ORIGIN_DEFAULT)
while True:
try:
variation = int(input("variation (有符号, q 退出): "))
except ValueError:
break
print()
print(forward(freq, cap, variation, origin))
def main():
p = argparse.ArgumentParser(
description="vd960Loop variation ↔ ΔL/ΔL-L 换算工具",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__)
p.add_argument("--freq", type=float, help="上报频率 (默认 kHz, <1e6 视为 kHz)")
p.add_argument("--cap", type=int, choices=CAP_OPTIONS,
help="电容档位 nF: 33/43/66/76")
p.add_argument("--variation", type=int, help="variation (3B 有符号, 计数域)")
p.add_argument("--origin", type=int, default=ORIGIN_DEFAULT,
help=f"Origin 基线 (默认 {ORIGIN_DEFAULT})")
p.add_argument("--dl-rel", type=float,
help="反推模式: 给定 |ΔL/L|%% → 所需 variation")
p.add_argument("--parse-var", metavar="HEX6",
help="解析 V1.05 协议 3B LE 有符号补码 (如 F6FFFF = 10)")
p.add_argument("--sens-table", nargs="?", const="4", type=int, choices=[4, 9],
help="打印灵敏度档位对照表 (4=4级制默认, 9=9级制设计稿)")
p.add_argument("--interactive", action="store_true", help="交互模式")
args = p.parse_args()
if args.sens_table:
print(fmt_sens_table(args.sens_table))
return
if args.parse_var:
v = parse_var_3byte(args.parse_var)
print(f"3B LE 补码 {args.parse_var.upper()}{v}")
return
if args.interactive:
interactive()
return
if args.dl_rel is not None:
if not args.freq or not args.cap:
p.error("反推模式需要 --freq 和 --cap")
print(reverse(args.freq, args.cap, args.dl_rel, args.origin))
return
if None in (args.freq, args.cap, args.variation):
p.error("需要 --freq --cap --variation (或 --dl-rel / --sens-table / --interactive)")
print(forward(args.freq, args.cap, args.variation, args.origin))
if __name__ == "__main__":
sys.exit(main())
@@ -33,7 +33,10 @@
#define ALFA_CAP1 79 // IIR α = 79/256 ≈ 0.31 (@10ms → τ≈32ms)
#define MAX_SLOPE_RATE 5 // 斜率限幅: 单次最大变化 5%
#define ENTRY_CONFIRM 3 // 进入确认: 连续 N 次低于阈值
#define STABLE_SAMPLES 128 // 稳定期样本数
#define STABLE_SAMPLES 128 // 稳定期最少样本数 (128×10ms≈1.3s)
#define STABLE_ORIGIN_PPT 1 // 基准稳定判据: 窗口均值漂移 ≤ Origin×0.1%
#define STABLE_SETTLE_WINDOWS 2 // 连续 2 窗(100样本/窗≈1s)漂移达标 → 判稳
#define STABLE_MAX_SAMPLES 500 // 硬兜底 ≈5s (防环境长期不稳绿灯无限自检)
/*===========================================================================
* 线 (V2.3~V2.5)
@@ -41,6 +44,15 @@
#define FREEZE_TIMEOUT 1000 // 冻结超时: ~10s @ 10ms/tick
#define FREEZE_STABILITY_RATE 2 // 稳定性窗口: 参考值的 ±2%
/*===========================================================================
* (IWDT) - DLD154V4B V2.10
* : TMR15 ISR 5ms g_wdg_counter, poll_wdg() 500ms
* = WDT_DIV x (WDT_RLD+1) / LSI(40kHz) 1.64s
*===========================================================================*/
#define WDT_COUNTER_LIMIT 100 // 喂狗周期 500ms (TMR15 5ms x 100)
#define WDT_DIV 2 // IWDT 分频枚举值: 2 = WDT_CLK_DIV_16 (实际分频 16; 勿传 16!)
#define WDT_RLD 4095 // 12bit 重装载 -> 超时约 1.64s
/*===========================================================================
* : 1 = (CN200910309382), 0 = cnt_release
*===========================================================================*/
@@ -173,7 +185,8 @@ typedef struct {
uint16_t fault_tick;
/*--- 稳定期 ---*/
uint16_t stable_cnt;
uint16_t stable_cnt; // 稳定期累计样本 (每次 CAP_OK +1)
uint16_t settle_cnt; // 基准稳定连续窗口计数 (窗=100样本≈1s, 漂移≤0.1%则+1)
/*--- 调试 ---*/
uint32_t xn_counter;
@@ -228,7 +241,8 @@ uint8_t update_moving_average(uint32_t* p_sum, uint16_t* p_cnt,
void set_loops_relay_on(uint8_t loop_num);
void set_loops_relay_off(uint8_t loop_num);
void wdt_feed(void);
void poll_wdg(void);
extern volatile uint16_t g_wdg_counter; // 看门狗活性计数 (TMR15 5ms 递增)
extern TaskHandle_t usart_task_handler;
@@ -16,15 +16,20 @@
#include <stdint.h>
#define PRODUCT_MODEL "DLD960"
#define FIRMWARE_VER "1.00"
#define FIRMWARE_VER "1.03"
#define HARDWARE_VER "1.00"
#define FIRMWARE_VER_MAIN 0
#define FIRMWARE_VER_SUB 30
#define FIRMWARE_VER_SSUB 0
#define FIRMWARE_VER_MAIN 1
#define FIRMWARE_VER_SUB 3
#define FIRMWARE_VER_SSUB 1
#define HARDWARE_VER_MAIN 1
#define HARDWARE_VER_SUB 0
#define HARDWARE_VER_SSUB 0
// LOOP_MEASURE_BASE: variation 归一化基数 (协议 0x64 响应尾部 3B LE 上报)
// ⚠ 必须与 TaskLoop.h 的 MEASUREMENT_BASE 同步 (AT32F421 @120MHz = 2^17 = 131072)
// 第三方评估换算: Δf/f ≈ variation/LOOP_MEASURE_BASE; ΔL/L ≈ -2×variation/LOOP_MEASURE_BASE
#define LOOP_MEASURE_BASE 131072L
#define FUNCTION_A 0x01 // 二次判断
#define FUNCTION_B 0x02 // 二次判断增强条件判断
@@ -111,7 +111,7 @@ typedef enum
typedef struct
{
uint8_t sensitvity; // 高四位表示灵敏度的数量,低四位表示当前灵敏度的序号(从0开始)
uint8_t sensitvity; // 灵敏度序号: 4级制 0~3 (值越大越灵敏, 默认2); 9级制后续版本实现
uint8_t delay_time;
uint8_t output_mode;
uint8_t direction_mode; // 方向判别模式,0 触发模式, 非0 表示方向判别模式
@@ -171,6 +171,9 @@ typedef struct _DBN_BLE_STATE_
void set_flp_level(uint8_t loop_num, uint8_t freq_level);
/* 灵敏度配置 → 检测档位映射 (4级制: 0~3 一一对应; 9级制后续实现) */
uint8_t sens_level_from_config(uint8_t sensitvity);
void set_factory_param(void);
void storage_dev(void);
void para_store_init(void);
@@ -6,6 +6,7 @@
#include "task.h"
#include <stdlib.h>
#include <string.h>
#include "storage.h"
/*===========================================================================
* DLD154V4B / M1H
@@ -23,6 +24,7 @@ Loop154_States g_loop_states = {0};
uint32_t g_sys_freq = 0;
uint8_t g_input_div = 1;
uint32_t g_safe_max_cnt = LC_HOLD_TIME;
volatile uint16_t g_wdg_counter = 0; // 看门狗活性计数 (TMR15 ISR 5ms 递增)
/*===========================================================================
* IIR DLD154V4B
@@ -107,7 +109,8 @@ void init_vd_single(Loop154_Unit *unit)
unit->loop_CAP_OK = 0;
unit->loop_CAPVD = 0;
unit->loop_SensLevel = 2; // 默认中灵敏度
unit->loop_SensLevel = sens_level_from_config(
g_loop_cng_info.loop_cng[unit->loop_num].sensitvity); // 从配置读, 不再硬编码 2 (修复: INIT_VDs 覆盖配置值)
unit->Flt_Reg = ALFA_CAP1;
unit->loop_entry_cnt = 0;
@@ -151,6 +154,7 @@ void init_vd_single(Loop154_Unit *unit)
unit->SET_SAFE = 1;
unit->stable_cnt = 0;
unit->settle_cnt = 0; // V1.03: 自检计数从零开始
unit->xn_counter = 0;
}
@@ -250,6 +254,9 @@ void TMR15_GLOBAL_IRQHandler(void)
if (tmr_interrupt_flag_get(TMR15, TMR_OVF_FLAG) != RESET) {
/*--- 看门狗活性计数 (5ms) ---*/
g_wdg_counter++;
/*--- 50ms tick 分频 ---*/
TM1cnt++;
if (TM1cnt >= 10) {
@@ -312,6 +319,8 @@ void TMR15_GLOBAL_IRQHandler(void)
unit->loop_INI_LOOP = 1;
unit->loop_LOOP_OK0 = 0;
unit->loop_stable = 0;
unit->stable_cnt = 0; // V1.03: 重学自检计数从零开始
unit->settle_cnt = 0;
unit->loop_ORG_CNT = 0;
unit->loop_ORG_SUM = 0;
unit->last_exit_tick = g_loop_states.misc_counter;
@@ -331,6 +340,8 @@ void TMR15_GLOBAL_IRQHandler(void)
unit->loop_INI_LOOP = 1;
unit->loop_LOOP_OK0 = 0;
unit->loop_stable = 0;
unit->stable_cnt = 0; // V1.03: 二次稳定期从零开始
unit->settle_cnt = 0;
#if USE_FLATNESS_EXIT
unit->exit_state = 0;
unit->max_slope = 0;
@@ -453,15 +464,40 @@ void vd1_task_per_channel(Loop154_Unit *unit)
unit->loop_CAPVD = get_flt_value(clamped_value, unit->loop_CAPVD);
}
/*--- 2. 稳定期:绕过 IIR 和斜率限幅,小窗口快速收敛 ---*/
/*--- 2. 稳定期:绕过 IIR 和斜率限幅,小窗口快速收敛;
* ( Origin ) (V1.03, V4B V4.23) ---*/
if (!unit->loop_stable) {
uint32_t _prev_origin = unit->loop_Origin;
uint8_t _win_done;
unit->loop_CAPVD = unit->loop_Value;
update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
&unit->loop_Origin, unit->loop_CAPVD, 100);
_win_done = update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
&unit->loop_Origin, unit->loop_CAPVD, 100);
unit->stable_cnt++;
if (unit->stable_cnt >= STABLE_SAMPLES) {
/* 每窗(100样本≈1s)完成时比较 Origin 均值漂移;
* Origin×STABLE_ORIGIN_PPT/1000 settle +1,
* ( Origin , ) */
if (_win_done) {
uint32_t _drift = (unit->loop_Origin > _prev_origin)
? (unit->loop_Origin - _prev_origin)
: (_prev_origin - unit->loop_Origin);
uint32_t _band = (uint32_t)(unit->loop_Origin * STABLE_ORIGIN_PPT / 1000);
if (_drift <= _band) {
if (unit->settle_cnt < 0xFFFF) unit->settle_cnt++;
} else {
unit->settle_cnt = 0;
}
}
/* 判稳条件: 最少样本 + 连续 2 窗基准稳定; 硬兜底 STABLE_MAX_SAMPLES */
if ((unit->stable_cnt >= STABLE_SAMPLES &&
unit->settle_cnt >= STABLE_SETTLE_WINDOWS) ||
unit->stable_cnt >= STABLE_MAX_SAMPLES) {
unit->loop_stable = 1;
unit->stable_cnt = 0; // 计数清零: 重连/安全复位二次稳定期从零开始
unit->settle_cnt = 0;
PRINT("Loop%d stable, Origin:%d\n", unit->loop_num + 1, unit->loop_Origin);
}
return;
@@ -472,14 +508,21 @@ void vd1_task_per_channel(Loop154_Unit *unit)
*
*================================================================*/
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * SensTable[unit->loop_SensLevel]) >> 16;
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * g_loop_sens_list.sens[unit->loop_SensLevel].sens_in) >> 16;
{
/* V4B V4.20 同步: 双向对称保护 — 负variation(铁块磁导率)也冻结基线
* dev>+4×dlt, variation会把Origin污染抬高
* : [-4×dlt, +4×dlt] */
int32_t dev = (int32_t)unit->loop_CAPVD - (int32_t)unit->loop_Origin;
if (dev < (int32_t)(unit->loop_dlt_ORG * 4)) {
int32_t freeze_band = (int32_t)(unit->loop_dlt_ORG * 4);
if (dev < freeze_band && dev > -freeze_band) {
/* 对称窗口内 → 正常基线跟踪 */
unit->loop_freeze_cnt = 0;
unit->loop_freeze_ref = 0;
update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
&unit->loop_Origin, unit->loop_CAPVD, WINDOW_ORIGIN);
} else {
} else if (dev < 0) {
/* 正variation (疑似车) → 冻结, 保留超时兜底更新 */
if (unit->loop_freeze_cnt == 0) {
unit->loop_freeze_ref = unit->loop_CAPVD;
} else {
@@ -503,6 +546,15 @@ void vd1_task_per_channel(Loop154_Unit *unit)
unit->loop_ORG_CNT = 0;
unit->loop_ORG_SUM = 0;
}
} else {
/* 负variation (CAPVD高于Origin, 特殊铁块磁导率主导) → 只冻结, 永不更新 Origin */
if (unit->loop_freeze_cnt == 0) {
unit->loop_freeze_ref = unit->loop_CAPVD;
}
unit->loop_freeze_cnt++;
if (unit->loop_freeze_cnt > FREEZE_TIMEOUT) unit->loop_freeze_cnt = FREEZE_TIMEOUT;
unit->loop_ORG_CNT = 0;
unit->loop_ORG_SUM = 0;
}
}
@@ -590,7 +642,7 @@ void vd1_task_per_channel(Loop154_Unit *unit)
} else {
int32_t dev = (int32_t)unit->loop_CAPVD - (int32_t)unit->loop_Origin;
int32_t cond1 = (dev >= 0) ? dev : -dev;
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * SensTable_1[unit->loop_SensLevel]) >> 16;
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * g_loop_sens_list.sens[unit->loop_SensLevel].sens_out) >> 16;
if (cond1 < (int32_t)unit->loop_dlt_ORG && abs_fd < (int32_t)unit->delta2
&& abs_sd < (int32_t)unit->delta3) {
unit->flat_ok_cnt++;
@@ -624,7 +676,7 @@ void vd1_task_per_channel(Loop154_Unit *unit)
/*================================================================
* cnt_release
*================================================================*/
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * SensTable_1[unit->loop_SensLevel]) >> 16;
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * g_loop_sens_list.sens[unit->loop_SensLevel].sens_out) >> 16;
if ((unit->loop_Origin - unit->loop_dlt_ORG) < unit->loop_CAPVD) {
unit->loop_cnt_release++;
@@ -655,6 +707,20 @@ void vd1_task_per_channel(Loop154_Unit *unit)
/*===========================================================================
* loop_task_function FreeRTOS DLD154V4B main
/*===========================================================================
* (, DLD154V4B V2.10)
* TMR15 ISR 5ms g_wdg_counter; 100 (500ms) reload
* ISR -> -> ; -> ->
*===========================================================================*/
void poll_wdg(void)
{
if (g_wdg_counter >= WDT_COUNTER_LIMIT) { // 100 x 5ms = 500ms
g_wdg_counter = 0;
wdt_counter_reload();
}
}
/*===========================================================================
* 10ms 线
*===========================================================================*/
void loop_task_function(void *pvParameters)
@@ -703,7 +769,7 @@ void loop_task_function(void *pvParameters)
}
}
wdt_feed();
poll_wdg();
vTaskDelay(10); // 10ms tick, M4 优化: 双路 IIR + 进入确认 + 斜率限幅
}
}
@@ -59,9 +59,8 @@ __IO uint32_t TimingDelayDec;
TaskHandle_t loop_task_handler;
// 看门狗相关
#define WDT_TIMEOUT_MS 3000 // 看门狗超时时间 3秒
#define WDT_TIMEOUT_MS 1600 // 看门狗超时时间: IWDT div16 rld4095, LSI约40kHz -> ~1.64s
void wdt_init(void);
void wdt_feed(void);
tmr_output_config_type tmr_oc_init_structure;
@@ -104,20 +103,19 @@ void InitPkgUart(Pkg_Uart * pkg){
* @param none
* @retval none
* @note = ( × ) / LSI频率
* LSI频率约 40kHz64=4687 3
* LSI频率约 40kHz16 WDT_CLK_DIV_16=4095 1.64
*/
void wdt_init(void)
{
// 使能看门狗寄存器写访问
wdt_register_write_enable(TRUE);
// 设置预分频系数为 64
wdt_divider_set(WDT_CLK_DIV_64);
// 设置预分频: 必须用枚举 WDT_CLK_DIV_16 (=0x02), 勿传数值 16! (V4B 踩坑)
wdt_divider_set(WDT_CLK_DIV_16);
// 设置重载值
// 超时时间 = (64 × 4687) / 40000 ≈ 7.5
// 如果需要 3秒: (64 × 1875) / 40000 = 3秒
wdt_reload_value_set(1875);
// 设置重载值 4095 (12bit 最大)
// 超时时间 = (16 × 4096) / 40000 ≈ 1.64
wdt_reload_value_set(4095);
// 重载计数器
wdt_counter_reload();
@@ -128,15 +126,6 @@ void wdt_init(void)
PRINT("Watchdog initialized, timeout: %d ms\n", WDT_TIMEOUT_MS);
}
/**
* @brief
* @param none
* @retval none
*/
void wdt_feed(void)
{
wdt_counter_reload();
}
uint16_t timer_period = 0;
uint16_t channel1_pulse = 0;
@@ -584,8 +573,8 @@ static void unpack_pkg_set_mcjq_param(uint8_t *pkg, uint8_t len)
for(j = 0; j < _amount; j++)
{
k = pkg[i++];
unit->sensitvity = k & 0x0F;
unit->loopFreq_Level = k >> 4;
unit->sensitvity = k & 0x0F; // 低4位: 灵敏度序号 (4级制 0~3)
unit->loopFreq_Level = k >> 4; // 高4位: 频率档
unit->delay_time = pkg[i++];
k = pkg[i++];
unit->output_mode = k & 0x03;
@@ -593,6 +582,9 @@ static void unpack_pkg_set_mcjq_param(uint8_t *pkg, uint8_t len)
unit->exist_mode = pkg[i++];
unit->direction_mode = pkg[i++];
// 同步灵敏度到检测单元 (修复: 运行中配置必须立即生效, 不依赖重启)
g_loop_states.loop_unit[j].loop_SensLevel = sens_level_from_config(unit->sensitvity);
unit++;
}
@@ -836,6 +828,11 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len)
_cng++;
}
// LOOP_MEASURE_BASE: variation 归一化基数 (3B LE, 与 TaskLoop.h MEASUREMENT_BASE 同步)
tmp_ble_buf[i++] = (uint8_t)(LOOP_MEASURE_BASE & 0xFF);
tmp_ble_buf[i++] = (uint8_t)((LOOP_MEASURE_BASE >> 8) & 0xFF);
tmp_ble_buf[i++] = (uint8_t)((LOOP_MEASURE_BASE >> 16) & 0xFF);
response_uart_pkg(_cmd, tmp_ble_buf, i);
} break;
@@ -113,6 +113,12 @@ void storage_dev(void)
}
/* 灵敏度配置 → 检测档位映射 (4级制: 0~3 一一对应; 9级制后续实现) */
uint8_t sens_level_from_config(uint8_t sensitvity)
{
return sensitvity & 0x03;
}
// set frequent level
void set_flp_level(uint8_t loop_num, uint8_t freq_level)
{
@@ -181,7 +187,7 @@ void para_store_init(void)
set_flp_level(i, unitout->loopFreq_Level);
// 同步灵敏度到检测单元
g_loop_states.loop_unit[i].loop_SensLevel = unitout->sensitvity & 0x03;
g_loop_states.loop_unit[i].loop_SensLevel = sens_level_from_config(unitout->sensitvity);
g_loop_states.loop_unit[i].SET_PLUS = unitout->output_mode & 0x01;
g_loop_states.loop_unit[i].SET_DLY = (unitout->output_mode >> 1) & 0x01;
g_loop_states.loop_unit[i].hold_time = unitout->exist_mode * 20 * 5;