35 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
45 changed files with 6514 additions and 142 deletions
+38
View File
@@ -5,6 +5,44 @@
--- ---
## 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 V1.02.04 — 2026-08-19DBN 通信板单侧更新)
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。 > 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。
+89
View File
@@ -9,6 +9,7 @@ DLD960 IoT MQTT 协议定义
import json import json
import time import time
import struct import struct
import zlib
from typing import Optional, Any from typing import Optional, Any
from dataclasses import dataclass, field, asdict from dataclasses import dataclass, field, asdict
@@ -273,6 +274,94 @@ def data_log_clear(stream: str = STREAM_EVENT) -> dict:
return {} 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}
# ============================================================ # ============================================================
# 解析设备上报 # 解析设备上报
# ============================================================ # ============================================================
+25
View File
@@ -6,6 +6,31 @@
--- ---
## 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 修订) ## 2026-08-18 — log_query 响应改 hex 原始字节解析展示(协议 V1.07 修订)
### 背景 ### 背景
+327 -1
View File
@@ -7,6 +7,8 @@ DBN MQTT Tool — DLD960 IoT MQTT 设备管理工具
import sys import sys
import os import os
import json import json
import queue
import time
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@@ -18,8 +20,9 @@ from PySide6.QtWidgets import (
QTabWidget, QTextEdit, QGroupBox, QGridLayout, QCheckBox, QTabWidget, QTextEdit, QGroupBox, QGridLayout, QCheckBox,
QSplitter, QMessageBox, QHeaderView, QFrame, QSplitter, QMessageBox, QHeaderView, QFrame,
QComboBox, QSpinBox, QPlainTextEdit, 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 from PySide6.QtGui import QFont, QColor, QTextCursor
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
@@ -39,8 +42,128 @@ from dbn_mqtt_tool.protocol import (
data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear, data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
build_request, next_msg_id, build_request, next_msg_id,
topic_down, topic_up, topic_down, topic_up,
# 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): class MainWindow(QMainWindow):
_mqtt_status = Signal(bool, str) _mqtt_status = Signal(bool, str)
_mqtt_msg = Signal(str, str, object) _mqtt_msg = Signal(str, str, object)
@@ -91,6 +214,7 @@ class MainWindow(QMainWindow):
self._notebook.addTab(self._build_config_tab(), "参数配置") self._notebook.addTab(self._build_config_tab(), "参数配置")
self._notebook.addTab(self._build_data_tab(), "实时数据") self._notebook.addTab(self._build_data_tab(), "实时数据")
self._notebook.addTab(self._build_log_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_simulate_tab(), "模拟上报")
self._notebook.addTab(self._build_proto_topic_tab(), "协议Topic") self._notebook.addTab(self._build_proto_topic_tab(), "协议Topic")
self._notebook.addTab(self._build_custom_topic_tab(), "自定义Topic") self._notebook.addTab(self._build_custom_topic_tab(), "自定义Topic")
@@ -429,6 +553,192 @@ class MainWindow(QMainWindow):
layout.addLayout(layout2) layout.addLayout(layout2)
return w 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)))
# ================================================================ # ================================================================
# 模拟设备上报 # 模拟设备上报
# ================================================================ # ================================================================
@@ -571,6 +881,8 @@ class MainWindow(QMainWindow):
"model": "DLD960", "model": "DLD960",
"hard_ver": "1.0", "hard_ver": "1.0",
"soft_ver": "1.0", "soft_ver": "1.0",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"extra_info": { "extra_info": {
"code": "869756049404948", "code": "869756049404948",
"csq": "21", "csq": "21",
@@ -946,7 +1258,15 @@ class MainWindow(QMainWindow):
self._apply_log_query_data(data) self._apply_log_query_data(data)
elif cmd == CMD_LOG_CLEAR: elif cmd == CMD_LOG_CLEAR:
self._log(f"脱机日志已清除 (审计留痕, 需重新统计确认)") 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: 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, '?')})"}) self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
elif cmd == CMD_LOOP_DATA: elif cmd == CMD_LOOP_DATA:
@@ -982,6 +1302,12 @@ class MainWindow(QMainWindow):
self._log_recv(topic, payload, f"initialize sn={dev_sn} model={model}") self._log_recv(topic, payload, f"initialize sn={dev_sn} model={model}")
self._show_json({"online": dev_sn, "model": model, "extra": extra}) 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 # 旧协议 / 无 cmd 字段 的消息(如 Initialize
elif "Method" in payload: elif "Method" in payload:
self._log_recv(topic, payload, f"Method={payload.get('Method', '?')}") self._log_recv(topic, payload, f"Method={payload.get('Method', '?')}")
+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)
+6 -6
View File
@@ -1,6 +1,6 @@
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构) # vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
> 当前发布:**vd960DBN V1.02.04**2026-08-19,DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md) > 当前发布:**vd960DBN V1.02.05**2026-08-21,DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md)
## 产品概述 ## 产品概述
@@ -41,7 +41,7 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
| 子项目 | 目录 | MCU | 固件版本 | 功能 | | 子项目 | 目录 | MCU | 固件版本 | 功能 |
|--------|------|-----|---------|------| |--------|------|-----|---------|------|
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 | | vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
| vd960DBN | `vd960DBN/` | CH32V208 | 1.02.04 | 蓝牙管理、TCP/IPWCHNET)、MQTT、串口桥接、4G 扩展(RFU) | | vd960DBN | `vd960DBN/` | CH32V208 | 1.02.05 | 蓝牙管理、TCP/IPWCHNET)、MQTT、串口桥接、4G 扩展(RFU) |
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。 > ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
@@ -49,8 +49,8 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
| 文档 | 版本 | 说明 | | 文档 | 版本 | 说明 |
|------|------|------| |------|------|------|
| [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.02 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 | | [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.03 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 |
| [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.02 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 | | [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.03 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 |
## 协议文档(docs/ ## 协议文档(docs/
@@ -59,8 +59,8 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
| [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 | | [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 |
| [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 TTL 串口 | 设备管理、参数配置、数据上报 | | [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 TTL 串口 | 设备管理、参数配置、数据上报 |
| [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.03 | 以太网 TCP :5960 | 鉴权 + 命令 + event_report 客户端必答 + 脱机日志(事件/快照流) | | [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.07 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、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 远程 OTAota_* 命令,先存后刷)、**4G 通道适配(方案 C:Air780 协议转换,标准 JSON 命令面 + linkV1.12** |
| [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.02 | 蓝牙 BLE | 帧格式 + 分包 + 脱机日志(OFFLOG_STAT/QUERY/CLEAR+ 传感快照(SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A),与 MQTT/TCP 同语义 | | [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 分配、继电器、指示灯、拨码 | | [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
## 开发文档 ## 开发文档
+27 -6
View File
@@ -147,7 +147,7 @@ A2 00 51 00: 第0级灵敏度,0x00A2为进入 1620x0051为离开 81
* AutoMode: 是否处于自动模式(自动调频),0表示不启用,默认0。 * 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秒\]; * 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 | | **Value(Hex)** | 00 xx 64 | XX |
| **Length** | 3 Byte | (2 + 8 \* Amount) Byte | | **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) ## 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 | 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 | 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、继电器输出次数。 | | 杂项 | 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: eg:
@@ -261,3 +279,6 @@ FE 38 校验字节(XOR=0xFE, SUM=0x38
| V1.04 | 2026-07-02 | 主动上报增加继电器输出次数 类型 | 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.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 |
+276 -33
View File
@@ -1,15 +1,13 @@
# DLD960 BLE 通信协议(脱机事件日志 + 传感快照) # DLD960 BLE 通信协议
> 版本: V1.022026-08-12,新增传感快照 3 命令;分包上限随协商 MTU 动态 > 版本: V1.032026-08-31,命令总表补齐既有配置命令 0x09~0xC5 + 透传通道;未实现命令如实标注
> 适用: vd960DBNCH32V208WCH BLE 协议栈) > 适用: vd960DBNCH32V208WCH BLE 协议栈)
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Qxx 环形日志(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形) > 用途: 小程序/APP 经蓝牙对 vd960DBN 进行**配置管理、参数查询、脱机日志/传感快照读取**(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
--- ---
## 1 帧格式 ## 1 帧格式
与既有 BLE 配置命令一致:
``` ```
Magic | Header | Data | CheckByte Magic | Header | Data | CheckByte
| Addr/Sub Len CMD | | Xor Sum | Addr/Sub Len CMD | | Xor Sum
@@ -18,11 +16,21 @@ Magic | Header | Data | CheckByte
- `Len = len(CMD) + len(Data)`(即 `pkg[2] = 1 + data_len` - `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 - 校验:`Xor = XOR(pkg[1..Len+2])``Sum = SUM(pkg[1..Len+2])`,覆盖 header + cmd + data(不含 magic
- 本命令族 Magic = `0x8F`MAGIC_BYTE_DBN_DEFAULT - 校验实现:`check_pkg()` / `compute_ckb()`dbn_ble_srv.c
### 分包(长响应 ### 1.1 魔数(Magic
响应单包数据上限**随协商 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` | 魔数 | 名称 | 用途 |
|------|------|------|
| `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 | 整包最大长度 | | 协商 MTU | 单包最大 dat | 整包最大长度 |
|---------|-------------|-------------| |---------|-------------|-------------|
@@ -36,22 +44,256 @@ Magic | Header | Data | CheckByte
--- ---
## 2 命令 ## 2 命令总表
| 命令码 | 名称 | 方向 | 说明 | | 命令码 | 名称 | 方向 | 实现状态 |
|--------|------|------|------| |--------|------|------|---------|
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) | | `0x09` | UPDATE_DEV_SERIAL | APP→设备 | ✅ §3.1 |
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 | | `0x10` | GET_DEV_INFO | APP→设备 | ✅ §3.2 |
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) | | `0x11` | SET_LSSC_NET | APP→设备 | ✅ §3.3 |
| `0x28` | `SNAP_STAT` | APP→设备 | 查询传感快照统计(分页定位) | | `0x12` | GET_LSSC_NET | APP→设备 | ✅ §3.4 |
| `0x29` | `SNAP_QUERY` | APP→设备 | 按全局序号分页拉取快照记录 | | `0x13` | SET_IOT_NET | APP→设备 | ✅ §3.5 |
| `0x2A` | `SNAP_CLEAR` | APP→设备 | 清除快照(审计写入事件流) | | `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 V1.06 / TCP JSON V1.02 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。 > 与 MQTT / TCP JSON 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同V1.02 起)
--- ---
## 3 查询日志统计 `OFFLOG_STAT` (0x25) ## 3 设备配置命令
### 3.1 更改设备序列码 `UPDATE_DEV_SERIAL` (0x09)
**请求 data(6B):** 新序列码(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** 无。
**响应 data(30B):** 与请求布局一致:`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 字节)。 **请求 data** 无(`Len=1`,仅 cmd 字节)。
@@ -61,10 +303,10 @@ Magic | Header | Data | CheckByte
|------|------|------|------| |------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用(Flash 初始化失败) | | 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用(Flash 初始化失败) |
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) | | 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
| 3 | 4 | `count` | 有效记录条数(0~8064,环形覆盖后 < capacity |
| 7 | 4 | `capacity` | 容量上限(8064 |
| 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity | | 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| 7 | 4 | `capacity` | 容量上限(**随存储芯片动态**W25Q32=16256 / W25Q64=32640 / W25Q128=65408 / W25Q256=130944 | | 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): 示例(boot_seq=2, count=1234, capacity=16256, seq_first=100, seq_last=1333):
@@ -77,7 +319,7 @@ Magic | Header | Data | CheckByte
--- ---
## 4 分页拉取日志 `OFFLOG_QUERY` (0x26) ## 7 分页拉取日志 `OFFLOG_QUERY` (0x26)
**请求 data5B):** **请求 data5B):**
@@ -92,7 +334,7 @@ Magic | Header | Data | CheckByte
|------|------|------|------| |------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用;`0x02`=请求帧过短 | | 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用;`0x02`=请求帧过短 |
| 1 | 1 | `count` | 本次实际返回记录条数(0~4) | | 1 | 1 | `count` | 本次实际返回记录条数(0~4) |
| 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §6 | | 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §10 |
记录顺序 = 逻辑序号升序(与 `start_seq` 一致)。越界/空日志:`count=0` 记录顺序 = 逻辑序号升序(与 `start_seq` 一致)。越界/空日志:`count=0`
@@ -109,7 +351,7 @@ Magic | Header | Data | CheckByte
--- ---
## 5 清除日志 `OFFLOG_CLEAR` (0x27) ## 8 清除日志 `OFFLOG_CLEAR` (0x27)
**请求 data** 无。 **请求 data** 无。
@@ -123,17 +365,17 @@ Magic | Header | Data | CheckByte
--- ---
## 6 传感快照日志(0x28/0x29/0x2AV1.02 新增) ## 9 传感快照日志(0x28/0x29/0x2AV1.02 新增)
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。 **用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。 **线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
### 6.1 查询快照统计 `SNAP_STAT` (0x28) ### 9.1 查询快照统计 `SNAP_STAT` (0x28)
**请求 data** 无(`Len=1`)。 **请求 data** 无(`Len=1`)。
**响应 data(19B,全小端):** 与 §3 `OFFLOG_STAT` 布局相同: **响应 data(19B,全小端):** 与 §6 `OFFLOG_STAT` 布局相同:
| 偏移 | 长度 | 字段 | 说明 | | 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------| |------|------|------|------|
@@ -144,7 +386,7 @@ Magic | Header | Data | CheckByte
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) | | 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
| 15 | 4 | `seq_last` | 最新一条全局序号 | | 15 | 4 | `seq_last` | 最新一条全局序号 |
### 6.2 分页拉取快照 `SNAP_QUERY` (0x29) ### 9.2 分页拉取快照 `SNAP_QUERY` (0x29)
**请求 data5B):** **请求 data5B):**
@@ -159,11 +401,11 @@ Magic | Header | Data | CheckByte
|------|------|------|------| |------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=快照未启用;`0x02`=请求帧过短 | | 0 | 1 | `status` | `0x00`=OK`0x01`=快照未启用;`0x02`=请求帧过短 |
| 1 | 1 | `count` | 本次实际返回记录条数(0~2) | | 1 | 1 | `count` | 本次实际返回记录条数(0~2) |
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §6.4 | | 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §9.4 |
记录顺序 = 逻辑序号升序。QUERY 响应最大 130BMTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。 记录顺序 = 逻辑序号升序。QUERY 响应最大 130BMTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
### 6.3 清除快照 `SNAP_CLEAR` (0x2A) ### 9.3 清除快照 `SNAP_CLEAR` (0x2A)
**请求 data** 无。**响应 data1B):** `status``0x00`=OK。 **请求 data** 无。**响应 data1B):** `status``0x00`=OK。
@@ -172,7 +414,7 @@ Magic | Header | Data | CheckByte
成功后 `SNAP_STAT``count` 归 0`seq_last` 继续递增(序号不复位)。 成功后 `SNAP_STAT``count` 归 0`seq_last` 继续递增(序号不复位)。
### 6.4 快照记录格式(SnapRec,64B 定长,小端) ### 9.4 快照记录格式(SnapRec,64B 定长,小端)
| 偏移 | 长度 | 字段 | 说明 | | 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------| |------|------|------|------|
@@ -200,7 +442,7 @@ Magic | Header | Data | CheckByte
--- ---
## 7 记录格式(OfflogEvt,32B 定长,小端) ## 10 记录格式(OfflogEvt,32B 定长,小端)
| 偏移 | 长度 | 字段 | 说明 | | 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------| |------|------|------|------|
@@ -234,10 +476,11 @@ Magic | Header | Data | CheckByte
--- ---
## 8 版本历史 ## 11 版本历史
| 版本 | 日期 | 说明 | | 版本 | 日期 | 说明 |
|------|------|------| |------|------|------|
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR | | 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.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.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 上报未实现) |
+658 -4
View File
@@ -2,6 +2,7 @@
> 基于《DLD960 串口通信协议》V1.01,将设备管理、参数配置、数据上报映射到 MQTT 协议。 > 基于《DLD960 串口通信协议》V1.01,将设备管理、参数配置、数据上报映射到 MQTT 协议。
> 交互格式:JSON。 > 交互格式:JSON。
> 版本:V1.142026-09-10,新增 §6.9 4G 配置同步(BLE → DBN → Air780);0x8F 定义为本机侧私有帧,帧长上限 260B)
--- ---
@@ -96,6 +97,19 @@ dld960/{dev_serial}/{direction}
| 5 | 内部错误 | | 5 | 内部错误 |
| 6 | 数据超长 | | 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` 语义) ## 2.3 设备时钟同步(`ts` 语义)
设备无 RTC/SNTP 时间源,上电后本地时钟为**上电秒数**(从 0 递增)。为使上行数据带真实 Unix 时间: 设备无 RTC/SNTP 时间源,上电后本地时钟为**上电秒数**(从 0 递增)。为使上行数据带真实 Unix 时间:
@@ -140,10 +154,22 @@ dld960/{dev_serial}/{direction}
| `log_stat` | 查询脱机日志统计(事件/快照流) | srv→dev | — | | `log_stat` | 查询脱机日志统计(事件/快照流) | srv→dev | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | srv→dev | — | | `log_query` | 分页拉取脱机日志(事件/快照流) | srv→dev | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | 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 | — | | `initialize` | 设备上电初始化登陆 | dev→srv | — |
| `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 | | `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 |
| `event_report` | 事件上报(**平台须应答**,见 §5.3 | dev→srv | — | | `event_report` | 事件上报(**平台须应答**,见 §5.3 | dev→srv | — |
| `ota_report` | OTA 进度/结果主动上报 | dev→srv | — |
| `heartbeat` | 设备心跳 | 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 未实现转换的命令 / 未识别帧透传),有线通道不使用。
--- ---
@@ -205,6 +231,8 @@ dld960/{dev_serial}/{direction}
"dev_serial": "A1B2C3D4E5F6", "dev_serial": "A1B2C3D4E5F6",
"hard_ver": "1.1", "hard_ver": "1.1",
"soft_ver": "1.1", "soft_ver": "1.1",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"model": "DLD960", "model": "DLD960",
"product_code": "960001", "product_code": "960001",
"sub_code": { "sub_code": {
@@ -224,14 +252,18 @@ dld960/{dev_serial}/{direction}
| data 字段 | 类型 | 说明 | | data 字段 | 类型 | 说明 |
|-----------|------|------| |-----------|------|------|
| `dev_serial` | string | 12 位十六进制序列码 | | `dev_serial` | string | 12 位十六进制序列码 |
| `hard_ver` | string | 硬件版本,格式 `"主.次"` | | `hard_ver` | string | 硬件版本(整机),格式 `"主.次"` |
| `soft_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 字符 | | `model` | string | 产品型号,1~10 字符 |
| `product_code` | string | 产品编码,6 位数字字符串 | | `product_code` | string | 产品编码,6 位数字字符串 |
| `sub_code.net` | bool | 网络功能是否启用 | | `sub_code.net` | bool | 网络功能是否启用 |
| `sub_code.iot` | bool | IoT/MQTT 功能是否启用 | | `sub_code.iot` | bool | IoT/MQTT 功能是否启用 |
| `bus.bus1~4` | uint8 | 各总线探头数 | | `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` ## 4.3 设置 SSC 网络配置 `ssc_net_set`
> Topic: `dld960/{sn}/srv` > Topic: `dld960/{sn}/srv`
@@ -798,6 +830,298 @@ dld960/{dev_serial}/{direction}
--- ---
## 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 设备主动上报
## 5.1 设备上电登陆信息 `initialize` ## 5.1 设备上电登陆信息 `initialize`
@@ -814,8 +1138,12 @@ dld960/{dev_serial}/{direction}
"model": "DLD960", "model": "DLD960",
"hard_ver": "1.0", "hard_ver": "1.0",
"soft_ver": "1.0", "soft_ver": "1.0",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"extra_info": { "extra_info": {
"code": "869756049404948", "code": "869756049404948",
"imei": "860012345678901",
"iccid": "89860012345678901234",
"csq": "21", "csq": "21",
"location": "113.9237976,022.6400375", "location": "113.9237976,022.6400375",
} }
@@ -827,12 +1155,18 @@ dld960/{dev_serial}/{direction}
|------|------|------| |------|------|------|
| `data.dev_serial` | string | 设备序列码 | | `data.dev_serial` | string | 设备序列码 |
| `data.model` | string | 产品型号 | | `data.model` | string | 产品型号 |
| `data.hard_ver` | string | 硬件版本 | | `data.hard_ver` | string | 硬件版本(整机) |
| `data.soft_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.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.csq` | string | 可选,当前信号强度 |
| `data.extra_info.location` | string | 可选,经纬度(经度,纬度) | | `data.extra_info.location` | string | 可选,经纬度(经度,纬度) |
> **V1.10 说明**`loop_ver`/`loop_hw_ver` 为**尽力携带**(上电后异步 0x4A 查询,initialize 发出时可能未就绪 → 回空);平台核对版本请用 `loop_version_query`(§4.25)。
## 5.2 线圈传感数据 `loop_data` ## 5.2 线圈传感数据 `loop_data`
@@ -967,6 +1301,7 @@ dld960/{dev_serial}/{direction}
| `car_leave` | 车辆离开 | 通过时间 (×50ms) | | `car_leave` | 车辆离开 | 通过时间 (×50ms) |
| `loop_cut` | 线圈断开 | 0 | | `loop_cut` | 线圈断开 | 0 |
| `loop_restore` | 线圈恢复 | 断开持续时长 (×50ms) | | `loop_restore` | 线圈恢复 | 断开持续时长 (×50ms) |
| `ota_error` | OTA 刷写失败告警(重试×3 仍失败,V1.08) | 0x1001=启动帧无响应 / 0x1002=地址帧错误 / 0x1003=数据块 ACK 超限 / 0x1004=全镜像校验失败 / 0x1005=安全窗口拒绝后强制失败 |
**平台应答(srv topic,收到后必须立即回复):** **平台应答(srv topic,收到后必须立即回复):**
@@ -1040,6 +1375,317 @@ dld960/{dev_serial}/{direction}
| `net_status` | bool | 以太网连接状态 | | `net_status` | bool | 以太网连接状态 |
| `iot_status` | bool | MQTT 连接状态 | | `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` 改为重连时重读 | 待实现 |
--- ---
# 修订记录 # 修订记录
@@ -1054,4 +1700,12 @@ dld960/{dev_serial}/{direction}
| V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq | | V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq |
| V1.06 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);§2.3 补充日志双时间戳语义(`ts_ms` 相对 + `unix_ts` 已同步,0=未同步,锚点回算规则) | wangfq | | V1.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.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 |
+13 -4
View File
@@ -324,6 +324,11 @@ typedef struct { /* 64B */
**响应:** 标准成功/失败(`code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report`/`ota_status` 为准)。 **响应:** 标准成功/失败(`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` ## 5.6 查询状态 `ota_status`
> Topic: `dld960/{sn}/srv` > Topic: `dld960/{sn}/srv`
@@ -366,11 +371,13 @@ typedef struct { /* 64B */
▲ │ ▲ │ ▲ │ ▲ │
│ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓) │ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓)
│ / flash_failed │ └──────────────┐ ▼ │ / flash_failed │ └──────────────┐ ▼
└────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ (Loop 重启) ──▶ IDLE(清槽/保留) └────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ IDLE(镜像保留, last_result=0)
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE └──失败×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` # 6 主动上报 `ota_report`
@@ -403,6 +410,8 @@ typedef struct { /* 64B */
**上报节奏**`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按块进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 msg_id/ts),平台去重窗口建议 10 分钟(与 event_report 同策略)。 **上报节奏**`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 ## 6.1 失败告警(扩展 event_report
刷写整体失败(重试 ×3 仍失败)时,设备经 `event_report` 上报(**平台必答**,复用 §5.3 ACK + 重发机制): 刷写整体失败(重试 ×3 仍失败)时,设备经 `event_report` 上报(**平台必答**,复用 §5.3 ACK + 重发机制):
@@ -485,10 +494,10 @@ typedef struct { /* 64B */
| 协议文档 | 变更 | | 协议文档 | 变更 |
|----------|------| |----------|------|
| `DLD960_IoT_MQTT协议.md` | 本设计并入后升 **V1.08**:§3 命令表 + §4.19~4.24ota_* 详情)+ §5.5ota_report+ §5.3 事件类型表补 `ota_error` + §8 错误码补 err_code 约定 + 修订记录 | | `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_* 扩展同套路) | | `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` | | `DLD960_BLE协议.md` | 不变(BLE OTA 维持流式透传现状;本地刷写状态机与 BLE 透传共用 `lup_feed_byte_ota` |
| `README.md` / `DLD960_技术规格书.md` | 协议矩阵同步 V1.08;规格书补 OTA 分区/安全约束(并入主文档时执行) | | `README.md` / `DLD960_技术规格书.md` | 协议矩阵同步 V1.10 |
**固件代码落点(协议拍板后动):** **固件代码落点(协议拍板后动):**
- `iot_mqtt_srv.c`:命令分发链加 6 个 ota_* 分支(现有 if-else 链) - `iot_mqtt_srv.c`:命令分发链加 6 个 ota_* 分支(现有 if-else 链)
+10 -6
View File
@@ -1,7 +1,7 @@
# DLD960 四通道车辆检测器 产品手册 # DLD960 四通道车辆检测器 产品手册
> Product Manual · 产品型号:**DLD960GA** > Product Manual · 产品型号:**DLD960GA**
> 文档版本 V1.02 · 2026-08-19 · 适用固件:整机 V1.02.04Loop 1.0 + DBN 1.02.04 > 文档版本 V1.04 · 2026-09-02 · 适用固件:整机 V1.02.05Loop V1.03 + DBN 1.02.05
--- ---
@@ -26,6 +26,8 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
- 以太网双协议:TCP JSON(局域网对接)+ MQTT(云平台),关键事件确认送达 - 以太网双协议:TCP JSON(局域网对接)+ MQTT(云平台),关键事件确认送达
- 通信可靠性加固:串口硬件 DMA 接收 + 固件内存优化,长时间运行稳定不丢帧、不异常重启 - 通信可靠性加固:串口硬件 DMA 接收 + 固件内存优化,长时间运行稳定不丢帧、不异常重启
- BLE OTA 升级链路加固:检测 MCU 升级响应帧(0x9F 协议)完整透传回小程序,停等 ACK 不再丢失,升级成功率提升 - BLE OTA 升级链路加固:检测 MCU 升级响应帧(0x9F 协议)完整透传回小程序,停等 ACK 不再丢失,升级成功率提升
- 网络上报携带地感版本:云平台可实时核对地感固件/硬件版本(MQTT `loop_version_query`,配合远程 OTA 升级前后版本核对)
- 联网稳定性加固:MQTT 发送退避与断连重连修复,长时间联网不掉线、不死循环
## 2. 硬件说明 ## 2. 硬件说明
@@ -48,7 +50,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
| LED_LP1~LP4 | 绿色 | 对应线圈 1~4:有车亮 / 无车灭 | | LED_LP1~LP4 | 绿色 | 对应线圈 1~4:有车亮 / 无车灭 |
| ETH_LED_G / ETH_LED_Y | 绿/黄 | 网口连接/活动指示 | | ETH_LED_G / ETH_LED_Y | 绿/黄 | 网口连接/活动指示 |
> 上电后 LED_PWR 呼吸、四路车检灯全灭(无车),约 1.3 秒完成基线建立进入检测状态。若某路车检灯常亮且现场无车,参见 §7 故障排查。 > 上电后 LED_PWR 呼吸;各通道车检灯自检慢闪,该路绿灯闪至**本路基准稳定**才停(约 2~5 s,最长 5 s 兜底),随后无车全灭进入检测状态。若某路车检灯常亮且现场无车,参见 §7 故障排查。
### 2.3 线圈施工要求(关键!) ### 2.3 线圈施工要求(关键!)
@@ -67,7 +69,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
## 3. 快速上手 ## 3. 快速上手
1. **接线**:接入线圈、继电器输出至道闸/控制器,需要联网则接网线。 1. **接线**:接入线圈、继电器输出至道闸/控制器,需要联网则接网线。
2. **上电**LED_PWR 呼吸闪烁,约 1.3 秒后进入检测状态(上电 3 秒内不主动上报数据)。 2. **上电**LED_PWR 呼吸闪烁;各通道绿灯自检慢闪至基准稳定(约 2~5 s,最长 5 s 兜底)后进入检测状态(上电 3 秒内不主动上报数据)。
3. **配置**:手机小程序经蓝牙连接设备 → 验证密码 → 设置各路灵敏度/频率/输出模式。 3. **配置**:手机小程序经蓝牙连接设备 → 验证密码 → 设置各路灵敏度/频率/输出模式。
4. **验证**:车辆压线,对应绿色车检灯亮、继电器吸合;离开后灯灭、继电器释放。 4. **验证**:车辆压线,对应绿色车检灯亮、继电器吸合;离开后灯灭、继电器释放。
5. **联网(可选)**:小程序或 TCP 命令配置 IP/MQTT 参数,接入平台。 5. **联网(可选)**:小程序或 TCP 命令配置 IP/MQTT 参数,接入平台。
@@ -132,7 +134,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
| 通信 MCU | 蓝牙 OTA | 小程序选择固件直接升级 | | 通信 MCU | 蓝牙 OTA | 小程序选择固件直接升级 |
| 检测 MCU | ISP 透传 | 小程序经蓝牙→通信 MCU 串口透传升级 | | 检测 MCU | ISP 透传 | 小程序经蓝牙→通信 MCU 串口透传升级 |
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.04 > ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.05
> >
> ⚠ **检测 MCU 升级完成后,若小程序无响应,请将设备断电重启**(DBN OTA 透传模式需重启恢复,V1.02.03 已知约束)。 > ⚠ **检测 MCU 升级完成后,若小程序无响应,请将设备断电重启**(DBN OTA 透传模式需重启恢复,V1.02.03 已知约束)。
@@ -148,7 +150,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
| 继电器输出正常但平台无数据 | 网络配置错误;MQTT Topic/序列码不符 | 查 IP/Broker 配置;核对 dev_serial 与 Topic | | 继电器输出正常但平台无数据 | 网络配置错误;MQTT Topic/序列码不符 | 查 IP/Broker 配置;核对 dev_serial 与 Topic |
| 设备频繁自动重启 / 偶发数据丢失 | 通信 MCU 旧固件资源不足(V1.02.01 已修复) | 升级整机固件至 V1.02.01 及以上版本 | | 设备频繁自动重启 / 偶发数据丢失 | 通信 MCU 旧固件资源不足(V1.02.01 已修复) | 升级整机固件至 V1.02.01 及以上版本 |
| BLE OTA 升级失败/进度卡住 | 通信 MCU 旧固件 0x9F 透传缺陷(V1.02.03 已修复) | 升级整机固件至 V1.02.03 及以上版本 | | BLE OTA 升级失败/进度卡住 | 通信 MCU 旧固件 0x9F 透传缺陷(V1.02.03 已修复) | 升级整机固件至 V1.02.03 及以上版本 |
| 配置无法保存/每次上电恢复出厂 | 旧固件 factory 写入被禁用;换新存储芯片后参数区无出厂配置(V1.02.04 已修复) | 升级整机固件至 V1.02.04 及以上版本 | | 配置无法保存/每次上电恢复出厂 | 旧固件 factory 写入被禁用;换新存储芯片后参数区无出厂配置(V1.02.04 已修复) | 升级整机固件至 V1.02.05 及以上版本 |
| 事件重复上报 | 平台未按协议应答 event_report | 平台须回显 msg_id 应答(先落库后应答) | | 事件重复上报 | 平台未按协议应答 event_report | 平台须回显 msg_id 应答(先落库后应答) |
| 线圈断开告警 | 线圈开路/引线断裂 | 万用表量线圈回路电阻(正常几欧姆以内) | | 线圈断开告警 | 线圈开路/引线断裂 | 万用表量线圈回路电阻(正常几欧姆以内) |
@@ -168,7 +170,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
| 《DLD960 技术规格书》 | 完整技术参数 | | 《DLD960 技术规格书》 | 完整技术参数 |
| 《DLD960 串口通信协议》V1.01 | 串口 对接开发 | | 《DLD960 串口通信协议》V1.01 | 串口 对接开发 |
| 《DLD960 TCP JSON 协议》V1.03 | 局域网对接开发 | | 《DLD960 TCP JSON 协议》V1.03 | 局域网对接开发 |
| 《DLD960 IoT MQTT 协议》V1.07 | 云平台对接开发 | | 《DLD960 IoT MQTT 协议》V1.10 | 云平台对接开发(含 Loop 远程 OTA 与地感版本查询) |
| 《DLD960 BLE 协议》V1.02 | 蓝牙小程序/APP 对接开发(脱机日志与传感快照读写) | | 《DLD960 BLE 协议》V1.02 | 蓝牙小程序/APP 对接开发(脱机日志与传感快照读写) |
| 《环路车辆检测器验收标准》 | 采购/部署验收 | | 《环路车辆检测器验收标准》 | 采购/部署验收 |
@@ -178,6 +180,8 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
| 版本 | 修订时间 | 修订说明 | 修订人 | | 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------| |------|----------|----------|--------|
| 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.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.01 | 2026-08-18 | 配套整机发布 V1.02.01:通信稳定性修复说明(UART2 DMA 防丢帧、异常重启修复);新增 BLE 脱机日志/传感快照读写说明(§5.1) | wangfq |
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq | | V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
+11 -8
View File
@@ -1,7 +1,7 @@
# DLD960 四通道车辆检测器 技术规格书 # DLD960 四通道车辆检测器 技术规格书
> Technical Specification · 产品型号:**DLD960GA** > Technical Specification · 产品型号:**DLD960GA**
> 文档版本 V1.02 · 2026-08-19 · 配套整机发布 V1.02.04 > 文档版本 V1.04 · 2026-09-02 · 配套整机发布 V1.02.05Loop V1.03 单侧更新)
--- ---
@@ -62,7 +62,7 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
| 基线跟踪 | 滑动平均窗口 500 样本(5s 更新周期,噪声抑制 22×) | 跟随温漂/环境慢变 | | 基线跟踪 | 滑动平均窗口 500 样本(5s 更新周期,噪声抑制 22×) | 跟随温漂/环境慢变 |
| 基线冻结保护 | 偏差超限即冻结基线更新 | 防止过车信号污染基线 | | 基线冻结保护 | 偏差超限即冻结基线更新 | 防止过车信号污染基线 |
| 冻结超时自适应 | 10 s 超时 + ±2% 稳定性窗校验 | 温漂/换线圈后自动重建基线;慢速进车不误吸收 | | 冻结超时自适应 | 10 s 超时 + ±2% 稳定性窗校验 | 温漂/换线圈后自动重建基线;慢速进车不误吸收 |
| 上电稳定期 | 128 样本(≈1.3 s)快速收敛,上电 3 s 内抑制主动上报 | 快速可用且不上报无效数据 | | 上电稳定期 | 基准稳定判定(Loop V1.03 起):连续 2 窗(100 样本/窗≈1 sOrigin 均值漂移 ≤0.1% + 最少 128 样本(≈1.3 s),硬兜底 500 样本(≈5 s);上电 3 s 内抑制主动上报 | 基准真正稳定才进入检测,避免上电误判;绿灯自检闪至本路基准稳定才停 |
| 线圈安全模式 | 可配置超时(单位 10 s,0=关闭) | 异常长时间占用强制复位重建基线 | | 线圈安全模式 | 可配置超时(单位 10 s,0=关闭) | 异常长时间占用强制复位重建基线 |
### 3.4 检测性能指标(设计目标,依据《环路车辆检测器验收标准》v1.0) ### 3.4 检测性能指标(设计目标,依据《环路车辆检测器验收标准》v1.0)
@@ -100,13 +100,13 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
| 外部存储 | SPI NOR Flash | 参数存储 + 日志存储 | | 外部存储 | SPI NOR Flash | 参数存储 + 日志存储 |
| 按键 | K1 多功能按键 | 复位等 | | 按键 | K1 多功能按键 | 复位等 |
### 5.1 协议矩阵(V1.02.04 配套) ### 5.1 协议矩阵(V1.02.05 配套)
| 协议 | 版本 | 通道 | 要点 | | 协议 | 版本 | 通道 | 要点 |
|------|------|------|------| |------|------|------|------|
| DLD960 串口通信协议 | V1.01 | TTL | 设备管理、参数配置、数据上报 | | DLD960 串口通信协议 | V1.01 | TTL | 设备管理、参数配置、数据上报 |
| DLD960 TCP JSON 协议 | V1.03 | ETH :5960 | 密码鉴权 + 18 条命令;event_report 客户端必答(5s×3 重发);脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) | | DLD960 TCP JSON 协议 | V1.03 | ETH :5960 | 密码鉴权 + 18 条命令;event_report 客户端必答(5s×3 重发);脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
| DLD960 IoT MQTT 协议 | V1.07 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) | | 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 有符号) | | 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 同语义 | | DLD960 BLE 协议 | V1.02 | 蓝牙 BLE | 帧格式 + 分包;脱机日志 OFFLOG_STAT/QUERY/CLEAR + 传感快照 SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
@@ -166,19 +166,20 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
> ⚠ **自内部协议 V1.05 起,vd960Loop 与 vd960DBN 固件必须同版本配套刷写**variation 字段 2B→3B 帧格式变更,混刷导致数据静默错位)。 > ⚠ **自内部协议 V1.05 起,vd960Loop 与 vd960DBN 固件必须同版本配套刷写**variation 字段 2B→3B 帧格式变更,混刷导致数据静默错位)。
| 组件 | V1.02.04 配套版本 | | 组件 | V1.02.05 配套版本 |
|------|-------------------| |------|-------------------|
| vd960Loop 固件 | 1.0(不变) | | vd960Loop 固件 | 1.0(不变) |
| vd960DBN 固件 | 1.02.04 | | vd960DBN 固件 | 1.02.05 |
### 10.1 已知约束(V1.02.01 ### 10.1 已知约束(V1.02.01
- 固件版本三段式:`FIRMWARE_VER="1.02.04"`MAIN=1 / SUB=2 / SUBSUB=4);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。 - 固件版本三段式:`FIRMWARE_VER="1.02.05"`MAIN=1 / SUB=2 / SUBSUB=5);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)。 - 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)。
- CH32V208 RAM 预算:BLE 协议栈固定占用低 16KB(不可裁剪),用户可用 48KB;固件经 RAM 瘦身优化,栈余量约 6KB,现场升级务必使用 V1.02.01 及以上版本。 - CH32V208 RAM 预算:BLE 协议栈固定占用低 16KB(不可裁剪),用户可用 48KB;固件经 RAM 瘦身优化,栈余量约 6KB,现场升级务必使用 V1.02.01 及以上版本。
- 传感快照区延后 3s 初始化(开机 3s 内传感数据不落盘,设计接受)。 - 传感快照区延后 3s 初始化(开机 3s 内传感数据不落盘,设计接受)。
- UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)。 - UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)。
- **OTA 透传模式无自动退出机制**:进入 OTA 态(`g_flag_counter_ota.flag=1`)后需断电重启才恢复 0x7F 正常通信(V1.02.03 已知约束,升级完成后重启设备)。 - **OTA 透传模式无自动退出机制**:进入 OTA 态(`g_flag_counter_ota.flag=1`)后需断电重启才恢复 0x7F 正常通信(V1.02.03 已知约束,升级完成后重启设备)。
- **UART2 0x4A 版本查询与 `loop_data`/事件上报共用总线**:低频命令理论无冲突,时序待板上验证(V1.02.05)。
--- ---
@@ -186,6 +187,8 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
| 版本 | 修订时间 | 修订说明 | 修订人 | | 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------| |------|----------|----------|--------|
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出;存储适配(SPI 识别去厂商代码 + factory 写入恢复) | wangfq | | 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.01 | 2026-08-18 | 配套整机发布 V1.02.01:版本矩阵更新、UART2 RX DMA 通信可靠性说明、已知约束补充 | wangfq |
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq | | V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
@@ -11,11 +11,11 @@
#include <stdint.h> #include <stdint.h>
#define PRODUCT_MODEL "DLD960GA" #define PRODUCT_MODEL "DLD960GA"
#define FIRMWARE_VER "1.02.04" #define FIRMWARE_VER "1.02.05"
#define HARDWARE_VER "1.0" #define HARDWARE_VER "1.0"
#define FIRMWARE_VER_MAIN 1 #define FIRMWARE_VER_MAIN 1
#define FIRMWARE_VER_SUB 2 #define FIRMWARE_VER_SUB 2
#define FIRMWARE_VER_SUBSUB 4 #define FIRMWARE_VER_SUBSUB 5
#define HARDWARE_VER_MAIN 1 #define HARDWARE_VER_MAIN 1
#define HARDWARE_VER_SUB 1 #define HARDWARE_VER_SUB 1
@@ -154,4 +154,16 @@ void uart_srv(void);
void UART2_SendString(uint8_t *buf, uint16_t len); void UART2_SendString(uint8_t *buf, uint16_t len);
void UART1_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_ */ #endif /* INCLUDE_CMCNG_H_ */
@@ -52,6 +52,9 @@ void iot_mqtt_poll(void); // 主循环轮询 (状态机 + 心跳 +
void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理 void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理
void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT
void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04) void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04)
void iot_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_init(void); // 初始化硬件 IWDG (无条件, 主循环卡死兜底)
void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用) void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用)
@@ -137,6 +137,17 @@ typedef struct {
char version_str[32]; // 格式化字符串 char version_str[32]; // 格式化字符串
} LUP_VersionInfo; } 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 { typedef enum {
LUP_STATE_IDLE = 0, // 空闲 LUP_STATE_IDLE = 0, // 空闲
@@ -160,6 +171,7 @@ typedef struct {
* Global State * Global State
*===========================================================================*/ *===========================================================================*/
extern LUP_CmdTracker g_lup_cmd; extern LUP_CmdTracker g_lup_cmd;
extern LoopVerCache g_lup_ver_cache; /* V1.10: 0x4A 版本缓存 */
/*=========================================================================== /*===========================================================================
* Public API * Public API
@@ -194,6 +206,10 @@ void lup_cmd_done(void);
int lup_cmd_check_timeout(void); int lup_cmd_check_timeout(void);
void lup_cmd_on_response(const uint8_t *pkg, uint16_t len); 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) --- */ /* --- High-level Commands (send + wait handled by state machine) --- */
void lup_send_get_version(void); void lup_send_get_version(void);
void lup_send_reset(void); void lup_send_reset(void);
@@ -116,6 +116,8 @@ enum {
OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */ 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_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_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, /* 日志清除 (审计) */ OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */
}; };
@@ -150,6 +152,8 @@ void offlog_evt_retry(uint32_t msg_id, uint8_t retry);
void offlog_evt_giveup(uint32_t msg_id); void offlog_evt_giveup(uint32_t msg_id);
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value); void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value);
void offlog_time_anchor(uint32_t unix_ts); 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 序号 */ uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */
uint16_t offlog_count(void); /* 有效记录数 */ uint16_t offlog_count(void); /* 有效记录数 */
@@ -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__ */
@@ -26,6 +26,7 @@
#include "offlog.h" #include "offlog.h"
#include "snapshot.h" #include "snapshot.h"
#include "fault_diag.h" #include "fault_diag.h"
#include "ota_srv.h"
#include "ch32v20x_iwdg.h" #include "ch32v20x_iwdg.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@@ -58,6 +59,10 @@ static LUP_SensorReport _cached_sr; // 最新传感器数据缓存
static uint8_t _cached_sr_valid; // 缓存有效标志 static uint8_t _cached_sr_valid; // 缓存有效标志
static uint32_t _last_publish_ms; // 上次上报时刻 (ms) static uint32_t _last_publish_ms; // 上次上报时刻 (ms)
/* 上行发布共享缓冲 (V1.09): event_report / ota_report 复用, 互斥调用
(RAM 90% : , ) */
static char _iot_pub_payload[512];
/*=========================================================================== /*===========================================================================
* event_report V1.04: + * event_report V1.04: +
* *
@@ -73,7 +78,8 @@ static uint32_t _last_publish_ms; // 上次上报时刻 (ms)
#define IOT_EVT_ACK_TIMEOUT_MS 5000 // 确认超时 (协议 V1.04) #define IOT_EVT_ACK_TIMEOUT_MS 5000 // 确认超时 (协议 V1.04)
#define IOT_EVT_MAX_RETRY 3 // 最大重发次数 (含首发共4次) #define IOT_EVT_MAX_RETRY 3 // 最大重发次数 (含首发共4次)
enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE }; enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE,
IOT_EVT_OTA_ERROR }; /* V1.08: OTA 刷写失败告警 (value=OTA_ERR_*) */
typedef struct { typedef struct {
uint8_t type; // IOT_EVT_xxx uint8_t type; // IOT_EVT_xxx
@@ -112,9 +118,11 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH]; IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH];
e->type = type; e->ch = ch; e->value = value; e->type = type; e->ch = ch; e->value = value;
_evt_count++; _evt_count++;
/* 事件日志: 线圈事件 (主循环上下文) */ /* 事件日志: 线圈事件 (主循环上下文); OTA 会话期间暂停落盘 (V1.08) */
if (!ota_silent_active()) {
offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */ offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */
} }
}
_evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报 _evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报
PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count); PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count);
} }
@@ -170,7 +178,9 @@ static void iot_sensor_ingest(const LUP_SensorReport *sr) {
iot_evt_feed(sr); // 事件沿检测 iot_evt_feed(sr); // 事件沿检测
memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照 memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照
_cached_sr_valid = 1; _cached_sr_valid = 1;
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */ if (!ota_silent_active()) {
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘; OTA 会话暂停 (V1.08) */
}
FAULT_MARKER(MK_INGEST_OUT); FAULT_MARKER(MK_INGEST_OUT);
} }
@@ -187,12 +197,18 @@ static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) {
/* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */ /* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */
static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) { static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) {
static const char *evt_names[] = {"car_enter", "car_leave", "loop_cut", "loop_restore"}; static const char *evt_names[] = {"car_enter", "car_leave", "loop_cut", "loop_restore", "ota_error"};
static char payload[512]; // static: 避免 2KB 栈溢出 char *payload = _iot_pub_payload; // V1.09: 文件级共享缓冲
int w, rem = sizeof(payload); int w, rem = sizeof(_iot_pub_payload);
char *p = payload; char *p = payload;
uint8_t i; uint8_t i;
/* OTA 会话期间静默 (V1.08): 暂停发送, 事件保留队列, 结束后补发 */
if (ota_silent_active()) {
PRINT("EVT: silent (OTA), skip publish\n");
return;
}
w = snprintf(p, rem, "{\"msg_id\":%lu,\"cmd\":\"event_report\",\"ts\":%lu," w = snprintf(p, rem, "{\"msg_id\":%lu,\"cmd\":\"event_report\",\"ts\":%lu,"
"\"data\":{\"events\":[", id, ts); "\"data\":{\"events\":[", id, ts);
if (w < 0 || w >= rem) return; if (w < 0 || w >= rem) return;
@@ -201,7 +217,7 @@ static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) {
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
const IotEvent *e = &_evt_queue[(_evt_head + i) % IOT_EVT_QUEUE_DEPTH]; const IotEvent *e = &_evt_queue[(_evt_head + i) % IOT_EVT_QUEUE_DEPTH];
w = snprintf(p, rem, "%s{\"type\":\"%s\",\"ch\":%d,\"value\":%lu}", w = snprintf(p, rem, "%s{\"type\":\"%s\",\"ch\":%d,\"value\":%lu}",
(i > 0) ? "," : "", evt_names[e->type & 0x03], e->ch, e->value); (i > 0) ? "," : "", evt_names[(e->type < 5) ? e->type : 0], e->ch, e->value);
if (w < 0 || w >= rem) return; if (w < 0 || w >= rem) return;
p += w; rem -= w; p += w; rem -= w;
} }
@@ -230,6 +246,118 @@ void iot_evt_handle_ack(uint32_t msg_id, int code) {
_evt_retry = 0; _evt_retry = 0;
} }
/* OTA 刷写失败告警入队 (V1.08): 走 event_report ACK 闭环, 平台必答
(ota_srv.c ota_flash_fail ; OTA_ERR_* ota_srv.h) */
void iot_evt_report_ota_error(uint32_t err)
{
iot_evt_enqueue(IOT_EVT_OTA_ERROR, 0, err);
PRINT("EVT: ota_error enqueue err=0x%lX\n", (unsigned long)err);
}
/*===========================================================================
* OTA ota_report V1.08 §5.5 / V1.09
*
* done/failed , ACK 3 ×5s ( msg_id/ts),
* (sn,msg_id) ( 10min) _iot_pub_payload
* iot_ota_report_result(): + ; iot_ota_report_poll():
* , 5s 3
*===========================================================================*/
#define IOT_OTA_REP_MAX_RETRY 3 /* 重发次数 (含首发共 4 次) */
#define IOT_OTA_REP_INTERVAL_MS 5000
static uint8_t _ota_rep_pend; /* 1=有未完成的 ota_report */
static uint8_t _ota_rep_ok; /* 1=done 0=failed */
static uint32_t _ota_rep_err; /* failed 的错误码 (OTA_ERR_*) */
static uint32_t _ota_rep_id; /* 固定 msg_id (重发不变) */
static uint32_t _ota_rep_ts; /* 固定 ts (重发不变) */
static uint8_t _ota_rep_times; /* 已发次数 (首发=1) */
static uint32_t _ota_rep_sent_ms; /* 上次发送时刻 */
/* V1.10: loop_version_query 异步查询 (0x4A) 状态 — 与 TCP 共用 g_lup_cmd 单命令通道 */
static uint8_t _iot_verq_pending; /* 1=有查询在途 (等 0x4A 响应回包) */
static uint32_t _iot_verq_msg_id; /* 命令 msg_id (回包回显) */
static uint32_t _iot_verq_deadline; /* 回包超时时刻 (ms), 与 TCP json_check_pending 一致 300ms */
static uint8_t _iot_ver_cache_dirty; /* 1=需后台刷新版本缓存 (上电 / OTA done 后置位) */
/* 版本缓存 → "x.y.z" 字符串 (缓存无效则空串) */
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);
}
/* 序列化并发布 ota_report (首发与重发共用: 同 id/ts 同内容) */
static void iot_ota_report_send(void)
{
char *p = _iot_pub_payload;
int w, rem = sizeof(_iot_pub_payload);
w = snprintf(p, rem,
"{\"msg_id\":%lu,\"cmd\":\"ota_report\",\"ts\":%lu,"
"\"data\":{\"target\":\"loop\",\"stage\":\"%s\","
"\"progress\":{\"sent\":%lu,\"total\":%lu},\"code\":%lu,\"msg\":\"\"}}",
_ota_rep_id, _ota_rep_ts,
_ota_rep_ok ? "done" : "failed",
(unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(),
_ota_rep_ok ? 0UL : (unsigned long)_ota_rep_err);
if (w < 0 || w >= rem) return;
mqtt_publish((char *)g_iot_topic.topic_pub, p, 1);
_ota_rep_times++;
_ota_rep_sent_ms = mstick();
PRINT("IOT: ota_report %s msg_id=%lu sent=%lu/%lu times=%u\n",
_ota_rep_ok ? "done" : "failed", _ota_rep_id,
(unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(),
_ota_rep_times);
}
/* OTA 刷写结果上报入口 (ota_srv.c ota_flash_done/fail 调用):
ok=1 stage=done, ok=0 stage=failed */
void iot_ota_report_result(uint8_t ok, uint32_t err)
{
_ota_rep_pend = 1;
_ota_rep_ok = ok ? 1 : 0;
_ota_rep_err = err;
_ota_rep_id = ++_evt_msg_id; /* 与 event_report 同源递增 (唯一性) */
_ota_rep_ts = dev_time_now();
_ota_rep_times = 0;
if (ok) _iot_ver_cache_dirty = 1; /* V1.10: 刷写成功 → Loop 跑新固件, 后台刷新版本缓存 */
iot_ota_report_send();
}
/* 主循环轮询: 5s×3 定时重发 (无 ACK, 纯推送 + 平台去重) */
static void iot_ota_report_poll(void)
{
if (!_ota_rep_pend) return;
if (g_iot_state != IOT_STATE_READY) return; /* 断网不消耗次数 */
if (_ota_rep_times > IOT_OTA_REP_MAX_RETRY) {
_ota_rep_pend = 0; /* 重发耗尽 (共 4 次) */
PRINT("IOT: ota_report retry exhausted\n");
return;
}
if (mstick() - _ota_rep_sent_ms < IOT_OTA_REP_INTERVAL_MS) return;
iot_ota_report_send();
}
/* 任一通道有车 (ota_flash 安全窗口检查用, 基于最新 0xC0 缓存) */
uint8_t iot_any_car(void)
{
uint8_t i;
if (!_cached_sr_valid) return 0;
for (i = 0; i < _cached_sr.coil_count && i < LUP_COIL_COUNT; i++) {
if (_cached_sr.coils[i].car_state) return 1;
}
return 0;
}
/* 事件发送状态机: 主循环每轮调用 (置于 READY 检查之前, 以感知重连沿) */ /* 事件发送状态机: 主循环每轮调用 (置于 READY 检查之前, 以感知重连沿) */
static void iot_evt_process(void) { static void iot_evt_process(void) {
static uint8_t _was_ready = 0; static uint8_t _was_ready = 0;
@@ -320,7 +448,17 @@ static int iot_mqtt_send(const uint8_t *buf, uint16_t len) {
uint32_t slen = len - total_sent; uint32_t slen = len - total_sent;
uint8_t ret = WCHNET_SocketSend(g_iot_socket, (uint8_t *)(buf + total_sent), &slen); uint8_t ret = WCHNET_SocketSend(g_iot_socket, (uint8_t *)(buf + total_sent), &slen);
total_sent += (uint16_t)slen; total_sent += (uint16_t)slen;
if (slen == 0 || ret != WCHNET_ERR_SUCCESS || ++retries > 10) { if (slen == 0 || ret != WCHNET_ERR_SUCCESS) {
/* 0x11 = WCHNET_ERR_MEM: 瞬时发送缓冲不足 (WCHNET_NUM_TCP_SEG=2 极小,
)200ms 退 WCHNET SEG
: WCHNET socket
(SocketCreat ISCONN , 2026-08-20 ) */
if (ret == WCHNET_ERR_MEM && retries < 10) {
uint32_t w = mstick();
while ((mstick() - w) < 200) { } /* 退避 (IWDG 4s 内安全) */
}
retries++;
if (retries > 10) {
_iot_send_fail_cnt++; _iot_send_fail_cnt++;
PRINT("IOT: SocketSend FAIL#%d ret=0x%02X sent=%u/%u\n", PRINT("IOT: SocketSend FAIL#%d ret=0x%02X sent=%u/%u\n",
_iot_send_fail_cnt, ret, total_sent, len); _iot_send_fail_cnt, ret, total_sent, len);
@@ -328,12 +466,15 @@ static int iot_mqtt_send(const uint8_t *buf, uint16_t len) {
PRINT("IOT: SocketSend 3 consecutive failures, forcing reconnect\n"); PRINT("IOT: SocketSend 3 consecutive failures, forcing reconnect\n");
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL); WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
g_iot_socket = 0xFF; g_iot_socket = 0xFF;
SocketId_TCP = 0xFF; /* 防重连 SocketCreat 沿用旧 id (2026-08-20) */
g_iot_state = IOT_STATE_DISCONNECTED; g_iot_state = IOT_STATE_DISCONNECTED;
_iot_send_fail_cnt = 0; _iot_send_fail_cnt = 0;
_iot_reconnect_deadline = mstick() + 3000; // 3s 后重连, 让 WCHNET 清旧 socket _iot_reconnect_deadline = mstick() + 3000; // 3s 后重连, 让 WCHNET 清旧 socket
} }
return -1; return -1;
} }
continue; /* 重试 (MEM 已退避 200ms, 其他错误立即重试) */
}
} }
_iot_send_fail_cnt = 0; // 成功一次就清零 _iot_send_fail_cnt = 0; // 成功一次就清零
return 0; return 0;
@@ -418,6 +559,12 @@ static int iot_mqtt_publish(const char *topic, const char *payload,
/* 处理一条收到的 MQTT PUBLISH 消息 */ /* 处理一条收到的 MQTT PUBLISH 消息 */
static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_len) { static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_len) {
static char json[IOT_MQTT_RECV_BUF_LEN]; // static: 避免 1KB 栈开销 static char json[IOT_MQTT_RECV_BUF_LEN]; // static: 避免 1KB 栈开销
/* OTA 命令 IO 缓冲 (V1.08): 各分支响应组包 + ota_data hex 提取共用
(RAM 90% : 6 static ~2.5KB , union ~2KB) */
static union {
char resp[512]; /* ota_* 命令响应 JSON */
char hexbuf[512 + 1]; /* ota_data hex 字符串 (512 hex + NUL) */
} _ota_io;
int copy_len = payload_len < (int)sizeof(json) - 1 ? payload_len : (int)sizeof(json) - 1; int copy_len = payload_len < (int)sizeof(json) - 1 ? payload_len : (int)sizeof(json) - 1;
memcpy(json, payload, copy_len); memcpy(json, payload, copy_len);
json[copy_len] = '\0'; json[copy_len] = '\0';
@@ -454,17 +601,24 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
/* 目前仅实现基本响应框架, 后续逐步对接 Loop MCU 命令 */ /* 目前仅实现基本响应框架, 后续逐步对接 Loop MCU 命令 */
if (strcmp(cmd_str, "dev_info_query") == 0) { if (strcmp(cmd_str, "dev_info_query") == 0) {
/* V1.10: data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 查询未完成/失败为空串) */
char loop_ver_str[16], loop_hw_ver_str[16];
char data_json[512]; char data_json[512];
iot_loop_ver_str(loop_ver_str, sizeof(loop_ver_str));
iot_loop_hw_ver_str(loop_hw_ver_str, sizeof(loop_hw_ver_str));
snprintf(data_json, sizeof(data_json), snprintf(data_json, sizeof(data_json),
"{\"msg_id\":%lu,\"cmd\":\"dev_info_query\"," "{\"msg_id\":%lu,\"cmd\":\"dev_info_query\","
"\"ts\":%lu,\"code\":0,\"msg\":\"success\"," "\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{" "\"data\":{"
"\"dev_serial\":\"%s\",\"hard_ver\":\"%s\",\"soft_ver\":\"%s\"," "\"dev_serial\":\"%s\",\"hard_ver\":\"%s\",\"soft_ver\":\"%s\","
"\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\","
"\"model\":\"%s\",\"product_code\":\"960001\"," "\"model\":\"%s\",\"product_code\":\"960001\","
"\"sub_code\":{\"net\":%s,\"iot\":%s}," "\"sub_code\":{\"net\":%s,\"iot\":%s},"
"\"bus\":{\"bus1\":0,\"bus2\":0,\"bus3\":0,\"bus4\":0}}}", "\"bus\":{\"bus1\":0,\"bus2\":0,\"bus3\":0,\"bus4\":0}}}",
msg_id, dev_time_now(), msg_id, dev_time_now(),
g_dev_number_str, HARDWARE_VER, FIRMWARE_VER, PRODUCT_MODEL, g_dev_number_str, HARDWARE_VER, FIRMWARE_VER,
loop_ver_str, loop_hw_ver_str,
PRODUCT_MODEL,
g_sub_code_enable.net_enable ? "true" : "false", g_sub_code_enable.net_enable ? "true" : "false",
g_sub_code_enable.iot_enable ? "true" : "false"); g_sub_code_enable.iot_enable ? "true" : "false");
iot_mqtt_publish(resp_topic, data_json, strlen(data_json), 1); iot_mqtt_publish(resp_topic, data_json, strlen(data_json), 1);
@@ -677,6 +831,186 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
msg_id, dev_time_now()); msg_id, dev_time_now());
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1); iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} else if (strcmp(cmd_str, "ota_begin") == 0) {
/* V1.08: 开启 OTA 会话 / 断点续传定位 (Loop MCU 远程 OTA, 先存后刷) */
uint32_t size = 0, crc32 = 0;
char version[16] = {0};
uint8_t force = 0;
int rc;
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"size\"", tmp);
if (strlen(tmp) > 0) size = (uint32_t)strtoul(tmp, NULL, 10);
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"crc32\"", tmp);
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"version\"", tmp);
strncpy(version, tmp, sizeof(version) - 1);
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"force\"", tmp);
if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0);
rc = ota_cmd_begin(size, crc32, version, force);
if (rc == 0) {
const OtaMeta *m = ota_meta();
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"target\":\"loop\",\"slot\":\"%s\",\"offset\":%lu,\"received\":%lu,"
"\"size\":%lu,\"crc32\":%lu,\"state\":\"%s\"}}",
msg_id, dev_time_now(),
(m->slot == 0) ? "a" : "b",
(unsigned long)m->received, (unsigned long)m->received,
(unsigned long)m->size, (unsigned long)m->crc32,
ota_state_str((uint8_t)m->state));
} else {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\","
"\"data\":{\"target\":\"loop\",\"err_code\":%d}}",
msg_id, dev_time_now(), (rc == 3) ? 2 : 4);
}
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
PRINT("IOT: ota_begin size=%lu rc=%d\n", (unsigned long)size, rc);
} else if (strcmp(cmd_str, "ota_data") == 0) {
/* V1.08: 分片下发 (256B/片, 单片 CRC32; 重复片幂等, 乱序拒收指示续传) */
uint32_t offset = 0, crc32 = 0;
int rc;
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"offset\"", tmp);
if (strlen(tmp) > 0) offset = (uint32_t)strtoul(tmp, NULL, 10);
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"crc32\"", tmp);
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
/* hex 值提取: ota_json_extract_hex 兼容 "data":"..." 与 "data": "..." (json.dumps 带空格) */
ota_json_extract_hex(json, _ota_io.hexbuf, (int)sizeof(_ota_io.hexbuf));
rc = ota_cmd_data(offset, crc32, _ota_io.hexbuf);
if (rc == 0) {
const OtaMeta *m = ota_meta();
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"offset\":%lu,\"received\":%lu}}",
msg_id, dev_time_now(), (unsigned long)offset, (unsigned long)m->received);
} else if (rc == 1) {
const OtaMeta *m = ota_meta();
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":1,\"msg\":\"crc/gap error\","
"\"data\":{\"err_code\":1,\"offset\":%lu,\"received\":%lu}}",
msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received);
} else {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
"\"data\":{\"err_code\":2}}",
msg_id, dev_time_now());
}
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
PRINT("IOT: ota_data offset=%lu rc=%d\n", (unsigned long)offset, rc);
} else if (strcmp(cmd_str, "ota_end") == 0) {
/* V1.08: 结束下载, 全镜像 CRC32 复核 */
uint32_t crc32 = 0;
int rc;
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"crc32\"", tmp);
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
rc = ota_cmd_end(crc32);
if (rc == 0) {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"crc_ok\":true}}",
msg_id, dev_time_now());
} else if (rc == 5) {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":5,\"msg\":\"crc mismatch\","
"\"data\":{\"err_code\":5,\"crc_ok\":false}}",
msg_id, dev_time_now());
} else if (rc == 3) {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
"\"data\":{\"err_code\":2}}",
msg_id, dev_time_now());
} else {
const OtaMeta *m = ota_meta();
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":1,\"msg\":\"incomplete\","
"\"data\":{\"offset\":%lu,\"received\":%lu}}",
msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received);
}
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
PRINT("IOT: ota_end rc=%d\n", rc);
} else if (strcmp(cmd_str, "ota_abort") == 0) {
/* V1.08: 中止会话, 释放暂存 */
int rc = ota_cmd_abort();
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_abort\",\"ts\":%lu,\"code\":%d,\"msg\":\"%s\"}",
msg_id, dev_time_now(), (rc == 0) ? 0 : 1, (rc == 0) ? "success" : "internal error");
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
PRINT("IOT: ota_abort rc=%d\n", rc);
} else if (strcmp(cmd_str, "ota_flash") == 0) {
/* V1.08: 触发本地 ISP 刷写 (同步安全检查 → 异步执行) */
char slot_buf[8] = {0};
uint8_t slot = 0, force = 0;
int rc;
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"slot\"", tmp);
strncpy(slot_buf, tmp, sizeof(slot_buf) - 1);
if (strcmp(slot_buf, "b") == 0) slot = 1;
memset(tmp, 0, sizeof(tmp));
simple_parse_json(json, "\"force\"", tmp);
if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0);
rc = ota_cmd_flash(slot, force);
if (rc == 0) {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":0,\"msg\":\"success, flashing async\"}",
msg_id, dev_time_now());
} else if (rc == 3) {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
"\"data\":{\"err_code\":3}}",
msg_id, dev_time_now());
} else {
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\"}",
msg_id, dev_time_now());
}
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
PRINT("IOT: ota_flash slot=%d force=%d rc=%d\n", slot, force, rc);
} else if (strcmp(cmd_str, "ota_status") == 0) {
/* V1.08: 查询 OTA 状态 (含刷写进度) */
const OtaMeta *m = ota_meta();
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"ota_status\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
"\"data\":{\"target\":\"loop\",\"state\":\"%s\",\"slot\":\"%s\",\"size\":%lu,"
"\"received\":%lu,\"crc32\":%lu,\"version\":\"%s\","
"\"progress\":{\"sent\":%lu,\"total\":%lu},\"last_result\":%lu,\"last_error\":%lu}}",
msg_id, dev_time_now(),
ota_state_str((uint8_t)m->state), (m->slot == 0) ? "a" : "b",
(unsigned long)m->size, (unsigned long)m->received, (unsigned long)m->crc32,
m->version,
(unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(),
(unsigned long)m->last_result, (unsigned long)m->last_result);
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
PRINT("IOT: ota_status state=%s\n", ota_state_str((uint8_t)m->state));
} else if (strcmp(cmd_str, "loop_version_query") == 0) {
/* V1.10 §4.25: 实时查询地感 Loop 版本 — 0x4A 异步, 响应在 iot_verq_poll() 回包
( TCP json_check_pending : msg_id, g_lup_cmd ) */
if (_iot_verq_pending) {
/* 已有查询在途, 拒绝重复查询 */
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,"
"\"code\":3,\"msg\":\"busy\"}",
msg_id, dev_time_now());
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
} else {
lup_send_get_version();
_iot_verq_pending = 1;
_iot_verq_msg_id = msg_id;
_iot_verq_deadline = mstick() + 300; /* 与 TCP json_check_pending 一致 */
PRINT("IOT: loop_version_query sent (async)\n");
}
} else if (strcmp(cmd_str, "ssc_net_query") == 0) { } else if (strcmp(cmd_str, "ssc_net_query") == 0) {
/* SSC 网络配置查询 (只读全局, 协议 V1.02) */ /* SSC 网络配置查询 (只读全局, 协议 V1.02) */
char resp[400]; char resp[400];
@@ -740,15 +1074,21 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
/* 发送 initialize 上线消息 (IoT 路径) */ /* 发送 initialize 上线消息 (IoT 路径) */
static void iot_send_initialize(void) { static void iot_send_initialize(void) {
/* V1.10: data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 尽力携带可为空) */
char loop_ver_str[16], loop_hw_ver_str[16];
char payload[512]; char payload[512];
char topic[IOT_MQTT_TOPIC_MAX_LEN]; char topic[IOT_MQTT_TOPIC_MAX_LEN];
iot_loop_ver_str(loop_ver_str, sizeof(loop_ver_str));
iot_loop_hw_ver_str(loop_hw_ver_str, sizeof(loop_hw_ver_str));
snprintf(payload, sizeof(payload), snprintf(payload, sizeof(payload),
"{\"msg_id\":%lu,\"cmd\":\"initialize\",\"ts\":%lu," "{\"msg_id\":%lu,\"cmd\":\"initialize\",\"ts\":%lu,"
"\"data\":{\"dev_serial\":\"%s\",\"model\":\"%s\"," "\"data\":{\"dev_serial\":\"%s\",\"model\":\"%s\","
"\"hard_ver\":\"%s\",\"soft_ver\":\"%s\"," "\"hard_ver\":\"%s\",\"soft_ver\":\"%s\","
"\"extra_info\":{\"code\":\"0\",\"csq\":\"0\",\"location\":\"\"}}}", "\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\","
"\"extra_info\":{\"code\":\"0\",\"imei\":\"\",\"iccid\":\"\",\"csq\":\"0\",\"location\":\"\"}}}",
(unsigned long)(g_iot_msg_id + 1), dev_time_now(), (unsigned long)(g_iot_msg_id + 1), dev_time_now(),
g_iot_dev_serial, PRODUCT_MODEL, HARDWARE_VER, FIRMWARE_VER); g_iot_dev_serial, PRODUCT_MODEL, HARDWARE_VER, FIRMWARE_VER,
loop_ver_str, loop_hw_ver_str);
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial); snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
iot_mqtt_publish(topic, payload, strlen(payload), 0); iot_mqtt_publish(topic, payload, strlen(payload), 0);
} }
@@ -899,6 +1239,9 @@ void iot_mqtt_publish_sensor(void) {
/*--- 事件上报状态机: 发送/重发/重连补报 ---*/ /*--- 事件上报状态机: 发送/重发/重连补报 ---*/
iot_evt_process(); iot_evt_process();
/*--- OTA 结果上报重发 (V1.09): done/failed 5s×3, 同 msg_id/ts ---*/
iot_ota_report_poll();
if (g_iot_state != IOT_STATE_READY) return; if (g_iot_state != IOT_STATE_READY) return;
if (!g_report_cfg.enable) return; if (!g_report_cfg.enable) return;
@@ -1105,9 +1448,97 @@ void iot_watchdog_kick(void) {
/*=========================================================================== /*===========================================================================
* *
*===========================================================================*/ *===========================================================================*/
/* V1.10: Loop 版本查询回包 (loop_version_query 异步) + 版本缓存后台刷新
TCP g_lup_cmd (, ) */
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(); /* 清残留超时态 (后台查询失败或被其它命令覆盖) */
}
}
}
void iot_mqtt_poll(void) { void iot_mqtt_poll(void) {
iot_watchdog_kick(); // 喂硬件 IWDG iot_watchdog_kick(); // 喂硬件 IWDG
/* V1.10: Loop 版本查询回包 + 缓存后台刷新 (放最前: 断连提前 return 前也要推进状态机) */
iot_verq_poll();
/* 事件日志: MQTT 状态沿检测 (主循环上下文, 不在 socket 中断里写 SPI) /* 事件日志: MQTT 状态沿检测 (主循环上下文, 不在 socket 中断里写 SPI)
BOOT + boot_seq ; */ BOOT + boot_seq ; */
if (g_iot_state != _prev_iot_state) { if (g_iot_state != _prev_iot_state) {
@@ -1194,6 +1625,9 @@ void iot_mqtt_init(void) {
_iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接 _iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接
g_iot_state = IOT_STATE_DISCONNECTED; g_iot_state = IOT_STATE_DISCONNECTED;
/* V1.10: 上电后台查询一次 Loop 版本 (0x4A), 缓存供 dev_info_query/initialize */
_iot_ver_cache_dirty = 1;
/* 初始化硬件看门狗 IWDG (LSI≈40kHz, 4s 超时, 主循环喂狗) */ /* 初始化硬件看门狗 IWDG (LSI≈40kHz, 4s 超时, 主循环喂狗) */
iot_watchdog_init(); iot_watchdog_init();
} }
@@ -509,6 +509,31 @@ void lup_send_get_version(void)
lup_cmd_begin(LUP_CMD_GET_VERSION, 200); 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) void lup_send_reset(void)
{ {
uint8_t buf[LUP_MAX_PKG_LEN]; uint8_t buf[LUP_MAX_PKG_LEN];
@@ -122,7 +122,7 @@ uint32_t dev_time_now(void)
void mStopIfError(u8 iError) void mStopIfError(u8 iError)
{ {
if (iError == WCHNET_ERR_SUCCESS) return; 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) void mqtt_connect(void)
@@ -346,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); 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); mStopIfError(i);
i = WCHNET_SocketConnect(SocketId_TCP); //make a TCP connection i = WCHNET_SocketConnect(SocketId_TCP); //make a TCP connection
if(i == WCHNET_ERR_SUCCESS) if(i == WCHNET_ERR_SUCCESS)
@@ -342,6 +342,28 @@ void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value)
offlog_evt(OFFLOG_EVT_COIL, p, 6); 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) void offlog_time_anchor(uint32_t unix_ts)
{ {
offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */ offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */
@@ -398,6 +420,8 @@ const char *offlog_type_str(uint8_t type)
case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup"; case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup";
case OFFLOG_EVT_COIL: return "coil"; case OFFLOG_EVT_COIL: return "coil";
case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor"; 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"; case OFFLOG_EVT_LOG_CLEAR: return "log_clear";
default: return "unknown"; default: return "unknown";
} }
@@ -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;
}
}
@@ -27,6 +27,7 @@
#include "tcp_json_srv.h" #include "tcp_json_srv.h"
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "iot_mqtt_srv.h" #include "iot_mqtt_srv.h"
#include "ota_srv.h"
#include "fault_diag.h" #include "fault_diag.h"
/********************************************************************* /*********************************************************************
@@ -274,6 +275,8 @@ void Main_Circulation(void)
FAULT_MARKER(MK_UART_SRV_IN); FAULT_MARKER(MK_UART_SRV_IN);
uart_srv(); uart_srv();
FAULT_MARKER(MK_UART_SRV_OUT); 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 模式不落盘) */ snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */
FAULT_MARKER(MK_POLL_BLE_IN); FAULT_MARKER(MK_POLL_BLE_IN);
poll_dbn_ble(); poll_dbn_ble();
@@ -328,6 +331,8 @@ int main(void)
PRINT("INIT: storage ok\n"); PRINT("INIT: storage ok\n");
offlog_init(); offlog_init();
PRINT("INIT: offlog ok\n"); PRINT("INIT: offlog ok\n");
ota_init(); /* OTA 元数据恢复 (V1.08): 下载中断续传 / 刷写中断标记失败 */
PRINT("INIT: ota ok\n");
/* 快照区: 延后初始化 (2026-08-17) — snap_init 移到主循环 3s 后 /* 快照区: 延后初始化 (2026-08-17) — snap_init 移到主循环 3s 后
(snap_delayed_init), SPI (+ ~80ms); (snap_delayed_init), SPI (+ ~80ms);
3s snap_enqueue/snap_flush !_ready */ 3s snap_enqueue/snap_flush !_ready */
@@ -8,9 +8,12 @@
#include "config.h" #include "config.h"
#include "cmcng.h" #include "cmcng.h"
#include "debug.h" /* DEBUG 宏: 4G 版本以 -DDEBUG=0 编译, 此时 printf 不占用 USART1 */
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "dbn_ble_srv.h" /* MAGIC_BYTE_DBN_DEFAULT(0x8F): DBN 私有指令魔数 */
#include <string.h> #include <string.h>
#include "dbn_ble_srv.h" #include "dbn_ble_srv.h"
#include "ota_srv.h"
void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast"))); void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
@@ -112,6 +115,245 @@ void uart2_dma_poll(void)
} }
} }
/*==================== 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){ void uart_init(void){
GPIO_InitTypeDef GPIO_InitStructure = {0}; GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0}; USART_InitTypeDef USART_InitStructure = {0};
@@ -151,6 +393,7 @@ void uart_init(void){
USART_Cmd(USART2, ENABLE); USART_Cmd(USART2, ENABLE);
uart2_dma_init(); /* DMA 循环接收接管 (2026-08-17) */ 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_Pin = GPIO_Pin_0;
@@ -246,6 +489,14 @@ void uart_srv(void)
// 其他: 校验后匹配挂起命令 // 其他: 校验后匹配挂起命令
lup_process_frame(g_pkg_uart_2.pkg, g_pkg_uart_2.offset); 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) 分流 --- // --- 传感器上报 (0xC0) 分流 ---
// 回调已处理 TCP 推送,此处仅处理 BLE 转发 // 回调已处理 TCP 推送,此处仅处理 BLE 转发
if(cmd == LUP_CMD_SENSOR_REPORT) if(cmd == LUP_CMD_SENSOR_REPORT)
@@ -291,10 +542,14 @@ void uart_srv(void)
} }
} }
else{ else{
/* OTA 模式 (2026-08-19): Loop bootloader 回的 0x9F /* OTA 模式: Loop bootloader 回的 0x9F ACK 帧 (pre_ok/addr_ok/data ACK) 分发:
(back_9F_pre_ok / addr_ok / data ACK) BLE , - OTA (g_ota_flash_active, V1.08) ota_srv
- BLE (, V1.02.03) BLE
(0xA7 WITH_BACK) ACK */ (0xA7 WITH_BACK) ACK */
if(g_flag_bt_state){ 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); 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 结束后被误处理 */ /* OTA 模式同样清 flag — 残留帧会在 OTA 结束后被误处理 */
+508
View File
@@ -4,6 +4,513 @@
> >
> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 > 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接
## 2026-09-10 — BLE 分包机制核实 + net 229B 载荷可达性修正(§6.9 前置改造项)
> 触发:用户 2026-09-10 两问 ——「net 的 229B 是怎么计算出来的?」「我记得蓝牙小程序有分包机制,看下是不是」。
> **性质:勘察 + 归档,本条目对 `vd960DBN` 源码零改动**;结论用于修订协议 §6.9 的落地前置条件。
### 1. BLE 分包机制 —— 确认存在,收发双向对称
分包头复用 **`pkg[1]`** 字段(原地是 Addr),把总包数与序号压进高/低 4 位:
| 位段 | 含义 |
|------|------|
| `pkg[1] >> 4` | 总包数 `pkg_amount` |
| `pkg[1] % 0x10` | 当前包序号 `pkg_seq` |
**接收**`dbn_ble_srv.c:466-504``unpack_packs()` 内):
- `high == 0` → 单包(不分包),整包直取 `_len - 1` 字节;
- `low == 1` → 第一包,重置缓冲与偏移;
- `low > 1` → 后续包,校验 `amount`/`cmd` 一致 + `seq` 连续,`dat_len += (_len - 1)``memcpy(&dat[_offset], ...)`
- `high == low` → 收包完成,进入命令处理。
**发送**`dbn_ble_srv.c:420-421 / 436-437`):`_pkg_seq += 1`(**先自增再编码**,故线上第一包即 `seq == 1`)→ `buf[1] = (pkg_amount << 4) | pkg_seq`;单包时 `buf[1] = 0``dat_offset += _remain_len` 推进;发完 `pkg_amount == pkg_seq` 清缓冲。
→ **与接收侧 `low == 1` 起点、`high == low` 收尾完全配对,设计自洽。**
**每包 dat 上限随 MTU 协商**`ble_notify_chunk_max()``_limit = peripheral_get_mtu() - 9`,再被 `MAX_BLE_Notify_Buf_LEN - 6` 夹住;`BLE_BUFF_MAX_LEN = 100` @ `BLE/HAL/include/config.h:115`):
| MTU | 每包 dat | 4 位总包数上限(≤15 包) |
|-----|----------|--------------------------|
| 23(默认) | 14 B | 210 B |
| 96 | 87 B | 1305 B |
| **≥103(现用)** | **94 B** | **1410 B** |
> `dbn_ble_srv.h:81``//24` 注释是 `BLE_BUFF_MAX_LEN` 的**过期旧值**(原 24,现 100),不可据此推算分块。
### 2. 关键修正:229B **真的可达** ⇒ 越界**真的可达**
上一轮结论「`dat[132]` 装不下 229B,实际硬上限 131B,现场跑不到」**是错的** —— 分包让 `dat_len` 跨包累加,229B = 3 包 × ≤94B **可以送达**
| 方向 | 代码事实 | 后果 |
|------|----------|------|
| 发送 | `set_response_iot_net()``dat[132]` 写最多 229B`i` 全程无钳制 | **越界写**(同文件 `set_response_buf()` 有守卫,这两个没有) |
| 接收 | `dat_len += (_len - 1)``memcpy(&dat[_offset], ...)` 全程无钳制 | 多包累加即越界 |
| 通用 | `dat_len` / `_offset` / `_len` / `i` 均为 `uint8_t` | >255 回绕,解析循环行为错乱 |
定性由「理论上限、跑不到」升级为「**可达缺陷**」。**先撞的永远是 132 那道墙**(`pkg_amount` 4 位 ⇒ 15 包 = 1410B 的上限在更远处)。
### 3. 自我纠错(两处)
1. ❌「实际硬上限 131B」→ ✅ 131B 只是**缓冲区**上限;分包使更长载荷**可达**,越界非纸面。
2. ❌ 曾疑「收方要求 `low == 1` 起、发方 `pkg_seq = 0` 起,序号基准不一致」→ ✅ 发送侧**先自增再编码**,线上第一包即 `seq = 1`,**配对正确,疑点作废**(不该写进文档)。
### 4. §6.9 落地前置改造项(新增,实现前必做)
| # | 项 | 依据 |
|---|-----|------|
| 1 | `g_buf_ble_response.dat` / `tmp_ble_buf` 由 132 B 扩至 **≥260 B** | 229B 载荷可达 ⇒ 现缓冲必越界(132B 当初为 offlog QUERY 129B 所定) |
| 2 | `set_response_iot_net()` / `set_response_iot_topic()` 补长度钳制 | 照抄 `dbn_ble_srv.c:288` `set_response_buf()` 的守卫 |
| 3 | `unpack_packs()` 接收侧累加加钳制 | `dat_len + (_len - 1) > MAX_BLE_DAT_BUF_LEN` 即丢包清缓冲,防 `uint8_t` 回绕 |
| 4 | `unpack_packs()` 用起 `len` 形参 | 该形参在 263 行函数体里从未被引用,memcpy 长度全取自 `pkg[2]` |
改动项 1 需**两侧(DBN 与蓝牙小程序/MRS 工程)同步**,漏改一边即越界踩 `g_notify_buftemp` 一类相邻全局。
### 5. 待现场验证
- **小程序侧源码不在本机**`find` 无命中)→ 其分包实现的**末包判定**与**是否同样按 MTU 协商 94 B 切包**无法核对,需上板抓包。
- 现场是否已出现「host 写长了 MQTT 参数就乱」:按门槛(端口 4 位时 `host + clientid + username + password` 字符数 > 124 即越界)**应已偶发,只是未归因到缓冲区**。
### 6. 文件改动(本条目)
| 文件 | 改动 |
|------|------|
| `vd960DBN` 源码 | **零改动**`TaskLoop.c` 等一律未动) |
| `docs/DLD960_IoT_MQTT协议.md` | §6.9.4.3 补分包承载说明;§6.9.9 加 4 条前置改造项;修订记录加 V1.15 |
| skill `payload-sizing-and-buffer-audit.md` | 修正 §0/§3 定性、新增「BLE 分包机制」节、§5 补第 4 项 |
| skill `dbn-air780-config-sync.md` | 补分包与缓冲边界指针 |
## 2026-09-10 — 4G 配置同步协议定稿(《DLD960_IoT_MQTT协议》§6.90x7D 作废)
> 用户需求 2026-09-10:蓝牙小程序读写网络配置指令,哪些需要同步/转发给 4G(服务器域名/IP、端口、MQTT 账号密码、主题等)。
> **关闭**V1.12 条目"vd960DBN 侧开发计划"第 3 条(BLE 设置服务器/topic 时同步下发 Air780 配置);Air780 readme §2.2 / §3 的 0x7D 帧设计稿。
### 结论(协议 §6.9 定稿)
**权威源规则**:配置权威源 = **DBN 侧 flash**`IOT_NET_INFO` / `IOT_Topic``cfig_flash.c` 持久化)。
Air780 侧 fskv **仅作启动缓存**,唯一合法写入者 = **收到的 DBN 配置帧**;平台经 4G 下发配置一律拒绝
(§6.6 回 `code=4`)——**BLE 是网络配置的唯一入口**,故同步不是可选优化,是 4G 通道能否工作的前提。
**载体**:复用 **0x8F 本机侧私有帧**(与 BLE 侧 `MAGIC_BYTE_DBN_DEFAULT` 同值)。语义自洽:
`0x7F` = 业务帧(转发 UART2/Loop),`0x8F` = 链路两端本机侧私有(各自本地消费,不转发)。
| 项 | 定稿 |
|----|------|
| 命令 | 复用 BLE 侧既有命令码:`0x14`(GET_IOT_NET) / `0x16`(GET_IOT_TOPIC),双向对称(请求 DATA 空,应答带载荷) |
| 载荷 | 复用 DBN 侧 `set_response_iot_net()` / `set_response_iot_topic()` 构造器 → **与 BLE 读响应逐字节同源**,零新增序列化代码 |
| 拆分 | net / topic **两条独立帧 + 两条 fskv 记录**net 最坏 229B、topic 129B,均 < fskv 单值 255B 上限) |
| 交互 | **拉取为主**Air780 上电先读 fskv 立即连,链路建立后拉权威值校准)+ **推送为辅**(BLE 写成功后链路可用则立即推) |
| 状态 | DBN **不维护"待同步"标志位**:推送时链路不可用则等 Air780 下次拉取(无状态,天然自愈) |
| 同步项 | 核心 8 项:`remote_addr` / `mqtt_port` / `client_id` / `username` / `password` / `clientid_enable` / `topic_pub` / `topic_sub` |
| 边界项 | `UPDATE_DEV_SERIAL`(0x09) 必同步(Topic 族与 JSON `sn` 均由序列号派生);`iot_enable`sub_code 0x22)必同步;`RW_UART_BAUD` **禁止在线改 UART1** |
| 同步载荷不含 | `IOT_NET_INFO.mode`:实测**无 BLE 写入路径**、恒为 `0``IOT_Addr_IP_Mode`),仅 DBN 有线 MQTT 读取;Air780 侧按主机串自行判别(LuatOS `socket.connect` 对 IP/域名均兼容) |
### 勘察中发现的硬约束与隐患(均为代码事实)
| # | 发现 | 处置 |
|---|------|------|
| 1 | **`usart_biz.c` 帧缓冲 `uart1_rx_frame[LUP_MAX_PKG_LEN]` = 70B** → 单帧载荷上限 64B**装不下 net 最坏 229B**,会被按 "LEN 非法" 丢弃 | `0x8F` 帧长上限**独立放宽到 260B**LEN 1B ≤255);`0x7F` 业务帧维持 70B 纪律。**两侧帧缓冲需同步扩容** |
| 2 | `iot_net_info` **是两条传输通道共享**的:`peripheral_main.c:248` 用它走 **DBN 自身 WCHNET 有线 MQTT**`WCHNET_CreateTcpMqttSocket()`) | 更须保证 DBN 为唯一权威源 |
| 3 | 该处 `else //DNS` **空分支** → 配置成域名时**有线通道 MQTT 不工作**(4G 不影响) | 已知缺口,是否补齐由产品决定(协议 §6.9.8 已标注) |
| 4 | `CMD_DBN_RW_UART_BAUD` 可改**任意串口**波特率,含 UART1 这条 4G 链路 → 在线改而两侧不同步 = 永久失联,只能拆机重刷 | 建议固件侧对 `uart_num == 1` **直接拒绝** |
| 5 | Air780 侧 `mqtt_main.lua` 在 require 期把配置读进 `local`,**运行时改 CFG 不生效** | 须改造为**重连时重读** + 断开重连重订阅 |
| 6 | Air780 **已支持 fskv 持久化**`components/fskv/`littlefs**掉电不丢**key ≤64B / 单值 ≤255B,见官方 `demo/fskv`);原设计稿 §2.2 即已指向 fskv | 用作启动缓存;**仅在值变化时写入**(littlefs 擦写磨损) |
### 文件改动(文档口径)
| 文件 | 改动 |
|------|------|
| `docs/DLD960_IoT_MQTT协议.md` | **+§6.9「4G 配置同步」**(9 个子节:背景与权威源 / 帧格式 / 命令 / 载荷 / 交互流程 / 其他触发命令 / 明确不同步项 / 异常处理 / 实施状态);§6.6 增补充说明;版本 V1.13→**V1.14**;修订记录加行 |
| `docs/DLD960_IoT_MQTT协议.md` | 行尾核查:全文**纯 LF**(非 CRLF),改动按 LF 落盘,零行尾污染 |
| vd960Air `readme.md` | §2.2 重写为权威源 + 拉取/推送设计;**§3「0x7D 帧设计稿 v0.2」整节作废**,改写为「0x7F 业务帧 + 0x8F 本机侧私有帧」定稿;3.1 对比表改成 0x7F vs 0x8F;待确认项/开发计划 P1 同步更新 |
| vd960Air `uart_app.lua` / `config.lua` | 注释中的 0x7D 计划改为 0x8F 配置同步 |
**历史记录不动**devlog V1.11/V1.12 条目、协议 V1.11 修订行中的 0x7D 描述均保留(记录彼时决定)。
### 待办(协议已定稿,两侧待实现)
- [ ] DBN0x8F 配置请求**应答**(读 flash → 组帧)+ BLE 写成功后**主动推送**
- [ ] DBN`uart1_rx_frame` 扩容至 260B(含 TX 侧缓冲)
- [ ] DBN`RW_UART_BAUD` 拒绝 `uart_num == 1`
- [ ] Air780`frame_parser.lua``0x8F` 分支(本地消费,不转发)
- [ ] Air780fskv 缓存 + 上电即连 + 拉取校准 + 变化重连;配置读取改为重连时重读
- [ ] Air780`config.lua` 明文凭据处置(改占位符 + 非跟踪文件读取;⚠ 已在 git 历史中,商用前须换号)
## 2026-09-10 — vd960DBN UART1 ↔ Air780 4G 通道实现(协议 §6.1 纯透传)
> 用户需求 2026-09-10:把 vd960DBN 与 vd960Air(Air780) 两侧的串口对接逻辑做出来 —— DBN 侧 UART1(PB6/PB7) 通道 + UART2↔UART1 双向透传。
> **关闭**V1.12 条目"vd960DBN 侧开发计划"第 1 条;V1.11 条目第 1、2 条(UART1 通道 / 双向透传)。
### 实现
| 文件 | 改动 |
|------|------|
| `APP/usart_biz.c` | +242 行:UART1 通道整段(118-347);上行转发插入 `uart_srv()`(484-490) |
| `APP/include/cmcng.h` | 声明 `uart1_dma_init/poll` + 5 个通道计数 |
| `APP/peripheral_main.c` | 主循环加 `uart1_dma_poll()`(置于 `MK_UART_SRV` fault marker **之外**,不改变既有 marker 语义) |
**上行**Loop → Air780):`uart_srv()``lup_process_frame()` 之后、**`InitPkgUart` 消费之前**插入转发,
且**只转发 `lup_verify_checksum()==0` 的完整帧**。插入点必须在 BLE 分支改写 `pkg[0]=0x8F` 之前 ——
否则会把已改魔数的帧发给 Air780。
**下行**Air780 → DBN):UART1 收帧 → **魔数分流**`0x7F``UART2_SendString()` 给 Loop
`0x8F`**DBN(CH32V208GBU6) 私有指令**:只作用在自身读写配置上,**一般不转发**(用户 2026-09-10 澄清)。
落地方式:直接复用 BLE 侧同一个分发器 `manage_dbn_ble_default()` —— 两边帧布局完全一致
`pkg[1]=Addr, pkg[2]=LEN, pkg[3]=CMD, pkg[4..]=Value`),零适配。响应仍走 `g_buf_ble_response`→BLE
不回 4GAir780 侧 parser 只认 `0x7F`,回 `0x8F` 也没人接)。**OTA 期间拒绝**:私有指令会写自身配置(碰 flash),与刷写并发是自找麻烦。
### 三个关键设计决定(含理由)
1. **接收走 DMADMA1_Ch5 循环 + 512B),不用 RXNE 中断。** 理由与 UART2 方案A(2026-08-17) 完全相同:
`PRINT` 临界区关中断 ~7.4ms / BLE 栈回调 / SPI 擦除 45ms 等长阻塞窗口会屏蔽 RXNE → 丢字节 → 校验失败。
本通道"不丢帧"是 Air780 沿检测的硬前提。UART2 占 DMA1_Ch6/Ch7**Ch5 空闲,无冲突**。
2. **帧装配器独立于 `g_lup_parser`。** `lup_feed_byte()` 硬编码操作全局 `g_lup_parser`(专供 UART2/Loop 通道),
两条字节流混进同一状态机必然互相残杀。故 UART1 用独立装配器,只复用纯函数 `lup_verify_checksum()`
3. **坏帧回找魔数(resync)。** 校验失败时在已收缓冲里从 idx=1 起回找下一个 `0x7F/0x8F`,残余搬到头部继续装配,
防"单帧损坏 → 后续帧全部失步、链路长时间瞎掉"。
### 与 printf 共用 USART1 的处理(按用户指示)
用户 2026-09-10 指示:**printf 对应串口不动;不会同时使用 Debug 打印和 4G 通信,4G 通信时禁用 debug 即可。**
- 现状:`debug.c``USART_Printf_Init()` 把 USART1 重映射到 PB6 做 printf,且 `PRINT``__disable_irq()`
- 落地:本通道用编译开关 `UART1_AIR780_EN`(默认 `(DEBUG == 0)`)—— **4G 版本以 `-DDEBUG=0` 编译即自动启用**
此时 `#if(DEBUG)` 为假:`PRINT` 是空宏,且 `USART_Printf_Init()` 里 USART1 的配置分支整段不参与编译
(已核对 `USART_Init/USART_Cmd` 均在 `#if(DEBUG == DEBUG_UART1)` 内)→ USART1 干净地交给本文件。
- 调试版(DEBUG!=0)本通道编译为空实现,**`debug.h`/`debug.c` 一行未改**,printf 行为完全不变。
- 已处理:`net_srv.c:125``printf()`(错误路径)→ 改 `PRINT``DEBUG=0` 时随 `PRINT` 一起消失,
不再出现"往未初始化的 USART1 写字节"这种恶心事。该文件本身已用 `PRINT` 53 处,无需补 include。
### 验证
- **host 单测**(本机无 RISC-V 工具链,无法编译固件):从 `usart_biz.c` **原样抽取**装配器代码 + 逐字复刻
`loop_uart_proto.c` 的 XOR/SUM 校验,gcc 编译(`-Wall -Wextra -Werror` **零警告**)跑 13 组用例 **全部通过**
单帧 `0x7F`/`0x8F` 分流、带数据帧逐字节一致、70B 最大帧、校验和破坏不转发、
**坏帧后紧跟好帧仍送达**、**截断帧后只交付 1 帧完整好帧**、乱码前缀、`LEN=0` 非法、`LEN` 超长、三帧连发、
`0x9F`(Loop OTA 魔数) 忽略、上行闸门只放行 verify 通过的帧。
- **待板级验证**:需 MounRiver 工具链编译 + 实机(本机无 riscv 工具链,固件编译尚未跑过)。
### 现场核对手段(比翻日志可靠)
新增 5 个全局计数(`cmcng.h` 已声明,可调试器 Watch):
`g_uart1_fwd_to_air`(上行转发帧数)/ `g_uart1_fwd_to_loop`(下行 0x7F/ `g_uart1_local_cnt`(下行 0x8F/
`g_uart1_badchk`(校验失败)/ `g_uart1_drop`DMA 溢出)。
**判据:跑一段时间后 `g_uart1_fwd_to_air` 与 Air780 侧实收帧数必须相等。**
### 接线(供硬件核对)
- DBN `USART1` 重映射:**PB6 = TX / PB7 = RX**115200 8N1
- 与 Air780 必须**交叉**`DBN_PB6(TX) → Air780_RX``DBN_PB7(RX) ← Air780_TX`
- 波特率两侧一致:115200Air780 侧日志 `Uart_ChangeBR uart1, 115200`
### 已定案
- `0x7D` 帧:**删除/不理会**(用户 2026-09-10)—— V1.11 曾提的 DBN↔Air780 握手/配置同步构想作废,
代码里本就没有实现,`docs/DLD960_IoT_MQTT协议.md` 正式协议也从未收录。历史条目保留不动(记录当时的想法)。
### 仍待办
- [ ] BLE 设置服务器/topic 时同步下发 Air780 配置(V1.12 第 2 条)
- [ ] 通道切换策略(V1.12 第 3 条,待讨论;暂定默认 4G)
- [ ] 板级联调:编译烧录 + 帧计数对齐 + 断线重连
---
## 2026-08-31 — MQTT 协议 V1.13initialize extra_info 增加 imei/iccid 可选字段
> 用户需求 2026-08-31initialize 指令的 `extra_info` 增加可选字段 `imei` / `iccid`4G 模块 IMEI / 流量卡 ICCID)。
### 协议变更(V1.12 → V1.13
- §5.1 initialize 示例 + 字段表:`extra_info.imei` / `extra_info.iccid` 可选
- 语义:无 4G 模块时省略或空串;**4G 通道由 Air780 填真实值**(§6.2 说明)
- 用途:平台解析 initialize 即可拿到 4G 模块/流量卡标识(配合 4G 通道设备管理)
### 固件同步(vd960DBN iot_mqtt_srv.c iot_send_initialize
- `extra_info` 增加 `imei`/`iccid` 字段(当前空串占位——DBN 固件无 4G 模块信息)
- ⚠ 待 vd960DBN 实现"BLE→Air780 配置同步"后,可考虑从 Air780 回读 4G 信息填充(列入开发计划)
### 同步
- `README.md` 文档索引 V1.12 → V1.13
- 协议文档 §5.1 + §6.2 + 修订记录 V1.13
---
## 2026-08-31 — MQTT 协议 V1.124G 通道适配修订为方案 C(Air780 协议转换)
> **协议先行修订**(同日 V1.11 方案 B 未落地即修订,固件均未实现)。
### 修订动因
V1.11 曾定**方案 B**vd960DBN 纯转发 + frame_report/frame_cmd hex 透传)。讨论后改为**方案 C**:Air780 解析 0x7F 帧并转换为**标准 JSON 命令**loop_data / event_report / initialize / heartbeat),平台零改动、与有线通道一致。
### 四个决策点(用户 2026-08-31
| # | 决策 | 结果 |
|---|------|------|
| 1 | event_report | Air780 以 Lua 复刻 DBN iot_evt_* 逻辑(沿检测 + ACK + 5s×3 + 16 深队列 + 跨重连同 msg_id),可行性已评估:全部逻辑块可映射 Lua,无硬障碍 |
| 2 | 事件面 | **4G 仅线圈事件**car_enter/car_leave/loop_cut/loop_restore),不含 DBN 内部网络事件(协议 §6.4 明确) |
| 3 | 命令响应 | Air780 单命令状态机 + 超时回 code=5(与 DBN g_lup_cmd 同模式),协议 §6.5 |
| 4 | frame_* 去留 | **保留为可选兜底**(Air780 未实现转换命令/未识别帧透传),协议 §6.8 |
### 协议变更(V1.11 → V1.12
- §6 重写:方案 C 架构(Air780 协议转换)/ 命令面 = 标准 JSON / 转换职责表(0x7F↔JSON)/ 事件上报 / 命令响应链路 / link / 平台要求
- §3 命令详表:frame_cmd/frame_report 标注改为"可选兜底"
- 修订记录 V1.12
### vd960DBN 侧开发计划(固件未实现,不变)
- [ ] UART1(PB6/PB7) 通道 + UART2↔UART1 双向透传(⚠ 转发不丢帧是 Air780 沿检测的前提)
- [ ] BLE 设置服务器/topic 参数时同步下发 Air780 配置
- [ ] 通道切换策略(待讨论;暂定默认 4G)
### 同步
- `README.md` 文档索引 V1.11 → V1.12
- 协议文档 §6 重写 + 修订记录 V1.12(V1.11 保留作历史)
---
## 2026-08-31 — MQTT 协议 V1.11:4G 通道适配(方案 B:原始帧透传 + hex 封装)协议先行
> **本次为协议文档先行**Air8781P 4G 兜底通道设计稿并入主协议;vd960DBN 固件未实现,列入开发计划)。
### 背景
vd960DBN 有线网络失效时需要 4G 兜底上报。接入方案定为 **方案 Bvd960DBN 纯转发**——Loop 传感数据(0x7F 帧)经 UART1 原样透传 Air780(Air8781P/Air780EPM, LuatOS vd960Air 工程),由 Air780 走 MQTT;4G 下行地感指令经 Air780 透传回 Loop。
### 协议变更(DLD960_IoT_MQTT协议 V1.10 → V1.11)
| 项 | 内容 |
|----|------|
| 新增命令 | `frame_report`(dev→srv, 上行 0x7F 帧 hex 封装, 每条上行携带 link)+ `frame_cmd`(srv→dev, 下行 0x7F/0x8F 帧 hex) |
| 4G 通道命令面 | 不使用有线标准 JSON 业务命令(loop_data/event_report);initialize 由 Air780 发(JSON + link, 用于上线识别 + report_config 时钟校准) |
| 不适用命令 | ssc_net_*/iot_net_*/iot_topic_*(4G 通道下发回 code=4) |
| link 对象 | imei/iccid/imsi/msisdn/csq/net(4G 特有字段, 流量卡管理/信号监控) |
| 链路层 | 数据面 0x7F 帧字节流透传(魔数分流在 DBN 侧: 0x7F→UART2 / 0x8F→本地);0x7D 帧仅 DBN↔Air780 配置同步/握手 |
| 平台侧 | 双通道区分解析 + 新增《DLD960Loop_串口通信协议》解析依赖 |
### vd960DBN 侧开发计划(固件未实现)
- [ ] UART1(PB6/PB7)通道: 0x7F 帧魔数分流
- [ ] UART2↔UART1 双向透传(复用 manage_dbn_ble_transparent 模式)
- [ ] BLE 设置服务器/topic 参数时同步下发 Air780 配置
- [ ] 通道切换策略(待讨论: 动态自动 vs 人工;暂定默认 4G)
### 同步
- `README.md` 文档索引 MQTT V1.10 → V1.11
- 协议文档 §3 命令详表 + §6 4G 通道适配章节 + 修订记录 V1.11
- CHANGELOG 历史条目保留不动
---
## 2026-08-31 — BLE 协议文档补全 V1.03:配置命令全表 + 未实现如实标注
> **本次为文档补录**(固件代码未变,不升固件版本):对照 `dbn_ble_srv.c`GBK+CRLF)逐命令梳理,`DLD960_BLE协议.md` 由 V1.02(仅脱机日志/快照 6 命令)补齐为完整 BLE 协议。
### 背景
`DLD960_BLE协议.md` 此前只覆盖 0x25~0x2AOFFLOG/SNAP 6 命令),既有的 BLE 配置命令(0x09~0x24 等)没有文档——小程序/APP 对接无从下手。本次从代码反向梳理全部命令。
### 文档补全内容
| 命令码 | 名称 | 实现状态(代码核实) |
|--------|------|---------------------|
| 0x09/0x10/0x11/0x12 | 序列码/设备信息/SSC 网络读写 | ✅ manage_dbn_ble_default |
| 0x13/0x14/0x15/0x16 | IoT 网络/Topic 读写 | ✅ unpack_packs(字符串数组 0x00 分隔) |
| 0x1C/0x1D | 验证/修改密码 | ✅ unpack_packs0x1D 不在 switch,分包收尾处理) |
| 0x1E/0x92 | 出厂初始化 | ✅ 回 [0] + factory_dev_info() |
| 0x1F | 设备复位 | ✅ 无响应直接 NVIC_SystemReset |
| 0x22 | 子功能码 | ✅ 2B LE,无响应 |
| 0x23/0x24 | 车检器参数读写 | ⚠ 空壳(0x23 仅回 [0] 不处理参数;0x24 空响应) |
| 0x31 | UART 波特率读写 | ✅(⚠ 写响应多 1B 重复低字节,文档已标注) |
| 0x8A | Loop 灵敏度列表 | ⚠ 空响应 |
| 0xC5 | 传感上报使能 | ✅ 设置生效无响应;⚠ report_sens_acs 无定义 → 0xC0 上报未实现 |
| 0x17/0x18/0x20/0x21/0x87/0x88/0x89 | 交通参数/通知/Loop 采样等 | ⚠ 头文件有定义,switch 无 case(未实现) |
| 0x7F/0x9F | 透传通道 | ✅ UART2 原样转发 / OTA 透传 |
### 关键发现
- **0x1D MODIFY_PASS 实际已实现**:不在 manage_dbn_ble_default 的 switch,而在 unpack_packs 分包收尾分支(头文件有定义、switch 无 case ≠ 未实现,须看 unpack_packs
- **0xC0 BLE 主动上报已废弃**`report_sens_acs()` 仅 dbn_ble_srv.h 声明,全工程无定义无调用;0xC5 使能后无上报动作(上报走 MQTT/TCP)
- **0x31 写响应固件 bug**`tmp_ble_buf[i++] = g_storage_uart_baud;` 后紧跟 `tmp_ble_buf[i++] = (uint8_t)g_storage_uart_baud;` 重复写低字节,响应 10B 而非 9B——文档按语义描述 + 标注实际多 1B,待板级联调时确认是否修固件
- 请求分包(unpack_packs)与响应分包(set_response_to_notify)格式一致:header 高 4 位=包数、低 4 位=序号(从 1 起)
### 同步
- `README.md` 文档索引 BLE 协议 V1.02 → V1.03(描述更新为完整命令表)
- CHANGELOG 历史发布条目(V1.02 不变)保留不动
- 待板级:0x31 写响应长度、0xC5 使能后无上报是否符合预期
---
## 2026-08-21 — MQTT 协议 V1.10 代码落地:网络上报携带地感版本(loop_ver/loop_hw_ver
> **固件版本:归入 vd960DBN V1.02.05 发布**2026-08-21 升版,cmcng.h 三段式一致:FIRMWARE_VER="1.02.05" + MAIN=1/SUB=2/SUBSUB=5)。V1.02.05 涵盖 8-20 联网稳定性修复系列(SocketSend 0x11 / OTA hex 空格 / union 合并 / 刷写状态语义)+ 本次 V1.10 协议实现。
### 背景
协议 V1.10`DLD960_IoT_MQTT协议.md`commit 3be9e50)三处新增,本次为固件实现:
1. `dev_info_query` 响应 data 补 `loop_ver` / `loop_hw_ver`(地感 Loop 版本,0x4A 缓存值)
2. `initialize` 上报同样携带(尽力,可为空)
3. 新增 §4.25 `loop_version_query` 命令:实时查询 Loop 版本,配合远程 OTA 升级前后核对
### 改动
| 位置 | 内容 |
|------|------|
| `loop_uart_proto.h/c` | 新增 `LoopVerCache` 结构 + 全局 `g_lup_ver_cache`0x4A 查询结果缓存);`lup_cache_from_info()` 解析结果写缓存、`lup_refresh_version_cache()` 后台刷新(仅命令通道空闲时发 0x4A) |
| `iot_mqtt_srv.c` dev_info_query | data 补 `loop_ver` / `loop_hw_ver`(缓存格式化字符串,查询未完成/失败为空串) |
| `iot_mqtt_srv.c` initialize | data 补 `loop_ver` / `loop_hw_ver`(尽力携带,可为空) |
| `iot_mqtt_srv.c` 新命令 `loop_version_query` | 0x4A 异步发起 + 暂存 msg_id / deadline(300ms)`iot_verq_poll()``g_lup_cmd.state == RESPONSE_READY` 时解析回包(code=0 + loop_ver/loop_hw_ver/version_str)并同步刷新缓存;TIMEOUT/超时回 code=5loop_ver 保持缓存值) |
| `iot_mqtt_srv.c` 后台刷新 | 上电 `iot_mqtt_init()` 置 dirty + OTA done`iot_ota_report_result ok=1`)置 dirty;poll 在命令通道空闲时自动发 0x4A 刷新缓存 |
### 关键设计
- **版本来源 = 0x4A 实时查询**`lup_parse_version`),不是 OTA 元数据 `version`——那是"目标版本"(审计用),0x4A 才是 Loop 当前真实固件版本
- **缓存值语义**`dev_info_query`/`initialize` 带缓存值(上电自动查一次 + OTA done 后刷新);MQTT 命令处理是同步回包,不能干等 0x4A 异步响应,故查询未完成/失败回空串;平台要最新版本走 `loop_version_query` 实时查询
- **异步回包**:与 TCP `json_check_pending` 同模式——暂存 msg_id(回包 topic 固定 `dld960/{sn}/dev`,无需存 topic),等 `g_lup_cmd.state` 状态机推进
- **单命令通道互斥**`g_lup_cmd` 全局唯一,TCP/MQTT/后台刷新共用;后发覆盖、先发者超时(低频命令,理论无冲突)
### 验证
- gcc 隔离单测 `tests/test_iot_loop_ver.c` **44 断言全过**
- 版本帧解析:正常帧(Status/Hard=1.0.0/Soft=1.2.3+ 错误路径(len<10 / bad magic / bad cmd / LEN<7
- 缓存:`lup_cache_from_info` 写入、`iot_loop_ver_str`/`iot_loop_hw_ver_str` 格式化(含无效缓存空串、NULL 防护)
- verq_poll 命令成功回包:code=0 + 三字段 + msg_id 回显 + topic 正确 + 缓存同步刷新 + 通道释放
- 超时回包:code=5 + `no response` + 缓存值保留
- 后台刷新:dirty=1+IDLE 发起查询(不设 pending)、通道忙不消费、响应消费更新缓存、残留超时清理
- `check_c_balance.py` 三个改动文件语法平衡 OK
- **待板上验证**0x4A 在 MQTT 工作期间与 `loop_data`/事件上报共用 UART2 总线的时序(查询是低频命令,理论无冲突);OTA done 后缓存刷新(Loop 复位时间窗内查询可能超时,可接受——平台可用 `loop_version_query` 主动核对)
---
## 2026-08-20 — 联网 SocketSend 0x11 死循环修复:发送退避 + SocketCreat 失败中止
### 现象(板级)
收 report_config 后 loop_data606B)发送失败:`SocketSend FAIL ret=0x11 sent=0/606` → 立即重试 10 次无效 → 3 次强制断连 → 重连 SocketCreat 返回 **0x1D(ISCONN)** + Connect 0x17(CLSD) → TCP connect timeout 死循环,永久失联。
### 根因
1. **WCHNET_NUM_TCP_SEG = WCHNET_NUM_TCP×2 = 2**(RAM 瘦身改小):TCP 发送段缓冲仅 2 个,重传队列占用即无缓冲 → 606B 发送 `WCHNET_ERR_MEM(0x11)`
2. **iot_mqtt_send 失败处理过激**:0x11 是瞬时(SEG 被占),却立即重试 10 次(无退避,WCHNET 没时间释放)+ 3 次就强制断连
3. **重连缺陷**`WCHNET_CreateTcpMqttSocket` 里 SocketCreat 失败(0x1D ISCONN,旧 socket 未清干净)后**仍继续 Connect**mStopIfError 只打印不中止)→ 无效 socket 等超时 → 死循环;且强制重连时 `SocketId_TCP` 未置 0xFFSocketCreat 沿用旧 id
### 修复
| 位置 | 改动 |
|------|------|
| `iot_mqtt_srv.c` iot_mqtt_send | 0x11 时 **200ms 退避重试**(等 WCHNET 释放 SEG,最多 10 次×200ms≈2s),不立即断连;非 MEM 错误立即重试 |
| `iot_mqtt_srv.c` 强制重连 | 补 `SocketId_TCP = 0xFF`(防 SocketCreat 沿用旧 id |
| `net_srv.c` WCHNET_CreateTcpMqttSocket | SocketCreat 失败 → 打印 + `SocketId_TCP=0xFF` + **return 不 Connect**,由上层指数退避重试 |
### 验证
- 语法 0 错误
- **待板级确认**:重发场景(瞬时 0x11)应自动恢复不断连;真断连重连失败应退避而非死循环
---
## 2026-08-20 — OTA 下载失败修复:hex 提取不兼容 json.dumps 空格(单片 CRC 全错)
### 现象(板级)
DBNMQTTool 下载第一片即失败:`ota_data code=1 crc/gap error`received=0,重试 4 次全败。
### 根因
工具 `json.dumps` 默认**带空格**`"data": "hex..."`),设备端手工 `strstr(json, "\"data\":\"")` 找**无空格**格式 → 匹配不到 → hexbuf 空 → `ota_hex_decode` 失败 → code=1。其他命令走 `simple_parse_json`(容错空格)不受影响,只有 OTA 的 hex 提取踩坑。
### 修复
- `ota_srv.c/h` 新增 `ota_json_extract_hex()`:扫描 `"data"` 键后跳过任意空格/冒号/引号,兼容 `"data":"..."``"data": "..."` 两种格式
- `iot_mqtt_srv.c` ota_data 分支改用该函数(删除 strstr 手工提取 + 残留 hp 变量)
- 单测新增 `test_json_extract_hex`:无空格/带空格(事故场景)/512 hex 满片/找不到,4 场景全过
### 教训
嵌入式 JSON 协议字段提取**统一用 simple_parse_json 或兼容空格的扫描**,不要 strstr 精确匹配——上位机 JSON 序列化器(json.dumps/不同语言库)的空格行为不可控。
---
## 2026-08-20 — 联网复位事故修复:OTA 命令缓冲合并 union(RAM 90% 栈余量不足)
### 现象(板级)
联网后 SUBACK → 平台下发 MQTT PUBLISHlen=197,即 `report_config` 时钟同步)→ **HardFault → NVIC_SystemReset**`RST_REASON: 0x10000000` = SFT 软件复位位,非 IWDG 0x20000000CH32V20x 默认 HardFault_Handler 即软件复位)→ 死循环复位。
### 根因
MRS 编译 RAM **90.02%**44248/48KB)逼近历史 .bss 挤栈红线。本次 OTA 实现 6 个命令分支各声明独立 `static char resp[256~400]` + `static char hexbuf[513]`(合计 ~2.5KB BSS)→ 栈余量被挤 → `iot_handle_publish` 深调用链(MQTT 解析 → simple_json → snprintfreport_config 分支本身还有局部 `data[512]`**栈溢出 → HardFault**。复现稳定(每次收 report_config 必崩)。
### 修复
| 项 | 改动 | 省 |
|----|------|-----|
| iot_mqtt_srv.c | 6 个 resp + hexbuf 合并为函数级 `static union _ota_io { resp[512]; hexbuf[513]; }`(互斥复用) | **~2KB BSS** |
| ota_srv.c | `_chunk_buf[256]` + `_fs_frame[254]` 合并 union(下载/校验与刷写帧互斥) | ~260B |
| ota_srv.c | 删无用 `_fs_block_crc` | 4B |
**教训**RAM 90% 环境下"局部栈改 static"是**伪优化**——BSS 增加 = 栈余量减少,深调用链照样溢出。正确方向是**合并复用缓冲减少总占用**(union),不是搬家。
### 验证
- 语法 0 错误(基线 interrupt 假阳性除外);gcc 隔离单测 10/10 PASS
- **待板级确认**:拉最新代码重编,RAM 应回 ~88%,联网收 report_config 不再复位
---
## 2026-08-20 — Loop MCU 远程 OTA 固件实现(MQTT V1.08ROADMAP P1.4 ①)
### 背景
协议设计稿 V1.01 拍板 + 并入 MQTT 协议 V1.08 + DBNMQTTool OTA 页签/协议单测先行。本次固件实现**先存后刷**:MQTT 分片下载 → W25Qxx OTA 暂存区(0x010000 起 512KB)→ 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCUAT32F421)。
### 变更
| 文件 | 变更 |
|------|------|
| `ota_srv.c/h`(新增) | CRC32ISO-HDLC 逐位,零查表 RAM)、OtaMeta 元数据(双备份 0x010000/0x010200,节流 4KB flush)、会话状态机(begin/data/end/abort/flash)、本地刷写状态机(非阻塞 tick 驱动:A5→pre_ok→A6→addr_ok→A7×N 停等 ACK,1s 超时 ×3)、安全窗口(有车拒绝,force 跳过)、失败 event_report ota_error |
| `iot_mqtt_srv.c` | 命令分发加 6 个 ota_* 分支;会话静默(downloading/flashing 暂停 event_report 发送 + offlog/快照落盘,队列积压结束后补发);IOT_EVT_OTA_ERROR 事件类型 + `iot_evt_report_ota_error` + `iot_any_car` |
| `usart_biz.c` | uart_srv OTA 分支 ACK 分发:`g_ota_flash_active` 时交给 `ota_flash_feed_ack`(本地刷写),否则透传 BLE(现状) |
| `peripheral_main.c` | `ota_init()` 初始化 + `ota_poll()` 挂主循环(uart_srv 后,先消费 ACK 再推进) |
| `offlog.c/h` | 事件类型 OTA_START 0x60 / OTA_RESULT 0x61 + 包装函数(升级开始/结果审计留痕) |
### 关键参数
| 项 | 值 |
|----|-----|
| 下载单片 | 256B + 单片 CRC32hex 512 字符 < RECV_BUF 1024 |
| 镜像上限 | 96KBSlot 100KB - 4KB 边界余量) |
| 刷写块 | 248B0x9F LEN 为 uint8 → DATA ≤254Bbootloader 2KB 扇区累积写) |
| 分区 | 元数据 4KB + Slot A/B 各 100KB0x011000/0x02A000+ 预留 308KB |
| 断点续传 | 元数据 received 节流 4KB flush,掉电丢 ≤4KB(平台重发幂等,协议已定义) |
| 刷写窗口 | 64KB ≈ 265 块 × ~20ms ≈ 6~8s;非阻塞调度保 MQTT 保活/IWDG |
### 单测(gcc 隔离,`tests/test_ota_srv.c`mock NOR/UART2/时钟)
9 断言组全过:CRC32 标准向量 0xCBF43926 / hex 解码 / 元数据双备份恢复 / 会话流 20 片→ready / 断点续传(16 片 flush 后精确续传)/ 幂等·乱序·单片 CRC / 刷写全流程(21 块 A7 逐块 ACK)/ 有车拒绝 + force 跳过 / pre_ok 超时重试失败告警。
**抓到 2 个真 bug(单测价值实证)**:
1. **A7 块序号未递减**`_fs_sub` 每块 ACK 后不减 → bootloader 永远等不到末包(sub==1)→ 刷写卡死。修复:ACK 成功路径 `_fs_sub--`
2. **重发时 retry 清零**START/ADDR 分支 `_fs_retry=0` 每次重发清零 → 超时重试永不失败。修复:重发保持计数(首发由 ota_cmd_flash 置 0ACK 成功由 feed_ack 清 0)。
### 待板上验证
- UART2 TX 阻塞轮询(248B ≈ 13ms)与主循环时序、擦除 45ms×N 分散阻塞对 MQTT 保活影响
- Loop 实际 bin 大小(验证 96KB 余量)、bootloader 真实 ACK 时序
- 会话静默后 event_report 补发行为(队列积压恢复)
---
## 2026-08-19 — 固件版本 V1.02.04(SPI 存储适配两项修复) ## 2026-08-19 — 固件版本 V1.02.04(SPI 存储适配两项修复)
### 内容 ### 内容
@@ -1486,6 +1993,7 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`,
| 版本 | 时间 | 说明 | | 版本 | 时间 | 说明 |
|------|------|------| |------|------|------|
| V4.8 | 2026-08-21 | MQTT 协议 V1.10 固件实现: dev_info_query/initialize 补 loop_ver/loop_hw_ver + 新命令 loop_version_query 异步回包 + 版本缓存(上电/OTA done 刷新), 单测 44 例 |
| V4.7 | 2026-08-19 | 固件版本 1.02.03→1.02.04 (SPI 存储适配: 去厂商识别 + 恢复 factory 写入, 板级验证通过) | | V4.7 | 2026-08-19 | 固件版本 1.02.03→1.02.04 (SPI 存储适配: 去厂商识别 + 恢复 factory 写入, 板级验证通过) |
| V4.6 | 2026-08-19 | 恢复 factory 配置写入: 解除 8-13 止血, 空片首启写 magic+默认参数 (换 W25Q128 后配置永久丢失修复) | | V4.6 | 2026-08-19 | 恢复 factory 配置写入: 解除 8-13 止血, 空片首启写 magic+默认参数 (换 W25Q128 后配置永久丢失修复) |
| V4.5 | 2026-08-19 | SPI Flash 识别去厂商代码: W25Qxx 宏去 0XEF 前缀, ReadJEDEC_ID 删厂商校验, storage_init 低字节匹配 (兼容多厂家) | | V4.5 | 2026-08-19 | SPI Flash 识别去厂商代码: W25Qxx 宏去 0XEF 前缀, ReadJEDEC_ID 删厂商校验, storage_init 低字节匹配 (兼容多厂家) |
+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
+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;
}
+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;
}
+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) ## 2026-07-14 — variation 上报量 2B→3B 有符号 (协议 V1.05)
### 背景 ### 背景
+216 -4
View File
@@ -2,7 +2,7 @@
> 分析对象:`uart_report_packet_loop_acs()` 中上报的 **变化量 variation** > 分析对象:`uart_report_packet_loop_acs()` 中上报的 **变化量 variation**
> 涉及文件:`src/main.c`(组包)、`src/TaskLoop.c`(基线/IIR 更新)、`inc/TaskLoop.h`(参数宏) > 涉及文件:`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** - 旧版(≤V1.04)为 2B 无符号绝对值,缺陷与变更动机见 **§5**
--- ---
@@ -136,7 +136,7 @@ variation: | | | | ← 锯齿波!
|---|---|---| |---|---|---|
| 计算式 | `\|Origin CAPVD\|` 取绝对值 | `Origin CAPVD` 带符号 | | 计算式 | `\|Origin CAPVD\|` 取绝对值 | `Origin CAPVD` 带符号 |
| 编码 | 2 字节无符号,小端 | **3 字节有符号补码**,小端 | | 编码 | 2 字节无符号,小端 | **3 字节有符号补码**,小端 |
| 值域 | 0~65535**溢出回绕** | ±8388607**发送端饱和限幅** | | 值域 | 0~65535**溢出回绕** | 8388608 ~ +8388607**发送端饱和限幅** |
| 方向信息 | 无 | 正 = 车/金属进入,负 = 反向漂移 | | 方向信息 | 无 | 正 = 车/金属进入,负 = 反向漂移 |
| 每通道单元 | 11 字节 | **12 字节** | | 每通道单元 | 11 字节 | **12 字节** |
| 帧 Len / 总长(4 路无分包) | 0x2F=47 / 52B | **0x33=51 / 56B**(仍 < `BUFF_STACK_SIZE=64`,缓冲安全) | | 帧 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 校验依然通过**(校验的是整帧,不感知单元边界)——这是**静默数据损坏**,不会报错,只会得到一堆"看起来合理"的错值。 1. **Loop 与 DBN 固件必须同版本发布**。单元步长 11↔12 死绑定,任一端用旧固件,4 通道数据整体错位。**且帧级 XOR/SUM 校验依然通过**(校验的是整帧,不感知单元边界)——这是**静默数据损坏**,不会报错,只会得到一堆"看起来合理"的错值。
2. 上位机若想兼容新旧固件,可用 **Len 字节区分**4 路无分包时 Len=0x2F(47) 为旧格式(2B 无符号),Len=0x33(51) 为新格式(3B 有符号),按帧动态选解析器。 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 对分析工作的直接增益 ### 5.7 对分析工作的直接增益
@@ -218,3 +218,215 @@ if (v & 0x800000) v |= 0xFF000000; // bit23 = 1 → 负数,符号扩展
- 将 variation 改为上报 **斜率 / 趋势** 而非瞬时差,削掉锯齿混叠 - 将 variation 改为上报 **斜率 / 趋势** 而非瞬时差,削掉锯齿混叠
- ~~补 variation 饱和保护~~ → V1.05 已完成 - ~~补 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 ALFA_CAP1 79 // IIR α = 79/256 ≈ 0.31 (@10ms → τ≈32ms)
#define MAX_SLOPE_RATE 5 // 斜率限幅: 单次最大变化 5% #define MAX_SLOPE_RATE 5 // 斜率限幅: 单次最大变化 5%
#define ENTRY_CONFIRM 3 // 进入确认: 连续 N 次低于阈值 #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) * 线 (V2.3~V2.5)
@@ -41,6 +44,15 @@
#define FREEZE_TIMEOUT 1000 // 冻结超时: ~10s @ 10ms/tick #define FREEZE_TIMEOUT 1000 // 冻结超时: ~10s @ 10ms/tick
#define FREEZE_STABILITY_RATE 2 // 稳定性窗口: 参考值的 ±2% #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 * : 1 = (CN200910309382), 0 = cnt_release
*===========================================================================*/ *===========================================================================*/
@@ -173,7 +185,8 @@ typedef struct {
uint16_t fault_tick; 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; 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_on(uint8_t loop_num);
void set_loops_relay_off(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; extern TaskHandle_t usart_task_handler;
@@ -16,15 +16,20 @@
#include <stdint.h> #include <stdint.h>
#define PRODUCT_MODEL "DLD960" #define PRODUCT_MODEL "DLD960"
#define FIRMWARE_VER "1.00" #define FIRMWARE_VER "1.03"
#define HARDWARE_VER "1.00" #define HARDWARE_VER "1.00"
#define FIRMWARE_VER_MAIN 0 #define FIRMWARE_VER_MAIN 1
#define FIRMWARE_VER_SUB 30 #define FIRMWARE_VER_SUB 3
#define FIRMWARE_VER_SSUB 0 #define FIRMWARE_VER_SSUB 1
#define HARDWARE_VER_MAIN 1 #define HARDWARE_VER_MAIN 1
#define HARDWARE_VER_SUB 0 #define HARDWARE_VER_SUB 0
#define HARDWARE_VER_SSUB 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_A 0x01 // 二次判断
#define FUNCTION_B 0x02 // 二次判断增强条件判断 #define FUNCTION_B 0x02 // 二次判断增强条件判断
@@ -111,7 +111,7 @@ typedef enum
typedef struct typedef struct
{ {
uint8_t sensitvity; // 高四位表示灵敏度的数量,低四位表示当前灵敏度的序号(从0开始) uint8_t sensitvity; // 灵敏度序号: 4级制 0~3 (值越大越灵敏, 默认2); 9级制后续版本实现
uint8_t delay_time; uint8_t delay_time;
uint8_t output_mode; uint8_t output_mode;
uint8_t direction_mode; // 方向判别模式,0 触发模式, 非0 表示方向判别模式 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); 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 set_factory_param(void);
void storage_dev(void); void storage_dev(void);
void para_store_init(void); void para_store_init(void);
@@ -6,6 +6,7 @@
#include "task.h" #include "task.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "storage.h"
/*=========================================================================== /*===========================================================================
* DLD154V4B / M1H * DLD154V4B / M1H
@@ -23,6 +24,7 @@ Loop154_States g_loop_states = {0};
uint32_t g_sys_freq = 0; uint32_t g_sys_freq = 0;
uint8_t g_input_div = 1; uint8_t g_input_div = 1;
uint32_t g_safe_max_cnt = LC_HOLD_TIME; uint32_t g_safe_max_cnt = LC_HOLD_TIME;
volatile uint16_t g_wdg_counter = 0; // 看门狗活性计数 (TMR15 ISR 5ms 递增)
/*=========================================================================== /*===========================================================================
* IIR DLD154V4B * IIR DLD154V4B
@@ -107,7 +109,8 @@ void init_vd_single(Loop154_Unit *unit)
unit->loop_CAP_OK = 0; unit->loop_CAP_OK = 0;
unit->loop_CAPVD = 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->Flt_Reg = ALFA_CAP1;
unit->loop_entry_cnt = 0; unit->loop_entry_cnt = 0;
@@ -151,6 +154,7 @@ void init_vd_single(Loop154_Unit *unit)
unit->SET_SAFE = 1; unit->SET_SAFE = 1;
unit->stable_cnt = 0; unit->stable_cnt = 0;
unit->settle_cnt = 0; // V1.03: 自检计数从零开始
unit->xn_counter = 0; unit->xn_counter = 0;
} }
@@ -250,6 +254,9 @@ void TMR15_GLOBAL_IRQHandler(void)
if (tmr_interrupt_flag_get(TMR15, TMR_OVF_FLAG) != RESET) { if (tmr_interrupt_flag_get(TMR15, TMR_OVF_FLAG) != RESET) {
/*--- 看门狗活性计数 (5ms) ---*/
g_wdg_counter++;
/*--- 50ms tick 分频 ---*/ /*--- 50ms tick 分频 ---*/
TM1cnt++; TM1cnt++;
if (TM1cnt >= 10) { if (TM1cnt >= 10) {
@@ -312,6 +319,8 @@ void TMR15_GLOBAL_IRQHandler(void)
unit->loop_INI_LOOP = 1; unit->loop_INI_LOOP = 1;
unit->loop_LOOP_OK0 = 0; unit->loop_LOOP_OK0 = 0;
unit->loop_stable = 0; unit->loop_stable = 0;
unit->stable_cnt = 0; // V1.03: 重学自检计数从零开始
unit->settle_cnt = 0;
unit->loop_ORG_CNT = 0; unit->loop_ORG_CNT = 0;
unit->loop_ORG_SUM = 0; unit->loop_ORG_SUM = 0;
unit->last_exit_tick = g_loop_states.misc_counter; 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_INI_LOOP = 1;
unit->loop_LOOP_OK0 = 0; unit->loop_LOOP_OK0 = 0;
unit->loop_stable = 0; unit->loop_stable = 0;
unit->stable_cnt = 0; // V1.03: 二次稳定期从零开始
unit->settle_cnt = 0;
#if USE_FLATNESS_EXIT #if USE_FLATNESS_EXIT
unit->exit_state = 0; unit->exit_state = 0;
unit->max_slope = 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); 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) { if (!unit->loop_stable) {
uint32_t _prev_origin = unit->loop_Origin;
uint8_t _win_done;
unit->loop_CAPVD = unit->loop_Value; unit->loop_CAPVD = unit->loop_Value;
update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT, _win_done = update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
&unit->loop_Origin, unit->loop_CAPVD, 100); &unit->loop_Origin, unit->loop_CAPVD, 100);
unit->stable_cnt++; 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->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); PRINT("Loop%d stable, Origin:%d\n", unit->loop_num + 1, unit->loop_Origin);
} }
return; 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; 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_cnt = 0;
unit->loop_freeze_ref = 0;
update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT, update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
&unit->loop_Origin, unit->loop_CAPVD, WINDOW_ORIGIN); &unit->loop_Origin, unit->loop_CAPVD, WINDOW_ORIGIN);
} else { } else if (dev < 0) {
/* 正variation (疑似车) → 冻结, 保留超时兜底更新 */
if (unit->loop_freeze_cnt == 0) { if (unit->loop_freeze_cnt == 0) {
unit->loop_freeze_ref = unit->loop_CAPVD; unit->loop_freeze_ref = unit->loop_CAPVD;
} else { } else {
@@ -503,6 +546,15 @@ void vd1_task_per_channel(Loop154_Unit *unit)
unit->loop_ORG_CNT = 0; unit->loop_ORG_CNT = 0;
unit->loop_ORG_SUM = 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 { } else {
int32_t dev = (int32_t)unit->loop_CAPVD - (int32_t)unit->loop_Origin; int32_t dev = (int32_t)unit->loop_CAPVD - (int32_t)unit->loop_Origin;
int32_t cond1 = (dev >= 0) ? dev : -dev; 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 if (cond1 < (int32_t)unit->loop_dlt_ORG && abs_fd < (int32_t)unit->delta2
&& abs_sd < (int32_t)unit->delta3) { && abs_sd < (int32_t)unit->delta3) {
unit->flat_ok_cnt++; unit->flat_ok_cnt++;
@@ -624,7 +676,7 @@ void vd1_task_per_channel(Loop154_Unit *unit)
/*================================================================ /*================================================================
* cnt_release * 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) { if ((unit->loop_Origin - unit->loop_dlt_ORG) < unit->loop_CAPVD) {
unit->loop_cnt_release++; unit->loop_cnt_release++;
@@ -655,6 +707,20 @@ void vd1_task_per_channel(Loop154_Unit *unit)
/*=========================================================================== /*===========================================================================
* loop_task_function FreeRTOS DLD154V4B main * 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 线 * 10ms 线
*===========================================================================*/ *===========================================================================*/
void loop_task_function(void *pvParameters) 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 + 进入确认 + 斜率限幅 vTaskDelay(10); // 10ms tick, M4 优化: 双路 IIR + 进入确认 + 斜率限幅
} }
} }
@@ -59,9 +59,8 @@ __IO uint32_t TimingDelayDec;
TaskHandle_t loop_task_handler; 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_init(void);
void wdt_feed(void);
tmr_output_config_type tmr_oc_init_structure; tmr_output_config_type tmr_oc_init_structure;
@@ -104,20 +103,19 @@ void InitPkgUart(Pkg_Uart * pkg){
* @param none * @param none
* @retval none * @retval none
* @note = ( × ) / LSI频率 * @note = ( × ) / LSI频率
* LSI频率约 40kHz64=4687 3 * LSI频率约 40kHz16 WDT_CLK_DIV_16=4095 1.64
*/ */
void wdt_init(void) void wdt_init(void)
{ {
// 使能看门狗寄存器写访问 // 使能看门狗寄存器写访问
wdt_register_write_enable(TRUE); wdt_register_write_enable(TRUE);
// 设置预分频系数为 64 // 设置预分频: 必须用枚举 WDT_CLK_DIV_16 (=0x02), 勿传数值 16! (V4B 踩坑)
wdt_divider_set(WDT_CLK_DIV_64); wdt_divider_set(WDT_CLK_DIV_16);
// 设置重载值 // 设置重载值 4095 (12bit 最大)
// 超时时间 = (64 × 4687) / 40000 ≈ 7.5 // 超时时间 = (16 × 4096) / 40000 ≈ 1.64
// 如果需要 3秒: (64 × 1875) / 40000 = 3秒 wdt_reload_value_set(4095);
wdt_reload_value_set(1875);
// 重载计数器 // 重载计数器
wdt_counter_reload(); wdt_counter_reload();
@@ -128,15 +126,6 @@ void wdt_init(void)
PRINT("Watchdog initialized, timeout: %d ms\n", WDT_TIMEOUT_MS); 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 timer_period = 0;
uint16_t channel1_pulse = 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++) for(j = 0; j < _amount; j++)
{ {
k = pkg[i++]; k = pkg[i++];
unit->sensitvity = k & 0x0F; unit->sensitvity = k & 0x0F; // 低4位: 灵敏度序号 (4级制 0~3)
unit->loopFreq_Level = k >> 4; unit->loopFreq_Level = k >> 4; // 高4位: 频率档
unit->delay_time = pkg[i++]; unit->delay_time = pkg[i++];
k = pkg[i++]; k = pkg[i++];
unit->output_mode = k & 0x03; 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->exist_mode = pkg[i++];
unit->direction_mode = pkg[i++]; unit->direction_mode = pkg[i++];
// 同步灵敏度到检测单元 (修复: 运行中配置必须立即生效, 不依赖重启)
g_loop_states.loop_unit[j].loop_SensLevel = sens_level_from_config(unit->sensitvity);
unit++; unit++;
} }
@@ -836,6 +828,11 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len)
_cng++; _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); response_uart_pkg(_cmd, tmp_ble_buf, i);
} break; } 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 // set frequent level
void set_flp_level(uint8_t loop_num, uint8_t freq_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); 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_PLUS = unitout->output_mode & 0x01;
g_loop_states.loop_unit[i].SET_DLY = (unitout->output_mode >> 1) & 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; g_loop_states.loop_unit[i].hold_time = unitout->exist_mode * 20 * 5;