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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- 新增 ota_srv.c/h: CRC32(ISO-HDLC 逐位零表)/OtaMeta 双备份(节流 4KB flush)/
  会话状态机(begin/data/end/abort/flash)/本地刷写状态机(非阻塞 tick: A5→A6→A7
  停等 ACK 1s×3)/安全窗口(有车拒绝 force 跳过)/失败 event_report ota_error
- iot_mqtt_srv.c: 6 个 ota_* 命令分发 + 会话静默(event_report 积压/offlog·快照
  落盘暂停, 结束补发) + IOT_EVT_OTA_ERROR + iot_any_car
- usart_biz.c: uart_srv OTA 分支 ACK 分发 (本地刷写→ota_flash_feed_ack, BLE 透传不变)
- peripheral_main.c: ota_init + ota_poll 挂主循环
- offlog: OTA_START 0x60/OTA_RESULT 0x61 审计事件
- tests/test_ota_srv.c: gcc 隔离单测 9 组全过 (mock NOR/UART2/时钟, 嵌入真实实现)
  抓到 2 个真 bug: ①A7 序号未递减(bootloader 等不到末包) ②重发时 retry 清零(超时永不失败)
- 语法检查: ota_srv/iot_mqtt_srv/usart_biz/offlog 0 新增错误 (基线 interrupt 假阳性除外)
- devlog 置顶条目 + 待板上验证清单
2026-08-20 12:09:13 +08:00
wangfq 6c1f276002 feat(DBNMQTTool): OTA 页签 + 协议单测 (MQTT V1.08 工具先行)
- protocol.py: OTA 模块 — 命令常量/ota_crc32(ISO-HDLC)/ota_split_bin(256B×96KB)/
  data_ota_* 构建器/状态·阶段·错误码描述
- main.py: OTA 页签 — bin 选择(≤96KB+CRC32 显示)/版本·force·slot 参数/
  下载(OtaDownloadThread: begin→data×N→end, 断点续传·缺片重定位·CRC 重发)/
  刷写(二次确认)/中止/状态查询; 进度条 + ota_report/ota_status 实时显示
- tests/test_ota_protocol.py: 22 断言 — CRC32 标准向量/分片边界/构建器/
  OtaDeviceSim 设备状态机(顺序·幂等·乱序·单片CRC·全镜像复核·续传)/完整下载流
- devlog 置顶条目
2026-08-20 11:53:50 +08:00
wangfq 2ccf0804a8 docs(vd_960): MQTT 协议并入 Loop 远程 OTA → V1.08 (ROADMAP P1.4 ①)
- DLD960_IoT_MQTT协议.md V1.07→V1.08: 命令表加 ota_begin/ota_data/ota_end/
  ota_abort/ota_flash/ota_status/ota_report; §4.19~4.24 命令详情; §5.5 ota_report;
  event_report 扩展 type=ota_error; §2.2 OTA 细分错误码 (err_code); 修订记录
- DLD960_MQTT_OTA协议.md V1.01 (按修改意见): Slot A/B 100KB / 维持 Loop 现状 /
  会话期间静默 / 非阻塞 tick 驱动
- README 协议索引 + 技术规格书 §5.1 协议矩阵同步 V1.08 (CRLF 保持)
2026-08-20 11:53:36 +08:00
wangfq 9d6f5ea412 docs(vd_960): MQTT OTA 协议设计稿按修改意见修订 → V1.01
- Slot A/B 容量 60KB→100KB (镜像上限 96KB, 为 DBN CH32V208 镜像预留)
- 删除继电器 GPIO 默认态待验证项, 维持 Loop 现状 (与 BLE OTA 行为一致, 不额外干预)
- 新增会话期间静默: 暂停 MQTT event_report 发送 (队列积压结束补发) + offlog/快照落盘 (开始/结果日志除外)
- 刷写调度明确非阻塞 tick 驱动, 刷写窗口内 MQTT 保活 (PINGREQ/心跳/IWDG 喂狗) 不受影响
- 待板上验证项重新编号 (删 1 项, 保活项升级为实现约束+验证)
2026-08-20 11:37:38 +08:00
wangfq b20fc57954 docs(vd_960): MQTT 远程 OTA 协议设计稿 (Loop MCU 先存后刷, ROADMAP P1.4 ①)
- 三阶段: MQTT 分片下载→W25Qxx 暂存(0x010000 512KB)→本地 0x9F ISP 透传刷写
- 7 命令: ota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status/ota_report
  + event_report 扩展 type=ota_error 告警
- 关键参数: 单片 256B(hex, <RECV_BUF 1024), 刷写块 248B(0x9F LEN uint8 上限 254B),
  镜像上限 60KB, CRC32=ISO-HDLC(zlib.crc32 兼容), Slot A/B 双槽回滚
- 安全底线: 升级窗口检查(有车拒绝)/继电器安全侧/双重 CRC/失败重试×3/审计留痕
- 6 项待板上验证(继电器默认态/DBN TX 缓冲/SPI 写阻塞/RAM 预算/bin 大小/MQTT 保活)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

注: 固件命令分发 (log_stat/log_query/log_clear 处理) 待 P1.3 实现,
本文档为协议先行
2026-08-04 18:34:31 +08:00
wangfq 90755bf465 feat(vd960DBN): offlog TIME_ANCHOR 严格用平台下发 unix_ts
offlog_time_anchor 原丢弃传参内部重取 dev_time_now() (毫秒级误差),
改为 offlog_evt_ts() 直接写入平台下发值, 锚点事件与平台 ts 严格一致。
新增单测 test_time_anchor_exact, 7 例 ALL PASS
2026-08-04 18:10:18 +08:00
wangfq 2b1e888a73 refactor(vd960DBN): tcp_json_srv data_json[2048] 宏化 → TCP_JSON_DATA_BUF_LEN(1024)
三处 char data_json[2048] (sensor_report×2 + loop_param_query×1)
改用头文件宏 TCP_JSON_DATA_BUF_LEN, 默认 1024。

容量核验: format_sensor_json 4通道~660B / format_loop_param_json~920B
均 < 1024, format 函数有 snprintf 溢出保护; 最终帧受 MAX_FRAME=800 截断。
附带: 栈上缓冲 2048→1024 每处省 1KB
2026-08-04 17:06:29 +08:00
wangfq 5155219c92 fix(vd960DBN): 修复 offlog 编译错误 — 声明补齐 + RCC 宏移入公共头
MRS 编译报错:
1. offlog.c: SPI_Flash_Erase_Sector 隐式声明 → storage.h 补声明
2. peripheral_main.c: OFFLOG_RCC_RSTSCKR 未声明 → RCC 宏从 offlog.c
   移到 offlog.h (peripheral_main.c 只 include offlog.h)
3. offlog.c 删除本地重复 RCC 宏定义

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- also: set_response_tran_to_notify 加返回值
- also: uart_srv BLE 通知逻辑重构
2026-07-23 09:26:18 +08:00
wangfq 95a1d2b5d2 docs(roadmap): 板载W25Q32(4MB)确认, 分区与容量预算按实际重算
- 分区: 参数64KB / 事件流256KB(~8000条) / OTA暂存512KB / 快照区~3.2MB
- 快照容量三场景预算: 快档压满~4.4h / 繁忙车道~1.5天 / 一般车道~18天
- 补充延长保留的两个手段: 落盘与上报解耦 / 沿事件预触发环形
- 移除'W25Q型号确认'前置项
2026-07-17 19:04:28 +08:00
wangfq 70e44e6241 docs(roadmap): 补充脱机日志子系统 + 日志导出管理 + 远程OTA路线细化
- P1.2 脱机日志: W25Qxx 分区规划(参数/事件流/快照流/OTA暂存),
  事件流必录清单, 快照流同频落盘挂 iot_sensor_ingest 同源出口,
  无RTC时间戳方案(boot_seq+mstick+时钟锚点)
- P1.3 导出与管理: MQTT断网补报/命令分页拉取/BLE小程序读取
  三通道, log_stat/log_clear(鉴权+审计自记录)
- P1.4 远程OTA: 先走现网MQTT+复用ISP透传底层(与BLE OTA同构);
  Loop先存后刷+断点续传+回滚, 继电器安全底线;
  DBN自身远程OTA三路线比选(过渡态=仍走BLE最稳妥)
- 风险表新增: 无RTC时间戳、日志写与总线争用
2026-07-17 18:57:13 +08:00
wangfq ea29433633 docs: 新增 ROADMAP — V1.0.0 之后的开发计划 (P0稳固→P1上云→P2算法→P3会车)
- P0 (v1.1.0): BLE透传重构收尾(0xC0三消费方真值表) + TODO清剿
  (loop_ok硬编码/MQTT接收溢出防护) + 工装回归清单
- P1 (v1.2.x): edc_server消费契约(先落库后ACK)、流量API、
  远程OTA方案评审、4G通道预研
- P2 (v2.0.0): 抗变频器干扰(先现场采CAPVD取证)、双线圈测速+
  车型粗分类、半截车双峰识别、方向判别
- P3 (v2.x): 会车控制 — 业务只认event_report、异常分支先行、
  纯平台/纯单机/混合三方案对比
2026-07-17 18:13:29 +08:00
wangfq 579eeecb23 docs: 校准通信接口表述 — DLD960 无 RS485 接口
- 对外通信 = 网口(TCP JSON/MQTT) + 蓝牙(小程序/OTA) + 扩展TTL串口(预留4G上云, RFU)
- 内部双 MCU 经 UART TTL 互联 (0x7F @192000)
- 修正范围: README/CHANGELOG/技术规格书/产品手册/硬件资源文档概述
- acceptance-standard 中 RS-485 为通用验收检查项, 保留
2026-07-16 16:39:02 +08:00
wangfq d61ff830aa fix(vd960DBN): BUFF_STACK_SIZE 512→64 对齐 Loop 侧串口帧缓冲
- 串口帧缓冲装的是 Loop↔DBN 的 0x7F 私有协议帧, 与 Loop 侧发送缓冲(64)同源
- 当前最大帧 0x0C 多线圈上报实测 56B (Len=51), 64 留 8B 余量, 512 严重浪费
- g_pkg_uart_1/2 各省 448B, 合计回收 ~896B SRAM
- 顺带删除零引用死结构体 USART_DMA_UNIT + RX_BUFFER_LEN 宏
2026-07-16 14:52:16 +08:00
wangfq c0bdcd956d docs: 新增产品手册 + 技术规格书 (V1.00, 配套整机 V1.0.0)
- 产品手册: 接口/指示灯/线圈施工要求/快速上手/参数配置/故障排查
- 技术规格书: 双MCU架构/检测规格/灵敏度阈值表(SensTable换算)/算法机制/协议矩阵/配套要求
- README: 文档索引挂接产品文档
2026-07-16 10:31:10 +08:00
73 changed files with 14903 additions and 1084 deletions
+8
View File
@@ -3,6 +3,9 @@
*.i *.i
*.old *.old
# 测试生成物 (extract_offlog_cases.py 输出, 嵌入源文件真实代码)
offlog_cases_embedded.c
# Object files # Object files
*.o *.o
*.ko *.ko
@@ -89,3 +92,8 @@ wchreference/
__pycache__/ __pycache__/
*.pyc *.pyc
# gcc 单测编译产物 (tests/*.c 保留)
test_offlog
test_ble_offlog
test_snapshot
+143 -5
View File
@@ -5,6 +5,142 @@
--- ---
## vd960DBN V1.02.05 — 2026-08-21DBN 通信板单侧更新)
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,可正常配套)。涵盖 2026-08-20 联网稳定性修复系列 + 2026-08-21 MQTT 协议 V1.10 实现。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.05** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(0x7F / 0x9F OTA | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
| DLD960 IoT MQTT 协议 | **V1.10** |
| DLD960 BLE 协议 | V1.02(不变) |
### 🆕 MQTT 协议 V1.10:网络上报携带地感版本(配合远程 OTA 版本核对)
- `dev_info_query` 响应 / `initialize` 上报 data 新增 **`loop_ver`**(地感固件 `"主.次.次"`+ **`loop_hw_ver`**(地感硬件)——来自 Loop `0x4A` 查询缓存(上电自动查一次 + OTA done 后刷新),查询未完成/失败为空串
- 新增 §4.25 **`loop_version_query`** 命令:平台实时查询 Loop 版本(0x4A 异步 → 回包 `loop_ver`/`loop_hw_ver`/`version_str`),超时回 `code=5`
- 固件实现:`loop_uart_proto.c/h` 新增 `LoopVerCache` 版本缓存 + `lup_cache_from_info()`/`lup_refresh_version_cache()``iot_mqtt_srv.c` 新增 `iot_verq_poll()` 异步回包 + 后台刷新
- **单测 44 断言全过**(版本解析/缓存/异步回包时序,`tests/test_iot_loop_ver.c`
### 🔧 联网稳定性修复(2026-08-20 系列)
- **SocketSend 0x11 死循环**WCHNET `WCHNET_ERR_MEM(0x11)` 为瞬时发送缓冲不足(`WCHNET_NUM_TCP_SEG=2`)——改为 200ms 退避重试(≤10 次),不再 3 次强制断连;`SocketCreat` 失败置 `SocketId_TCP=0xFF` + 中止 Connect(不再 timeout 死循环)
- **OTA 下载失败**`ota_json_extract_hex()` 兼容 `json.dumps` 空格(`"data": "..."`),单片 CRC 全错根因闭环
- **OTA 刷写状态语义**:刷写成功状态回 `idle`(镜像保留可重刷)+ 补 `ota_report done/failed` 主动上报(平台判定主依据)
- **联网复位事故**:OTA 命令缓冲合并 unionRAM 90% 栈余量不足 → HardFault 复位闭环)
### 📋 已知约束
- UART2 0x4A 版本查询与 `loop_data`/事件上报共用总线(低频命令,理论无冲突)——**待板上验证时序**
- OTA done 后缓存刷新存在 Loop 复位时间窗(查询可能超时,可接受;平台可用 `loop_version_query` 主动核对)
- BLE→Loop OTA 透传模式无自动退出(V1.02.03 遗留,升级后需断电重启)
### 验证
- ✅ gcc 隔离单测:`test_iot_loop_ver.c` 44 例(V1.10+ 既有 ota/offlog/snapshot/mqtt 单测全过
- [ ] 待板级回归:MQTT 联网长稳、`loop_version_query` 实际回包、OTA 全流程
---
## vd960DBN V1.02.04 — 2026-08-19DBN 通信板单侧更新)
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.04** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(0x7F / 0x9F OTA | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
| DLD960 IoT MQTT 协议 | V1.07(不变) |
| DLD960 BLE 协议 | V1.02(不变) |
### 🔧 SPI Flash 存储适配修复
- **识别去厂商代码**`W25Qxx` 宏去 `0XEF` 前缀、`SPI_Flash_ReadJEDEC_ID()` 删厂商校验、`storage_init` 低字节匹配——兼容其他厂家同容量型号(如 0x1A 厂商)
- **恢复 factory 配置写入**:解除 2026-08-13 止血(根因 8-17 已闭环),换新空片(W25Q128)后不再每次上电进出厂初始化,配置读写恢复正常
- **板级验证通过**2026-08-19):W25Q128 配置读写正常
---
## vd960DBN V1.02.03 — 2026-08-19DBN 通信板单侧更新)
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议未变更,可正常配套)。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.03** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(MCU 间 0x7F / 0x9F OTA | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.03(不变) |
| DLD960 IoT MQTT 协议 | V1.07(不变) |
| DLD960 BLE 协议 | V1.02(不变) |
### 🔧 BLE→Loop OTA 升级修复(0x9F 帧透传回 BLE)
- **现象**UART2 RX DMA 改造后,通过 BLE 给地感(Loop MCUOTA 升级失效
- **根因**DMA poll 帧解析 `lup_feed_byte()` 只认 0x7FLoop bootloader 回的 0x9F 响应帧(pre_ok/addr_ok/data ACK)全部被吞;OTA 为**停等协议**0xA7 WITH_BACK 每块必回 ACK),工具等不到 ACK 升级卡死
- **修复**:新增 `lup_feed_byte_ota()` 0x9F 帧状态机(SUM 校验无 XOR);`uart2_dma_poll``g_flag_counter_ota.flag` 切换 0x9F/0x7F 解析器;`uart_srv` OTA 分支 0x9F 帧透传回 BLE + 清 flagOTA 溢出阈值放宽 + 收帧 tick 归零
- **验证**:gcc 隔离单测 8 断言全过;**板级 BLE OTA 全流程实测通过**2026-08-19
- **已知约束**:OTA 模式无自动退出机制(`g_flag_counter_ota.flag` 置 1 后需断电重启恢复 0x7F 通信),维持现状
### 📦 脱机日志快照流 + hex 上报(2026-08-18 已含,协议 V1.03/V1.07
- TCP/MQTT `log_*` 命令支持 `stream=snapshot` 传感快照流(复用命令 + stream 字段,不新增命令码)
- `log_query` records 改 **hex 原始字节**上报(OfflogEvt 32B→64 hex / SnapRec 64B→128 hex2 条快照 406B < 800B 缓冲)
- MQTT 查询命令补齐:`ssc_net_query` / `iot_net_query` / `iot_topic_query`(协议命令表全对齐)
---
## vd960DBN V1.02.01 — 2026-08-17DBN 通信板单侧更新)
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,协议均未变更,可正常配套)。
**配套版本矩阵**
| 组件 | 版本 |
|------|------|
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.01** |
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
| DLD960Loop 串口协议(MCU 间 0x7F | V1.05(不变) |
| DLD960 TCP JSON 协议(:5960 | V1.02(不变) |
| DLD960 IoT MQTT 协议 | V1.06(不变) |
| DLD960 BLE 协议 | V1.02(不变) |
### 🔴 P0 稳定性修复:频繁"复位"根因闭环(栈溢出)
- **现象**:恢复快照区功能后设备 100~180ms 一轮"复位"循环(RSTSCKR 无复位标志 = PC 跑飞,非硬件复位)
- **根因**`.bss ≈ 41.7KB` 挤占 RAM → 栈仅 ~1.9KB → `load_cfg/output_cfg` 的 printf 栈峰值触顶 → 覆盖返回地址 → PC 跑飞跳回 0 重启
- **修复**
- RAM 瘦身两轮:ETH_MAX_PACKET_SIZE 1520→768、RECE_BUF_LEN 1400→1024、ARP 表 50→16、MQTT 发送缓冲 1024→800、loop_data payload 1400→800 → **栈 1.9KB → 6.3KB**
- load_cfg 字符串 0 终止保险(防 Flash 配置字符串 %s 越界打印)
- 快照区初始化延后至开机 3s(避开启动早期 SPI 重负载窗口,非根因但减压)
### 🟢 可靠性:UART2 RX DMA 循环接收
- 根治遗留问题"PRINT 关中断(~7.4ms)期间 UART2 丢字节 → checksum fail1-3 分钟一次)"
- DMA1_Ch6 循环模式硬件收字节(不依赖 CPU 中断),主循环轮询消费;512B 环形缓冲 + 溢出保护
- 帧解析状态机移入主循环,消除中断竞争
### 🟢 资源与代码卫生
- .bss 46KB → 41.7KB;中断路径大数组 static 化(栈峰值 -1.8KB
- 调试代码结案清理:移除 BOOT_CNT 诊断打印、_dbg_cnt 心跳打印;保留 fault_diag 基础设施(未来疑难杂症复用)
### 📋 其他
- 固件版本三段式:`FIRMWARE_VER="1.02.01"`MAIN=1 SUB=2 SUBSUB=1
### 已知约束
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)
- BLE 上报版本为 MAIN/SUB 两字节(1.02),SUBSUB 仅字符串上报(MQTT/TCP JSON
- 快照区延后 3s 初始化:开机 3s 内传感数据不落盘(设计接受)
- UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)
### 验证
- ✅ 板级:无异常重启;MQTT159.75.137.141:1883)、BLE 广播、配置加载正常
- [ ] 待回归:UART2 DMA 长时间运行(checksum fail 消失)、TCP JSON 命令交互、快照 BLE 0x28/0x29/0x2A 查询
---
## V1.0.0 — 2026-07-16(首个正式发布) ## V1.0.0 — 2026-07-16(首个正式发布)
**配套版本矩阵** **配套版本矩阵**
@@ -14,9 +150,9 @@
| vd960Loop 固件 (AT32F421) | 1.0 | | vd960Loop 固件 (AT32F421) | 1.0 |
| vd960DBN 固件 (CH32V208, DLD960GA) | 1.0 | | vd960DBN 固件 (CH32V208, DLD960GA) | 1.0 |
| DLD960Loop 串口协议(MCU 间 0x7F | V1.05 | | DLD960Loop 串口协议(MCU 间 0x7F | V1.05 |
| DLD960 串口通信协议(RS485/TTL | V1.01 | | DLD960 串口通信协议(TTL | V1.01 |
| DLD960 TCP JSON 协议(:5960 | V1.01 | | DLD960 TCP JSON 协议(:5960 | V1.02 |
| DLD960 IoT MQTT 协议 | V1.05 | | DLD960 IoT MQTT 协议 | V1.06 |
> ⚠ 自协议 V1.05 起,Loop 固件与 DBN 固件必须同版本配套刷写,禁止混跑(variation 字段宽度变更导致帧格式不兼容)。 > ⚠ 自协议 V1.05 起,Loop 固件与 DBN 固件必须同版本配套刷写,禁止混跑(variation 字段宽度变更导致帧格式不兼容)。
@@ -37,11 +173,12 @@
### vd960DBN(通信 MCUCH32V208 ### vd960DBN(通信 MCUCH32V208
**TCP JSON 服务(:5960,协议 V1.01** **TCP JSON 服务(:5960,协议 V1.02**
- 密码鉴权 + 18 条命令(新增脱机日志 log_stat/log_query/log_clear
- 鉴权状态机 + 15 条命令;服务异常自动重启(3 条件 + 3 次限额 + 10min 冷却) - 鉴权状态机 + 15 条命令;服务异常自动重启(3 条件 + 3 次限额 + 10min 冷却)
- event_report 客户端必答:5s 超时重发同 msg_id/原始 ts ×3 - event_report 客户端必答:5s 超时重发同 msg_id/原始 ts ×3
**IoT MQTT(协议 V1.05** **IoT MQTT(协议 V1.06**
- 双主题 `dld960/{sn}/srv` + `dld960/{sn}/dev`,消息类型由 `cmd` 区分 - 双主题 `dld960/{sn}/srv` + `dld960/{sn}/dev`,消息类型由 `cmd` 区分
- 上电 initialize 上线消息(dev_serial/model/hard_ver/soft_ver - 上电 initialize 上线消息(dev_serial/model/hard_ver/soft_ver
- loop_data 三档调度:空闲按配置间隔 / 活动 300ms / **car_state 翻转沿立即上报** - loop_data 三档调度:空闲按配置间隔 / 活动 300ms / **car_state 翻转沿立即上报**
@@ -49,6 +186,7 @@
- 设备时钟同步方案B:平台经 report_config 下发 Unix ts,设备无 RTC 也能上报真实时间 - 设备时钟同步方案B:平台经 report_config 下发 Unix ts,设备无 RTC 也能上报真实时间
- 稳定性修复:mqtt_publish 缓冲溢出发垃圾包致 broker RST 风暴(现场 P0)、packet_id=0 断连、PINGREQ 保活、分批发送(MSS=576Publish≤500B - 稳定性修复:mqtt_publish 缓冲溢出发垃圾包致 broker RST 风暴(现场 P0)、packet_id=0 断连、PINGREQ 保活、分批发送(MSS=576Publish≤500B
- loop_data 陈旧快照修复:0xC0 帧双消费路径统一摄取,事件与缓存同源 - loop_data 陈旧快照修复:0xC0 帧双消费路径统一摄取,事件与缓存同源
- **脱机事件日志(W25Q32 环形 8064 条,V1.06 新增)**log_stat / log_query / log_clear 三命令;BOOT 复位原因、MQTT 连接/断开、event ACK 超时、线圈事件、时钟锚点共 10 类;掉电不丢
**基础设施** **基础设施**
- BLE 小程序配置 + OTALoop MCU ISP 串口透传升级 - BLE 小程序配置 + OTALoop MCU ISP 串口透传升级
+6
View File
@@ -0,0 +1,6 @@
venv/
__pycache__/
*.pyc
*.spec.bak
build/
dist/
+236
View File
@@ -8,6 +8,8 @@ DLD960 IoT MQTT 协议定义
import json import json
import time import time
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
@@ -61,6 +63,11 @@ CMD_LOOP_PARAM_SET = "loop_param_set"
CMD_LOOP_PARAM_QUERY = "loop_param_query" CMD_LOOP_PARAM_QUERY = "loop_param_query"
CMD_REPORT_CONFIG = "report_config" CMD_REPORT_CONFIG = "report_config"
# V1.06: 脱机事件日志
CMD_LOG_STAT = "log_stat"
CMD_LOG_QUERY = "log_query"
CMD_LOG_CLEAR = "log_clear"
# 设备上报 # 设备上报
CMD_LOOP_DATA = "loop_data" CMD_LOOP_DATA = "loop_data"
CMD_EVENT_REPORT = "event_report" CMD_EVENT_REPORT = "event_report"
@@ -71,6 +78,11 @@ CMD_INITIALIZE = "initialize" # V1.03: 设备上电初始化
CONFIG_SET_COMMANDS = {CMD_DEV_SERIAL_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_LOOP_PARAM_SET} CONFIG_SET_COMMANDS = {CMD_DEV_SERIAL_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_LOOP_PARAM_SET}
CONFIG_QUERY_COMMANDS = {CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY} CONFIG_QUERY_COMMANDS = {CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY}
CTRL_COMMANDS = {CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_REPORT_CONFIG} CTRL_COMMANDS = {CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_REPORT_CONFIG}
LOG_COMMANDS = {CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR} # V1.06 脱机日志
# V1.07: log_* 命令日志流 (与 BLE 0x28/0x2A 同语义)
STREAM_EVENT = "event"
STREAM_SNAPSHOT = "snapshot"
# ============================================================ # ============================================================
@@ -104,6 +116,29 @@ FREQ_LEVELS = ["high", "mid_high", "mid_low", "low"]
OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"] OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"]
EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"] EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"]
# 脱机事件日志类型描述 (log_query 响应 records[].type, MQTT V1.06)
LOG_EVENT_TYPE_DESC = {
"boot": "上电/复位",
"iot_connect": "MQTT TCP 连接成功",
"iot_ready": "MQTT 订阅完成→发 initialize",
"iot_disconnect": "MQTT 断连",
"iot_reconn": "重连退避",
"evt_retry": "event ACK 超时重发",
"evt_giveup": "event 重试耗尽挂起",
"coil": "线圈事件",
"time_anchor": "时钟同步锚点",
"log_clear": "日志清除(审计)",
"unknown": "未知类型",
}
# 传感快照 channels[].misc_type 描述 (log_query stream=snapshot, MQTT V1.07)
SNAP_MISC_TYPE_DESC = {
"time": "时间量(50ms)",
"cut_count": "线圈断开次数",
"flow_count": "车流量",
"relay_count": "继电器输出次数",
}
# ============================================================ # ============================================================
# 消息构建 # 消息构建
@@ -214,6 +249,119 @@ def data_report_config(sensor_type: int = 12, enable: bool = True,
} }
def data_log_query(start_seq: int = 1, count: int = 4, stream: str = STREAM_EVENT) -> dict:
"""V1.07 log_query 分页拉取脱机日志
stream=event: 事件流, count 上限 4 (设备侧超限按 4 处理)
stream=snapshot: 快照流, count 上限 2 (hex 原始字节上报); 显式传 stream 字段
"""
d: dict[str, Any] = {
"start_seq": max(1, int(start_seq)),
"count": min(4, max(1, int(count))),
}
if stream == STREAM_SNAPSHOT:
d["stream"] = STREAM_SNAPSHOT
d["count"] = min(2, max(1, int(count)))
return d
def data_log_clear(stream: str = STREAM_EVENT) -> dict:
"""V1.07 log_clear 清除脱机日志 (审计留痕)
stream=snapshot: 快照流清除 (~45ms, 审计写事件流)
event 流省略 data (设备缺省 event, 兼容老固件)
"""
if stream == STREAM_SNAPSHOT:
return {"stream": STREAM_SNAPSHOT}
return {}
# ============================================================
# OTA 远程升级 (V1.08: Loop MCU 先存后刷, ROADMAP P1.4 ①)
# 协议依据《DLD960_IoT_MQTT协议.md》§4.19~4.24 / §5.5
# ============================================================
CMD_OTA_BEGIN = "ota_begin"
CMD_OTA_DATA = "ota_data"
CMD_OTA_END = "ota_end"
CMD_OTA_ABORT = "ota_abort"
CMD_OTA_FLASH = "ota_flash"
CMD_OTA_STATUS = "ota_status"
CMD_OTA_REPORT = "ota_report" # dev→srv 主动上报
OTA_TARGET_LOOP = "loop"
OTA_CHUNK_SIZE = 256 # 单片 256B(协议常量, 不接受协商)
OTA_MAX_SIZE = 96 * 1024 # 镜像上限 96KB (Slot 100KB - 4KB 边界余量)
OTA_SLOT_A = "a"
OTA_SLOT_B = "b"
OTA_STATE_DESC = {
"idle": "空闲(镜像保留,已刷写完成则 idle+size>0)",
"downloading": "下载中",
"ready": "校验通过(可刷写)",
"flashing": "刷写中",
"flash_failed": "刷写失败",
"aborted": "已中止",
}
OTA_STAGE_DESC = {
"begin": "会话开启",
"downloading": "分片落盘",
"ready": "校验通过",
"flashing": "刷写中",
"done": "刷写成功(Loop 已重启)",
"failed": "刷写失败",
}
OTA_ERROR_DESC = {
0x1001: "启动帧无响应",
0x1002: "地址帧错误",
0x1003: "数据块 ACK 超限",
0x1004: "全镜像校验失败",
0x1005: "安全窗口拒绝后强制失败",
}
def ota_crc32(data: bytes) -> int:
"""CRC-32/ISO-HDLC (协议 §4.19.1; zlib.crc32 即此算法, 设备查表法一致)"""
return zlib.crc32(data) & 0xFFFFFFFF
def ota_split_bin(data: bytes, chunk_size: int = OTA_CHUNK_SIZE) -> list[tuple[int, int, str]]:
"""bin → 分片列表 [(offset, crc32, hex_str), ...] (单片 256B, hex 512 字符)"""
if len(data) > OTA_MAX_SIZE:
raise ValueError(f"镜像 {len(data)}B 超上限 {OTA_MAX_SIZE}B (96KB)")
return [(off, ota_crc32(data[off:off + chunk_size]), data[off:off + chunk_size].hex())
for off in range(0, len(data), chunk_size)]
def data_ota_begin(size: int, crc32: int, version: str = "",
target: str = OTA_TARGET_LOOP, force: bool = False) -> dict:
"""ota_begin 请求 data (开启会话 / 断点续传定位)"""
return {"target": target, "size": size, "crc32": crc32,
"version": version, "force": force}
def data_ota_data(offset: int, crc32: int, data_hex: str,
target: str = OTA_TARGET_LOOP) -> dict:
"""ota_data 请求 data (分片下发, offset 256 对齐)"""
return {"target": target, "offset": offset, "crc32": crc32, "data": data_hex}
def data_ota_end(crc32: int, target: str = OTA_TARGET_LOOP) -> dict:
"""ota_end 请求 data (结束下载, 全镜像 CRC32 复核)"""
return {"target": target, "crc32": crc32}
def data_ota_abort(target: str = OTA_TARGET_LOOP) -> dict:
"""ota_abort 请求 data (中止会话, 释放暂存)"""
return {"target": target}
def data_ota_flash(slot: str = OTA_SLOT_A, force: bool = False,
target: str = OTA_TARGET_LOOP) -> dict:
"""ota_flash 请求 data (触发本地 ISP 刷写, 仅 ready 态)"""
return {"target": target, "slot": slot, "force": force}
# ============================================================ # ============================================================
# 解析设备上报 # 解析设备上报
# ============================================================ # ============================================================
@@ -224,3 +372,91 @@ def parse_topic_dev_serial(topic: str) -> Optional[str]:
if len(parts) >= 2 and parts[0] == "dld960": if len(parts) >= 2 and parts[0] == "dld960":
return parts[1] return parts[1]
return None return None
# ============================================================
# 脱机日志 hex 解析 (V1.07: log_query 响应 records[].hex 原始字节)
# 字段表与《DLD960 BLE 协议》§6.4 (SnapRec) / §7 (OfflogEvt) 一致
# ============================================================
# OfflogEvt 事件类型 (BLE 协议 §7 事件类型表)
OFFLOG_TYPE_NAMES = {
0x01: "boot", 0x10: "iot_connect", 0x11: "iot_ready", 0x12: "iot_disconnect",
0x13: "iot_reconn", 0x30: "evt_retry", 0x31: "evt_giveup", 0x40: "coil",
0x50: "time_anchor", 0x70: "log_clear",
}
_OFFLOG_SUB_NAMES = {1: "car_enter", 2: "car_leave", 3: "loop_cut", 4: "loop_restore"}
_OFFLOG_DISC_REASONS = {1: "断开", 2: "超时", 3: "CONNACK拒绝", 4: "连接超时"}
def parse_offlog_hex(h: str) -> dict:
"""OfflogEvt 32B hex → dict (magic/type/seq/ts_ms/unix_ts/boot_seq/payload)"""
b = bytes.fromhex(h)
if len(b) != 32:
return {"error": f"bad len {len(b)}"}
magic, etype, elen, flags, seq, ts_ms, unix_ts, boot_seq, rsvd, payload = \
struct.unpack('<BBBBIIIHH12s', b)
return {"magic": magic, "type": etype, "len": elen, "flags": flags, "seq": seq,
"ts_ms": ts_ms, "unix_ts": unix_ts, "boot_seq": boot_seq, "payload": payload}
def offlog_payload_desc(etype: int, payload: bytes) -> str:
"""事件 payload 人类可读描述 (BLE 协议 §7 payload 定义, 大端)"""
if etype == 0x01: # boot: 复位原因寄存器
rst = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
bits = [n for n, b in [("LPWR", 31), ("WWDG", 30), ("IWDG", 29),
("SFT", 28), ("POR", 27), ("NRST", 26)] if rst & (1 << b)]
return f"rst=0x{rst:08X} ({'+'.join(bits) if bits else 'none'})"
if etype == 0x12: # iot_disconnect
r = payload[0] if payload else 0
return f"reason={r} ({_OFFLOG_DISC_REASONS.get(r, '?')})"
if etype == 0x13: # iot_reconn
return f"backoff_ms={int.from_bytes(payload[0:4], 'big')}" if len(payload) >= 4 else ""
if etype == 0x30: # evt_retry
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
retry = payload[4] if len(payload) >= 5 else 0
return f"msg_id={mid} retry={retry}"
if etype == 0x31: # evt_giveup
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
return f"msg_id={mid}"
if etype == 0x40: # coil
sub = _OFFLOG_SUB_NAMES.get(payload[0], payload[0]) if payload else "?"
ch = payload[1] if len(payload) >= 2 else 0
val = int.from_bytes(payload[2:6], 'big') if len(payload) >= 6 else 0
return f"sub={sub} ch={ch} value={val}"
if etype in (0x10, 0x11, 0x50, 0x70): # 无 payload
return ""
return payload.hex() if payload else ""
def parse_snap_hex(h: str) -> dict:
"""SnapRec 64B hex → dict (seq/boot_seq/ts_ms/coil_count/channels[])"""
b = bytes.fromhex(h)
if len(b) != 64:
return {"error": f"bad len {len(b)}"}
magic, slen, flags, rsvd, seq, ts_ms, boot_seq, rsvd2, coils = \
struct.unpack('<BBBBIIHH48s', b)
coil_count = min(slen // 12, 4)
channels = []
for c in range(coil_count):
p = coils[c * 12:(c + 1) * 12]
cfg, cond = p[0], p[1]
freq = p[2] | (p[3] << 8) | (p[4] << 16)
var = p[5] | (p[6] << 8) | (p[7] << 16)
if var & 0x800000:
var -= 0x1000000
misc = int.from_bytes(p[8:12], 'little')
channels.append({
"ch": c + 1,
"freq_level": ("high", "mid_high", "mid_low", "low")[(cfg >> 6) & 3],
"direction": (cfg >> 5) & 1,
"freq_type": (cfg >> 4) & 1,
"sensitivity": cfg & 0x0F,
"condition": (cond >> 4) & 0x0F,
"loop_ok": not ((cond >> 3) & 1),
"has_car": bool((cond >> 2) & 1),
"misc_type": ("time", "cut_count", "flow_count", "relay_count")[cond & 3],
"freq": freq, "variation": var, "misc": misc,
})
return {"seq": seq, "boot_seq": boot_seq, "ts_ms": ts_ms,
"coil_count": coil_count, "channels": channels}
+125
View File
@@ -6,6 +6,104 @@
--- ---
## 2026-08-20 — OTA 页签:Loop 远程 OTA 分片下载(协议 V1.08 工具先行)
### 背景
ROADMAP P1.4 ①:Loop MCU (AT32F421) 远程 OTA,先存后刷。协议设计稿 V1.01 已定(`docs/DLD960_MQTT_OTA协议.md`),并入 MQTT 协议 V1.08。按"工具先行,固件后到"惯例,先在 DBNMQTTool 实现 OTA 页签 + 协议单测,验证协议自洽后再动固件。
### 变更
- **protocol.py**:新增 OTA 模块——`CMD_OTA_*` 命令常量、`ota_crc32`CRC-32/ISO-HDLC = zlib.crc32,标准向量 0xCBF43926)、`ota_split_bin`256B/片 hex96KB 上限)、`data_ota_begin/data/end/abort/flash` 构建器、状态/阶段/错误码中文描述
- **main.py**:新增 **OTA 页签**——固件 bin 选择(≤96KB 校验 + 全镜像 CRC32 显示)、目标版本/force/slot 参数、①下载(begin→data×N→end`OtaDownloadThread` QThread 后台 + 响应队列,断点续传/缺片重定位/CRC 错重发)②刷写(ota_flash 二次确认)③中止 ④查询状态;进度条 + 状态日志(ota_report/ota_status 实时显示)
- **tests/test_ota_protocol.py**22 断言——CRC32 标准向量、分片边界(1B/256B/257B/超限)、构建器字段、**OtaDeviceSim 设备侧状态机**(顺序/幂等/乱序拒绝/单片 CRC/全镜像复核/断点续传)+ 完整下载流(正常/缺片补发)
### 验证
- `venv/bin/python -m unittest tests.test_ota_protocol`22 全过
- offscreen MainWindow 冒烟:OTA tab 构建正常、控件齐全、512B bin → 2 片 CRC 正确
- 协议规则自洽性验证:设备模拟器按文档规则(offset==received 顺序片 / offset<received 幂等 / offset>received 拒收 code=1)与工具分片流程对跑,断点续传与缺片重定位全通
### 已知约束
- 下载线程每片停等 ACK(~512 片 × RTT);量产平台可改窗口并发,协议层无需变更
- 固件未实现 ota_* 前,工具下发会收 `code=4 unsupported`——能力探测见协议 §4.19
---
## 2026-08-18 — log_query 响应改 hex 原始字节解析展示(协议 V1.07 修订)
### 背景
MQTT 快照流实测 `MQTTSerialize_publish failed`——JSON 化快照记录超 800B 发送缓冲。协议修订为**原始字节 hex 上报**(与 BLE 通道同语义),工具同步改为 hex 解析。
### 变更
- **protocol.py**:新增 `parse_offlog_hex`OfflogEvt 32B`<BBBBIIIHH12s`)、`parse_snap_hex`SnapRec 64B`<BBBBIIHH48s`)、`offlog_payload_desc`boot 复位位/coil/evt_retry/evt_giveup/iot_disconnect/reconn)、`OFFLOG_TYPE_NAMES`——字段表与 BLE 协议 §6.4/§7 一致
- `data_log_query` snapshot count 恢复 **2**hex 体积可控)
- **main.py**`_apply_log_query_data` 解析 `records[].hex`hex 长度 128=快照 / 64=事件);事件流展示 type 描述 + payload 可读化,快照流逐通道字段
### 验证
- 构造事件/快照二进制→hex→解析回断言全过(POR/SFT 复位位、coil payload、evt_retry、快照 4ch、负 variation 符号扩展)
- 2 条快照 hex 响应 payload = 406B < 800B
- offscreen UI:事件流(上电/复位、线圈事件、event 重发)+ 快照流(4ch/2ch)展示 + 翻页正常
---
## 2026-08-18 — 脱机日志翻页:上一页/下一页 + 自动填充起始序号
### 变更
- 脱机日志区按钮行新增 **◀ 上一页 / 下一页 ▶**(初始禁用,拉取成功后启用)
- **下一页**:起始序号 = 当前页最后一条 `seq` + 1(锚点法,不依赖请求条数,快照流 2 条/事件流 4 条均精确);空记录兜底按 `start + count` 步进
- **上一页**:起始序号 = `max(1, 当前起始 - 条数)`,边界保护
- 翻页后自动填充起始序号 SpinBox 并触发拉取;页锚点在 `_apply_log_query_data` 更新(`_log_page_last_seq`
### 验证
- offscreen 实测 6 场景全过:初始禁用 / 锚点=103 / 下一页→104 / 上一页→100 / 边界→1 / 空记录步进→504 / 快照流 2 条→12
---
## 2026-08-18 — 命令集对齐:禁用固件未实现的 MQTT 命令按钮
### 背景
实测发现 MQTT 通道 `ssc_net_query` / `iot_net_query` 返回 `code=4 unsupported command`——固件 MQTT 分发只实现 6 条命令,工具按协议文档命令表做了全按钮。
### 变更
- **固件同步补齐**vd960DBN V4.2):`ssc_net_query` / `iot_net_query` / `iot_topic_query` 三条查询命令已实现,工具对应查询按钮保持可用
- **工具禁用**固件仍未实现的命令按钮:`ssc_net_set` / `iot_net_set` / `iot_topic_set` / `pwd_set` / `factory_reset` / `device_reset``setEnabled(False)` + tooltip 引导走 TCP JSON/BLE 通道)
- 设备刷新列表移除 `CMD_LOOP_PARAM_QUERY`(固件未实现,刷新时不再报 error)
### 验证
- offscreen 实例化:6 个按钮禁用 + tooltip 正确,查询类/验证密码/report_config/log_* 可用
---
## 2026-08-18 — 脱机日志支持快照流 (stream=snapshot, 协议 V1.07)
### 变更
- **protocol.py**:新增 `STREAM_EVENT` / `STREAM_SNAPSHOT` 常量;`data_log_query``stream` 参数(快照流显式传 `stream` 字段 + count≤2);新增 `data_log_clear(stream)`(事件流省略 data 兼容老固件);新增 `SNAP_MISC_TYPE_DESC`time/cut_count/flow_count/relay_count 中文描述)
- **main.py**:脱机日志区加"日志流"选择器(事件日志/传感快照);统计/拉取/清除三按钮按当前流发请求(log_clear 确认框提示阻塞时长 2.8s/45ms);`log_stat`/`log_query` 响应展示区分流——快照记录解析 `channels[]` 逐字段(freq_level/direction/freq_type/sens/cond/loop/car/freq/variation/misc
### 验证
- protocol 数据构建器断言 6 例全过(事件流不带 stream / 快照流 count≤2 / log_clear 流参数)
- `QT_QPA_PLATFORM=offscreen` 实例化 MainWindow 实测:`log_stat(snapshot)` capacity=48064 展示、`log_query(snapshot)` 2 条快照记录逐字段展示(含负 variation、misc_type 中文)、事件流展示回归无损、空记录边界
- 依赖:venv + PySide6 + paho-mqtt`venv/` 已加 .gitignore
### 配套
- 固件 vd960DBN `log_*` stream=snapshot 分支(同日实现,devlog V4.1
- 协议文档:TCP JSON V1.03 / IoT MQTT V1.07
---
## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465` ## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465`
### 1. 模拟上报 Tab ### 1. 模拟上报 Tab
@@ -139,3 +237,30 @@ DBNMQTTool/
- **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关 - **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关
- `right` 局部变量 → `self._notebook`,修复 tab 引用丢失 - `right` 局部变量 → `self._notebook`,修复 tab 引用丢失
---
## 2026-08-05 — 脱机事件日志命令支持 (MQTT V1.06)
配合 vd960DBN 固件 P1.3commit 1a01316+ 协议 V1.06,工具新增脱机事件日志面板(参数配置 Tab 底部):
### 新增命令
| 命令 | 按钮 | 说明 |
|------|------|------|
| `log_stat` | 统计 | 日志统计/分页定位: enabled/boot_seq/count/capacity/seq_first/seq_last |
| `log_query` | 拉取日志 | 按全局序号分页拉取, 起始序号 + 条数(1~4) 可调 |
| `log_clear` | 清除日志 | 二次确认弹窗, 审计留痕不可撤销 |
### 实现要点
- `protocol.py`: 新增 `CMD_LOG_*` 常量、`LOG_COMMANDS` 集合、`data_log_query()` 构建器(start_seq≥1 / count 1~4 边界钳制)、`LOG_EVENT_TYPE_DESC` 事件类型中文描述(10 类 + unknown
- `main.py`: 参数配置 Tab 新增 g6 组(起始序号/条数 spinbox + 三按钮 + 只读显示区 QPlainTextEdit
- `_on_message` code==0 分支新增 `log_stat`/`log_query`/`log_clear` 响应处理
- 显示优化: log_query 记录带事件类型中文描述; unix_ts 已同步显示真实时间, 未同步显示 `boot+ts_ms(未同步)`
- 清除日志前 QMessageBox 二次确认(防误触, 与出厂初始化/设备复位同模式)
### 验证
- `py_compile` 四个模块通过
- protocol 逻辑单测: data_log_query 边界钳制 (0→1, 99→4, 0→1)、命令枚举一致性、build_request 带 data
+528 -4
View File
@@ -7,6 +7,8 @@ DBN MQTT Tool — DLD960 IoT MQTT 设备管理工具
import sys import 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
@@ -31,13 +34,136 @@ from dbn_mqtt_tool.protocol import (
CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET,
CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET,
CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE, CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE,
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR,
STREAM_EVENT, STREAM_SNAPSHOT,
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC, SNAP_MISC_TYPE_DESC,
OFFLOG_TYPE_NAMES, parse_offlog_hex, offlog_payload_desc, parse_snap_hex,
data_ssc_net_set, data_iot_net_set, data_iot_topic_set, data_ssc_net_set, data_iot_net_set, data_iot_topic_set,
data_pwd_verify, data_pwd_set, data_report_config, data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
build_request, next_msg_id, build_request, next_msg_id,
topic_down, topic_up, topic_down, topic_up,
# 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)
@@ -88,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")
@@ -194,6 +321,8 @@ class MainWindow(QMainWindow):
b1.clicked.connect(self._query_ssc_net) b1.clicked.connect(self._query_ssc_net)
b2 = QPushButton("设置") b2 = QPushButton("设置")
b2.clicked.connect(self._set_ssc_net) b2.clicked.connect(self._set_ssc_net)
b2.setEnabled(False)
b2.setToolTip("MQTT 固件暂未实现 ssc_net_set,请用 TCP JSON 或蓝牙通道配置")
btn_row.addWidget(b1) btn_row.addWidget(b1)
btn_row.addWidget(b2) btn_row.addWidget(b2)
btn_row.addStretch() btn_row.addStretch()
@@ -222,6 +351,8 @@ class MainWindow(QMainWindow):
b3.clicked.connect(self._query_iot_net) b3.clicked.connect(self._query_iot_net)
b4 = QPushButton("设置") b4 = QPushButton("设置")
b4.clicked.connect(self._set_iot_net) b4.clicked.connect(self._set_iot_net)
b4.setEnabled(False)
b4.setToolTip("MQTT 固件暂未实现 iot_net_set,请用 TCP JSON 或蓝牙通道配置")
btn_row2.addWidget(b3) btn_row2.addWidget(b3)
btn_row2.addWidget(b4) btn_row2.addWidget(b4)
btn_row2.addStretch() btn_row2.addStretch()
@@ -249,6 +380,8 @@ class MainWindow(QMainWindow):
b5.clicked.connect(self._query_iot_topic) b5.clicked.connect(self._query_iot_topic)
b6 = QPushButton("设置") b6 = QPushButton("设置")
b6.clicked.connect(self._set_iot_topic) b6.clicked.connect(self._set_iot_topic)
b6.setEnabled(False)
b6.setToolTip("MQTT 固件暂未实现 iot_topic_set,请用 TCP JSON 或蓝牙通道配置")
btn_row3.addWidget(b5) btn_row3.addWidget(b5)
btn_row3.addWidget(b6) btn_row3.addWidget(b6)
btn_row3.addStretch() btn_row3.addStretch()
@@ -276,6 +409,9 @@ class MainWindow(QMainWindow):
]: ]:
btn = QPushButton(label) btn = QPushButton(label)
btn.clicked.connect(slot) btn.clicked.connect(slot)
if label != "验证密码":
btn.setEnabled(False)
btn.setToolTip(f"MQTT 固件暂未实现 {label},请用 TCP JSON 或蓝牙通道")
btn_row4.addWidget(btn) btn_row4.addWidget(btn)
btn_row4.addStretch() btn_row4.addStretch()
g4_layout.addLayout(btn_row4, 1, 0, 1, 4) g4_layout.addLayout(btn_row4, 1, 0, 1, 4)
@@ -326,6 +462,61 @@ class MainWindow(QMainWindow):
g5_layout.addLayout(btn_row5, 2, 0, 1, 6) g5_layout.addLayout(btn_row5, 2, 0, 1, 6)
layout.addWidget(g5) layout.addWidget(g5)
# -- 脱机日志 (V1.06 事件流 / V1.07 快照流) --
g6 = QGroupBox("脱机日志 (log_stat / log_query / log_clear)")
g6_layout = QGridLayout(g6)
g6_layout.addWidget(QLabel("日志流:"), 0, 0)
self._combo_log_stream = QComboBox()
self._combo_log_stream.addItem("事件日志 (event)", STREAM_EVENT)
self._combo_log_stream.addItem("传感快照 (snapshot)", STREAM_SNAPSHOT)
self._combo_log_stream.setCurrentIndex(0)
g6_layout.addWidget(self._combo_log_stream, 0, 1)
g6_layout.addWidget(QLabel("起始序号:"), 0, 2)
self._edit_log_start_seq = QSpinBox()
self._edit_log_start_seq.setRange(1, 2**31 - 1)
self._edit_log_start_seq.setValue(1)
g6_layout.addWidget(self._edit_log_start_seq, 0, 3)
g6_layout.addWidget(QLabel("条数:"), 0, 4)
self._edit_log_count = QSpinBox()
self._edit_log_count.setRange(1, 4)
self._edit_log_count.setValue(4)
g6_layout.addWidget(self._edit_log_count, 0, 5)
btn_row6 = QHBoxLayout()
b9 = QPushButton("统计")
b9.clicked.connect(self._query_log_stat)
self._btn_log_prev = QPushButton("◀ 上一页")
self._btn_log_prev.clicked.connect(self._page_log_prev)
self._btn_log_prev.setEnabled(False)
self._btn_log_prev.setToolTip("起始序号 = 当前页第一条 - 条数(下限 1)")
b10 = QPushButton("拉取日志")
b10.clicked.connect(self._query_log)
self._btn_log_next = QPushButton("下一页 ▶")
self._btn_log_next.clicked.connect(self._page_log_next)
self._btn_log_next.setEnabled(False)
self._btn_log_next.setToolTip("起始序号 = 当前页最后一条 seq + 1")
b11 = QPushButton("清除日志")
b11.clicked.connect(self._clear_log)
btn_row6.addWidget(b9)
btn_row6.addWidget(self._btn_log_prev)
btn_row6.addWidget(b10)
btn_row6.addWidget(self._btn_log_next)
btn_row6.addWidget(b11)
btn_row6.addStretch()
g6_layout.addLayout(btn_row6, 1, 0, 1, 4)
self._log_rec_text = QPlainTextEdit()
self._log_rec_text.setReadOnly(True)
# 脱机日志翻页状态 (翻页锚点)
self._log_page_last_seq = 0 # 当前页最后一条 seq; 0 = 无有效页
self._log_rec_text.setMaximumHeight(150)
self._log_rec_text.setFont(QFont("Consolas", 9))
g6_layout.addWidget(self._log_rec_text, 2, 0, 1, 4)
layout.addWidget(g6)
layout.addStretch() layout.addStretch()
return w return w
@@ -362,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)))
# ================================================================ # ================================================================
# 模拟设备上报 # 模拟设备上报
# ================================================================ # ================================================================
@@ -504,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",
@@ -873,7 +1252,21 @@ class MainWindow(QMainWindow):
self._show_json({cmd: data}) self._show_json({cmd: data})
elif cmd == CMD_REPORT_CONFIG: elif cmd == CMD_REPORT_CONFIG:
self._apply_report_config_data(data) self._apply_report_config_data(data)
elif cmd == CMD_LOG_STAT:
self._apply_log_stat_data(data)
elif cmd == CMD_LOG_QUERY:
self._apply_log_query_data(data)
elif cmd == CMD_LOG_CLEAR:
self._log(f"脱机日志已清除 (审计留痕, 需重新统计确认)")
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:
@@ -909,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', '?')}")
@@ -967,7 +1366,7 @@ class MainWindow(QMainWindow):
if not sn: if not sn:
return return
for cmd in [CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, for cmd in [CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY,
CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY]: CMD_IOT_TOPIC_QUERY]:
try: try:
self._mqtt.send_command(sn, cmd) self._mqtt.send_command(sn, cmd)
except Exception: except Exception:
@@ -1078,6 +1477,131 @@ class MainWindow(QMainWindow):
timeout=self._edit_report_timeout.value(), timeout=self._edit_report_timeout.value(),
)) ))
# ---- 脱机日志 (V1.06 事件流 / V1.07 快照流) ----
def _selected_log_stream(self) -> str:
"""当前 UI 选择的日志流 (event/snapshot)"""
return self._combo_log_stream.currentData() or STREAM_EVENT
def _query_log_stat(self):
"""log_stat: 日志统计/分页定位 (按当前流)"""
stream = self._selected_log_stream()
data = {"stream": STREAM_SNAPSHOT} if stream == STREAM_SNAPSHOT else None
self._send_cmd(CMD_LOG_STAT, data)
def _query_log(self):
"""log_query: 按全局序号分页拉取 (事件流 count≤4 / 快照流 count≤2)"""
self._send_cmd(CMD_LOG_QUERY, data_log_query(
start_seq=self._edit_log_start_seq.value(),
count=self._edit_log_count.value(),
stream=self._selected_log_stream(),
))
def _page_log_prev(self):
"""上一页: 起始序号 = max(1, 当前起始 - 条数), 自动拉取"""
start = self._edit_log_start_seq.value()
count = self._edit_log_count.value()
new_start = max(1, start - count)
self._edit_log_start_seq.setValue(new_start)
self._log(f"翻页: 上一页 → 起始序号 {new_start}")
self._query_log()
def _page_log_next(self):
"""下一页: 起始序号 = 当前页最后一条 seq + 1, 自动拉取"""
if self._log_page_last_seq > 0:
new_start = self._log_page_last_seq + 1
else:
# 无有效页锚点 (拉取返回空), 按页大小步进
new_start = self._edit_log_start_seq.value() + self._edit_log_count.value()
self._edit_log_start_seq.setValue(new_start)
self._log(f"翻页: 下一页 → 起始序号 {new_start}")
self._query_log()
def _clear_log(self):
"""log_clear: 清除脱机日志 (按当前流, 审计留痕, 设备侧阻塞)"""
stream = self._selected_log_stream()
target = "传感快照" if stream == STREAM_SNAPSHOT else "脱机事件日志"
block = "~45ms (逻辑清除+当前扇区)" if stream == STREAM_SNAPSHOT else "~2.8s (63 扇区擦除)"
r = QMessageBox.question(self, "确认",
f"确定要清除设备{target}吗?\n"
f"清除动作本身会写入审计记录,不可撤销!\n"
f"设备侧阻塞约 {block}",
QMessageBox.Yes | QMessageBox.No)
if r == QMessageBox.Yes:
self._send_cmd(CMD_LOG_CLEAR, data_log_clear(stream))
def _apply_log_stat_data(self, data: dict):
stream = data.get("stream", "event")
stream_name = "传感快照" if stream == "snapshot" else "事件日志"
lines = [
f"日志统计[{stream_name}]: enabled={data.get('enabled', False)} boot_seq={data.get('boot_seq', 0)}",
f"记录数: {data.get('count', 0)} / {data.get('capacity', 0)}",
f"全局序号范围: {data.get('seq_first', 0)} ~ {data.get('seq_last', 0)}",
f"提示: 拉取日志用 log_query, 起始序号填 seq_first"
+ (" (快照流 count 上限 2)" if stream == "snapshot" else ""),
]
self._log_rec_text.setPlainText("\n".join(lines))
self._log(f"log_stat[{stream_name}]: count={data.get('count', 0)} seq={data.get('seq_first', 0)}~{data.get('seq_last', 0)}")
def _apply_log_query_data(self, data: dict):
records = data.get("records", [])
start_seq = data.get("start_seq", 0)
# V1.07: 记录为 {"seq":N,"hex":"..."} 原始字节; hex 长度 128=快照(64B) / 64=事件(32B)
is_snap = bool(records) and len(records[0].get("hex", "")) == 128
stream_name = "传感快照" if is_snap else "事件日志"
lines = [f"log_query[{stream_name}] start_seq={start_seq} → 返回 {len(records)} 条:"]
for rec in records:
hex_str = rec.get("hex", "")
if is_snap:
s = parse_snap_hex(hex_str)
if "error" in s:
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {s['error']}")
continue
lines.append(
f" seq={s['seq']} boot={s['boot_seq']} "
f"boot+{s['ts_ms']}ms ({s['coil_count']}ch)"
)
for ch in s["channels"]:
mt = ch["misc_type"]
misc_desc = SNAP_MISC_TYPE_DESC.get(mt, mt)
lines.append(
f" ch{ch['ch']}: {ch['freq_level']} "
f"dir={ch['direction']} ftype={ch['freq_type']} "
f"sens={ch['sensitivity']} cond={ch['condition']} "
f"loop={'OK' if ch['loop_ok'] else ''} "
f"car={'' if ch['has_car'] else ''} "
f"freq={ch['freq']}Hz Δ={ch['variation']} "
f"[{misc_desc}] misc={ch['misc']}"
)
else:
e = parse_offlog_hex(hex_str)
if "error" in e:
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {e['error']}")
continue
tname = OFFLOG_TYPE_NAMES.get(e["type"], f"0x{e['type']:02x}")
desc = LOG_EVENT_TYPE_DESC.get(tname, tname)
if e["unix_ts"]:
ts_str = datetime.fromtimestamp(e["unix_ts"]).strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = f"boot+{e['ts_ms']}ms (未同步)"
pdesc = offlog_payload_desc(e["type"], e["payload"])
lines.append(
f" seq={e['seq']} boot={e['boot_seq']} "
f"{ts_str} [{desc}] {pdesc}"
)
if not records:
lines.append(" (无记录 / 起始序号越界, 可先用 log_stat 确认范围)")
self._log_rec_text.setPlainText("\n".join(lines))
self._log(f"log_query[{stream_name}]: start_seq={start_seq} got {len(records)}")
# 更新翻页状态: 锚点 = 当前页最后一条 seq; 拉取成功即启用翻页按钮
if records:
self._log_page_last_seq = records[-1].get("seq", 0)
else:
self._log_page_last_seq = 0
self._btn_log_prev.setEnabled(True)
self._btn_log_next.setEnabled(True)
# ================================================================ # ================================================================
# UI 更新 # UI 更新
# ================================================================ # ================================================================
+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)
+16 -8
View File
@@ -1,10 +1,10 @@
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构) # vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
> 当前发布版本**V1.0.0**2026-07-16)· 变更历史见 [CHANGELOG.md](CHANGELOG.md) > 当前发布:**vd960DBN V1.02.05**2026-08-21,DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md)
## 产品概述 ## 产品概述
DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、RS485、蓝牙(小程序配置 + OTA),面向停车场出入口控制、流量计数、会车控制等场景。 DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、蓝牙(小程序配置 + OTA)、4G 扩展(RFU,面向停车场出入口控制、流量计数、会车控制等场景。
产品型号:**DLD960GA** 产品型号:**DLD960GA**
@@ -20,7 +20,7 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
┌───────────────▼─────────────────────────┐ ┌───────────────▼─────────────────────────┐
蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │ 蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │
网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │ 网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │
RS485 ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │ 4G(RFU) ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │
│ · 参数/日志外部存储 │ │ · 参数/日志外部存储 │
└─────────────────────────────────────────┘ └─────────────────────────────────────────┘
``` ```
@@ -28,7 +28,7 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
| 芯片 | 型号 | 主频 | Flash | SRAM | 职责 | | 芯片 | 型号 | 主频 | Flash | SRAM | 职责 |
|------|------|------|-------|------|------| |------|------|------|-------|------|------|
| 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 | | 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 |
| 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz | 128KB | 64KB | 蓝牙、网口、RS485、外部存储 | | 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz | 128KB | 64KB | 蓝牙、网口、双路 TTL 串口(内部互联 + 4G 扩展)、外部存储 |
- 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。 - 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。
- 支持 OTACH32V208 经蓝牙小程序 OTAAT32F421 经 CH32V208 串口透传 ISP 升级。 - 支持 OTACH32V208 经蓝牙小程序 OTAAT32F421 经 CH32V208 串口透传 ISP 升级。
@@ -41,18 +41,26 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
| 子项目 | 目录 | MCU | 固件版本 | 功能 | | 子项目 | 目录 | MCU | 固件版本 | 功能 |
|--------|------|-----|---------|------| |--------|------|-----|---------|------|
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 | | vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
| vd960DBN | `vd960DBN/` | CH32V208 | 1.0 | 蓝牙管理、TCP/IPWCHNET)、MQTT、RS485、串口桥接 | | vd960DBN | `vd960DBN/` | CH32V208 | 1.02.05 | 蓝牙管理、TCP/IPWCHNET)、MQTT、串口桥接、4G 扩展(RFU |
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。 > ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
## 产品文档(docs/
| 文档 | 版本 | 说明 |
|------|------|------|
| [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.03 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 |
| [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.03 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 |
## 协议文档(docs/ ## 协议文档(docs/
| 文档 | 版本 | 适用层 | 说明 | | 文档 | 版本 | 适用层 | 说明 |
|------|------|--------|------| |------|------|--------|------|
| [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 | 整机 RS485/TTL | 设备管理、参数配置、数据上报 | | [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 TTL 串口 | 设备管理、参数配置、数据上报 |
| [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.01 | 以太网 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.05 | 云平台 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 远程 OTA(ota_* 命令,先存后刷)、**4G 通道适配(方案 C:Air780 协议转换,标准 JSON 命令面 + linkV1.12** |
| [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.03 | 蓝牙 BLE | 帧格式 + 分包 + 配置命令全表(0x09~0x24/0x31/0x8A/0x92/0xC5,未实现如实标注)+ 脱机日志(OFFLOG_STAT/QUERY/CLEAR+ 传感快照(SNAP_STAT/QUERY/CLEAR+ 透传(0x7F/0x9F |
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 | | [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 |
+486
View File
@@ -0,0 +1,486 @@
# DLD960 BLE 通信协议
> 版本: V1.032026-08-31,命令总表补齐既有配置命令 0x09~0xC5 + 透传通道;未实现命令如实标注)
> 适用: vd960DBNCH32V208WCH BLE 协议栈)
> 用途: 小程序/APP 经蓝牙对 vd960DBN 进行**配置管理、参数查询、脱机日志/传感快照读取**(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
---
## 1 帧格式
```
Magic | Header | Data | CheckByte
| Addr/Sub Len CMD | | Xor Sum
1 Byte | 1B 1B 1B | xx | 1B 1B
```
- `Len = len(CMD) + len(Data)`(即 `pkg[2] = 1 + data_len`
- 校验:`Xor = XOR(pkg[1..Len+2])``Sum = SUM(pkg[1..Len+2])`,覆盖 header + cmd + data(不含 magic
- 校验实现:`check_pkg()` / `compute_ckb()`dbn_ble_srv.c
### 1.1 魔数(Magic
| 魔数 | 名称 | 用途 |
|------|------|------|
| `0x8F` | MAGIC_BYTE_DBN_DEFAULT | 设备配置/查询/日志/快照命令族(本协议主体) |
| `0x7F` | MAGIC_BYTE_DBN_TRANSPARENT | 透传:显示屏协议、地感协议、探头协议等 → 原样转发 UART2(Loop MCU 口) |
| `0x9F` | MAGIC_BYTE_DBN_SUB_OTA | 透传子设备 OTA 升级(g_flag_counter_ota.flag 时进入透传模式) |
### 1.2 分包(请求与响应)
**请求分包**unpack_packs0x13/0x15/0x1C/0x1D/0x22 等长请求):header 字节高 4 位 = 总包数,低 4 位 = 序号(从 1 递增);首包初始化重组缓冲,中间包校验 `amount/cmd/seq 连续`,末包 `amount == seq` 触发处理。校验失败丢弃整包。
**响应分包**set_response_to_notify):单包 data 上限**随协商 MTU 动态**V1.01,修复"Too large noti 丢包"):`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B)。分包时 header 字节 = `(pkg_amount << 4) | pkg_seq``pkg_seq` 从 1 递增;收端按 `pkg_amount`/`pkg_seq` 重组,最后一片 `pkg_amount == pkg_seq`
| 协商 MTU | 单包最大 dat | 整包最大长度 |
|---------|-------------|-------------|
| 23(未协商) | 14 | 20 |
| 96(常见 Android | 87 | 93 |
| 185iOS 常见) | 94 | 100 |
脱机日志 QUERY 响应最大 130BMTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
> 分包发送由 `performPeriodicTask`50ms TMOS 周期)主动续传,两包间隔 ≤50ms,不依赖收包事件(2026-08-12 修复)。
---
## 2 命令总表
| 命令码 | 名称 | 方向 | 实现状态 |
|--------|------|------|---------|
| `0x09` | UPDATE_DEV_SERIAL | APP→设备 | ✅ §3.1 |
| `0x10` | GET_DEV_INFO | APP→设备 | ✅ §3.2 |
| `0x11` | SET_LSSC_NET | APP→设备 | ✅ §3.3 |
| `0x12` | GET_LSSC_NET | APP→设备 | ✅ §3.4 |
| `0x13` | SET_IOT_NET | APP→设备 | ✅ §3.5 |
| `0x14` | GET_IOT_NET | APP→设备 | ✅ §3.6 |
| `0x15` | SET_IOT_TOPIC | APP→设备 | ✅ §3.7 |
| `0x16` | GET_IOT_TOPIC | APP→设备 | ✅ §3.8 |
| `0x17` | SET_SUB_TRAFFIC_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x18` | GET_SUB_TRAFFIC_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x1C` | CHECK_PASS | APP→设备 | ✅ §4.1 |
| `0x1D` | MODIFY_PASS | APP→设备 | ✅ §4.2 |
| `0x1E` | SET_FACTORY | APP→设备 | ✅ §4.3 |
| `0x1F` | RESET_DEV | APP→设备 | ✅ §4.4 |
| `0x20` | SET_NOTIFY | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x21` | SET_NOTIFY_LOOP | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x22` | SET_SUB_CODE | APP→设备 | ✅ §4.5 |
| `0x23` | SET_CJQ_PARAM | APP→设备 | ⚠ 空壳(仅回 [0],参数未处理)§4.6 |
| `0x24` | GET_CJQ_PARAM | APP→设备 | ⚠ 空响应(无数据)§4.7 |
| `0x25` | OFFLOG_STAT | APP→设备 | ✅ §6 |
| `0x26` | OFFLOG_QUERY | APP→设备 | ✅ §7 |
| `0x27` | OFFLOG_CLEAR | APP→设备 | ✅ §8 |
| `0x28` | SNAP_STAT | APP→设备 | ✅ §9 |
| `0x29` | SNAP_QUERY | APP→设备 | ✅ §9 |
| `0x2A` | SNAP_CLEAR | APP→设备 | ✅ §9 |
| `0x31` | RW_UART_BAUD | APP→设备 | ✅ §4.8 |
| `0x87` | LOOP_SAMPLE_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x88` | LOOP_BALANCE_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x89` | LOOP_RELEASE_PLANB | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
| `0x8A` | LOOP_SENS_LIST | APP→设备 | ⚠ 空响应(无数据)§4.9 |
| `0x92` | SET_CJQ_FACTORY | APP→设备 | ✅ §4.10 |
| `0xC0` | SUB_SENS_REPORT | 设备→APP | ⚠ 未实现(report_sens_acs 仅声明无定义;0xC5 使能后无上报动作) |
| `0xC5` | SENS_ACS_ENABLE | APP→设备 | ✅(设置生效,无响应)§4.11 |
> 与 MQTT / TCP JSON 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同(V1.02 起)。
---
## 3 设备配置命令
### 3.1 更改设备序列码 `UPDATE_DEV_SERIAL` (0x09)
**请求 data6B):** 新序列码(6 字节,g_dev_number 更新源为 `alter_dev_serila()`)。
**响应 data1B):** `status``0x01`=成功。
### 3.2 查询设备信息 `GET_DEV_INFO` (0x10)
**请求 data** 无(`Len=1`)。
**响应 data19 + N 字节):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 6 | `mac` | 设备 MAC 地址(gMacAddr |
| 6 | 1 | `hw_ver_main` | 硬件版本主 |
| 7 | 1 | `hw_ver_sub` | 硬件版本次 |
| 8 | 1 | `fw_ver_main` | 固件版本主 |
| 9 | 1 | `fw_ver_sub` | 固件版本次 |
| 10 | 1 | `model_len` | 型号字符串长度 N |
| 11 | N | `model` | 产品型号(PRODUCT_MODELASCII |
| 11+N | 6 | `dev_number` | 设备编号(6B |
| 17+N | 1 | `sub_code_lo` | 子功能码低字节(当前恒 0) |
| 18+N | 1 | `sub_code_hi` | 子功能码高字节(当前恒 0) |
| 19+N | 1 | `bus1_amount` | 总线1 设备数(当前恒 0) |
| 20+N | 1 | `bus2_amount` | 总线2 设备数(当前恒 0) |
> `sub_code` / `bus_amount` 字段代码中为占位(注释掉的旧逻辑),恒 0。
### 3.3 设置 SSC 网络配置 `SET_LSSC_NET` (0x11)
**请求 data30B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 4 | `lip` | 本机 IP |
| 4 | 4 | `subnet_mask` | 子网掩码 |
| 8 | 4 | `route_ip` | 网关 |
| 12 | 4 | `lssc_ip` | 中心 IP |
| 16 | 4 | `dns` | DNS(代码未读入,保留位) |
| 20 | 2 | `port_ssc_tcp` | SSC TCP 端口(小端) |
| 22 | 2 | `port_ssc_udp` | SSC UDP 端口(小端) |
| 24 | 2 | `port_ssc_udp_message` | SSC UDP 消息端口(小端) |
| 26 | 2 | `port_dev_tcp` | 设备 TCP 端口(小端) |
| 28 | 2 | `port_dev_udp` | 设备 UDP 端口(小端) |
**响应 data1B):** `status``0x00`=成功(配置写入 Flash)。
### 3.4 查询 SSC 网络配置 `GET_LSSC_NET` (0x12)
**请求 data** 无。
**响应 data30B):** 与请求布局一致:`lip(4)+sub(4)+gw(4)+lssc_ip(4)+dns(4)+` 5×端口(2B 小端)。注意响应含 `dns`(读 local_net_cfg.dns)。
### 3.5 设置 IoT 网络配置 `SET_IOT_NET` (0x13)
**请求 data** 字符串数组,`0x00` 分隔(支持分包,见 §1.2):
```
<host>\0<port(ascii)>\0<client_id>\0<username>\0<password>\0
```
- `host`MQTT 服务器地址
- `port`:MQTT 端口(ASCII 十进制,代码逐字符换算)
- `client_id`ClientID**单字节 `0x20`(空格)表示使用本地序列号**
- `username` / `password`MQTT 鉴权
**响应 data1B):** `status``0x00`=成功(写入 Flash)。
### 3.6 查询 IoT 网络配置 `GET_IOT_NET` (0x14)
**请求 data** 无。
**响应 data** 字符串数组,`0x00` 分隔(同 §3.5 布局):`<remote_addr>\0<port(ascii)>\0<client_id>\0<username>\0<password>\0`
### 3.7 设置设备 Topic `SET_IOT_TOPIC` (0x15)
**请求 data** 字符串数组,`0x00` 分隔(支持分包):
```
<clientid_enable+0x30>\0<topic_pub>\0<topic_sub>\0
```
- `clientid_enable`ASCII 数值,解析时减 `0x30` 还原
**响应 data1B):** `status``0x00`=成功(写入 Flash)。
### 3.8 查询设备 Topic `GET_IOT_TOPIC` (0x16)
**请求 data** 无。
**响应 data**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `clientid_enable` | `g_iot_topic.clientid_enable + 0x30` |
| 1 | 1 | `rsvd` | 保留(0 |
| 2 | N | `topic_pub` | 发布主题(`\0` 结尾) |
| 2+N+1 | M | `topic_sub` | 订阅主题(`\0` 结尾) |
---
## 4 安全与控制命令
### 4.1 验证密码 `CHECK_PASS` (0x1C)
**请求 data6B):** 密码明文(ASCII)。
**响应 data1B):** `status``0x00`=通过;`0x01`=失败(或长度≠6)。
### 4.2 修改密码 `MODIFY_PASS` (0x1D)
**请求 data12B):** 旧密码(6B+ 新密码(6B)。
**响应 data1B):** `status``0x00`=成功(旧密码校验通过并写入);`0x01`=失败(长度≠12 或旧密码错误)。
> 实现位置:`unpack_packs()`switch 无 case,属分包收尾处理),V1.03 文档补录。
### 4.3 设备出厂初始化 `SET_FACTORY` (0x1E)
**请求 data** 无。
**响应 data1B):** `status``0x00`(先回包后执行 `factory_dev_info()` 写默认参数)。⚠ 未整机复位(代码 TODO:仅复位蓝牙以外部分)。
### 4.4 设备复位 `RESET_DEV` (0x1F)
**请求 data** 无。**无响应**(直接 `NVIC_SystemReset()`)。
### 4.5 设置子功能码 `SET_SUB_CODE` (0x22)
**请求 data2B):** `code_set`(小端,bit 定义如下)。**无响应**(生效并持久化 `update_sub_code_enable()`)。
| bit | 子功能 | bit | 子功能 |
|-----|--------|-----|--------|
| 0 | net_enableSSC 网络) | 6 | radar_enable(雷达) |
| 1 | iot_enableIoT MQTT | 9 | laser_enable(激光) |
| 2 | custom_enable(自定义) | 15 | lora_enableLoRa |
| 3 | loop_enable(地感) | | |
| 4 | dgdus_enable(地感采集器) | | |
| 5 | wbdus_enable(称重采集器) | | |
### 4.6 设置车检器参数 `SET_CJQ_PARAM` (0x23)
**⚠ 空壳:** 代码中参数解析被注释(`unpack_pkg_set_cjq_param` 未调用),仅返回 `[0x00]` 伪成功。**参数实际未写入。**
### 4.7 读取车检器参数 `GET_CJQ_PARAM` (0x24)
**⚠ 空响应:** 仅构造响应帧,data 长度 = 0。**无有效数据返回。**
### 4.8 读写 UART 波特率 `RW_UART_BAUD` (0x31)
**请求 data** `rw(1B)` + 数据(`rw≠0` 时):`uart_num(1B) + baud(3B 小端)`
- `rw=0`:读。
**读响应 data10B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `rw` | 0(读) |
| 1 | 1 | `status` | 0x01 |
| 2 | 1 | `idle_timeout` | BLE 空闲超时(g_max_counter_bt_min |
| 3 | 3 | `baud_1` | UART1 波特率(3B 小端) |
| 6 | 1 | `idx_2` | 0x02(第二组标识) |
| 7 | 3 | `baud_2` | 第二组波特率(3B 小端) |
- `rw≠0`:写。`alter_dev_baud(&pkg[5])` + 更新 `g_storage_uart_num/baud`
**写响应 data9B 语义):** `rw(1B) + status(1B) + baud(3B 小端) + reserved(4B)`
> ⚠ **固件实际多发 1B**(baud 低字节重复写入,共 10B):接收端以前 3B(offset 2..4)为准。
### 4.9 读取 Loop 灵敏度列表 `LOOP_SENS_LIST` (0x8A)
**⚠ 空响应:** case 仅 `memset` 无组包。**无有效数据返回。**
### 4.10 车检器出厂初始化 `SET_CJQ_FACTORY` (0x92)
与 §4.3 `SET_FACTORY`0x1E)合并处理:响应 `[0x00]` + 执行 `factory_dev_info()`
### 4.11 传感主动上报使能 `SENS_ACS_ENABLE` (0xC5)
**请求 data3B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `enable` | 0=关闭;非0=开启 |
| 1 | 1 | `interval` | 上报间隔系数(实际 = 值 × 0x7F,单位 tick 待确认) |
| 2 | 1 | `timeout_min` | 超时分钟数(timeout_counter = 值 × 60 × 100 tick |
**无响应。** ⚠ 即使使能,`report_sens_acs()` 无定义无调用,**当前无 0xC0 BLE 主动上报**(上报走 MQTT/TCP 通道)。
---
## 5 透传通道
### 5.1 通用透传 (0x7F)
Magic=`0x7F` 的整帧(含校验)→ `manage_dbn_ble_transparent()`**原样转发 UART2**(Loop MCU 口)。用于显示屏协议、地感协议、探头协议等。响应分包时 header 的 Addr/Sub 字节 = pkg_seq(见 set_response_to_notify 单包分支)。
### 5.2 子设备 OTA 透传 (0x9F)
`g_flag_counter_ota.flag != 0` 时所有 BLE 接收直接走透传 → UART2。用于 Loop MCU OTA0x9F A5/A6/A7 帧透传,协议见《DLD960Loop 串口通信协议》OTA 章节)。
---
## 6 查询日志统计 `OFFLOG_STAT` (0x25)
**请求 data** 无(`Len=1`,仅 cmd 字节)。
**响应 data19B,全小端):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用(Flash 初始化失败) |
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
| 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| 7 | 4 | `capacity` | 容量上限(**随存储芯片动态**W25Q32=16256 / W25Q64=32640 / W25Q128=65408 / W25Q256=130944 |
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
| 15 | 4 | `seq_last` | 最新一条全局序号 |
示例(boot_seq=2, count=1234, capacity=16256, seq_first=100, seq_last=1333):
```
8F 00 14 25 00 02 00 D2 04 00 00 00 80 3F 00 00 00 64 00 00 00 35 05 00 00 00 XX XX
-- -- -- -- -------------------- -------------------- -------------------- -----
| | | | status count=1234 capacity=16256 seq_first=100
| | | | cmd=0x25 boot_seq=2 seq_last=1333
```
---
## 7 分页拉取日志 `OFFLOG_QUERY` (0x26)
**请求 data5B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界(< seq_first 或 > seq_last)返回 0 条 |
| 4 | 1 | `count` | 拉取条数,**上限 4**,超限按 4;0 按 4 处理 |
**响应 data**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用;`0x02`=请求帧过短 |
| 1 | 1 | `count` | 本次实际返回记录条数(0~4) |
| 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §10) |
记录顺序 = 逻辑序号升序(与 `start_seq` 一致)。越界/空日志:`count=0`
请求示例(start_seq=1330, count=4):
```
8F 00 06 26 32 05 00 00 04 XX XX
-- -- -- -- -- -- -- -- ---
| | | | | | | | count=4
| | | | +--+--+--+ start_seq=1330 (LE)
| | | cmd=0x26
| | Len=5+1=6
```
---
## 8 清除日志 `OFFLOG_CLEAR` (0x27)
**请求 data** 无。
**响应 data1B):** `status``0x00`=OK。
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——留痕不可清除)。设备侧应做权限控制(与 MQTT `log_clear` 同语义)。
>
> ⚠ **阻塞 ~2.8s**63 个数据扇区 SPI 擦除(~45ms/扇区),期间主循环阻塞。若设备 IoT MQTT 在线,此期间 WCHNET 无法轮询,可能导致断连重连(60s keepalive 内可恢复)。请勿高频调用。
成功后 `OFFLOG_STAT``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。
---
## 9 传感快照日志(0x28/0x29/0x2AV1.02 新增)
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
### 9.1 查询快照统计 `SNAP_STAT` (0x28)
**请求 data** 无(`Len=1`)。
**响应 data19B,全小端):** 与 §6 `OFFLOG_STAT` 布局相同:
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=快照未启用 |
| 1 | 2 | `boot_seq` | 当前启动序号 |
| 3 | 4 | `count` | 有效记录条数(0~capacity |
| 7 | 4 | `capacity` | 容量上限(**随芯片动态**W25Q32=48064 / W25Q64=105408 / W25Q128=220096 / W25Q256=449472 |
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
| 15 | 4 | `seq_last` | 最新一条全局序号 |
### 9.2 分页拉取快照 `SNAP_QUERY` (0x29)
**请求 data5B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界返回 0 条 |
| 4 | 1 | `count` | 拉取条数,**上限 2**64B×2+2=130B ≤ 单包缓冲),超限按 2;0 按 2 |
**响应 data**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=快照未启用;`0x02`=请求帧过短 |
| 1 | 1 | `count` | 本次实际返回记录条数(0~2) |
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §9.4) |
记录顺序 = 逻辑序号升序。QUERY 响应最大 130BMTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
### 9.3 清除快照 `SNAP_CLEAR` (0x2A)
**请求 data** 无。**响应 data1B):** `status``0x00`=OK。
> ⚠ **高风险操作**:清除动作本身写入**事件流** `log_clear` 审计(payload[0]=2=快照流,不可清除)。
> 阻塞 ~45ms:逻辑清除(count=0 旧数据立即不可读)+ 只擦写指针起点扇区,其余扇区由环形写覆盖时自动擦(原擦全部 751 扇区 ~34s,会触发 IWDG 4s 复位——2026-08-12 修复)。
成功后 `SNAP_STAT``count` 归 0`seq_last` 继续递增(序号不复位)。
### 9.4 快照记录格式(SnapRec64B 定长,小端)
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `magic` | `0xA6`(区别于事件 `0xA5` |
| 1 | 1 | `len` | 线圈数据有效长度 = coil_count×120~48 |
| 2 | 1 | `flags` | bit0=分包帧(预留) |
| 3 | 1 | `rsvd` | 保留(0 |
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,采集时刻) |
| 12 | 2 | `boot_seq` | 所属启动段 |
| 14 | 2 | `rsvd2` | 保留(0 |
| 16 | 48 | `coils` | 4×12B 线圈数据,与 0xC0 线上格式一致 |
**线圈 12B 单元**(与 0xC0 帧内每路线圈完全一致):
| 偏移(单元内) | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `cfg` | freq_level(2) + direction(1) + freq_type(1) + sensitivity(4) |
| 1 | 1 | `cond` | condition(4) + loop_state(1) + car_state(1) + misc_type(2) |
| 2 | 3 | `freq` | 频率,3B 小端无符号 |
| 5 | 3 | `variation` | 变化量,3B 小端有符号补码(= Origin CAPVD |
| 8 | 4 | `misc` | 杂项 4B 小端(misc_type=0 时=通过时间,50ms 单位) |
> 绝对时间回算:用事件流 `time_anchor`boot_seq ↔ unix_ts 映射)+ 本记录 `boot_seq`/`ts_ms`。未同步段仅相对时间。
---
## 10 记录格式(OfflogEvt32B 定长,小端)
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `magic` | `0xA5` |
| 1 | 1 | `type` | 事件类型(下表) |
| 2 | 1 | `len` | payload 有效字节数(0~12 |
| 3 | 1 | `flags` | bit0=1 → `unix_ts` 有效 |
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,断电归零) |
| 12 | 4 | `unix_ts` | 已同步 Unix 秒;`flags.bit0=0` → 0/无效(未同步段仅相对时间) |
| 16 | 2 | `boot_seq` | 所属启动段 |
| 18 | 2 | `rsvd` | 保留(0 |
| 20 | 12 | `payload` | 事件参数(下表) |
### 事件类型与 payload
| type | 事件 | payload | 说明 |
|------|------|---------|------|
| `0x01` | boot | `[4]` 复位原因寄存器原始值(大端),位解析:bit31=LPWR bit30=WWDG bit29=IWDG(看门狗) bit28=软件复位 bit27=POR(真断电) bit26=NRST引脚 | 上电/复位 |
| `0x10` | iot_connect | — | MQTT TCP 连接成功 |
| `0x11` | iot_ready | — | MQTT 订阅完成 → 发 initialize(重复上线直接证据) |
| `0x12` | iot_disconnect | `[1]` reason1=断开 2=超时 3=CONNACK拒绝 4=连接超时 | MQTT 断连 |
| `0x13` | iot_reconn | `[4]` 重连退避 ms(大端) | 重连退避 |
| `0x30` | evt_retry | `[5]` msg_id(大端4) + retry(1) | event_report ACK 超时重发 |
| `0x31` | evt_giveup | `[4]` msg_id(大端) | event_report 重试耗尽挂起 |
| `0x40` | coil | `[6]` sub(1) + ch(1) + value(大端4, 50ms 单位) | 线圈事件;sub1=car_enter 2=car_leave 3=loop_cut 4=loop_restore |
| `0x50` | time_anchor | —(`unix_ts` 即平台下发值,严格一致) | 时钟同步锚点 |
| `0x70` | log_clear | — | 日志清除(审计,不可清除) |
> payload 内多字节为**大端**(与 offlog 写入侧一致),OfflogEvt 其余字段为**小端**CPU 原生序,直接 memcpy)。
---
## 11 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |
| V1.01 | 2026-08-12 | 分包上限改为随协商 MTU 动态:`chunk = min(MTU-9, 94)`;修复 Too large noti 丢包 |
| V1.02 | 2026-08-12 | 新增传感快照 3 命令:SNAP_STAT / SNAP_QUERY / SNAP_CLEAR0x28/0x29/0x2A |
| V1.03 | 2026-08-31 | 命令总表补齐既有配置命令(0x09~0x24、0x31、0x8A、0x92、0xC5+ 透传通道(0x7F/0x9F);对照 dbn_ble_srv.c 如实标注未实现命令(0x17/0x18/0x20/0x21/0x87/0x88/0x89 无 case0x23/0x24/0x8A 空壳;0xC0 上报未实现) |
+238
View File
@@ -0,0 +1,238 @@
# DLD960 BLE 日志查询交互过程(实测示例)
> 版本: V1.002026-08-12
> 适用: vd960DBNCH32V208+ 微信小程序
> 目的: 以**真实串口日志**为样例,完整描述"小程序发指令 → 设备处理 → 设备回包"的交互过程,供固件/小程序两端排查对照
---
## 1 帧格式回顾(与 DLD960_BLE协议.md 一致)
```
Magic | Header | Data | CheckByte
| Addr/Sub Len CMD | | Xor Sum
1 Byte | 1B 1B 1B | xx | 1B 1B
```
- `Len = len(CMD) + len(Data)`,即 `pkg[2] = 1 + data_len`
- 校验:`Xor = XOR(pkg[1..Len+2])``Sum = SUM(pkg[1..Len+2])`(覆盖 header+cmd+data,不含 magic
- Magic = `0x8F`MAGIC_BYTE_DBN_DEFAULT
- **Header 字节分包语义**`(pkg_amount << 4) | pkg_seq`
- 单包:`0x00`amount=0, seq=0
- 分包:高 4 位 = 总包数,低 4 位 = 当前包序号(从 1 递增),最后一片 `amount == seq`
- **单包数据上限(动态,V1.01+)**:`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B
| 协商 MTU | chunk(单包最大 dat | 整包最大长度 |
|---------|----------------------|-------------|
| 23(未协商) | 14 | 20 |
| 96(本样例手机) | 87 | 93 |
| 185iOS 常见) | 94 | 100 |
---
## 2 命令码
| 命令码 | 名称 | 方向 | 说明 |
|--------|------|------|------|
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计 |
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
---
## 3 交互示例一:查询日志统计 OFFLOG_STAT (0x25)
### 3.1 小程序发送(请求帧)
```
8F 00 01 25 24 26
```
| 字节 | 值 | 含义 |
|------|-----|------|
| pkg[0] | `8F` | magic |
| pkg[1] | `00` | header:单包(amount=0, seq=0 |
| pkg[2] | `01` | Len = 0 data + 1 cmd = 1 |
| pkg[3] | `25` | cmd = OFFLOG_STAT(查询统计,无 data |
| pkg[4..5] | `24 26` | ckbXor=0x24, Sum=0x26 |
### 3.2 设备处理日志
```
[Rx][13:45:41.577] profile ChangeCB CHAR1?..:, len:6
[Rx][13:45:41.577] 8F 00 01 25 24 26
[Rx][13:45:41.577] BLE: offlog_stat count=162 seq_first=1 seq_last=162
[Rx][13:45:41.588] BLE notify OK, len:25, MTU:96
```
### 3.3 设备响应(25B = 帧头4 + dat 19 + ckb 2,单包)
```
8F 00 14 25 <19B data> <ckb2>
```
响应 data(19B,全小端)与设备日志字段对照:
| data 偏移 | 长度 | 字段 | 本次值 | 说明 |
|-----------|------|------|--------|------|
| 0 | 1 | `status` | `00` | OK |
| 1 | 2 | `boot_seq` | 待补(需原始 hex) | 启动序号 |
| 3 | 4 | `count` | `A2 00 00 00` = 162 | 有效记录条数 |
| 7 | 4 | `capacity` | `80 3F 00 00` = 16256W25Q32 512KB127 数据扇区 × 128 条) | 容量上限 |
| 11 | 4 | `seq_first` | `01 00 00 00` = 1 | 逻辑首条全局序号 |
| 15 | 4 | `seq_last` | `A2 00 00 00` = 162 | 最新全局序号 |
> 单包响应无分包,`notify OK len:25`25 = 4+19+2),MTU=96 下 25 ≤ 93,正常。
---
## 4 交互示例二:读取日志记录 OFFLOG_QUERY (0x26)
### 4.1 小程序发送(请求帧)
```
8F 00 06 26 01 00 00 00 04 25 31
```
| 字节 | 值 | 含义 |
|------|-----|------|
| pkg[0] | `8F` | magic |
| pkg[1] | `00` | header:单包请求 |
| pkg[2] | `06` | Len = 5 data + 1 cmd = 6 |
| pkg[3] | `26` | cmd = OFFLOG_QUERY |
| pkg[4..7] | `01 00 00 00` | start_seq = 1LE32,含) |
| pkg[8] | `04` | count = 4(≤ OFFLOG_MAX_QUERY_RECORDS=4 |
| pkg[9..10] | `25 31` | ckb |
### 4.2 设备处理日志
```
[Rx][13:45:47.827] profile ChangeCB CHAR1?..:, len:11
[Rx][13:45:47.827] 8F 00 06 26 01 00 00 00 04 25 31
[Rx][13:45:47.827] BLE: offlog_query start_seq=1 req=4 fetched=4
[Rx][13:45:47.848] BLE notify OK, len:93, MTU:96
[Rx][13:45:47.848] 8F 21 58 26 00 04 A5 01 04 00 01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 18 00 00 00 00 00 00 00 00 00 00 00 A5 01 04 00 02 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 A5 01 04 00 03 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 18 F3 E5
```
### 4.3 响应分包说明
响应 dat 总量 = status(1) + count(1) + 4×32B = **130B** > 单包上限 87BMTU=96)→ **必须分包 2 包**
| 包 | header | 整包长 | dat 内容 |
|----|--------|--------|----------|
| 第 1 包 | `0x21`amount=2, seq=1 | 93B | status+count + 记录1(32B) + 记录2(32B) + 记录3前21B = 87B |
| 第 2 包 | `0x22`amount=2, seq=2 | **49B(预期)** | 记录3剩余11B + 记录4(32B) = 43B |
### 4.4 第一包逐字段解析(93B)
```
8F 21 58 26 00 04 <记录1 32B> <记录2 32B> <记录3 前21B> F3 E5
-- -- -- -- -- -- --------------------------------------- ----
| | | | | | | +-- ckb
| | | | | | +-- dat: status(1)=00, count(1)=04, 然后记录
| | | | | +-- status=00 OK
| | | | +-- cmd=0x26
| | | +-- Len=0x58=88 = 87 data + 1 cmd
| | +-- header=0x21: amount=2, seq=1
| +-- magic=0x8F
```
记录结构(OfflogEvt 32B,小端)——本次 3 条记录均为 `type=0x01 04 00`(事件类型,待对照 offlog.h 事件表):
```
记录1: A5 01 04 00 | 01 00 00 00 | 00×12 | 01 00 00 00 | 18 00 00 00
记录2: A5 01 04 00 | 02 00 00 00 | 00×12 | 02 00 00 00 | 08 00 00 00
记录3: A5 01 04 00 | 03 00 00 00 | 00×12 | 03 00 00 00 | 18(截断于第 21B
```
### 4.5 第二包(预期,未收到——当前问题)
```
8F 22 2C 26 <记录3剩余 11B> <记录4 32B> <ckb2>
```
- header `0x22`amount=2, seq=2(最后一片)
- Len = `0x2C` = 44 = 43 data + 1 cmd
- 整包 49B
### 4.6 当前问题(2026-08-12 排查中)
**现象**:设备只发出了第 1 包(93B),第 2 包(49B)未发出;小程序按 `amount=2` 等第 2 包永远等不到。
**已确认**
- MTU 分包粒度动态化已生效(93B ≤ 96-3=93,无 `Too large noti`
- `performPeriodicTask` 已加主动拉包逻辑(不依赖收包事件)
- 已加 `BLE pull FAIL` 诊断打印,但本次日志**未出现** → 说明拉包时 `g_buf_ble_response.flag==0`(队列已被清)或 `_pull==1` 但发送周期未到/被阻塞
**下一步排查**
1.`performPeriodicTask` 周期心跳打印(每 500ms),确认 TMOS 周期事件是否持续执行
2. 核对 `SBP_PERIODIC_EVT_PERIOD=50` 的 TMOS 时间单位(tick vs ms
3. 检查 `g_buf_ble_response` 在第 1 包发出后是否被意外 clear(`unpack_packs`/`set_response_buf` 调用路径)
---
## 5 交互示例三:清除日志 OFFLOG_CLEAR (0x27)
### 5.1 小程序发送
```
8F 00 01 27 XX XX
```
| 字节 | 值 | 含义 |
|------|-----|------|
| pkg[0] | `8F` | magic |
| pkg[1] | `00` | header:单包 |
| pkg[2] | `01` | Len = 1 |
| pkg[3] | `27` | cmd = OFFLOG_CLEAR |
| pkg[4..5] | `XX XX` | ckb |
### 5.2 设备响应
```
8F 00 02 27 00 XX XX
```
- dat`status(1)=00`OK
- **注意**:擦除 W25Q32 阻塞约 2.8s,期间 BLE 无响应、网络短时失服务(MQTT keepalive 60s 可吸收)
---
## 6 小程序端分包重组规则(伪代码)
```js
// 订阅 onBLECharacteristicValueChange, 按 cmd 维护重组缓冲
let frag = { cmd: null, amount: 0, seq: 0, data: [] };
function onNotify(buf) {
const magic = buf[0], header = buf[1], len = buf[2], cmd = buf[3];
const amount = header >> 4, seq = header & 0x0F;
const data = buf.slice(4, 4 + (len - 1)); // 去掉 cmd 的净数据
const ckb = buf.slice(-2); // 校验可选
if (amount === 0) { // 单包
dispatch(cmd, data);
} else { // 分包
if (seq === 1) { frag = { cmd, amount, seq, data: [] }; }
if (frag.cmd !== cmd) return; // 乱序/新响应, 丢弃
frag.data = frag.data.concat(data);
frag.seq = seq;
if (seq === amount) { // 收齐
dispatch(cmd, frag.data);
frag = null;
}
}
}
```
> 关键点:收到第 1 包(seq=1, amount=2)后**必须继续等待** seq=2 的第二个通知,不要在第 1 包到达时判完成。
---
## 7 排查时间线
| 时间 | 现象 | 结论 |
|------|------|------|
| 08:15 | `Too large noti, len:102, MTU:96` | 分包粒度写死 96B > MTU-3=93,丢包 |
| 11:51 | 修复后第一包 93B 正常发出 | MTU 动态分包生效 |
| 11:51~13:45 | 第二包 49B 未发出,无 pull FAIL 打印 | 分包续传/发送周期链路待查(见 §4.6) |
+819 -4
View File
@@ -2,6 +2,7 @@
> 基于《DLD960 串口通信协议》V1.01,将设备管理、参数配置、数据上报映射到 MQTT 协议。 > 基于《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 时间:
@@ -110,6 +124,12 @@ dld960/{dev_serial}/{direction}
- 校准前(含首个 `initialize`),`ts` 为上电秒数;平台应能容忍并可按数量级区分。 - 校准前(含首个 `initialize`),`ts` 为上电秒数;平台应能容忍并可按数量级区分。
- 设备重启后需重新校准(无掉电保持)。平台在每次设备 `initialize` 后都应下发一次带 `ts``report_config` - 设备重启后需重新校准(无掉电保持)。平台在每次设备 `initialize` 后都应下发一次带 `ts``report_config`
**脱机日志时间戳语义(V1.06 起)**
- 设备侧事件日志(`log_query` 拉取)每条记录携带双时间戳:`ts_ms`(boot 内相对时间,单位 ms,断电归零)+ `unix_ts`(已同步 Unix 秒,**0 = 未同步**)。
- 设备经本协议同步成功后,其后记录的 `unix_ts` 均为真实 Unix 时间;同步前(含每次上电的 `boot` 事件)`unix_ts = 0`,仅 `ts_ms` 相对时间。
- 回算规则:`绝对时间 = 锚点.unix_ts + (记录.ts_ms - 锚点.ts_ms)/1000`,锚点取该 boot 段内第一条 `time_anchor` 事件(`unix_ts` 即平台下发值,严格一致)。从未同步过的 boot 段只有相对时间。
--- ---
# 3 命令详表 # 3 命令详表
@@ -131,10 +151,25 @@ dld960/{dev_serial}/{direction}
| `loop_param_set` | 设置车检器多路参数 | srv→dev | 0x63 | | `loop_param_set` | 设置车检器多路参数 | srv→dev | 0x63 |
| `loop_param_query` | 读取车检器多路参数 | srv→dev | 0x64 | | `loop_param_query` | 读取车检器多路参数 | srv→dev | 0x64 |
| `report_config` | 设置主动上报 | srv→dev | 0xC5 | | `report_config` | 设置主动上报 | srv→dev | 0xC5 |
| `log_stat` | 查询脱机日志统计(事件/快照流) | srv→dev | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | srv→dev | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | srv→dev | — |
| `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 未实现转换的命令 / 未识别帧透传),有线通道不使用。
--- ---
@@ -196,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": {
@@ -215,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`
@@ -639,6 +680,448 @@ dld960/{dev_serial}/{direction}
--- ---
## 4.16 查询脱机日志统计 `log_stat`
> Topic: `dld960/{sn}/srv`
> 设备本地 W25Qxx 环形日志(事件区/快照区容量随存储芯片动态,掉电不丢)。用于日志拉取前的分页定位。
> 通过 `data.stream` 区分日志流:`event`(事件日志,缺省)/ `snapshot`(传感快照)。
**请求:**
```json
{
"msg_id": 16,
"cmd": "log_stat",
"ts": 1719000000,
"data": {
"stream": "event"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应 datastream=event):**
```json
{
"stream": "event",
"enabled": true,
"boot_seq": 2,
"count": 1234,
"capacity": 16256,
"seq_first": 100,
"seq_last": 1333
}
```
**响应 datastream=snapshot):**
```json
{
"stream": "snapshot",
"enabled": true,
"boot_seq": 2,
"count": 1234,
"capacity": 48064,
"seq_first": 100,
"seq_last": 1333
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event` / `snapshot` |
| `enabled` | bool | 日志功能是否启用(Flash 初始化成功) |
| `boot_seq` | uint16 | 当前启动序号(每次上电 +1,区分复位段) |
| `count` | uint32 | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| `capacity` | uint32 | 容量上限(**随存储芯片与流动态**):事件流 W25Q32=16256 / Q64=32640 / Q128=65408 / Q256=130944;快照流 W25Q32=48064 / Q64=105408 / Q128=220096 / Q256=449472 |
| `seq_first` | uint32 | 逻辑首条记录全局序号(`seq_last - count + 1`count=0 时为 0 |
| `seq_last` | uint32 | 最新一条记录全局序号(跨 boot 单调递增) |
---
## 4.17 分页拉取脱机日志 `log_query`
> Topic: `dld960/{sn}/srv`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**(hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`。
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),平台按《DLD960 BLE 协议》字段表解析。
**请求:**
```json
{
"msg_id": 17,
"cmd": "log_query",
"ts": 1719000000,
"data": {
"stream": "event",
"start_seq": 1330,
"count": 4
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 data**
```json
{
"start_seq": 1330,
"records": [
{
"seq": 1330,
"hex": "a53200000200000001000000..."
}
]
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**(flash 存储字节原样,小端) |
**解析字段表(与 BLE 通道完全一致):**
| 流 | 结构 | 字段表 |
|----|------|--------|
| `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算(见 §2.3)。
---
## 4.18 清除脱机日志 `log_clear`
> Topic: `dld960/{sn}/srv`
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——谁在何时清了日志,留痕不可清除)。平台侧应做权限控制。
**请求:**
```json
{
"msg_id": 18,
"cmd": "log_clear",
"ts": 1719000000,
"data": {
"stream": "event"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应:** 标准成功/失败。
- `stream=event`:成功后 `log_stat``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。**阻塞 ~2.8s**(63 个数据扇区 SPI 擦除),请勿高频调用。
- `stream=snapshot`:成功后 `log_stat``count` 归 0`seq_last` 继续递增;清除动作写入事件流审计(`log_clear`,payload 标记快照流)。**阻塞 ~45ms**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
---
## 4.19 开启 OTA 会话 `ota_begin`
> Topic: `dld960/{sn}/srv`
> 依据《DLD960_MQTT_OTA协议.md》设计稿 V1.01ROADMAP P1.4 ①:Loop MCU 远程 OTA,先存后刷)。
> 能力探测:老固件(V1.07 及以下)无 `ota_*` 命令,收到回 `code=4`;平台下发前用 `ota_status` 或 `dev_info_query.soft_ver` 判断。
**请求:**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000000,
"data": {
"target": "loop",
"size": 46864,
"crc32": 305419896,
"version": "1.1.0",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | `loop`(当前支持);`dbn` 预留 |
| `size` | uint32 | 镜像 bin 字节数(≤ 98304 = 96KBSlot 数据区 100KB 预留 4KB 边界余量) |
| `crc32` | uint32 | 全镜像 CRC32(十进制,算法见 §4.19.1 |
| `version` | string | 目标固件版本(写入元数据,审计用;bootloader 不校验版本) |
| `force` | bool | `true` = 覆盖现有暂存镜像 / 忽略版本冲突(默认 false) |
**设备行为:**
1. 读暂存元数据:若已有镜像且 `size+crc32` 与本次一致 →
- `state=ready` → 返回 `offset=size`(平台可直接 `ota_flash`
- `state=downloading` → 返回 `offset=received`(断点续传)
2. 不一致 → 分配 Slot(A 当前 / B 回滚),写元数据 `state=downloading, received=0`,返回 `offset=0`
3. `force=false` 且目标版本 == 当前运行版本 → 回 `code=1, err_code=4`(防重复刷写,可 force 绕过)
**响应 data**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000001,
"code": 0,
"msg": "success",
"data": {
"target": "loop",
"slot": "a",
"offset": 0,
"received": 0,
"size": 46864,
"crc32": 305419896,
"state": "downloading"
}
}
```
### 4.19.1 CRC32 算法(必须双方一致)
标准 **CRC-32/ISO-HDLC**poly `0x04C11DB7`reflected `0xEDB88320`),init `0xFFFFFFFF`refin/refout truexorout `0xFFFFFFFF`。平台侧 Python `zlib.crc32()` / `binascii.crc32()` 即此算法;设备侧查表法。全镜像 CRC = offset 0 ~ size-1 连续;单片 CRC = 仅该 256B。
---
## 4.20 OTA 分片下发 `ota_data`
> Topic: `dld960/{sn}/srv`
> 单片大小 **256B** 是协议常量,不接受协商;hex 512 字符 + JSON 外壳 ≈ 640B < 设备接收缓冲 1024B。
**请求:**
```json
{
"msg_id": 402,
"cmd": "ota_data",
"ts": 1719000002,
"data": {
"target": "loop",
"offset": 0,
"crc32": 2524764894,
"data": "6a6173646f6e...512 hex 字符 = 256B"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | 同 `ota_begin` |
| `offset` | uint32 | 本片在镜像中的绝对偏移(**256 对齐**,首片 0) |
| `crc32` | uint32 | 本片 256B 的 CRC32(十进制) |
| `data` | string | 256B 原始字节小写 hex512 字符 |
**设备行为:**
1. `offset == received`(顺序片)→ 单片 CRC32 校验 → 写 W25Qxx 暂存(256B 页对齐)→ `received += 256`(≥size 截断为 size)→ `code=0`
2. `offset < received`(重复片,平台重发)→ **幂等回 `code=0`**,不重写
3. `offset > received`(缺片/乱序)→ `code=1, err_code=1` + `data.offset=received`(指示平台从该处续传;协议不要求乱序重组)
4. 单片 CRC 失败 → `code=1, err_code=1`(平台重发本片;连续失败平台可 `ota_abort`
5. 会话未开始 / 状态非 downloading → `code=3, err_code=2`
6. `data` 非 512 hex / offset 非 256 对齐 / 超 size → `code=1` 参数错误
**响应:** 标准成功/失败 + data 回显 `{offset, received}`
---
## 4.21 结束下载 `ota_end`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 403,
"cmd": "ota_end",
"ts": 1719000003,
"data": {
"target": "loop",
"crc32": 305419896
}
}
```
**设备行为:**
1. `received != size``code=1` + `data.offset=received`(不完整,续传)
2. `received == size` → 读回暂存区全镜像计算 CRC32,与 ota_begin 声明值比对
- 一致 → 元数据 `state=ready, last_result=0``code=0, data={crc_ok:true}`
- 不一致 → 元数据 `state=downloading`(保留已下载数据,可重发错片)→ `code=5, err_code=5, data={crc_ok:false}`
---
## 4.22 中止会话 `ota_abort`
> Topic: `dld960/{sn}/srv`
```json
{
"msg_id": 404,
"cmd": "ota_abort",
"ts": 1719000004,
"data": { "target": "loop" }
}
```
设备行为:元数据 `state=aborted`,Slot 标记可覆盖;正在刷写时 abort → 停止发送后续 A7 块(Loop 端由 bootloader 超时复位回 APP 兜底)。响应标准成功。
---
## 4.23 触发刷写 `ota_flash`
> Topic: `dld960/{sn}/srv`
> ⚠ **会车安全关键命令**:平台应确认现场允许(无车压线圈、非高峰)再下发。
**请求:**
```json
{
"msg_id": 405,
"cmd": "ota_flash",
"ts": 1719000005,
"data": {
"target": "loop",
"slot": "a",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `slot` | string | `a` / `b`(缺省 = 当前 ready 的槽) |
| `force` | bool | `true` = 跳过安全窗口检查(高风险,平台授权) |
**设备行为(同步检查 → 异步刷写):**
1. **安全窗口检查**force=false 时):4 通道 Loop 有车(VD_FLAG 任一置位)→ `code=3, err_code=3`(有车,拒绝;平台提示"车辆离开后重试")。刷写期间会阻断检测与继电器控制 → 平台建议低峰执行
2. 元数据非 ready → `code=3`(先 `ota_end` 完成校验)
3. 通过 → 立即回 `code=0`(异步),进入刷写:
- 写 offlog 事件日志:`固件升级开始`target/slot/version/size
- **暂停事件上报与脱机日志(会话期间)**:MQTT `event_report` 暂停发送(入队积压,16 深溢出丢最旧,会话结束恢复后补发);offlog/快照落盘暂停("升级开始"日志在暂停前写入、"升级结果"在恢复后补记)
- 维持 Loop 现状:复位进 bootloader 后的 GPIO/继电器状态与现网 BLE OTA 升级一致,不额外干预
-`9F 01 00 01 A5 A7` 启动帧 → Loop APP 写 flag 复位 → bootloader 回 pre_ok
- 发 A6 地址帧(`0x08003400` 4 字节大端)→ addr_ok
- 从 W25Qxx 暂存读镜像,按 ≤254B/块发 A7(**非阻塞 tick 驱动**:每轮主循环发送 1~2 块并检查 ACK,绝不阻塞主循环,保证刷写窗口内 MQTT PINGREQ/心跳/IWDG 喂狗正常);停等 ACK,1s 超时重发 ×3
- 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP
4. 进度经 `ota_report` 上行(§5.5);失败重试 ×3 仍失败 → 元数据 `state=flash_failed` + `event_report{type:ota_error}` 告警(平台必答)
5. **刷写结果(设备侧,V1.09 明确)**
- 成功(末块 ACK)→ 元数据 `state=idle`**镜像保留**`size`/`crc32`/`version`/`slot` 不变,`last_result=0``flash_cnt+1`)→ 立即上报 `ota_report{stage:done}`(重发 3 次×5s,同 `msg_id`/`ts`)→ 恢复 `event_report` 发送与 offlog/快照落盘
- 失败 → 元数据 `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}`done/failed 同重发策略)
- **状态语义**:刷写完成后 DBN 侧状态回 `idle`("本轮刷写已结束"),与"下载完成待刷(`ready`)"严格区分——**平台不得把 `state=ready` 判为"刷写未启动"**:本地刷写 70 块(17224B)实测 <1s,平台轮询间隔可能错过 `flashing` 中间态
**响应:** `code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report` / `ota_status` 为准。
---
## 4.24 查询状态 `ota_status`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{ "msg_id": 406, "cmd": "ota_status", "ts": 1719000006 }
```
**响应 data**
```json
{
"target": "loop",
"state": "flashing",
"slot": "a",
"size": 46864,
"received": 46864,
"crc32": 305419896,
"version": "1.1.0",
"progress": { "sent": 42112, "total": 46864 },
"last_result": 0,
"last_error": 0
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted`**`idle``size>0` = 已刷写完成(镜像保留可重刷)** |
| `progress.sent` | uint32 | 刷写阶段已送 Loop 的字节数 |
| `last_result` | uint32 | 上次刷写结果:0=无/成功,非 0=错误码 |
| `last_error` | uint32 | 上次失败细分错误码 |
**设备状态机:**
```
ota_begin(新会话) ota_data×N ota_end(CRC✓)
IDLE ─────────────────▶ DOWNLOADING ────────────▶ READY
▲ │ ▲ │
│ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓)
│ / flash_failed │ └──────────────┐ ▼
└────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ IDLE(镜像保留, last_result=0)
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
```
**平台判定指引(V1.09):**
- **主依据**`ota_report` 主动上报(`stage=done` 成功 / `stage=failed` 失败,§5.5)——`ota_flash` 后平台应优先等该信号
- **兜底**`ota_status` 查询——`state=idle 且 size>0 且 last_result=0` = 刷写成功(镜像保留,可重刷);`state=flash_failed` = 失败;`state=ready` = 下载完成待刷(**不是**"刷写未启动"
- 刷写窗口极短(<1s),轮询可能捕捉不到 `flashing` 中间态,不得以此判失败
---
## 4.25 查询地感版本 `loop_version_query`
> Topic: `dld960/{sn}/srv`
> 用途:实时查询地感 Loop MCU 固件/硬件版本(V1.10,配合远程 OTA 做升级前后版本核对)。
**请求:**
```json
{ "msg_id": 407, "cmd": "loop_version_query", "ts": 1719000007 }
```
**设备行为(异步):**
1. 经 UART2 向 Loop MCU 发 `0x4A` 查询帧(`7F 00 01 4A ...`,见 `DLD960Loop_串口通信协议.md` §3.01
2. 收到 Loop 响应后解析(Soft/Hard 各三段)→ 更新本地缓存 → 立即回包
3. Loop 无响应/超时 → 回 `code=5``loop_ver` 保持缓存值(可为空)
**响应:**
```json
{
"msg_id": 407,
"cmd": "loop_version_query",
"ts": 1719000008,
"code": 0,
"msg": "success",
"data": {
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"version_str": "V1.2.3 (HW:1.0.0)"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `loop_ver` | string | 地感固件版本,格式 `"主.次.次"`Soft_Main.Sub.SSub |
| `loop_hw_ver` | string | 地感硬件版本,格式 `"主.次.次"`Hard_Main.Sub.SSub |
| `version_str` | string | 人类可读串,`"V{soft} (HW:{hard})"`Loop 无响应时可能为空 |
> **时序与缓存**:设备上电后自动查询一次并缓存;`ota_report stage=done`Loop 复位跑新固件)后自动刷新缓存。平台升级流程建议:`ota_flash` 前 `loop_version_query` 记录旧版本 → 刷写完成后再次查询核对新版本是否生效。
---
# 5 设备主动上报 # 5 设备主动上报
## 5.1 设备上电登陆信息 `initialize` ## 5.1 设备上电登陆信息 `initialize`
@@ -655,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",
} }
@@ -668,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`
@@ -808,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,收到后必须立即回复):**
@@ -881,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` 改为重连时重读 | 待实现 |
--- ---
# 修订记录 # 修订记录
@@ -893,4 +1698,14 @@ dld960/{dev_serial}/{direction}
| V1.03 | 2026-07-09 | 增加设备上电初始化指令 | wangfq | | V1.03 | 2026-07-09 | 增加设备上电初始化指令 | wangfq |
| V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq | | V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq |
| V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq | | V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq |
| V1.06 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);§2.3 补充日志双时间戳语义(`ts_ms` 相对 + `unix_ts` 已同步,0=未同步,锚点回算规则) | wangfq |
| V1.07 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
| V1.15 | 2026-09-10 | §6.9 补充 **BLE 分包承载与缓冲边界**:核实 BLE 链路**分包机制存在且收发双向对称**(分包头编码于 `pkg[1]` 高/低 4 位 = 总包数/序号;每包 dat ≤ 94 B @MTU ≥ 103;4 位 ⇒ 最多 15 包)⇒ 229 B 载荷**确可送达**,故「载荷 > 131 B 即越界写 `g_buf_ble_response.dat` / `tmp_ble_buf`(132 B,构造器无钳制)」属**可达缺陷**而非纸面;§6.9.9 新增 4 条**实现前置改造项**(缓冲扩 ≥260 B / 构造器补钳制 / 接收侧累加钳制防 `uint8_t` 回绕 / `unpack_packs` 用起 `len` 形参);§6.9.4.3 补承载前置条件说明 | wangfq |
| V1.14 | 2026-09-10 | 新增 **§6.9 4G 配置同步(BLE → DBN → Air780**:明确配置权威源为 DBN flashAir780 侧 fskv 仅作启动缓存,**唯一写入者 = DBN 配置帧**,避免第二权威源);复用 **0x8F 本机侧私有帧**(与 BLE 同魔数 `MAGIC_BYTE_DBN_DEFAULT`,链路两端各自本地消费、均不转发),命令复用 `GET_IOT_NET(0x14)` / `GET_IOT_TOPIC(0x16)`,载荷与 BLE 读响应**逐字节同源**(复用 `set_response_iot_net()` / `set_response_iot_topic()`,零新增序列化)、拆 net/topic 两条(最坏 229B / 129B,均 ≤ fskv 单值 255B);**0x8F 帧长上限独立定为 260B**(Loop 业务帧 70B 上限不变,两侧帧缓冲需同步扩容);同步触发点补 `UPDATE_DEV_SERIAL(0x09)` / `SET_SUB_CODE``iot_enable` 位 / **禁改 UART1 波特率**V1.11 的 **0x7D 帧配置同步方案作废**`iot_net_info.mode` 实测无 BLE 写入路径、恒为 IP 模式,故不随载荷同步 | wangfq |
| V1.13 | 2026-08-31 | `initialize``extra_info` 增加可选字段 **`imei`** / **`iccid`**4G 模块 IMEI / 流量卡 ICCID;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,§6.2 说明) | wangfq |
| V1.12 | 2026-08-31 | **4G 通道适配修订:方案 B(hex 透传)改为方案 C(Air780 协议转换)**——Air780 解析 0x7F 帧并转换为**标准 JSON 命令**loop_data / event_report / initialize / heartbeat,与有线通道一致),平台零改动;V1.11 的 frame_report / frame_cmd 降级为**可选兜底**4G 事件面 = **仅线圈事件**car_enter/car_leave/loop_cut/loop_restore,不含 DBN 内部网络事件);命令响应链路 Air780 单命令状态机(超时回 code=5);link 对象保留;Air780 以 Lua 复刻 iot_event_report 逻辑(沿检测 + ACK + 5s×3 重发 + 16 深队列 + 跨重连同 msg_id),可行性已评估(2026-08-31 | wangfq |
| V1.11 | 2026-08-31 | **4G 通道适配(方案 B:原始帧透传 + hex 封装)**:新增 `frame_report`dev→srv,§6.3/ `frame_cmd`srv→dev,§6.4)两命令(4G 通道专用);4G 通道**不使用**有线标准 JSON 业务命令(loop_data/event_report),不适用网络配置类命令(ssc_net_*/iot_net_*/iot_topic_*4G 通道下发回 code=4);上行附加 `link` 对象(IMEI/ICCID/IMSI/MSISDN/CSQ,§6.6);链路层数据面 0x7F 帧字节流透传(魔数分流在 vd960DBN 侧),0x7D 帧仅 DBN↔Air780 配置同步/握手;平台双通道区分解析 + 新增《DLD960Loop_串口通信协议》解析依赖(§6.7);实施主体:Air8781PAir780EPMvd960Air 工程,vd960DBN UART1 通道列入开发计划(固件未实现) | wangfq |
| V1.10 | 2026-08-20 | **网络上报携带地感版本**(配合远程 OTA 升级前后版本核对):`dev_info_query` 响应 + `initialize` 上报新增 `loop_ver`/`loop_hw_ver`(地感 Loop MCU 固件/硬件版本,格式 `"主.次.次"`,来自 0x4A 查询缓存,尽力携带可为空);新增命令 `loop_version_query`(§4.25,srv→dev 实时查询,设备经 UART2 0x4A 异步查询后回包,含 `loop_ver`/`loop_hw_ver`/`version_str`);版本语义:`soft_ver`=整机 DBN 固件(`主.次`),`loop_ver`=地感 Loop 固件(`主.次.次`),二者区分 | wangfq |
| V1.09 | 2026-08-20 | **OTA 刷写结果判定修复**(现场:刷写物理成功但平台误判"刷写未启动"):① 刷写成功后台侧状态回 `idle`(镜像保留:`size`/`crc32`/`version` 不变,`last_result=0`,可重刷),与"下载完成待刷 `ready`"严格区分;② `ota_report` 升级为刷写结果**主依据**`stage=done`/`failed` 设备必报,重发 3 次×5s),`ota_status` 仅兜底;③ 明确平台判定指引——`idle+size>0+last_result=0`=成功,不得以轮询未见 `flashing` 或状态持续 `ready` 判"刷写未启动"(本地刷写 <1s,轮询大概率错过中间态);④ `ota_begin` 兼容 `idle+size/crc32 一致` → 免下载直接可刷(重刷) | wangfq |
| V1.08 | 2026-08-20 | 增加 **Loop MCU 远程 OTA**ROADMAP P1.4 ①,先存后刷):命令 `ota_begin` / `ota_data` / `ota_end` / `ota_abort` / `ota_flash` / `ota_status`srv→dev+ `ota_report`dev→srv);单片 256B + 单片/全镜像 CRC32ISO-HDLC,§4.19.1);断点续传(`ota_begin` 返回 offset);Slot A/B 双槽回滚(镜像 ≤96KB);`ota_flash` 安全窗口检查 + 非阻塞 tick 驱动刷写(刷写窗口内 MQTT 保活/IWDG 不受影响);会话期间暂停 `event_report` 发送与脱机日志落盘;`event_report` 扩展 `type=ota_error` 失败告警;§2.2 补 OTA 细分错误码(err_code);老固件兼容(`ota_*` 回 code=4 | wangfq |
+516
View File
@@ -0,0 +1,516 @@
# DLD960 MQTT 远程 OTA 协议(Loop MCU 先存后刷)
> 文档版本 V1.01(设计稿·按修改意见修订)· 2026-08-20 · 适用范围:并入《DLD960 IoT 接口协议(MQTT + JSON)》V1.08 后生效
> 依据:`docs/ROADMAP.md` P1.4 ① —— Loop MCU (AT32F421) 远程 OTAv1.2.x 落地
> 设计原则:**下载阶段纯 MQTT 分片 → W25Qxx 暂存;刷写阶段纯本地 ISP 透传(复用 BLE OTA 0x9F 状态机)**;传输层无关,4G 通道原样复用。
---
# 1 背景与目标
## 1.1 现状基线
| 项 | 说明 |
|----|------|
| 双 MCU 架构 | DBN = CH32V208(通信 MCU,跑 MQTT/BLE/TCP+ Loop = AT32F421(地感 MCU,跑检测算法) |
| 现有 OTA 通道 | BLE → DBN 透传 → UART2 0x9F ISP → Loop bootloader2026-08-19 修复闭环,V1.02.03 |
| 本轮目标 | MQTT → DBN 分片下载到 W25Qxx 暂存区 → 校验后**本地** ISP 刷写 Loop |
| 暂存介质 | DBN 外挂 W25QxxSPI1),OTA 镜像暂存区 **0x010000 起 512KB**`SNAP_FIXED_SIZE = 参数区64KB + OTA区512KB`,已在 snapshot.c 分区模型预留) |
## 1.2 为什么"先存后刷"(相对 BLE 流式直透)
| 维度 | 流式直透(BLE 现状) | 先存后刷(本设计) |
|------|---------------------|-------------------|
| 网络抖动影响 | 任一分片丢失 → 停等卡死,全流程重来 | 下载阶段断网无所谓,**断点续传** |
| 刷写窗口 | 与传输时间重合(网络慢则窗口长) | 下载完成后刷写纯本地,窗口短且可控 |
| 校验 | 依赖传输层(BLE 无 CRC) | 单片 CRC32 + 全镜像 CRC32 双重校验 |
| 回滚 | 无 | 暂存区 Slot A/B 双槽,保留上一版重刷 |
| 安全窗口 | 无检查 | 刷写前检查无车压线圈;继电器维持 Loop 现状(与 BLE OTA 一致) |
## 1.3 范围
- 本版只做 **Loop (AT32F421) 远程 OTA**ROADMAP P1.4 ①)
- DBN (CH32V208) 自身 OTAROADMAP P1.4 ②):需 bootloader 改造,**单独评审,不阻塞本设计**;协议命令预留 `target:"dbn"`
- 传输层:本版 MQTT;4G(UART1 AT 透传)复用同一分片下载协议(`target` 字段扩展即可)
---
# 2 总体架构与数据流
```
┌─────────┐ MQTT (dld960/{sn}/srv) ┌──────────────┐ UART2 192000 ┌─────────────┐
│ 平台 │ ── ota_begin / ota_data ─▶ │ DBN (CH32V208) │ ── 0x9F ISP ──▶ │ Loop (AT32) │
│ │ ◀─ 响应 / ota_report ───── │ │ ◀─ ACK ──────── │ bootloader │
└─────────┘ │ W25Qxx 暂存 │ └─────────────┘
│ 0x010000 512KB │
└──────────────┘
阶段1 下载:平台 MQTT 分片 → DBN 写 W25Qxx(单片 CRC32 校验)
阶段2 校验:ota_end 全镜像 CRC32 复核,state → ready
阶段3 刷写:ota_flash → DBN 从 W25Qxx 读出 → 0x9F A5/A6/A7 逐块透传 → Loop bootloader 写 flash
```
**镜像语义**:平台下发的是 **bin 原始字节**hex 编码传输),即 AT32F421 0x08003400 起的 APP 代码映像(hex→bin 转换由平台侧完成,设备不解析 Intel HEX)。刷写时 A6 地址固定 `0x08003400`=`APP_START_ADDR`),A7 数据按 bin 顺序发送。
---
# 3 关键参数
## 3.1 协议参数(平台 ↔ 设备契约)
| 参数 | 值 | 依据 |
|------|-----|------|
| 下载单片原始字节 | **256B** | 2 的幂 + W25Qxx 页(256B)对齐;hex 512 字符 + JSON 外壳 ≈ 640B < `IOT_MQTT_RECV_BUF_LEN=1024` |
| 单片 CRC | **CRC32**(256B 独立计算) | 与全镜像同算法,查表实现复用 |
| 全镜像 CRC | **CRC32**offset 0 ~ size-1 连续) | 标准 CRC-32/ISO-HDLC |
| 镜像上限 | **96KB** | Slot 数据区 100KB 预留 4KB 边界余量;Loop APP 区 0x08003400~0x08010000 = 51KB 硬上限绰绰有余,100KB 槽为 DBN (CH32V208) 自身镜像(~100KB APP 区)预留 |
| 下载片序号语义 | offset 绝对字节偏移(256 对齐) | 支持乱序/重复片幂等处理 |
| 断点续传 | ota_begin 返回已接收字节数(片对齐) | 设备持久化 received 到元数据 |
## 3.2 刷写参数(设备内部实现约束,平台无需感知)
| 参数 | 值 | 依据 |
|------|-----|------|
| A7 数据块 | **≤254B**(实现取 248B | bootloader `LEN` 为 uint8 → DATA = LEN-1 ≤ 254iap.c `CMD_9F_DATA_LEN` |
| 块间超时 | 1s(可配) | 0x9F 停等协议,超时重发该块 |
| 刷写调度 | **非阻塞 tick 驱动** | 刷写状态机挂主循环轮询,每轮最多发送 1~2 块并检查 ACK,绝不阻塞主循环 → 刷写窗口内 MQTT PINGREQ/心跳/IWDG 喂狗节奏完全不受影响 |
| 刷写重试 | 每块失败重发 ×3,整体失败重试 ×3 | ROADMAP 安全底线 |
| 刷写速率 | 248B/帧 ≈ 13ms + ACK~20ms/帧 | 64KB ≈ 265 帧 ≈ **6~8s 窗口**Loop 典型 40~50KB 更快) |
| 升级触发 | DBN 发 `9F 01 00 01 A5 A7` → Loop APP 写 `IAP_UPGRADE_FLAG_9F=0x444C4439` → 复位 | 复用 BLE OTA 启动帧(dbn_ble_srv.c check_pkg 同款) |
## 3.3 CRC32 算法定义(必须双方一致)
- 标准 **CRC-32/ISO-HDLC**poly `0x04C11DB7`reflected `0xEDB88320`),init `0xFFFFFFFF`refin/refout truexorout `0xFFFFFFFF`
- 平台侧 Python `zlib.crc32()` / `binascii.crc32()` 即此算法,直接可用;设备侧查表法实现
- 全镜像 CRC = 从 offset 0 连续计算 size 字节;单片 CRC = 仅该 256B
- JSON 中 crc32 字段以**十进制无符号数**传输(与现有 uint32 字段风格一致)
---
# 4 暂存区布局(设备内部实现,命令接口不依赖)
```
0x010000 ┌─────────────────────────┐
│ OTA 元数据扇区 (4KB) │ SlotA/SlotB 头 + 会话状态 + 审计
0x011000 ├─────────────────────────┤
│ Slot A 数据区 (100KB) │ 当前新镜像(bin 原始字节)
0x02A000 ├─────────────────────────┤
│ Slot B 数据区 (100KB) │ 上一版镜像(回滚重刷)
0x043000 ├─────────────────────────┤
│ 预留 308KB │ DBN 自身镜像 / 4G 扩展
0x090000 └─────────────────────────┘
```
- Slot A/B 各 **100KB 数据区**`0x011000`/`0x02A000` 起,4KB 对齐),镜像上限 96KB(预留 4KB 边界余量)
- 100KB 槽位容量同时覆盖 DBN (CH32V208) 自身镜像(APP 区约 100KB),为 P1.4 ② 预留
- 4KB 边界余量:防止镜像写满后越界擦到下一槽;元数据/数据区起始均 4KB 扇区对齐
元数据结构(建议,实现可调,不进入协议字段):
```c
typedef struct { /* 64B */
uint32_t magic; /* 'DLD9' */
uint32_t state; /* idle/downloading/ready/flashing/flash_failed/aborted */
uint8_t target; /* 0=loop, 1=dbn(预留) */
uint8_t slot; /* 0=A, 1=B */
uint16_t rsv;
uint32_t size; /* 镜像字节数 */
uint32_t crc32; /* 全镜像 CRC32 */
uint32_t received; /* 已下载字节数(片对齐,断点续传依据) */
uint32_t last_result; /* 上次刷写结果码 */
uint32_t begin_ts; /* 会话开始(同步后 Unix 秒,0=未同步) */
char version[16]; /* 目标固件版本字符串(审计) */
uint32_t flash_cnt; /* 刷写尝试次数 */
uint32_t rsv2;
} OtaMeta;
```
**掉电恢复**:元数据每次 ota_begin / 每片落盘 / ota_end / 刷写结果后回写。重启后:
- state=downloading → 平台 ota_begin 时返回已接收 offset 续传
- state=ready → 平台 ota_begin 直接确认可刷写,或 ota_flash 直接触发
- state=flashing → 视为上次刷写中断(掉电/异常),可重试 ota_flash
---
# 5 命令详表(新增,并入 MQTT 协议 V1.08)
| cmd | 方向 | 说明 |
|-----|------|------|
| `ota_begin` | srv→dev | 开启 OTA 会话 / 断点续传定位 |
| `ota_data` | srv→dev | 分片下发(256B/片,单片 CRC32 |
| `ota_end` | srv→dev | 结束下载,全镜像 CRC32 复核 |
| `ota_abort` | srv→dev | 中止会话,释放暂存 |
| `ota_flash` | srv→dev | 触发本地 ISP 刷写(仅 state=ready |
| `ota_status` | srv→dev | 查询 OTA 状态(含进度) |
| `ota_report` | dev→srv | 刷写进度/结果主动上报(QoS 1) |
| `event_report` | dev→srv | 扩展 `type=ota_error`:重试 ×3 仍失败告警(**平台必答**,复用 §5.3 ACK 闭环) |
> 兼容:老固件(V1.07 及以下)收到 `ota_*` → 现有 default 分支回 `code=4 unsupported command`。平台下发前可用 `ota_status``dev_info_query.soft_ver` 探测能力,避免每命令必报错。
## 5.1 开启会话 `ota_begin`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000000,
"data": {
"target": "loop",
"size": 46864,
"crc32": 305419896,
"version": "1.1.0",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | `loop`(当前支持);`dbn` 预留 |
| `size` | uint32 | 镜像 bin 字节数(≤ 98304 = 96KB |
| `crc32` | uint32 | 全镜像 CRC32(十进制) |
| `version` | string | 目标固件版本(写入元数据,审计用;bootloader 不校验版本) |
| `force` | bool | `true` = 覆盖现有暂存镜像 / 忽略冲突(默认 false) |
**设备行为:**
1. 读元数据:若已有镜像且 `size+crc32` 与本次一致 →
- state=ready → 返回 `offset=size`(平台可直接 `ota_flash`
- state=downloading → 返回 `offset=received`(续传)
2. 不一致 → 分配 Slot(A 当前 / B 回滚),写元数据 `state=downloading, received=0`,返回 `offset=0`
3. `force=false` 且目标版本 == 当前运行版本 → 返回 `code=1`(防重复刷写,可 force 绕过)
**响应 data**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000001,
"code": 0,
"msg": "success",
"data": {
"target": "loop",
"slot": "a",
"offset": 0,
"received": 0,
"size": 46864,
"crc32": 305419896,
"state": "downloading"
}
}
```
## 5.2 分片下发 `ota_data`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 402,
"cmd": "ota_data",
"ts": 1719000002,
"data": {
"target": "loop",
"offset": 0,
"crc32": 2524764894,
"data": "6a6173646f6e...512 hex 字符 = 256B"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | 同 `ota_begin` |
| `offset` | uint32 | 本片在镜像中的绝对偏移(**256 对齐**,首片 0) |
| `crc32` | uint32 | 本片 256B 的 CRC32(十进制) |
| `data` | string | 256B 原始字节小写 hex512 字符 |
**设备行为:**
1. `offset == received`(顺序片)→ 单片 CRC32 校验 → 写 W25Qxx256B 页对齐)→ `received += 256`(≥size 时截断为 size)→ 回 `code=0`
2. `offset < received`(重复片,平台重发)→ **幂等直接回 `code=0`**,不重写
3. `offset > received`(缺片/乱序)→ 回 `code=1` + `data.offset=received`(指示平台从该处续传;协议不要求乱序重组,降低设备复杂度)
4. 单片 CRC 失败 → 回 `code=1` + `data.err_code=1`(平台重发本片;连续失败由平台策略控制,可 abort)
5. 会话未开始 / 状态非 downloading → `code=3`(先 `ota_begin`
6. `data` 非 512 hex 字符 / offset 非 256 对齐 / 超 size → `code=1` 参数错误
**响应:** 标准成功/失败 + data 回显 `{offset, received}`
> 单片大小 256B 是协议常量,**不接受协商**(避免双方尺寸漂移)。若未来单片加大需协议升版。
## 5.3 结束下载 `ota_end`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 403,
"cmd": "ota_end",
"ts": 1719000003,
"data": {
"target": "loop",
"crc32": 305419896
}
}
```
**设备行为:**
1. `received != size``code=1` + `data.offset=received`(不完整,续传)
2. `received == size` → 读回暂存区全镜像计算 CRC32,与 ota_begin 声明值比对
- 一致 → 元数据 `state=ready, last_result=0``code=0, data={crc_ok:true}`
- 不一致 → 元数据 `state=downloading`(保留已下载数据,可重发错片)→ `code=5, data={crc_ok:false}`msg 注明 CRC 不匹配)
## 5.4 中止会话 `ota_abort`
> Topic: `dld960/{sn}/srv`
```json
{
"msg_id": 404,
"cmd": "ota_abort",
"ts": 1719000004,
"data": { "target": "loop" }
}
```
设备行为:元数据 `state=aborted`,Slot 标记可覆盖;正在刷写时 abort → 停止发送后续 A7 块(Loop 端由 bootloader 超时复位回 APP 兜底)。响应标准成功。
## 5.5 触发刷写 `ota_flash`
> Topic: `dld960/{sn}/srv`
> ⚠ **会车安全关键命令**:平台应确认现场允许(无车压线圈、非高峰)再下发。
**请求:**
```json
{
"msg_id": 405,
"cmd": "ota_flash",
"ts": 1719000005,
"data": {
"target": "loop",
"slot": "a",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `slot` | string | `a` / `b`(缺省 = 当前 ready 的槽) |
| `force` | bool | `true` = 跳过安全窗口检查(高风险,平台授权) |
**设备行为(同步检查 → 异步刷写):**
1. **安全窗口检查**force=false 时):
- 4 通道 Loop 有车(VD_FLAG 任一置位)→ `code=3` + `data.err_code=1`(有车,拒绝;平台可提示"车辆离开后重试")
- 刷写期间会阻断检测与继电器控制 → 建议平台在低峰执行
2. 元数据非 ready → `code=3`(先 `ota_end` 完成校验)
3. 通过 → 立即回 `code=0`(异步),进入刷写:
- 写 offlog 事件日志:`固件升级开始`target/slot/version/size
- **暂停事件上报与脱机日志(会话期间)**MQTT `event_report` 暂停发送(入队积压,16 深溢出丢最旧,会话结束恢复后补发);offlog/快照落盘暂停——理由:刷写期间 Loop 复位不产生检测事件,DBN 让出 SPI 总线/主循环给刷写状态机,保证保活与喂狗
- 维持 Loop 现状:复位进 bootloader 后的 GPIO/继电器状态与现网 BLE OTA 升级完全一致(该通道已验证可用),不额外干预
- 发 `9F 01 00 01 A5 A7` 启动帧 → Loop APP 写 flag 复位 → bootloader 回 pre_ok
- 发 A6 地址帧(`0x08003400` 4 字节大端)→ addr_ok
- 从 W25Qxx 读镜像,按 248B/块发 A7(停等 ACK,1s 超时重发 ×3)
- 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP
4. 进度经 `ota_report` 上行(见 §6);失败重试 ×3 仍失败 → 元数据 `state=flash_failed` + `event_report{type:ota_error}` 告警(平台必答)
**响应:** 标准成功/失败(`code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report`/`ota_status` 为准)。
**刷写结果(V1.09 明确):**
- 成功(末块 ACK)→ 元数据 `state=idle`**镜像保留**`size`/`crc32`/`version`/`slot` 不变,`last_result=0``flash_cnt+1`)→ 上报 `ota_report{stage:done}`(重发 3 次×5s)→ 恢复 event_report/offlog 落盘
- 失败 → `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}`
- **状态语义**:刷写完成后回 `idle`("本轮刷写已结束"),与"下载完成待刷 `ready`"严格区分——平台**不得把 `state=ready` 判为"刷写未启动"**:本地刷写 <1s,轮询大概率错过 `flashing` 中间态;平台判定以 `ota_report done/failed` 为主依据,`ota_status` 兜底(`idle+size>0+last_result=0`=成功)
## 5.6 查询状态 `ota_status`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{ "msg_id": 406, "cmd": "ota_status", "ts": 1719000006 }
```
**响应 data**
```json
{
"target": "loop",
"state": "flashing",
"slot": "a",
"size": 46864,
"received": 46864,
"crc32": 305419896,
"version": "1.1.0",
"progress": { "sent": 42112, "total": 46864 },
"last_result": 0,
"last_error": 0
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted` |
| `progress.sent` | uint32 | 刷写阶段已送 Loop 的字节数 |
| `last_result` | uint32 | 上次刷写结果:0=无/成功,非 0=错误码 |
| `last_error` | uint32 | 上次失败细分错误码 |
**状态机(设备侧):**
```
ota_begin(新会话) ota_data×N ota_end(CRC✓)
IDLE ─────────────────▶ DOWNLOADING ────────────▶ READY
▲ │ ▲ │
│ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓)
│ / flash_failed │ └──────────────┐ ▼
└────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ IDLE(镜像保留, last_result=0)
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
```
**平台判定(V1.09)**:刷写结果以 `ota_report``stage=done`/`failed`)为主依据;`ota_status` 兜底——`state=idle 且 size>0 且 last_result=0` = 成功(镜像保留可重刷);`state=ready` = 待刷(**不是**"刷写未启动");轮询可能错过 `flashing` 中间态,不得以此判失败。
---
# 6 主动上报 `ota_report`
> Topic: `dld960/{sn}/dev` · QoS 1
> 用途:刷写进度与结果主动推送(进度可丢,结果可经 `ota_status` 兜底查询;设备侧元数据持久化 last_result
**上报:**
```json
{
"msg_id": 501,
"cmd": "ota_report",
"ts": 1719000007,
"data": {
"target": "loop",
"stage": "flashing",
"progress": { "sent": 42112, "total": 46864 },
"code": 0,
"msg": ""
}
}
```
| data 字段 | 说明 |
|-----------|------|
| `stage` | `begin`(会话开启)/ `downloading`(片落盘)/ `ready`(校验通过)/ `flashing`(刷写中)/ `done`(刷写成功,Loop 已重启)/ `failed`(刷写失败) |
| `progress` | `sent`/`total` 字节(刷写阶段) |
| `code` | stage 相关结果码 |
**上报节奏**`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按块进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 msg_id/ts),平台去重窗口建议 10 分钟(与 event_report 同策略)。
**平台判定(V1.09 明确)**`ota_report` 是刷写结果**主依据**——`stage=done` 判成功、`stage=failed` 判失败,无需轮询 `ota_status``ota_flash` 响应 `code=0` 仅表示已启动(异步),**不得以"轮询未见 `flashing`"或"状态持续 `ready`"判"刷写未启动"**(本地刷写 <1s,轮询大概率错过中间态);兜底判定见 §5.6。
## 6.1 失败告警(扩展 event_report
刷写整体失败(重试 ×3 仍失败)时,设备经 `event_report` 上报(**平台必答**,复用 §5.3 ACK + 重发机制):
```json
{
"msg_id": 502,
"cmd": "event_report",
"ts": 1719000008,
"data": {
"events": [
{ "type": "ota_error", "ch": 0, "value": 1003 }
]
}
}
```
| type | value | 说明 |
|------|-------|------|
| `ota_error` | 0x1000 起 | `0x1001`=启动帧无响应 / `0x1002`=地址帧错误 / `0x1003`=数据块 ACK 超限 / `0x1004`=全镜像校验失败 / `0x1005`=安全窗口拒绝后强制失败 |
---
# 7 安全设计(会车产品底线)
| # | 措施 | 说明 |
|---|------|------|
| 1 | **升级窗口检查** | `ota_flash` 前检查 4 通道无车;有车 → `code=3`,平台提示延迟。`force=true` 可跳过(高风险,需平台权限控制) |
| 2 | **维持 Loop 现状** | 刷写期间 Loop 行为与现网 BLE OTA 升级一致(复位进 bootloader → ISP 刷写 → 复位跑新 APP),继电器 GPIO 状态按当前硬件实测状态接受,不额外干预(BLE 通道已验证可用) |
| 3 | **双重 CRC** | 单片 CRC32(传输层抓错)+ 全镜像 CRC32(落盘完整性);刷写前读回复核 |
| 4 | **失败可重入** | 下载失败 → 断点续传;刷写失败 → 重试 ×3 → 保留镜像可重刷;AT32 ISP bootloader 兜底,不会真砖 |
| 5 | **版本回滚** | Slot A/B 双槽;平台可 `ota_begin` 指定槽刷旧版(`force=true` 覆盖) |
| 6 | **审计留痕** | 升级开始/结果写 offlog 事件日志(含目标版本、slot、结果码);平台侧 version 字段归档 |
| 7 | **能力探测** | 老固件 `ota_*``code=4`;平台按 `ota_status`/`dev_info_query.soft_ver` 判断,不盲目下发 |
| 8 | **幂等与防呆** | 重复片幂等 ACK;offset 乱序拒绝并要求续传;同版本默认拒绝重刷(force 绕过) |
| 9 | **会话期间静默** | OTA 会话期间(begin ~ 结束)暂停 MQTT `event_report` 发送(队列积压,结束后补发)与 offlog/快照落盘("升级开始"日志在暂停前写入、"升级结果"在恢复后补记);避免刷写窗口与上报/日志抢 SPI 总线与主循环 |
---
# 8 错误码约定
顶层 `code` 沿用通用语义(0 成功 / 1 参数 / 2 密码 / 3 忙 / 4 不支持 / 5 内部 / 6 超长),OTA 细分错误经 `data.err_code` 表达:
| err_code | 场景 | 顶层 code |
|----------|------|-----------|
| 1 | 单片 CRC 失败 / 乱序缺片(data.offset 指示续传点) | 1 |
| 2 | 会话状态不允许(未 begin / 非 downloading 收 ota_data | 3 |
| 3 | 安全窗口拒绝(有车压线圈) | 3 |
| 4 | 版本冲突(同版本且非 force) | 1 |
| 5 | 全镜像 CRC 不匹配(data.crc_ok=false | 5 |
| 6 | 暂存区写失败(SPI 异常/满) | 5 |
---
# 9 平台侧实现要点(edc_server / DBNMQTTool
1. **hex↔bin**`data` hex 解码(512 hex = 256B);bin 由 Intel HEX 转换(固定 0x08003400 偏移),转换在平台完成
2. **CRC32**`zlib.crc32(bin)` 全镜像;单片 `zlib.crc32(chunk)` 与设备逐字节一致(ISO-HDLC 即 Python 内置)
3. **分片循环**`for offset in range(0, size, 256)` 顺序下发,收到 `code=1 + data.offset` 时从该处续传;单片失败重发 ≤3 次后 abort
4. **断点续传**:会话中断后重新 `ota_begin`,读 `data.offset` 续传
5. **结果确认**:刷写启动后轮询 `ota_status`(或订阅 `ota_report`);`stage=done` 后经 dev_info_query 核对 Loop 版本(需 Loop 侧支持版本上报,见 §10 待办)
6. **告警闭环**:收到 `event_report{type:ota_error}` 先落库后应答
7. DBNMQTTool 增加 OTA 页签(工具先行惯例:先模拟分片下发,固件后到)
---
# 10 待板上验证项(实现前必须闭环)
| # | 项 | 影响 |
|---|-----|------|
| 1 | DBN UART2 TX 缓冲容量(能否容纳 254B A7 帧;现 BLE 透传块 ≤94B) | 刷写块大小 |
| 2 | W25Qxx 写 256B 页 + 4KB 扇区擦除在 MQTT 接收回调内的耗时(阻塞窗口 vs MQTT 保活) | 单片落盘是否需移主循环 |
| 3 | DBN RAM 预算:镜像块缓冲(248B)+ CRC 查表(1KB)是否挤占现有栈余量(历史 .bss 事故) | 内存方案 |
| 4 | Loop APP 当前固件实际 bin 大小(验证 100KB 槽余量) | 槽位容量 |
| 5 | 刷写期间 MQTT 保活验证(设计已保证非阻塞 tick 驱动,PINGREQ/心跳/IWDG 喂狗不受阻塞;板级抓包确认 6~8s 窗口内无 PINGREQ 超时断连、IWDG 不复位) | 刷写中断安全 |
---
# 11 与现有协议的关系
| 协议文档 | 变更 |
|----------|------|
| `DLD960_IoT_MQTT协议.md` | 本设计并入后升 **V1.08**:§3 命令表 + §4.19~4.24ota_* 详情)+ §5.5ota_report+ §5.3 事件类型表补 `ota_error` + §8 错误码补 err_code 约定 + 修订记录;**V1.10 补充版本核对**:`dev_info_query`/`initialize``loop_ver`/`loop_hw_ver`,新增 `loop_version_query`(§4.25)供平台升级前后核对地感版本 |
| `DLD960_TCP_JSON协议.md` | 本版**不扩展**ROADMAP 先走 MQTT);后续按"复用命令 + stream/字段"模式补(与 log_* 扩展同套路)。注:TCP 侧 `loop_version_query` 代码已有(`tcp_json_srv.c`),文档待同步 |
| `DLD960_BLE协议.md` | 不变(BLE OTA 维持流式透传现状;本地刷写状态机与 BLE 透传共用 `lup_feed_byte_ota` |
| `README.md` / `DLD960_技术规格书.md` | 协议矩阵同步 V1.10 |
**固件代码落点(协议拍板后动):**
- `iot_mqtt_srv.c`:命令分发链加 6 个 ota_* 分支(现有 if-else 链)
- 新增 `ota_srv.c/h`:会话状态机 + W25Qxx 暂存读写 + CRC32 查表
- `usart_biz.c` / `loop_uart_proto.c`:本地刷写状态机(复用 `lup_feed_byte_ota` 解析 + `g_flag_counter_ota` 门控,注意退出机制——现 BLE 透传 flag 无退出点,本地刷写必须自管)
- `offlog.c`:补"固件升级开始/结果"事件类型
- 单测:`tests/test_ota_srv.c`gcc 提取+嵌入模式,mock SPI/MQTT/Loop
---
# 修订记录
| 版本 | 时间 | 说明 |
|------|------|------|
| V1.00 | 2026-08-20 | 设计稿:Loop MCU MQTT 远程 OTA 先存后刷协议(依据 ROADMAP P1.4 ①) |
| V1.01 | 2026-08-20 | 按修改意见修订:①Slot A/B 容量 60KB→**100KB**(镜像上限 96KB,为 DBN 镜像预留)②删除"继电器 GPIO 默认态"待验证项,维持 Loop 现状(与 BLE OTA 行为一致,不额外干预)③新增"OTA 会话期间静默"约束:暂停 MQTT `event_report` 发送(队列积压结束后补发)+ offlog/快照落盘(开始/结果日志除外)④刷写调度明确**非阻塞 tick 驱动**,刷写窗口内 MQTT 保活(PINGREQ/心跳/IWDG 喂狗)不受影响 |
+105
View File
@@ -119,6 +119,9 @@ Client Device (DLD960)
| `loop_param_set` | 设置车检器多路参数 | 是 | 0x63 | | `loop_param_set` | 设置车检器多路参数 | 是 | 0x63 |
| `loop_param_query` | 读取车检器多路参数 | 是 | 0x64 | | `loop_param_query` | 读取车检器多路参数 | 是 | 0x64 |
| `report_config` | 设置主动上报 | 是 | 0xC5 | | `report_config` | 设置主动上报 | 是 | 0xC5 |
| `log_stat` | 查询脱机日志统计(事件/快照流) | 是 | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | 是 | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | 是 | — |
| 主动推送 | | | | | 主动推送 | | | |
| `loop_data` | 线圈传感数据(设备→客户端) | — | 0xC0 | | `loop_data` | 线圈传感数据(设备→客户端) | — | 0xC0 |
| `event_report` | 事件上报(设备→客户端,**客户端须应答**,见 §5.2) | — | — | | `event_report` | 事件上报(设备→客户端,**客户端须应答**,见 §5.2) | — | — |
@@ -342,6 +345,106 @@ Client Device (DLD960)
--- ---
## 4.16 查询脱机日志统计 `log_stat`
> 设备本地 W25Qxx 环形日志(事件区/快照区容量随存储芯片动态,掉电不丢)。用于日志拉取前的分页定位。
> 通过 `data.stream` 区分日志流:`event`(事件日志,缺省)/ `snapshot`(传感快照)。
**请求:**
```json
{"msg_id":16,"cmd":"log_stat","ts":1719000000,"data":{"stream":"event"}}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应 datastream=event):**
```json
{"stream":"event","enabled":true,"boot_seq":2,"count":1234,"capacity":16256,"seq_first":100,"seq_last":1333}
```
**响应 datastream=snapshot):**
```json
{"stream":"snapshot","enabled":true,"boot_seq":2,"count":1234,"capacity":48064,"seq_first":100,"seq_last":1333}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event` / `snapshot` |
| `enabled` | bool | 日志功能是否启用(Flash 初始化成功) |
| `boot_seq` | uint16 | 当前启动序号(每次上电 +1,区分复位段) |
| `count` | uint32 | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| `capacity` | uint32 | 容量上限(**随存储芯片与流动态**):事件流 W25Q32=16256 / Q64=32640 / Q128=65408 / Q256=130944;快照流 W25Q32=48064 / Q64=105408 / Q128=220096 / Q256=449472 |
| `seq_first` | uint32 | 逻辑首条记录全局序号(`seq_last - count + 1`count=0 时为 0 |
| `seq_last` | uint32 | 最新一条记录全局序号(跨 boot 单调递增) |
---
## 4.17 分页拉取脱机日志 `log_query`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),上位机按《DLD960 BLE 协议》字段表解析。
**请求:**
```json
{"msg_id":17,"cmd":"log_query","ts":1719000000,"data":{"stream":"event","start_seq":1330,"count":4}}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 data**
```json
{"start_seq":1330,"records":[{"seq":1330,"hex":"a53200000200000001000000..."}]}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**flash 存储字节原样,小端) |
**解析字段表(与 BLE 通道完全一致):**
| 流 | 结构 | 字段表 |
|----|------|--------|
| `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算。
---
## 4.18 清除脱机日志 `log_clear`
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——谁在何时清了日志,留痕不可清除)。客户端应做权限控制。
**请求:**
```json
{"msg_id":18,"cmd":"log_clear","ts":1719000000,"data":{"stream":"event"}}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应:** 标准成功/失败。
- `stream=event`:成功后 `log_stat``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。**阻塞 ~2.8s**(63 个数据扇区 SPI 擦除),请勿高频调用。
- `stream=snapshot`:成功后 `log_stat``count` 归 0`seq_last` 继续递增;清除动作写入事件流审计(`log_clear`,payload 标记快照流)。**阻塞 ~45ms**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
---
# 5 设备主动推送 # 5 设备主动推送
主动推送帧与请求-响应共用同一条 TCP 连接,由设备在任意时刻发出。 主动推送帧与请求-响应共用同一条 TCP 连接,由设备在任意时刻发出。
@@ -457,3 +560,5 @@ Client Device (DLD960)
|------|----------|----------|--------| |------|----------|----------|--------|
| V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq | | V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq |
| V1.01 | 2026-07-15 | `event_report` 增加**客户端必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、客户端去重与先落库后应答要求(与 MQTT 协议 V1.04 对称) | wangfq | | V1.01 | 2026-07-15 | `event_report` 增加**客户端必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、客户端去重与先落库后应答要求(与 MQTT 协议 V1.04 对称) | wangfq |
| V1.02 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);事件类型表与 MQTT 协议 V1.06 对齐 | wangfq |
| V1.03 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
+187
View File
@@ -0,0 +1,187 @@
# DLD960 四通道车辆检测器 产品手册
> Product Manual · 产品型号:**DLD960GA**
> 文档版本 V1.04 · 2026-09-02 · 适用固件:整机 V1.02.05Loop V1.03 + DBN 1.02.05
---
## 1. 产品简介
DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖四条车道/四个检测断面。采用双 MCU 架构:检测核心专注线圈信号的实时处理,通信核心提供蓝牙、以太网、4G 多种接入方式,可直连道闸控制器,也可对接云平台做流量统计与远程运维。
**典型应用场景**
- 停车场出入口道闸联动(存在检测、防砸车压线检测)
- 分车道流量计数(进出计数、车间距/通过时间统计)
- 单通道会车系统的车辆存在/方向输入
- 传感数据上云(MQTT 接入业务平台)
**核心特性**
- 四路线圈独立检测,10ms 采样周期,进入确认 + 斜率限幅双重抗干扰
- 四档灵敏度、四档工作频率,支持智能选频抑制相邻线圈串扰
- 基线自适应跟踪:温漂、雨天频率漂移自动补偿,不误报不漏检
- 四个双路继电器:存在/脉冲输出、方向判别、输出延时逐路可配
- 蓝牙小程序现场配置,双 MCU 均可无线升级(OTA/ISP 透传)
- 以太网双协议:TCP JSON(局域网对接)+ MQTT(云平台),关键事件确认送达
- 通信可靠性加固:串口硬件 DMA 接收 + 固件内存优化,长时间运行稳定不丢帧、不异常重启
- BLE OTA 升级链路加固:检测 MCU 升级响应帧(0x9F 协议)完整透传回小程序,停等 ACK 不再丢失,升级成功率提升
- 网络上报携带地感版本:云平台可实时核对地感固件/硬件版本(MQTT `loop_version_query`,配合远程 OTA 升级前后版本核对)
- 联网稳定性加固:MQTT 发送退避与断连重连修复,长时间联网不掉线、不死循环
## 2. 硬件说明
### 2.1 接口一览
| 接口 | 数量 | 说明 |
|------|------|------|
| 线圈接口 | 4 路 | 接环形地感线圈(LPA~LPD) |
| 继电器输出 | 4 个双路 | RLY1~RLY4,默认关联线圈 1~4 |
| 以太网口 | 1 路 | 10MTCP JSON / MQTT |
| TTL串口 | 1 路 | DLD960 内部 两个MCU之间的 串口通信协议 |
| 蓝牙 | — | 小程序配置与升级 |
| 按键 K1 | 1 个 | 多功能按键(复位) |
### 2.2 指示灯
| 指示灯 | 颜色 | 状态说明 |
|--------|------|---------|
| LED_PWR | 红色 | 呼吸闪烁 = 系统正常运行 |
| LED_LP1~LP4 | 绿色 | 对应线圈 1~4:有车亮 / 无车灭 |
| ETH_LED_G / ETH_LED_Y | 绿/黄 | 网口连接/活动指示 |
> 上电后 LED_PWR 呼吸;各通道车检灯自检慢闪,该路绿灯闪至**本路基准稳定**才停(约 2~5 s,最长 5 s 兜底),随后无车全灭进入检测状态。若某路车检灯常亮且现场无车,参见 §7 故障排查。
### 2.3 线圈施工要求(关键!)
线圈质量直接决定检测效果,施工务必达标:
| 项目 | 要求 |
|------|------|
| 线圈形状 | 矩形/八角形,典型 2m × 1m(依车道宽度调整) |
| 匝数 | 4~6 匝(视周长,目标电感 80~300 μH) |
| 线材 | ≥1.0mm² 耐高温多股软线(如硅橡胶线) |
| 槽深 | 30~50mm,切槽后清理干净,线圈压实密封 |
| 引线 | 双绞(≥20 绞/米),长度尽量短(≤100m),不与动力电缆同管 |
| 相邻线圈间距 | ≥1m;无法保证时开启智能选频并错开频率档 |
| 绝缘 | 线圈对地绝缘电阻 > 10 MΩ(500V 摇表) |
## 3. 快速上手
1. **接线**:接入线圈、继电器输出至道闸/控制器,需要联网则接网线。
2. **上电**:LED_PWR 呼吸闪烁;各通道绿灯自检慢闪至基准稳定(约 2~5 s,最长 5 s 兜底)后进入检测状态(上电 3 秒内不主动上报数据)。
3. **配置**:手机小程序经蓝牙连接设备 → 验证密码 → 设置各路灵敏度/频率/输出模式。
4. **验证**:车辆压线,对应绿色车检灯亮、继电器吸合;离开后灯灭、继电器释放。
5. **联网(可选)**:小程序或 TCP 命令配置 IP/MQTT 参数,接入平台。
## 4. 参数配置
四种配置通道功能等价:**蓝牙小程序**(现场推荐)、**UART TTL 串口**、**TCP JSON**、**MQTT**(远程运维)。
### 4.1 每通道可配参数
| 参数 | 出厂默认 | 说明 |
|------|---------|------|
| 灵敏度 | 档位 1(中) | 4 档;大车/干扰大调低,摩托车/高底盘车调高 |
| 工作频率 | 各路错开 | 4 档(高/中高/中低/低);相邻线圈必须错开档位 |
| 输出模式 | 存在输出 | 存在 / 脉冲 |
| 输出延时 | 0 | 继电器动作延时 |
| 方向判别 | 触发模式 | 双线圈可组方向判别 |
| 有限存在 | 0(永久) | 非 0 时,车辆持续占用超时后自动复位重建基线(单位 10 秒) |
| 线圈安全模式 | 0(关闭) | 线圈异常保护超时(单位 10 秒) |
### 4.2 灵敏度选择建议
| 场景 | 建议档位 | 说明 |
|------|---------|------|
| 标准轿车出入口 | 1~2 | 兼顾灵敏与抗干扰 |
| 大型车/货车通道 | 0~1 | 高底盘需要覆盖,但防相邻车道干扰 |
| 摩托车/非机动车混行 | 2~3 | 高灵敏度,注意误触发风险 |
| 强电磁干扰环境(变频器等) | 0~1 + 错开频率档 | 灵敏度让位于稳定性 |
### 4.3 网络配置要点
- **TCP JSON**:默认端口 **5960**,连接后须先 `pwd_verify` 鉴权,未鉴权命令一律拒绝。
- **MQTT**:配置 Broker 地址与设备 Topic 后,设备上电自动发布 `initialize` 上线消息;平台应答 `report_config` 下发时间戳完成时钟同步并配置上报间隔。
- 设备序列码(dev_serial)是 MQTT Topic 的组成部分(`dld960/{sn}/srv|dev`),出厂前须唯一设置。
## 5. 数据上报(联网模式)
| 消息 | 触发 | 内容 |
|------|------|------|
| `initialize` | 上电联网 | 序列码、型号、软硬件版本 |
| `loop_data` | 周期性(空闲按配置间隔 / 有车活动 300ms / 车状态翻变立即) | 各通道频率、变化量、车状态 |
| `event_report` | 事件发生 | 进车/出车/线圈断开,**带确认重发**(5s 超时 ×3 次,平台须应答) |
| `heartbeat` | 周期 | 在线保活、运行时长 |
时间量(通过时间、车间距)分辨率 50ms,可用于速度估计与车型粗分类。
### 5.1 脱机数据取证(蓝牙 BLE)
设备本地 W25Q32 环形存储**事件日志**(容量 8064 条,掉电不丢)与**传感快照**(0xC0 传感波形按上报节奏落盘)。断网/故障期间事件照常记录,可事后经蓝牙小程序或 APP 读取:
- **事件日志**:上电/复位原因、MQTT 连接/断开/重连、event_report 重发/放弃、线圈事件、时钟同步锚点、日志清除审计——用于区分"设备真复位"与"MQTT 断连重连",故障溯源
- **传感快照**:断网期间 4 路线圈波形回放,用于信号质量分析
- **BLE 命令**OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR0x25~0x27+ SNAP_STAT / SNAP_QUERY / SNAP_CLEAR0x28~0x2A),与 MQTT/TCP 的 log_stat / log_query / log_clear 同语义
- **审计留痕**:日志/快照清除操作本身写入事件流,不可清除
详见《DLD960 BLE 协议》V1.02。
## 6. 固件升级
| 对象 | 方式 | 操作 |
|------|------|------|
| 通信 MCU | 蓝牙 OTA | 小程序选择固件直接升级 |
| 检测 MCU | ISP 透传 | 小程序经蓝牙→通信 MCU 串口透传升级 |
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.05。
>
> ⚠ **检测 MCU 升级完成后,若小程序无响应,请将设备断电重启**(DBN OTA 透传模式需重启恢复,V1.02.03 已知约束)。
## 7. 故障排查
| 现象 | 可能原因 | 处理 |
|------|---------|------|
| LED_PWR 不亮/不呼吸 | 供电异常 / 主板故障 | 查电源电压,重新上电 |
| 无车但车检灯常亮 | 线圈附近有大金属体移入;基线未收敛 | 移除金属物;等待 10s 冻结超时自适应,或断电重启重建基线 |
| 有车不触发 | 灵敏度过低;线圈匝数不足/电感异常;底盘过高 | 提高灵敏度档位;实测线圈电感;摩托车场景用 3 档 |
| 车检灯随机闪烁(无车误触发) | 相邻线圈串扰;变频器等电磁干扰;线圈绝缘劣化 | 错开频率档+开智能选频;降低灵敏度;摇表测绝缘 |
| 雨天频繁误报 | 线圈密封失效,频率漂移超阈值 | 基线自适应可吸收慢漂移;快速漂移需重新密封线圈 |
| 继电器输出正常但平台无数据 | 网络配置错误;MQTT Topic/序列码不符 | 查 IP/Broker 配置;核对 dev_serial 与 Topic |
| 设备频繁自动重启 / 偶发数据丢失 | 通信 MCU 旧固件资源不足(V1.02.01 已修复) | 升级整机固件至 V1.02.01 及以上版本 |
| BLE OTA 升级失败/进度卡住 | 通信 MCU 旧固件 0x9F 透传缺陷(V1.02.03 已修复) | 升级整机固件至 V1.02.03 及以上版本 |
| 配置无法保存/每次上电恢复出厂 | 旧固件 factory 写入被禁用;换新存储芯片后参数区无出厂配置(V1.02.04 已修复) | 升级整机固件至 V1.02.05 及以上版本 |
| 事件重复上报 | 平台未按协议应答 event_report | 平台须回显 msg_id 应答(先落库后应答) |
| 线圈断开告警 | 线圈开路/引线断裂 | 万用表量线圈回路电阻(正常几欧姆以内) |
## 8. 技术支持信息
排障时请提供以下信息,可大幅加快定位:
1. 整机固件版本(小程序设备信息页 / `dev_info_query`
2. 现场线圈参数:尺寸、匝数、实测电感、引线长度
3. 干扰源情况:变频器、大功率设备、相邻车道线圈间距
4. 联网问题附 `loop_data` 中该通道的频率与变化量数值(后台可据此判断信号质量)
**相关文档**
| 文档 | 用途 |
|------|------|
| 《DLD960 技术规格书》 | 完整技术参数 |
| 《DLD960 串口通信协议》V1.01 | 串口 对接开发 |
| 《DLD960 TCP JSON 协议》V1.03 | 局域网对接开发 |
| 《DLD960 IoT MQTT 协议》V1.10 | 云平台对接开发(含 Loop 远程 OTA 与地感版本查询) |
| 《DLD960 BLE 协议》V1.02 | 蓝牙小程序/APP 对接开发(脱机日志与传感快照读写) |
| 《环路车辆检测器验收标准》 | 采购/部署验收 |
---
## 修订记录
| 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------|
| V1.04 | 2026-09-02 | Loop V1.03 单侧更新:绿灯上电自检说明改为闪至基准稳定才停(约 2~5 s,最长 5 s 兜底),上电流程/故障排查同步 | wangfq |
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:核心特性补网络上报携带地感版本 + 联网稳定性加固;相关文档表 MQTT 协议 V1.07→V1.10(修正滞后);故障排查配置保存建议升至 V1.02.05 及以上 | wangfq |
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04BLE OTA 升级链路加固说明(0x9F 响应帧透传,停等 ACK 完整);故障排查新增 OTA 失败 + 配置无法保存条目;固件升级补断电重启提示;存储兼容适配(去厂商代码 + 恢复 factory 写入) | wangfq |
| V1.01 | 2026-08-18 | 配套整机发布 V1.02.01:通信稳定性修复说明(UART2 DMA 防丢帧、异常重启修复);新增 BLE 脱机日志/传感快照读写说明(§5.1) | wangfq |
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
+194
View File
@@ -0,0 +1,194 @@
# DLD960 四通道车辆检测器 技术规格书
> Technical Specification · 产品型号:**DLD960GA**
> 文档版本 V1.04 · 2026-09-02 · 配套整机发布 V1.02.05Loop V1.03 单侧更新)
---
## 1. 产品概述
DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆检测器,采用双 MCU 架构:检测 MCU 专注线圈信号采集与检测算法的实时性,通信 MCU 承载蓝牙、以太网、4G 多通道接入与云平台对接。适用于停车场出入口控制、道闸联动、分车道流量计数、会车控制等场景。
## 2. 系统架构
```
线圈1~4 ──► AT32F421(检测 MCU)──► 继电器1~4(存在/脉冲输出)
│ UART 0x7F 私有协议 @192000bps
CH32V208(通信 MCU)──► ETH / BLE / UART TTL / SPI Flash
```
| 项目 | 检测 MCU | 通信 MCU |
|------|---------|---------|
| 型号 | 雅特力 AT32F421ARM Cortex-M4 | 沁恒 CH32V208RISC-V |
| 主频 | 120 MHz | 120 MHz(芯片最高 144 MHz,启用网络时限定 120 MHz |
| Flash / SRAM | 64 KB / 16 KB | 128 KB / 64 KB |
| 职责 | 四路线圈驱动、频率采样、车辆检测算法、继电器输出 | BLE、以太网(TCP JSON / MQTT)、UART TTLRX DMA)、参数与日志外部存储 |
| RTOS/协议栈 | FreeRTOS V10.4.3 | WCH BLE 协议栈 + WCHNET TCP/IP |
双 MCU 间通过 TTL 串口互联(0x7F 私有帧协议,波特率 192000bpsUART2 RX 硬件 DMA 循环接收,抗打印关中断丢帧),传感数据主动上推、配置命令透传。OTA 升级时 UART2 RX 切换 0x9F 帧协议(DMA poll 按 `g_flag_counter_ota` 模式选择解析器),检测 MCU bootloader 的响应帧(pre_ok/addr_ok/data ACK)经 BLE 完整透传回小程序(V1.02.03)。
## 3. 检测规格
### 3.1 检测通道
| 参数 | 规格 |
|------|------|
| 检测通道数 | 4 路独立线圈通道 |
| 检测原理 | LC 振荡频率法(TMR3 四通道输入捕获) |
| 检测采样周期 | 10 ms/通道(算法主循环 tick |
| 工作频率档位 | 4 档(高 / 中高 / 中低 / 低),每路独立两级调频电容(33nF / 10nF)组合切换 |
| 智能选频 | 支持(smart_mode,自动错开相邻通道频率,抑制串扰) |
| 线圈电感适应范围 | 待确认(典型环形线圈 2~6 匝,需现场实测电感与振荡频率) |
### 3.2 灵敏度
进入阈值 = 基线 × SensTable[档位] / 65536,释放阈值带 ~50% 滞回(防止临界抖动)。
| 灵敏度档位 | 进入阈值(Δf/f) | 释放阈值(Δf/f) | 说明 |
|-----------|-----------------|-----------------|------|
| 0 | ≈ 0.330% | ≈ 0.165% | 最低灵敏度(大车/高底盘场景抗干扰) |
| 1 | ≈ 0.165% | ≈ 0.110% | |
| 2 | ≈ 0.055% | ≈ 0.027% | |
| 3 | ≈ 0.015% | ≈ 0.014% | 最高灵敏度(摩托车/高底盘车检出) |
### 3.3 检测算法(Loop 固件 1.0
| 机制 | 参数 | 作用 |
|------|------|------|
| IIR 低通滤波 | α = 79/256,τ ≈ 32 ms @10ms tick | 平滑高频噪声,保留车辆信号 |
| 斜率限幅 | 单步最大变化 5%(低基值下限 100) | 削除瞬态尖峰(继电器动作、电磁干扰) |
| 进入确认 | 连续 3 个采样周期低于阈值 | 抗尖峰误触发 |
| 基线跟踪 | 滑动平均窗口 500 样本(5s 更新周期,噪声抑制 22×) | 跟随温漂/环境慢变 |
| 基线冻结保护 | 偏差超限即冻结基线更新 | 防止过车信号污染基线 |
| 冻结超时自适应 | 10 s 超时 + ±2% 稳定性窗校验 | 温漂/换线圈后自动重建基线;慢速进车不误吸收 |
| 上电稳定期 | 基准稳定判定(Loop V1.03 起):连续 2 窗(100 样本/窗≈1 sOrigin 均值漂移 ≤0.1% + 最少 128 样本(≈1.3 s),硬兜底 500 样本(≈5 s);上电 3 s 内抑制主动上报 | 基准真正稳定才进入检测,避免上电误判;绿灯自检闪至本路基准稳定才停 |
| 线圈安全模式 | 可配置超时(单位 10 s,0=关闭) | 异常长时间占用强制复位重建基线 |
### 3.4 检测性能指标(设计目标,依据《环路车辆检测器验收标准》v1.0)
| 指标 | 设计目标 | 验收优秀档 |
|------|---------|-----------|
| 存在检测响应时间 T_onV2~V5 车型) | ≤ 100 ms | ≤ 50 ms |
| 释放时间 T_off | ≤ 300 ms | ≤ 100 ms |
| 漏检率(标准车型 30~40 km/h 通过) | 0/50 | 0/50 |
| 无车频率抖动 RMS | ≤ 3 Hz | ≤ 1 Hz |
| 基准频率漂移(10 min | ≤ 0.05% × f₀ | ≤ 0.01% × f₀ |
| 信噪比 SNR(标准轿车) | ≥ 35 dB | ≥ 45 dB |
> 以上指标基于标准化测试线圈与实验室环境,现场性能受线圈施工工艺、引线长度、电磁环境影响,以现场验收为准。
## 4. 输出规格
| 参数 | 规格 |
|------|------|
| 继电器数量 | 4 个双路继电器(RLY1~RLY4,默认一一关联线圈 1~4 |
| 输出模式 | 存在输出 / 脉冲输出(每路独立可配 output_mode |
| 输出延时 | 每路独立可配(delay_time |
| 方向判别 | 支持(direction_mode:触发模式 / 方向判别模式) |
| 有限存在 | 每路独立可配(exist_mode,单位 10 s,0=永久存在;超时全通道重启重建基线) |
| 触点容量 | 待确认(依整机原理图/继电器选型) |
## 5. 通信接口
| 接口 | 规格 | 用途 |
|------|------|------|
| 以太网 | 10MCH32V208 内置 PHY),支持 DHCP/静态 IP | TCP JSON 服务(默认端口 5960)、IoT MQTT 客户端 |
| 蓝牙 | BLE(WCH 低功耗蓝牙协议栈) | 小程序配置、OTA 升级、Loop MCU ISP 透传 |
| TTL 串口 | 1 路 | DLD960 内部两个MCU之间 的串口通信协议(Modbus 风格私有帧);UART2 RX 硬件 DMA 循环接收(抗打印关中断丢帧);OTA 升级态支持 0x9F 帧透传(V1.02.03 |
| TTL 串口 | 1 路(RFU | 预留 4G 模块扩展 |
| 外部存储 | SPI NOR Flash | 参数存储 + 日志存储 |
| 按键 | K1 多功能按键 | 复位等 |
### 5.1 协议矩阵(V1.02.05 配套)
| 协议 | 版本 | 通道 | 要点 |
|------|------|------|------|
| DLD960 串口通信协议 | V1.01 | TTL | 设备管理、参数配置、数据上报 |
| DLD960 TCP JSON 协议 | V1.03 | ETH :5960 | 密码鉴权 + 18 条命令;event_report 客户端必答(5s×3 重发);脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
| DLD960 IoT MQTT 协议 | V1.10 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分);Loop 远程 OTAota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status + ota_report256B/片 CRC32,先存后刷断点续传,Slot A/B 双槽回滚) |
| DLD960Loop 串口协议 | V1.05 | MCU 间(内部) | 0x7F 帧、0xC0 传感上报(variation 3B 有符号) |
| DLD960 BLE 协议 | V1.02 | 蓝牙 BLE | 帧格式 + 分包;脱机日志 OFFLOG_STAT/QUERY/CLEAR + 传感快照 SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
### 5.2 数据上报能力
| 上报项 | 说明 |
|--------|------|
| loop_data 传感数据 | 各通道频率、变化量 variation(有符号,正=车辆/裕量,负=反向漂移)、车状态;空闲按配置间隔 / 活动 300ms / 车状态翻转沿立即上报 |
| event_report 事件 | 进车 / 出车 / 线圈断开等关键事件,带确认重发机制(不可再生数据闭环送达) |
| 时间量 | 通过时间、车间距,分辨率 50 ms |
| 继电器输出次数 | 累计计数上报 |
| heartbeat 心跳 | 含 uptime |
| 时钟同步 | 设备无 RTC,平台经 report_config 下发 Unix 时间戳校准,上行 ts 为真实时间 |
## 6. 配置参数(每通道独立)
| 参数 | 范围 | 说明 |
|------|------|------|
| 灵敏度 sensitvity | 4 档 | 见 §3.2 |
| 工作频率 loopFreq_Level | 4 档 | 高/中高/中低/低 |
| 输出延时 delay_time | 0~255 | 继电器动作延时 |
| 输出模式 output_mode | — | 存在/脉冲 |
| 方向判别 direction_mode | 0/非0 | 触发模式 / 方向判别 |
| 线圈安全超时 loopSafe_Timeout | 0~255,单位 10s | 0=关闭 |
| 存在模式 exist_mode | 0~255,单位 10s | 0=永久存在 |
| 智能选频 smart_mode | 0/1 | 全局 |
配置通道:BLE 小程序 / UART TTL / TCP JSON / MQTT 四通道均可读写,配置持久化存储于检测 MCU 内部 Flash。
## 7. 固件升级
| 对象 | 方式 | 路径 |
|------|------|------|
| 通信 MCU (CH32V208) | BLE OTA | 手机小程序直接升级 |
| 检测 MCU (AT32F421) | ISP 透传 | 小程序 → BLE → CH32V208 → UART → AT32F421UART2 0x9F 停等协议(0xA7 每块必回 ACK),DBN 将 ACK 透传回小程序(V1.02.03 |
## 8. 电气与环境(待整机定型确认)
| 参数 | 规格 |
|------|------|
| 供电电压 | 待确认(依电源方案) |
| 整机功耗 | 待确认 |
| 工作温度 | 待确认(建议按 -25℃ ~ +75℃ 车检器行业规格验证) |
| 存储温度 / 湿度 | 待确认 |
| 外形尺寸 / 安装方式 | 待确认(导轨/壁挂,依结构件) |
| 防护等级 | 待确认 |
| EMC | 建议依 EN 50293 / GB/T 26941.3 验证 |
## 9. 引脚资源摘要
详见《DLD960硬件资源》。关键资源:
- **AT32F421**TMR3_CH1~4 线圈捕获(PA6/PA7/PB0/PB1)、8 路调频电容控制、RLY1~4PB4~PB7)、LED_PWR 红色呼吸灯(PA8+ 4 路绿色车检灯、USART1PA9/PA10 ↔ CH32V208
- **CH32V208**:内置 ETH PHYPC6~PC9+ 双色 ETH LED、USART2PA2/PA3 ↔ AT32F421)、扩展 USART1PB6/PB7RFU 4G)、K1 按键(PA0)、SPI1 外部 FlashPA4~PA7
## 10. 版本配套要求
> ⚠ **自内部协议 V1.05 起,vd960Loop 与 vd960DBN 固件必须同版本配套刷写**variation 字段 2B→3B 帧格式变更,混刷导致数据静默错位)。
| 组件 | V1.02.05 配套版本 |
|------|-------------------|
| vd960Loop 固件 | 1.0(不变) |
| vd960DBN 固件 | 1.02.05 |
### 10.1 已知约束(V1.02.01
- 固件版本三段式:`FIRMWARE_VER="1.02.05"`MAIN=1 / SUB=2 / SUBSUB=5);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)。
- CH32V208 RAM 预算:BLE 协议栈固定占用低 16KB(不可裁剪),用户可用 48KB;固件经 RAM 瘦身优化,栈余量约 6KB,现场升级务必使用 V1.02.01 及以上版本。
- 传感快照区延后 3s 初始化(开机 3s 内传感数据不落盘,设计接受)。
- UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)。
- **OTA 透传模式无自动退出机制**:进入 OTA 态(`g_flag_counter_ota.flag=1`)后需断电重启才恢复 0x7F 正常通信(V1.02.03 已知约束,升级完成后重启设备)。
- **UART2 0x4A 版本查询与 `loop_data`/事件上报共用总线**:低频命令理论无冲突,时序待板上验证(V1.02.05)。
---
## 修订记录
| 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------|
| V1.04 | 2026-09-02 | Loop V1.03 单侧更新:上电稳定期由固定 128 样本改为基准判稳(连续 2 窗 Origin 漂移 ≤0.1% + 5 s 硬兜底),绿灯自检闪至基准稳定才停止 | wangfq |
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:协议矩阵标题/版本配套矩阵/固件三段式版本同步;新增已知约束 UART2 0x4A 版本查询总线时序;MQTT V1.10 地感版本上报与 loop_version_query 说明 | wangfq |
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出;存储适配(SPI 识别去厂商代码 + factory 写入恢复);2026-08-20 协议矩阵 MQTT 同步 V1.08Loop 远程 OTA 协议设计先行,固件待实现,Slot A/B 100KB | wangfq |
| V1.01 | 2026-08-18 | 配套整机发布 V1.02.01:版本矩阵更新、UART2 RX DMA 通信可靠性说明、已知约束补充 | wangfq |
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
+2 -2
View File
@@ -2,9 +2,9 @@
# 概述 # 概述
vd\_960是一个四通道车辆检测器,支持网口、RS485、蓝牙等。功能说明: vd\_960是一个四通道车辆检测器,支持网口、蓝牙、4G扩展(RFU等。功能说明:
* 双核心(双MCU),雅特力芯片AT32F421120MHz,64KB Flash,16KB SRAM)负责线圈功能,沁恒CH32V208144MHz,实际采用的是120MHz 128KB Flash, 64KB SRAM)负责通信(蓝牙、网口,一路RS485和一路TTL串口)和外部存储(参数存储和日志存储) * 双核心(双MCU),雅特力芯片AT32F421120MHz,64KB Flash,16KB SRAM)负责线圈功能,沁恒CH32V208144MHz,实际采用的是120MHz 128KB Flash, 64KB SRAM)负责通信(蓝牙、网口,两路TTL串口:一路内部对接AT32F421,一路预留4G模块上云)和外部存储(参数存储和日志存储)
* 四路线圈,四个双路继电器。 * 四路线圈,四个双路继电器。
+230
View File
@@ -0,0 +1,230 @@
# vd960 (DLD960) 开发计划 — V1.0.0 之后
> 制定日期:2026-07-17 · 基线:整机 V1.0.02026-07-16 发布)
> 原则:先稳固、再上云、后算法深化;任何涉及继电器输出的改动,防砸车逻辑优先。
---
## 0. 现状基线
| 项 | 状态 |
|----|------|
| 整机版本 | V1.0.0(首发),Loop/DBN 固件 1.0 配套 |
| 协议 | Loop 串口 V1.05 / TTL 串口 V1.01 / TCP JSON V1.01 / IoT MQTT V1.05 |
| 在途改动(未提交) | BLE 透传路径重构:`usart_biz.c` 0x7F→0x8F 帧改走 `set_response_tran_to_notify()`,原 `_report_flag` 保留机制移除 |
| 已知约束 | 无 RTC(依赖平台下发 ts);CH32V208 栈紧张;Loop/DBN 固件禁止混刷 |
---
## P0 — 发布后稳固(目标 V1.1.0,约 2~4 周)
### P0.1 BLE 透传路径重构收尾 🔴(在途)
当前工作区改动把 0xC0 传感帧的 BLE 转发从"置 flag 延迟消费"改为"立即 notify"。收尾必须核对三件事:
1. **帧消费方真值表**0xC0 帧现在有 **三个消费方**TCP JSON 回调 / MQTT `iot_sensor_ingest` / BLE notify)。2026-07-15 的"陈旧快照 34 秒"事故根因就是消费路径数据源分裂——BLE 加入后必须重画一张"谁消费、谁清理(InitPkgUart)"的真值表,确保:
- 每条路径都能拿到帧(同源汇聚不漏帧);
- 帧最终一定被清理(无滞留 → 无假"新帧");
- BLE ACS 分支改 Magic 为 0x8F 后,MQTT/TCP 路径若还要读同一缓冲,读到的是被改过的帧头(**待确认是否有影响**)。
2. `set_response_tran_to_notify()` 新增返回值的语义与调用方检查。
3. 板上验证:BLE 小程序连上时,TCP/MQTT 上报不受影响;BLE 断开后无帧泄漏。
### P0.2 代码 TODO 清剿
| 位置 | 问题 | 优先级 |
|------|------|--------|
| `iot_mqtt_srv.c:753` | `loop_ok[i] = 1` 硬编码,未取 Loop MCU 真实线圈状态 → 平台看到的线圈健康度是假的 | 🔴 高 |
| `net_srv.c:528` | MQTT 接收长度未做溢出防护(`len-received` 越界风险)| 🔴 高 |
| `storage.c:494`DBN | Flash_Model 读失败无停机/降级保护 | 中 |
| `net_srv.c:1225` | 序列号修改命令未实现 | 中 |
| `dbn_ble_srv.c:870` | ACS timeout `min→ms` 换算待确认(当前 ×60×100,疑似按 10ms tick,需对时钟源) | 中 |
| `main.c`Loop)多处 | "不整个设备复位,只复位蓝牙以外部分"——历史注释,评估后关闭或立项 | 低 |
### P0.3 回归验证清单(配合 vd_test_fixture 工装)
- [ ] 长车双线圈场景复测:car_leave 事件后**下一条** loop_data 的 iscar 必须已翻 false(同源验证)
- [ ] event_report 断网入队 → 重连补报闭环;5s 超时重发 ×3 后的放弃行为
- [ ] 时钟同步:校准前 ts=上电秒数的平台侧兼容;mstick 49.7 天回绕仿真(gcc 隔离单测)
- [ ] MQTT 稳定性:>512B 帧、PINGREQ 保活、broker 主动踢线后的重连节奏
- [ ] 拨码/灵敏度/hold_time/relay_delay 全参数配置读写一致性(BLE 链路 + MQTT 链路交叉验证)
**P0 出口条件**:以上全绿 → 两侧 `FIRMWARE_VER` 升 1.1 → 整机 tag v1.1.0。
---
## P1 — 传感数据上云闭环(目标 V1.2.x,约 1~2 月,与 edc_server 平台侧并行)
这是你"传感数据云 + 业务平台"目标的主线,设备侧固件已基本就绪,重心在平台侧消费。
### P1.1 平台侧对接(edc_server
- **event_report 消费契约**:平台先落库、后 ACK(code=0)——严禁先 ACK 后落库(设备出队后事件不可再生)
- **loop_data 落库 + 可视化**variation 有符号曲线(正=车/裕量,负=基线污染——免费的诊断维度)、频率漂移趋势图;锯齿波形态识别(5s 基线阶跃是正常形态,别当故障)
- **流量/占用 API**:基于 car_enter/car_leave 事件 + 通过时间/车间距(50ms 单位)产出分通道流量计数、占用率、平均通过时间
- **设备台账**initialize 上线登记、心跳离线判定(阈值=心跳间隔×3)、report_config 远程下发界面
### P1.2 脱机日志子系统 🆕(W25Qxx SPI NOR
**硬件底子**DBN 外挂 SPI NORSPI1)。默认 **W25Q3232Mbit = 4MB,已确认)****兼容 W25Q64 / W25Q128 / W25Q256**,上电先读 JEDEC ID 识别存储类型,再按类型映射各分区容量。
**分区规划(顺序:参数区 → OTA 镜像暂存区 → 事件日志区 → 传感快照区)**:
| 分区 | 容量策略 | 用途 |
|------|---------|------|
| 参数区 | **固定 64KB**0x000000 起,现用 <1KB | 现有配置,留余量 |
| OTA 镜像暂存区 | **固定 512KB** | Loop 固件(64KB×2 版本回滚)+ 预留 DBN 镜像 |
| 事件日志区 | **随存储类型**(见下表) | 环形,关键事件流,不被快照冲掉 |
| 传感快照区 | **随存储类型**(= 总容量 − 参数区 − OTA − 事件日志区) | 环形,0xC0 帧原样落盘 |
**存储类型 ↔ 容量映射表**(上电读 JEDEC ID 后选档):
| JEDEC ID | 芯片 | 总容量 | 参数区 | OTA 暂存 | 事件日志区 | 传感快照区 |
|----------|------|--------|--------|----------|-----------|-----------|
| EF 40 16 | W25Q32(默认) | 4MB | 64KB | 512KB | **512KB**~16000 条) | **~2.94MB** |
| EF 40 17 | W25Q64 | 8MB | 64KB | 512KB | **1MB**~32000 条) | **~6.44MB** |
| EF 40 18 | W25Q128 | 16MB | 64KB | 512KB | **2MB**~64000 条) | **~13.44MB** |
| EF 40 19 | W25Q256 | 32MB | 64KB | 512KB | **4MB**~128000 条) | **~27.44MB** |
- 事件日志区容量随芯片**等比翻倍**(512KB→4MB),保证日志保留时长不随硬件降配而缩水
- 传感快照区吃剩余容量;**W25Q32(4MB)为最小可部署配置**
- 分区边界在**上电初始化时**由 JEDEC ID 计算得出,offlog/快照代码按运行时分区表寻址,不写死
**两条日志流分开存**
1. **事件流(必录,低速率)**car_enter/car_leave(含时间量)、线圈断线/恢复、继电器动作、上电/复位(含复位原因)、MQTT/TCP 连接与断开、event_report ACK 超时/重发/放弃、配置变更(含来源:BLE/MQTT/TCP)、灵敏度换档、**时钟同步锚点**、固件升级开始/结果、日志清除操作(审计自记录)——**已实现(offlog)**
2. **快照流(环形可覆盖)**:0xC0 帧 4 线圈数据原样(12B/线圈)+ 头部(序号/boot_seq/mstick),64B/条,**记录节奏与上报同频**——直接挂在 `iot_sensor_ingest()` 同源出口。**已实现(snapshot2026-08-12**:中断只入 RAM 暂存(8 深满丢新),主循环 flush 落盘;BLE 新增 SNAP_STAT/QUERY/CLEAR0x28/0x29/0x2A
**容量预算****以 W25Q32 为基准**:事件日志 512KB ≈ 16384 条 / 快照区 3008KB ≈ **4.8 万条** @64B):
| 场景 | 落盘节奏 | 保留时长(W25Q32) |
|------|---------|---------|
| 最坏情况:快档 300ms 连续压满 | 3.3 条/s | **~4.0 小时** |
| 繁忙出入口(日均 2000 车次,每车次 ~15 条突发 + 空闲 30s 底噪) | ~3.25 万条/天 | **~1.5 天** |
| 一般车道(空闲 30s 为主) | ~2900 条/天 | **~16.7 天** |
**各芯片快照保留时长对比**(同一落盘节奏下):
| 芯片 | 快照区 | 快照条数 | 最坏 300ms 压满 | 繁忙出入口 | 一般车道 |
|------|--------|----------|----------------|-----------|---------|
| W25Q32(默认) | 3008KB | 48128 | ~4.0 小时 | ~1.5 天 | ~16.7 天 |
| W25Q64 | 6592KB | 105472 | ~8.8 小时 | ~3.2 天 | ~36.6 天 |
| W25Q128 | 13760KB | 220160 | ~18.3 小时 | ~6.8 天 | ~76.4 天 |
| W25Q256 | 28096KB | 449536 | ~37.5 小时 | ~13.8 天 | ~156 天 |
> 事件日志区独立于快照:512KB→4MB16384→131072 条 @32B),**不受快照覆盖影响**。
若现场要求更长保留:把空闲档快照落盘间隔与上报解耦(如落盘固定 60s),或快照只存"有沿事件前后 ±N 条"(预触发环形),容量立刻翻数倍——**实施时按现场需求选**。
**⚠ 无 RTC 的时间戳设计(最容易踩的坑)**:断电后 mstick 归零,纯 Unix 时间戳不可行。方案:每条记录带 `(boot_seq, mstick)`;每次时钟同步成功时写一条**时间锚点事件**boot_seq ↔ unix_ts 映射);导出时由平台/小程序用锚点回算绝对时间。从未同步过的 boot 段只有相对时间——协议文档里要写明这个语义。
**磨损与 RAM**:环形顺序写天然磨损均衡(4KB sector 顺序擦除,W25Q 10 万次擦写寿命无忧);**复用现有 `SPI_FLASH_BUF[4096]`,不新增大缓冲**CH32V208 SRAM 紧张的教训)。
### P1.3 日志导出与管理 🆕
**导出通道(三条,统一按序号分页,不按时间——时间不可靠)**:
| 通道 | 机制 | 说明 |
|------|------|------|
| MQTT 自动补报 | 断网期间事件流落盘,重连后按序号续传 | 现有 16 深 RAM 队列扩展为 flash-backed,溢出转日志区;与 event_report ACK 闭环衔接 |
| MQTT/TCP 命令拉取 | `log_query {stream, start_seq, count}` 分页 | 复用现有 cmd 分发框架;注意 Publish≤500B 分批(MSS=576 教训) |
| BLE 小程序读取 | 复用 0x8F 通道新增日志读命令 | BLE MTU 分包(20~244B),小程序侧分页 UI |
**管理命令**
- `log_stat`:各流的序号区间、条数、容量占用
- `log_clear {stream}`:**需鉴权**;分流清除;清除动作本身写入事件流(审计——谁在什么时候清了日志)
- 自动管理:环形覆盖为常态,无需人工干预;满不停写
### P1.4 远程 OTA 🆕(路线已定:先走现网 MQTT,底层复用 ISP 透传)
**关键认知**BLE 给 Loop MCU 升级 = "镜像经手机流式送到 CH32V208 → ISP 串口透传刷写"。远程 OTA 只是把"送镜像"的传输层从 BLE 换成 MQTT,**ISP 透传底层完全复用**——且 Loop MCU 的 OTA 根本不需要 CH32V208 做 A/B 分区,镜像只是路过。
**分两档,难度和节奏完全不同:**
**① Loop MCU (AT32F421) 远程 OTA —— v1.2.x 落地**
采用**先存后刷**(优于流式直透):
1. 平台 MQTT 分片下发(每片带 offset+CRC,≤500B/片,支持断点续传)→ 写入 W25Qxx 暂存区
2. 全镜像 CRC 校验通过后,才启动 ISP 透传本地刷写(复用 BLE OTA 的透传状态机)
3. 抗网络抖动:下载阶段断网无所谓,续传即可;刷写阶段纯本地,窗口短
4. 暂存区保留上一版镜像 → 支持回滚重刷
**安全底线(会车产品,升级失败=继电器行为不可控)**:
- 升级窗口检查:有车压线圈时告警/延迟启动——**待定策略**
- ISP 期间 Loop MCU 停止检测,继电器处于什么状态?(复位后 GPIO 默认态应为断开=常开安全侧,**待板上验证**)
- 刷写失败自动重试 ×3,仍失败 → event_report 告警 + 保持 ISP 可重入(AT32 ISP bootloader 兜底,不会真砖)
- 升级开始/结果写入事件日志
**② DBN (CH32V208) 自身远程 OTA —— 方案评审先行,落地后置**
难点:128KB Flash 装不下 APP 双分区;现有 BLE OTA 走 WCH "OnlyUpdateApp" IAP 框架(bootloader 固定只认 BLE)。候选路线:
- (a) 镜像下到 W25Qxx 暂存 + 校验 → 改 bootloader 支持从外部 Flash 搬运(**动 bootloader 风险高**,刷坏只能拆壳烧录)
- (b) RAM 驻留搬运代码自刷写(掉电=砖,不推荐)
- (c) 维持现状:DBN 升级仍走 BLE(现场人员),只有 Loop 支持远程——**最稳妥的过渡态**
先出方案评审文档比选,v1.2.x 期间不阻塞 ①。
**③ 4G 通道下的 OTA**4G 模组(TTL 对接 UART1)跑 MQTT 后,①②的分片下载协议原样复用——传输层无关是这套设计的出发点。注意 4G 流量成本:64KB 镜像 + 重传开销可忽略,但快照流补报要设流量上限。
### P1.5 设备侧其他补强
- **loop_data 增补字段**(若 P1.1 分析需要):如 Origin 绝对值上报,便于平台重建基线轨迹——走协议 V1.06 流程,两侧同版本配套
- **DBNMQTTool** 同步:日志拉取/OTA 分片下发的模拟与断言(工具先行,固件后到)
### P1.6 4G 上云通道(UART1 RFU 启用,可后置到 P3
- 模组选型(Cat.1 足够:EC800 系列/Air780 级别),AT 透传 vs 模组内置 MQTT 两条路线对比
- 网口 MQTT 与 4G MQTT 复用同一 `iot_mqtt_srv` 状态机,仅换传输层——架构上先留接口
---
## P2 — 检测算法深化(Loop 侧,目标 V2.x,约 2~3 月)
### P2.1 抗变频器/电磁干扰 🎯(你的长期主目标)
1. **先取证再动手**:用工装+现场采集受扰线圈的 CAPVD 原始序列(10ms 粒度),确认干扰形态(周期性拍频 / 白噪 / 突发脉冲)——**待现场验证**
2. **频点错开**:4 路线圈两级调频电容(33nF/10nF)已有硬件基础,上电自检各路振荡频率,间距不足时自动切档,抑制相邻串扰
3. **算法层**:现有 IIR+斜率限幅+进入确认对突发脉冲已有基础免疫;针对周期性拍频评估陷波/中值预滤(注意 10ms tick 下的计算预算)
4. 产出:抗干扰测试报告(干扰源型号、距离、频谱、误检率前后对比)
### P2.2 双线圈测速 + 车型粗分类
- 利用现有 misc 时间量(通过时间/车间距,50ms 单位)+ 已知线圈间距 → 速度估计
- 信号长度(占用时间×速度)→ 车长 → 粗分类(小车/大车/拖挂)
- 拖挂车"半截车"识别:单通道内 variation 双峰形态检测——直接服务会车防砸
- 输出通道:event_report 新增 speed/class 字段(协议升版)
### P2.3 方向判别
- 线圈 1→2 与 2→1 的触发时序 → 方向语义,供会车/双向流量使用
- Loop 侧输出还是平台侧算:**倾向 Loop 侧**(时序精度 50ms 内,平台侧受上报调度抖动影响)
---
## P3 — 会车控制与融合(依赖 P1/P2 产出)
- **会车控制逻辑平台侧先行**:基于多台 DLD960 的 event_report(有 ACK 闭环,可靠交付)做单通道红绿灯会车状态机;业务逻辑只认 event_reportloop_data 仅做展示——两通道职责不混
- **异常分支优先设计**:设备离线(心跳超时)→ 会车灯降级策略;继电器粘连检测(relay_count 与命令数比对);车辆长时间停留(hold_time 超时全通道重启的平台侧感知)
- **单机版会车评估**:DLD960 四通道 + 继电器本地联动能否覆盖小型场景(无平台依赖,可靠性更高)——出对比方案:纯平台 / 纯单机 / 混合
- **雷达/相机融合接口预留**:扩展 UART1 或网口接入目标列表,融合仲裁放平台侧
---
## 版本映射与节奏
| 版本 | 内容 | 前置 |
|------|------|------|
| v1.1.0 | P0 全部(BLE 收尾 + TODO 清剿 + 回归全绿) | — |
| v1.2.x | P1 设备侧(脱机日志 + Loop 远程 OTA + 协议 V1.06 | edc_server 对接联调 |
| v2.0.0 | P2 算法深化(抗干扰 + 测速分类 + 方向) | 现场干扰数据采集 |
| v2.x | P3 会车控制 | P1 平台 + P2 方向判别 |
## 风险与待现场验证
| # | 风险 | 应对 |
|---|------|------|
| 1 | BLE 重构改动帧消费时序,复发"不同源"类 bug | P0.1 真值表 + 长车复测强制过 |
| 2 | 变频器干扰形态未知,算法方案可能推倒重来 | 先采数据后设计,不拍脑袋 |
| 3 | DBN 自身远程 OTA 无安全落点(128KB 无 A/B、bootloader 只认 BLE | 过渡态:DBN 走 BLE、Loop 走远程;bootloader 改造单独评审 |
| 4 | 协议再升版(V1.06)又是双侧死绑定 | 沿用 Len 字节区分格式的兼容方案,发布走配套矩阵 |
| 5 | 无 RTC,脱机日志绝对时间不可靠 | boot_seq+mstick+时钟锚点回算;协议明示"未同步段仅相对时间" |
| 6 | 日志高频写与 MQTT/BLE 共抢主循环与 SPI 总线 | 快照落盘按 page 缓写、擦除放空闲窗口;实测不达标就降快照档频率 |
@@ -241,11 +241,60 @@ void load_cfg_from_flash(void)
if(strncmp(mBuff, DEV_USER_FLASH_MAGIC, sizeof(DEV_USER_FLASH_MAGIC)) != 0) if(strncmp(mBuff, DEV_USER_FLASH_MAGIC, sizeof(DEV_USER_FLASH_MAGIC)) != 0)
{ {
free(mBuff); free(mBuff);
PRINT("Will_Factory_dev_____\n"); /* ===== 恢复 factory 写入 (2026-08-19) =====
8-13 : factory SPI 8-17
(.bss RAM + printf , SPI );
offlog/snapshot 8-18 SPI
(W25Q128) magic memory defaults,
write_net_config magic
: mismatch (magic+) flash, */
factory_dev_info(); factory_dev_info();
PRINT("CFG: magic mismatch - factory defaults written\n");
NVIC_SystemReset(); memset(g_dev_number, 0, 6);
memcpy(g_dev_number, gMacAddr, 6);
sprintf(g_dev_number_str, "%02X%02X%02X%02X%02X%02X",
g_dev_number[0], g_dev_number[1], g_dev_number[2],
g_dev_number[3], g_dev_number[4], g_dev_number[5]);
g_sub_code_enable.code_set = 0x0001;
g_max_counter_bt_min = BT_DISABLE_IDLE_TIMEOUT;
g_max_counter_bt_timeout = g_max_counter_bt_min * 60 * 1000;
g_storage_uart_baud = UART_BAUD_DEFAULT_PORT_1;
g_storage_uart_baud_2 = UART_BAUD_DEFAULT_PORT_2;
memcpy(g_dev_password, "123456", 6);
memcpy(local_net_cfg.mac, g_dev_number, 6);
local_net_cfg.lip[0] = 192; local_net_cfg.lip[1] = 168; local_net_cfg.lip[2] = 1; local_net_cfg.lip[3] = 188;
local_net_cfg.sub[0] = 255; local_net_cfg.sub[1] = 255; local_net_cfg.sub[2] = 255; local_net_cfg.sub[3] = 0;
local_net_cfg.gw[0] = 192; local_net_cfg.gw[1] = 168; local_net_cfg.gw[2] = 1; local_net_cfg.gw[3] = 1;
local_net_cfg.dns[0] = 192; local_net_cfg.dns[1] = 168; local_net_cfg.dns[2] = 1; local_net_cfg.dns[3] = 1;
local_net_cfg.port_ssc_tcp = 5550;
local_net_cfg.port_ssc_udp = NET_REPORT_INTERVAL;
local_net_cfg.port_ssc_udp_message = 5505;
local_net_cfg.port_dev_tcp = 5550;
local_net_cfg.port_dev_udp = 4900;
net_center_info.lssc_ip[0] = 192; net_center_info.lssc_ip[1] = 168; net_center_info.lssc_ip[2] = 1; net_center_info.lssc_ip[3] = 222;
net_center_info.msg_port = 4999;
net_center_info.tcp_port = 5550;
net_center_info.udp_port = NET_REPORT_INTERVAL;
net_center_info.sw_ver[0] = 1; net_center_info.sw_ver[1] = 2;
memset(iot_net_info.remote_addr, 0, 64);
memcpy(iot_net_info.remote_addr, "121.37.20.199", 13);
iot_net_info.mqtt_port = 1883;
memset(iot_net_info.client_id, 0, 64);
memset(iot_net_info.username, 0, 64); memcpy(iot_net_info.username, "admin", 5);
memset(iot_net_info.password, 0, 32); memcpy(iot_net_info.password, "password", 8);
iot_net_info.mode = 0;
g_iot_topic.clientid_enable = 0;
memset(g_iot_topic.topic_pub, 0, 64);
memset(g_iot_topic.topic_sub, 0, 64);
g_sub_code_enable.net_enable = 1;
g_sub_code_enable.iot_enable = 0;
g_sub_code_enable.custom_enable = 0;
g_sub_code_enable.loop_enable = 0;
g_sub_code_enable.dgdus_enable = 0;
g_sub_code_enable.wbdus_enable = 0;
g_sub_code_enable.radar_enable = 0;
PRINT("CFG: memory defaults OK (factory written)\n");
return;
} }
_offset = DEV_NUMER_ADDR_OFFSET; _offset = DEV_NUMER_ADDR_OFFSET;
@@ -305,6 +354,16 @@ void load_cfg_from_flash(void)
/* 2026-08-17: 字符串 0 终止保险 — Flash 中字符串若不足定长且后续 0xFF,
%s () '\0' */
iot_net_info.remote_addr[63] = '\0';
iot_net_info.client_id[63] = '\0';
iot_net_info.username[63] = '\0';
iot_net_info.password[31] = '\0';
g_iot_topic.topic_pub[63] = '\0';
g_iot_topic.topic_sub[63] = '\0';
free(mBuff); free(mBuff);
g_sub_code_enable.net_enable = g_sub_code_enable.code_set & 0x01; g_sub_code_enable.net_enable = g_sub_code_enable.code_set & 0x01;
@@ -315,6 +374,7 @@ void load_cfg_from_flash(void)
g_sub_code_enable.wbdus_enable = (g_sub_code_enable.code_set >> 5) & 0x01; g_sub_code_enable.wbdus_enable = (g_sub_code_enable.code_set >> 5) & 0x01;
g_sub_code_enable.radar_enable = (g_sub_code_enable.code_set >> 6) & 0x01; g_sub_code_enable.radar_enable = (g_sub_code_enable.code_set >> 6) & 0x01;
/* 2026-08-17 栈溢出修复后恢复 */
PRINT("\nSub_Code: 0x%04X, net_enable:%d, iot_enable:%d\n", g_sub_code_enable.code_set, g_sub_code_enable.net_enable, g_sub_code_enable.iot_enable); PRINT("\nSub_Code: 0x%04X, net_enable:%d, iot_enable:%d\n", g_sub_code_enable.code_set, g_sub_code_enable.net_enable, g_sub_code_enable.iot_enable);
// g_sub_code_enable.laser_enable = (g_sub_code_enable.code_set >> 9) & 0x01; // g_sub_code_enable.laser_enable = (g_sub_code_enable.code_set >> 9) & 0x01;
@@ -16,6 +16,8 @@
#include "ch32v20x_it.h" #include "ch32v20x_it.h"
#include "eth_driver.h" #include "eth_driver.h"
#include "CONFIG.h" #include "CONFIG.h"
#include "debug.h"
#include "fault_diag.h"
/********************************************************************* /*********************************************************************
* LOCAL FUNCTIONS * LOCAL FUNCTIONS
@@ -38,6 +40,40 @@ void NMI_Handler(void)
{ {
} }
/* 故障诊断现场: .noinit 段 (复位不清零) */
FaultDiag g_fault_diag __attribute__((section(".noinit")));
/*********************************************************************
* @fn fault_diag_init
*
* @brief main : + + boot_cnt
*
* @return none
*/
void fault_diag_init(void)
{
if (g_fault_diag.magic == 0xFA57FA57UL) {
PRINT("FAULT_DIAG: boot_cnt=%lu mcause=0x%08lx mepc=0x%08lx mtval=0x%08lx marker=0x%08lx\n",
(unsigned long)g_fault_diag.boot_cnt,
(unsigned long)g_fault_diag.mcause,
(unsigned long)g_fault_diag.mepc,
(unsigned long)g_fault_diag.mtval,
(unsigned long)g_fault_diag.marker);
} else if (g_fault_diag.marker != 0) {
/* 非 HardFault 复位 (IWDG 超时/卡死/掉电): marker 仍记录执行轨迹 */
PRINT("FAULT_DIAG: no-hardfault boot_cnt=%lu marker=0x%08lx (卡死/看门狗复位)\n",
(unsigned long)g_fault_diag.boot_cnt,
(unsigned long)g_fault_diag.marker);
}
/* 重置现场, boot_cnt 保留递增 */
g_fault_diag.magic = 0;
g_fault_diag.mcause = 0;
g_fault_diag.mepc = 0;
g_fault_diag.mtval = 0;
g_fault_diag.marker = 0;
g_fault_diag.boot_cnt++;
}
/********************************************************************* /*********************************************************************
* @fn HardFault_Handler * @fn HardFault_Handler
* *
@@ -47,6 +83,16 @@ void NMI_Handler(void)
*/ */
void HardFault_Handler(void) void HardFault_Handler(void)
{ {
/* 纯 RAM 写现场 (不依赖 UART/printf): 栈坏/中断禁用也能留痕 */
g_fault_diag.magic = 0xFA57FA57UL;
g_fault_diag.mcause = __get_MCAUSE();
g_fault_diag.mepc = __get_MEPC();
g_fault_diag.mtval = __get_MTVAL();
/* 尽力打印 (UART 可用时), 失败不影响 RAM 现场 */
PRINT("HARDFAULT: mcause=0x%08lx mepc=0x%08lx mtval=0x%08lx\n",
(unsigned long)g_fault_diag.mcause,
(unsigned long)g_fault_diag.mepc,
(unsigned long)g_fault_diag.mtval);
NVIC_SystemReset(); NVIC_SystemReset();
while(1) while(1)
{ {
@@ -10,7 +10,9 @@
#include "storage.h" #include "storage.h"
#include <string.h> #include <string.h>
#include "net_srv.h" #include "net_srv.h"
#include "offlog.h"
#include "snapshot.h"
extern uint16_t peripheral_get_mtu(void);
uint8_t g_flag_notify_temp = 0; //ÁÙʱ֪ͨnotify flag, 0 disable, 1 enable uint8_t g_flag_notify_temp = 0; //ÁÙʱ֪ͨnotify flag, 0 disable, 1 enable
@@ -29,6 +31,12 @@ extern uint8_t loop1_FLAG_CUT;
void clear_ble_notify_buf(BLE_Notify_Buf * buf) void clear_ble_notify_buf(BLE_Notify_Buf * buf)
{ {
if(buf->flag || buf->len)
{
PRINT("CLR_NTF: buf=%08lx flag=%d len=%d ret=%08lx\n",
(unsigned long)buf, buf->flag, buf->len,
(unsigned long)__builtin_return_address(0));
}
buf->len = 0; buf->len = 0;
buf->flag = 0; buf->flag = 0;
memset(buf, 0, MAX_BLE_Notify_Buf_LEN); memset(buf, 0, MAX_BLE_Notify_Buf_LEN);
@@ -38,6 +46,12 @@ void clear_buf_dbn_ble(Buf_DBN_BLE * buf)
{ {
uint8_t i = 0; uint8_t i = 0;
if(buf->flag)
{
PRINT("CLEAR busy resp: amt=%d seq=%d off=%d ret=%08lx\n",
buf->pkg_amount, buf->pkg_seq, buf->dat_offset,
(unsigned long)__builtin_return_address(0));
}
buf->flag = 0; buf->flag = 0;
buf->magic = 0; buf->magic = 0;
@@ -65,7 +79,7 @@ void clear_buf_dbn_ble_all(Buf_DBN_BLE * buf)
buf->flag = 0; buf->flag = 0;
buf->magic = 0; buf->magic = 0;
buf->cmd = 0; buf->cmd = 0;
memset(buf->dat, 0, MAX_BLE_BUF_LEN); memset(buf->dat, 0, MAX_BLE_DAT_BUF_LEN);
buf->dat_len = 0; buf->dat_len = 0;
buf->dat_offset = 0; buf->dat_offset = 0;
buf->pkg_amount = 0; buf->pkg_amount = 0;
@@ -181,6 +195,25 @@ static uint8_t set_net_info_to_ready(uint8_t * dat_dst)
return i; return i;
} }
/* =============================================================
* ble_notify_chunk_max: max dat bytes per notify packet
* follow negotiated MTU to avoid 'Too large noti' drop:
* whole pkt = 4B header(magic/header/len/cmd) + dat + 2B ckb
* must satisfy whole pkt <= peripheralMTU - 3 (ATT opcode+handle)
* and whole pkt <= MAX_BLE_Notify_Buf_LEN (local buf, no overflow)
* MTU=23 -> 14, MTU=96 -> 87, MTU>=103 -> 94
* =============================================================
*/
static uint16_t ble_notify_chunk_max(void)
{
uint16_t _mtu = peripheral_get_mtu();
if (_mtu < 23) _mtu = ATT_MTU_SIZE; /* fallback before negotiation */
uint16_t _limit = _mtu - 9; /* pkt limit - header4 - ckb2 */
if (_limit > (MAX_BLE_Notify_Buf_LEN - 6))
_limit = MAX_BLE_Notify_Buf_LEN - 6;
return _limit;
}
void set_response_iot_net(Buf_DBN_BLE *response_dst) void set_response_iot_net(Buf_DBN_BLE *response_dst)
{ // config iot_net { // config iot_net
uint8_t ret = 0; uint8_t ret = 0;
@@ -205,11 +238,14 @@ void set_response_iot_net(Buf_DBN_BLE *response_dst)
memcpy(&(response_dst->dat[i]), iot_net_info.password, strlen(iot_net_info.password)); memcpy(&(response_dst->dat[i]), iot_net_info.password, strlen(iot_net_info.password));
i += strlen(iot_net_info.password); i += strlen(iot_net_info.password);
response_dst->dat_len = i; response_dst->dat_len = i;
response_dst->pkg_amount = i / MAX_BLE_DAT_RESPONSE_LEN; {
if((i % MAX_BLE_DAT_RESPONSE_LEN) > 0) uint16_t _chunk = ble_notify_chunk_max();
response_dst->pkg_amount = i / _chunk;
if((i % _chunk) > 0)
{ {
response_dst->pkg_amount += 1; response_dst->pkg_amount += 1;
} }
}
response_dst->pkg_seq = 0; response_dst->pkg_seq = 0;
response_dst->flag = 1; response_dst->flag = 1;
@@ -232,11 +268,14 @@ void set_response_iot_topic(Buf_DBN_BLE *response_dst)
memcpy(&(response_dst->dat[i]), g_iot_topic.topic_sub, strlen(g_iot_topic.topic_sub)); memcpy(&(response_dst->dat[i]), g_iot_topic.topic_sub, strlen(g_iot_topic.topic_sub));
i += strlen(g_iot_topic.topic_sub); i += strlen(g_iot_topic.topic_sub);
response_dst->dat_len = i; response_dst->dat_len = i;
response_dst->pkg_amount = i / MAX_BLE_DAT_RESPONSE_LEN; {
if((i % MAX_BLE_DAT_RESPONSE_LEN) > 0) uint16_t _chunk = ble_notify_chunk_max();
response_dst->pkg_amount = i / _chunk;
if((i % _chunk) > 0)
{ {
response_dst->pkg_amount += 1; response_dst->pkg_amount += 1;
} }
}
response_dst->pkg_seq = 0; response_dst->pkg_seq = 0;
response_dst->flag = 1; response_dst->flag = 1;
} }
@@ -246,12 +285,14 @@ uint8_t set_response_buf(Buf_DBN_BLE *response_dst, uint8_t magic, uint8_t cmd,
{ {
uint8_t ret = 0; uint8_t ret = 0;
uint8_t i = 0; uint8_t i = 0;
if(dat_len > MAX_BLE_DAT_BUF_LEN) dat_len = MAX_BLE_DAT_BUF_LEN; /* overflow guard */
clear_buf_dbn_ble(response_dst); clear_buf_dbn_ble(response_dst);
response_dst->magic = magic; response_dst->magic = magic;
response_dst->cmd = cmd; response_dst->cmd = cmd;
uint8_t _amount = dat_len / MAX_BLE_DAT_RESPONSE_LEN; uint16_t _chunk = ble_notify_chunk_max();
if((dat_len % MAX_BLE_DAT_RESPONSE_LEN) > 0) uint8_t _amount = dat_len / _chunk;
if((dat_len % _chunk) > 0)
{ {
_amount++; _amount++;
} }
@@ -308,6 +349,7 @@ uint8_t set_response_tran_to_notify(uint8_t *dat_ori, uint8_t dat_len, BLE_Notif
} }
notify_dst->len = dat_len; notify_dst->len = dat_len;
notify_dst->flag = 1; notify_dst->flag = 1;
return notify_dst->flag;
} }
@@ -368,9 +410,12 @@ uint8_t set_response_to_notify(Buf_DBN_BLE *response_ori, BLE_Notify_Buf * notif
uint8_t _pkg_amount = response_ori->pkg_amount; uint8_t _pkg_amount = response_ori->pkg_amount;
uint8_t _pkg_seq = response_ori->pkg_seq; uint8_t _pkg_seq = response_ori->pkg_seq;
uint8_t _remain_len = response_ori->dat_len - response_ori->dat_offset; uint8_t _remain_len = response_ori->dat_len - response_ori->dat_offset;
if(_remain_len > MAX_BLE_DAT_RESPONSE_LEN)
{ {
_remain_len = MAX_BLE_DAT_RESPONSE_LEN; uint16_t _chunk = ble_notify_chunk_max();
if(_remain_len > _chunk)
{
_remain_len = (uint8_t)_chunk;
}
} }
_pkg_seq += 1; _pkg_seq += 1;
notify_dst->buf[1] = (_pkg_amount << 4)|(_pkg_seq); notify_dst->buf[1] = (_pkg_amount << 4)|(_pkg_seq);
@@ -875,6 +920,190 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len)
} }
PRINT("Rcv_acs_enable_flag: %d\n", g_dbn_ble_state_acs_enable.flag); PRINT("Rcv_acs_enable_flag: %d\n", g_dbn_ble_state_acs_enable.flag);
} break; } break;
case CMD_DBN_OFFLOG_STAT: {
/* offlog stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */
uint8_t _i = 0;
uint32_t _total = offlog_count();
uint32_t _seq_last = offlog_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint32_t _boot = offlog_boot_seq();
uint32_t _cap = OFFLOG_MAX_RECORDS;
if (!offlog_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
} else {
tmp_ble_buf[_i++] = 0x00; /* ok */
tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF);
}
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: offlog_stat count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last);
} break;
case CMD_DBN_OFFLOG_QUERY: {
/* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..4)
resp: status(1) count(1) + N x 32B OfflogEvt raw (LE) */
uint32_t _start_seq = 0;
uint32_t _req_count = 0;
uint32_t _total = offlog_count();
uint32_t _seq_last = offlog_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint8_t _i = 0, _j = 0;
if (!offlog_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */
tmp_ble_buf[_i++] = 0x02; /* bad request */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
_start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8)
| ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24);
_req_count = pkg[8];
if (_req_count > OFFLOG_MAX_QUERY_RECORDS) _req_count = OFFLOG_MAX_QUERY_RECORDS;
if (_req_count == 0) _req_count = OFFLOG_MAX_QUERY_RECORDS;
/* overflow guard: resp dat = 2 + N*32 must fit tmp buf.
history: MAX_BLE_TMP_BUF_LEN was 100, 130B write overflowed
into g_notify_buftemp and erased pending chunk 2 */
{ uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ?
((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(OfflogEvt)) : 0;
if (_req_count > _max_rec) _req_count = _max_rec; }
tmp_ble_buf[_i++] = 0x00; /* status ok */
tmp_ble_buf[_i++] = 0; /* count placeholder */
if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) {
uint32_t _idx = _start_seq - _seq_first;
while (_j < _req_count && (_idx + _j) < _total) {
OfflogEvt _evt;
if (offlog_read_idx((uint16_t)(_idx + _j), &_evt) != 0) break;
memcpy(&tmp_ble_buf[_i], &_evt, sizeof(OfflogEvt));
_i += sizeof(OfflogEvt);
_j++;
}
}
tmp_ble_buf[1] = _j;
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: offlog_query start_seq=%lu req=%lu fetched=%u\n",
(unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j);
} break;
case CMD_DBN_OFFLOG_CLEAR: {
/* clear offlog (audit trail). blocking ~45ms (logical clear + current sector), main-loop ctx ok */
uint8_t _i = 0;
offlog_clear();
tmp_ble_buf[_i++] = 0x00; /* ok */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: offlog_clear done\n");
} break;
case CMD_DBN_SNAP_STAT: {
/* snap stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */
uint8_t _i = 0;
uint32_t _total = snap_count();
uint32_t _seq_last = snap_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint32_t _boot = snap_boot_seq();
uint32_t _cap = SNAP_MAX_RECORDS;
if (!snap_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
} else {
tmp_ble_buf[_i++] = 0x00; /* ok */
tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF);
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF);
}
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: snap_stat count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last);
} break;
case CMD_DBN_SNAP_QUERY: {
/* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..2)
resp: status(1) count(1) + N x 64B SnapRec raw (LE) */
uint32_t _start_seq = 0;
uint32_t _req_count = 0;
uint32_t _total = snap_count();
uint32_t _seq_last = snap_seq_last();
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
uint8_t _i = 0, _j = 0;
if (!snap_enabled()) {
tmp_ble_buf[_i++] = 0x01; /* disabled */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */
tmp_ble_buf[_i++] = 0x02; /* bad request */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
break;
}
_start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8)
| ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24);
_req_count = pkg[8];
if (_req_count > SNAP_MAX_QUERY_RECORDS) _req_count = SNAP_MAX_QUERY_RECORDS;
if (_req_count == 0) _req_count = SNAP_MAX_QUERY_RECORDS;
/* overflow guard: resp dat = 2 + N*64 must fit tmp buf (ODR lesson) */
{ uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ?
((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(SnapRec)) : 0;
if (_req_count > _max_rec) _req_count = _max_rec; }
tmp_ble_buf[_i++] = 0x00; /* status ok */
tmp_ble_buf[_i++] = 0; /* count placeholder */
if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) {
uint32_t _idx = _start_seq - _seq_first;
while (_j < _req_count && (_idx + _j) < _total) {
SnapRec _rec;
if (snap_read_idx(_idx + _j, &_rec) != 0) break;
memcpy(&tmp_ble_buf[_i], &_rec, sizeof(SnapRec));
_i += sizeof(SnapRec);
_j++;
}
}
tmp_ble_buf[1] = _j;
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: snap_query start_seq=%lu req=%lu fetched=%u\n",
(unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j);
} break;
case CMD_DBN_SNAP_CLEAR: {
/* clear snapshot (audit trail). blocking ~45ms (logical clear + current sector) */
uint8_t _i = 0;
snap_clear();
tmp_ble_buf[_i++] = 0x00; /* ok */
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
PRINT("BLE: snap_clear done\n");
} break;
default: default:
{ {
} break; } break;
@@ -893,6 +1122,7 @@ void manage_dbn_ble_transparent(uint8_t *pkg, uint8_t len)
void poll_dbn_ble(void) void poll_dbn_ble(void)
{ {
static uint8_t _last_ntf_flag = 0;
uint8_t i = 0; uint8_t i = 0;
uint8_t magic = 0; uint8_t magic = 0;
if(g_ble_rcv_buf.flag) if(g_ble_rcv_buf.flag)
@@ -935,8 +1165,24 @@ void poll_dbn_ble(void)
} }
if(g_notify_buftemp.flag == 0) if(g_notify_buftemp.flag == 0)
{ {
// report_sens_acs(); static uint8_t _last_resp_flag = 0;
g_flag_notify_temp = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp); uint8_t _pull = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
g_flag_notify_temp = _pull;
if(g_buf_ble_response.flag != _last_resp_flag)
{
PRINT("BLE resp flag %d->%d (pull=%d amt=%d seq=%d off=%d)\n",
_last_resp_flag, g_buf_ble_response.flag, _pull,
g_buf_ble_response.pkg_amount, g_buf_ble_response.pkg_seq,
g_buf_ble_response.dat_offset);
_last_resp_flag = g_buf_ble_response.flag;
}
}
if(g_notify_buftemp.flag != _last_ntf_flag)
{
PRINT("BLE ntf flag %d->%d (len=%d temp=%d)\n",
_last_ntf_flag, g_notify_buftemp.flag, g_notify_buftemp.len,
g_flag_notify_temp);
_last_ntf_flag = g_notify_buftemp.flag;
} }
} }
@@ -11,45 +11,46 @@
#include <stdint.h> #include <stdint.h>
#define PRODUCT_MODEL "DLD960GA" #define PRODUCT_MODEL "DLD960GA"
#define FIRMWARE_VER "1.0" #define FIRMWARE_VER "1.02.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 1 #define FIRMWARE_VER_SUB 2
#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
#define BUFF_STACK_SIZE 512 //128 #define BUFF_STACK_SIZE 64
#define MAX_UART_TXRX_LEN 32 #define MAX_UART_TXRX_LEN 32
#define MAX_TOUCHUAN_DISABLE_TIME 15 #define MAX_TOUCHUAN_DISABLE_TIME 15
#define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min #define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min
typedef enum typedef enum
{ {
DDType_DNT900 = 1, //1 ???????? DNT900 DDType_DNT900 = 1, //1 区位有线 DNT900
DDType_WireDUS, //2???ù?¨???????? DG DDType_WireDUS, //2超声波车位有线 DG
DDType_4G_1, DDType_4G_1,
DDType_WireDus_Wb, //4 ???ù?¨???????? ?ò?? DDType_WireDus_Wb, //4 超声波车位有线 万泊
DDType_LoraAreaMain, //5 Lora ?????÷?ú DDType_LoraAreaMain, //5 Lora 区位主机
DDType_LoraAreaSub, //6 Lora ???????ú DDType_LoraAreaSub, //6 Lora 区位从机
DDType_LoraLotMain, //7 Lora ?????÷?ú DDType_LoraLotMain, //7 Lora 车位主机
DDType_LoraLotSub, //8 Lora ???????ú DDType_LoraLotSub, //8 Lora 车位从机
DDType_LoraBroadcast, //9 Lora ???????? DDType_LoraBroadcast, //9 Lora 广播模式
DDType_LoraRelay, //10 Lora ?????? ???? DLR101 DDType_LoraRelay, //10 Lora 中继, 使用 DLR101
DDType_Relay, //11 ?????????? ?¨??RS485-2 ??·? DDType_Relay, //11 中继模式, 通过RS485-2 收发
DDType_4G_IOT, //12 4G ??±¨??????????????MQTT DDType_4G_IOT, //12 4G 上报传感数据,使用MQTT
DDType_Wire_Collect_IOT, //13 ????????±¨??????????????MQTT DDType_Wire_Collect_IOT, //13 有线,上报传感数据,使用MQTT
DDType_Wire_Display_IOT, //14 ????????·?????????MQTT DDType_Wire_Display_IOT, //14 有线显示发布,使用MQTT
DDType_Radar_Lot, //15 ?×?????? DDType_Radar_Lot, //15 雷达车位
DDType_DLD950 = 20, //DLD950 DDType_DLD950 = 20, //DLD950
DDType_DBN101 = 21, DDType_DBN101 = 21,
DDType_DNT910 = 22, //DNT910,DNT920 DDType_DNT910 = 22, //DNT910,DNT920
DDType_DNT920 = 23, DDType_DNT920 = 23,
DDType_DLD950V4 = 24, DDType_DLD950V4 = 24, // DLD960
}DG_Device_Type; }DG_Device_Type;
typedef enum typedef enum
@@ -67,12 +68,12 @@ typedef struct _SUB_CODE_ENABLE_
uint8_t net_enable; uint8_t net_enable;
uint8_t iot_enable; uint8_t iot_enable;
uint8_t custom_enable; uint8_t custom_enable;
uint8_t loop_enable; // ?÷?????? uint8_t loop_enable; // 流量计数
uint8_t dgdus_enable; uint8_t dgdus_enable;
uint8_t wbdus_enable; uint8_t wbdus_enable;
uint8_t radar_enable; uint8_t radar_enable;
uint8_t laser_enable; // lora?¤???×?????? uint8_t laser_enable; // lora激光雷达车位
uint8_t lora_enable; // Lora ???? uint8_t lora_enable; // Lora 车位
//uint8_t input_enable; //uint8_t input_enable;
} Sub_Code_Enable; } Sub_Code_Enable;
@@ -91,21 +92,12 @@ extern Pkg_Uart g_pkg_uart_1;
extern Pkg_Uart g_pkg_uart_2; extern Pkg_Uart g_pkg_uart_2;
#define RX_BUFFER_LEN BUFF_STACK_SIZE
typedef struct
{
volatile uint8_t DMA_USE_BUFFER;
uint8_t Rx_Buffer[2][RX_BUFFER_LEN];
} USART_DMA_UNIT;
typedef struct _DBN_BLE_STATE_ typedef struct _DBN_BLE_STATE_
{ {
uint8_t flag; // if need to report uint8_t flag; // if need to report
uint8_t enable; // 是否使能 uint8_t enable; // 使能
uint8_t send_flag; uint8_t send_flag;
uint8_t obj_amount; uint8_t obj_amount;
uint8_t cmd; uint8_t cmd;
@@ -162,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_ */
@@ -49,6 +49,13 @@
#define CMD_DBN_SET_CJQ_PARAM 0x23 // 设置 车检器参数 #define CMD_DBN_SET_CJQ_PARAM 0x23 // 设置 车检器参数
#define CMD_DBN_GET_CJQ_PARAM 0x24 // 读取 车检器参数 #define CMD_DBN_GET_CJQ_PARAM 0x24 // 读取 车检器参数
#define CMD_DBN_OFFLOG_STAT 0x25 // query offlog stat
#define CMD_DBN_OFFLOG_QUERY 0x26 // query offlog records by seq
#define CMD_DBN_OFFLOG_CLEAR 0x27 // clear offlog (audit trail)
#define CMD_DBN_SNAP_STAT 0x28 // query snapshot stat
#define CMD_DBN_SNAP_QUERY 0x29 // query snapshot records by seq
#define CMD_DBN_SNAP_CLEAR 0x2A // clear snapshot (audit trail)
#define CMD_DBN_RW_UART_BAUD 0x31 #define CMD_DBN_RW_UART_BAUD 0x31
@@ -73,7 +80,8 @@
#define MAX_BLE_DAT_RESPONSE_LEN (BLE_BUFF_MAX_LEN - 4) // 14 //返回 的所有字节数不能超过20个字节,除去 magic、header和ckb校验字节,剩下数据的字节数为14 #define MAX_BLE_DAT_RESPONSE_LEN (BLE_BUFF_MAX_LEN - 4) // 14 //返回 的所有字节数不能超过20个字节,除去 magic、header和ckb校验字节,剩下数据的字节数为14
#define MAX_BLE_Notify_Buf_LEN BLE_BUFF_MAX_LEN //24 #define MAX_BLE_Notify_Buf_LEN BLE_BUFF_MAX_LEN //24
#define MAX_BLE_BUF_LEN BLE_BUFF_MAX_LEN //128 #define MAX_BLE_BUF_LEN BLE_BUFF_MAX_LEN //128
#define MAX_BLE_TMP_BUF_LEN BLE_BUFF_MAX_LEN #define MAX_BLE_TMP_BUF_LEN 132 // offlog QUERY: 1 status + 4x32B records
#define MAX_BLE_DAT_BUF_LEN 132 // response data buffer (align tmp buf)
typedef struct _BLE_Notify_Buf_ typedef struct _BLE_Notify_Buf_
{ {
@@ -91,7 +99,7 @@ typedef struct _BUF_DBN_BLE_
uint8_t dat_offset; uint8_t dat_offset;
uint8_t pkg_amount; uint8_t pkg_amount;
uint8_t pkg_seq; uint8_t pkg_seq;
uint8_t dat[MAX_BLE_BUF_LEN]; uint8_t dat[MAX_BLE_DAT_BUF_LEN];
} Buf_DBN_BLE; } Buf_DBN_BLE;
extern Buf_DBN_BLE g_buf_ble_response; extern Buf_DBN_BLE g_buf_ble_response;
@@ -0,0 +1,56 @@
/********************************** (C) COPYRIGHT *******************************
* File Name : fault_diag.h
* Author : wfq
* Version : V1.0
* Date : 2026-08-13
* Description : .noinit RAM + marker
*
* : HardFault UART/printf (/), ;
* RST=0x00000000 , RSTSCKR
* : .noinit (, ):
* - HardFault_Handler RAM mcause/mepc/mtval ( UART)
* - FAULT_MARKER()
* - main fault_diag_init() + boot_cnt
* : FAULT_MARKER(MK_XXX) ; marker
*******************************************************************************/
#ifndef __FAULT_DIAG_H
#define __FAULT_DIAG_H
#include <stdint.h>
typedef struct {
uint32_t magic; /* 0xFA57FA57 = 现场有效 */
uint32_t mcause; /* RISC-V 异常原因 (5/7=野指针 2=非法指令 4/6=未对齐) */
uint32_t mepc; /* 异常指令 PC (对照 .map 反查函数) */
uint32_t mtval; /* 错误访问地址 */
uint32_t marker; /* 最后执行位置标记 */
uint32_t boot_cnt; /* 复位计数 */
} FaultDiag;
/* 定义在 ch32v20x_it.c (.noinit 段) */
extern FaultDiag g_fault_diag;
/* main 开头调用: 打印上次现场 + 重置 + boot_cnt++ */
void fault_diag_init(void);
/* 执行轨迹标记 */
#define FAULT_MARKER(s) (g_fault_diag.marker = (s))
#define MK_MAIN_ENTRY 0xA0000001UL /* main 入口 */
#define MK_INIT_DONE 0xA0000002UL /* 初始化完成, 进主循环前 */
#define MK_LOOP_TOP 0xA0000010UL /* 主循环 while 开头 */
#define MK_UART_SRV_IN 0xA0000011UL /* uart_srv 调用前 */
#define MK_UART_SRV_OUT 0xA0000012UL /* uart_srv 返回后 */
#define MK_SIM_SPI_IN 0xA0000013UL /* sim_snap_spi 调用前 */
#define MK_POLL_BLE_IN 0xA0000014UL /* poll_dbn_ble 调用前 */
#define MK_LUP_FRAME_IN 0xA0000020UL /* lup_process_frame 入口 */
#define MK_LUP_FRAME_OUT 0xA0000021UL /* lup_process_frame 出口 */
#define MK_INGEST_IN 0xA0000030UL /* iot_sensor_ingest 入口 */
#define MK_INGEST_OUT 0xA0000031UL /* iot_sensor_ingest 出口 */
#define MK_EVT_FEED_IN 0xA0000032UL /* iot_evt_feed 入口 */
#define MK_EVT_FEED_OUT 0xA0000033UL /* iot_evt_feed 出口 */
#define MK_EVT_CB_IN 0xA0000034UL /* iot_evt_sensor_cb 入口 */
#define MK_EVT_CB_OUT 0xA0000035UL /* iot_evt_sensor_cb 出口 */
#endif /* __FAULT_DIAG_H */
@@ -22,7 +22,7 @@
#define IOT_MQTT_RECONNECT_MAX_MS 60000 // 最大重连间隔 (指数退避上限) #define IOT_MQTT_RECONNECT_MAX_MS 60000 // 最大重连间隔 (指数退避上限)
#define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s) #define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s)
#define IOT_MQTT_RECV_BUF_LEN 1024 // 接收缓冲区 (单次交互 ≤1024) #define IOT_MQTT_RECV_BUF_LEN 1024 // 接收缓冲区 (单次交互 ≤1024)
#define IOT_MQTT_SEND_BUF_LEN 1024 // 发送缓冲区 (单次交互 ≤1024) #define IOT_MQTT_SEND_BUF_LEN 800 // 发送缓冲区 (2026-08-18: hex 上报方案, 快照 2 条 128hex×2+头 ~420B; loop_data ~604B; 不扩容)
#define IOT_MQTT_TOPIC_MAX_LEN 128 // Topic 最大长度 #define IOT_MQTT_TOPIC_MAX_LEN 128 // Topic 最大长度
/*=========================================================================== /*===========================================================================
@@ -52,5 +52,10 @@ 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_kick(void); // 喂硬件 IWDG (主循环调用)
#endif /* __IOT_MQTT_SRV_H__ */ #endif /* __IOT_MQTT_SRV_H__ */
@@ -7,7 +7,7 @@
* @brief DLD960Loop (7F) CH32V208 DLD960Loop MCU * @brief DLD960Loop (7F) CH32V208 DLD960Loop MCU
* *
* : 7F | Addr | LEN | CMD | Data... | XOR | SUM * : 7F | Addr | LEN | CMD | Data... | XOR | SUM
* UART: USART2, 115200bps, PA2(Tx)/PA3(Rx) * UART: USART2, 192000bps, PA2(Tx)/PA3(Rx)
* *
* MCU(CH32V208)MCU(DLD960Loop/AT32F421) * MCU(CH32V208)MCU(DLD960Loop/AT32F421)
* (TCP JSON/5960)MCU下发命令并获取数据 * (TCP JSON/5960)MCU下发命令并获取数据
@@ -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);
@@ -212,7 +228,11 @@ typedef enum {
LUP_FRAME_STATE_HEADER, // 接收 Header (Addr, LEN, CMD) LUP_FRAME_STATE_HEADER, // 接收 Header (Addr, LEN, CMD)
LUP_FRAME_STATE_VALUE, // 接收 Value LUP_FRAME_STATE_VALUE, // 接收 Value
LUP_FRAME_STATE_CHECK, // 接收 Checksum LUP_FRAME_STATE_CHECK, // 接收 Checksum
LUP_FRAME_STATE_COMPLETE // 帧完成 LUP_FRAME_STATE_COMPLETE, // 帧完成
/* 0x9F OTA 专用状态 (2026-08-19): 帧格式 9F SubL SubH LEN CMD DATA CHECK */
LUP_FRAME_STATE_OTA_LEN, // 0x9F: 收 LEN (含 CMD)
LUP_FRAME_STATE_OTA_CMD, // 0x9F: 收 CMD
LUP_FRAME_STATE_OTA_CHECK // 0x9F: 收 CHECK (仅 SUM, 无 XOR)
} LUP_FrameState; } LUP_FrameState;
typedef struct { typedef struct {
@@ -228,6 +248,11 @@ extern LUP_FrameParser g_lup_parser;
/* 喂一个字节给帧解析器,返回 1 表示帧接收完成 */ /* 喂一个字节给帧解析器,返回 1 表示帧接收完成 */
int lup_feed_byte(uint8_t byte); int lup_feed_byte(uint8_t byte);
/* 0x9F OTA 帧解析器 (OTA 模式专用, g_flag_counter_ota.flag=1 时使用)
* : 9F SubL SubH LEN CMD DATA(LEN-1) CHECK(SUM)
* 0x7F g_lup_parser /, (LUP_FRAME_STATE_OTA_*) */
int lup_feed_byte_ota(uint8_t byte);
/* 取完整帧的指针和数据长度 */ /* 取完整帧的指针和数据长度 */
const uint8_t *lup_frame_data(void); const uint8_t *lup_frame_data(void);
uint16_t lup_frame_len(void); uint16_t lup_frame_len(void);
@@ -36,7 +36,7 @@ extern "C" {
/* The number of sockets, the maximum is 31 */ /* The number of sockets, the maximum is 31 */
#define WCHNET_MAX_SOCKET_NUM (WCHNET_NUM_IPRAW+WCHNET_NUM_UDP+WCHNET_NUM_TCP+WCHNET_NUM_TCP_LISTEN) #define WCHNET_MAX_SOCKET_NUM (WCHNET_NUM_IPRAW+WCHNET_NUM_UDP+WCHNET_NUM_TCP+WCHNET_NUM_TCP_LISTEN)
#define WCHNET_TCP_MSS 576 //1024 //1460 /* Size of TCP MSS*/ #define WCHNET_TCP_MSS 700 //576 //1024 //1460 /* Size of TCP MSS*/
#define WCHNET_NUM_POOL_BUF (WCHNET_NUM_TCP*2+2) /* The number of POOL BUFs, the number of receive queues */ #define WCHNET_NUM_POOL_BUF (WCHNET_NUM_TCP*2+2) /* The number of POOL BUFs, the number of receive queues */
@@ -47,6 +47,12 @@ extern "C" {
#define ETH_RXBUFNB 4 /* Number of MAC received descriptors */ #define ETH_RXBUFNB 4 /* Number of MAC received descriptors */
/* 2026-08-17 RAM 瘦身: ETH_MAX_PACKET_SIZE 1520 → 768.
TCP (MSS=700) = 700+20+20+14+4 = 758B 768 ;
(TCP JSON/MQTT/SSC UDP ) >728B UDP
.bss: (1520-768)×(RX 4 + TX 1) = 3.76KB ( +3.76KB)
UDP , 1024 ( 2.48KB) */
#define ETH_MAX_PACKET_SIZE 768
#ifndef ETH_MAX_PACKET_SIZE #ifndef ETH_MAX_PACKET_SIZE
#define ETH_RX_BUF_SZE 1520 /* MAC receive buffer length, an integer multiple of 4 */ #define ETH_RX_BUF_SZE 1520 /* MAC receive buffer length, an integer multiple of 4 */
#define ETH_TX_BUF_SZE 1520 /* MAC send buffer length, an integer multiple of 4 */ #define ETH_TX_BUF_SZE 1520 /* MAC send buffer length, an integer multiple of 4 */
@@ -84,7 +90,10 @@ extern "C" {
/* If you want to achieve a higher transmission speed, /* If you want to achieve a higher transmission speed,
* try to increase RECE_BUF_LEN to (WCHNET_TCP_MSS*4) * try to increase RECE_BUF_LEN to (WCHNET_TCP_MSS*4)
* and increase WCHNET_NUM_TCP_SEG to (WCHNET_NUM_TCP*4)*/ * and increase WCHNET_NUM_TCP_SEG to (WCHNET_NUM_TCP*4)*/
#define RECE_BUF_LEN (WCHNET_TCP_MSS*2) /* socket receive buffer size */ /* 2026-08-17 RAM 瘦身: MSS×2(1400)→1024 — MSS=700 时完整 TCP 段+头≈740B,
1024 ; 5 RECE_BUF_LEN (SocketRecvBuf/MyBuf/tmp/_iot_wchnet_buf/
g_json_wchnet_buf) ~2.6KB .bss ( +2.6KB) */
#define RECE_BUF_LEN 1024 /* socket receive buffer size */
#define WCHNET_NUM_PBUF WCHNET_NUM_POOL_BUF /* Number of PBUF structures */ #define WCHNET_NUM_PBUF WCHNET_NUM_POOL_BUF /* Number of PBUF structures */
@@ -92,7 +101,7 @@ extern "C" {
#define WCHNET_MEM_HEAP_SIZE (((WCHNET_TCP_MSS+0x10+54+8)*WCHNET_NUM_TCP_SEG)+ETH_TX_BUF_SZE+64+2*0x18) /* memory heap size */ #define WCHNET_MEM_HEAP_SIZE (((WCHNET_TCP_MSS+0x10+54+8)*WCHNET_NUM_TCP_SEG)+ETH_TX_BUF_SZE+64+2*0x18) /* memory heap size */
#define WCHNET_NUM_ARP_TABLE 50 /* Number of ARP lists */ #define WCHNET_NUM_ARP_TABLE 16 /* 2026-08-17: 50→16 (停车设备同时连接 IP 少, 省 ~0.8KB .bss) */
#define WCHNET_MEM_ALIGNMENT 4 /* 4 byte alignment */ #define WCHNET_MEM_ALIGNMENT 4 /* 4 byte alignment */
@@ -0,0 +1,173 @@
/**
******************************************************************************
* @file offlog.h
* @author wfq
* @version V1.0
* @date 2026-08-04
* @brief DBN (W25Qxx SPI NOR, )
*
* : "平台重复上线/异常重启但现场无日志"
* vsMQTT ( initialize):
* - : OFFLOG_EVT_BOOT , boot_seq
* - : boot_seq , IOT_*
*
* (2026-08-12 ROADMAP ):
* | 0x000000 64KB ()
* | OTA 0x010000 512KB ()
* | 0x090000 : W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB
* | (snapshot.c, 2026-08-12 )
* JEDEC ID (0x9F) (g_offlog_part )
*
* :
* - 0 () = , (boot_seq//),
* - 1..63 = , 128 x 32B
* - , (),
* - , = ()
*
* (32B ):
* magic(1) type(1) len(1) flags(1) seq(4) boot_seq(2) rsvd(2)
* ts_ms(4) unix_ts(4) payload(14)
******************************************************************************
*/
#ifndef _OFFLOG_H__
#define _OFFLOG_H__
#include <stdint.h>
/*===========================================================================
*
*===========================================================================*/
/* 固定分区 (ROADMAP 2026-08-12): 参数区 64KB + OTA 512KB, 事件区紧随其后 */
#define OFFLOG_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */
#define OFFLOG_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */
#define OFFLOG_AREA_BASE (OFFLOG_PARAM_SIZE + OFFLOG_OTA_SIZE) /* 0x090000 */
#define OFFLOG_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
#define OFFLOG_EVT_PER_SECTOR 128 /* 4096 / 32 */
/* 芯片 JEDEC ID (0x9F 命令第 3 字节, EF 40 xx) */
#define OFFLOG_CHIP_W25Q32 0x16
#define OFFLOG_CHIP_W25Q64 0x17
#define OFFLOG_CHIP_W25Q128 0x18
#define OFFLOG_CHIP_W25Q256 0x19
/* 运行时分区表 (offlog_init 由 JEDEC ID 填充; 默认 W25Q32) */
typedef struct {
uint8_t chip; /* OFFLOG_CHIP_xxx */
uint32_t area_size; /* 事件日志区大小 */
uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */
uint32_t max_records; /* 最大记录数 = data_sectors * 128 */
} OfflogPart;
extern OfflogPart g_offlog_part;
/* 兼容宏: 展开为运行时值 (offlog.c 主体引用不变) */
#define OFFLOG_AREA_SIZE (g_offlog_part.area_size)
#define OFFLOG_DATA_SECTOR_CNT (g_offlog_part.data_sectors)
#define OFFLOG_DATA_SIZE ((g_offlog_part.data_sectors) * 4096UL)
#define OFFLOG_MAX_RECORDS (g_offlog_part.max_records)
#define OFFLOG_DATA_BASE (OFFLOG_AREA_BASE + 4096UL)
/* 头扇区记录 (32B, 扇区切换时更新) */
typedef struct {
uint8_t magic[4]; /* "OFFL" */
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
uint32_t wr_sector; /* 上次写扇区号 (区内 1..63) */
uint32_t wr_off; /* 上次写偏移 (相对 OFFLOG_AREA_BASE) */
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
uint32_t count; /* 有效记录数 (0..max_records) */
uint32_t rsvd[2];
} OfflogHead; /* 28B, 按 32B 对齐使用 */
#define OFFLOG_HEAD_MAGIC0 'O'
#define OFFLOG_HEAD_MAGIC1 'F'
#define OFFLOG_HEAD_MAGIC2 'L'
#define OFFLOG_HEAD_MAGIC3 'L'
/* 事件记录 (32B 定长, 字段重排保证自然对齐无 padding:
4×uint8 uint32×3 uint16×2 payload[12] = 4+12+4+12 = 32) */
typedef struct {
uint8_t magic; /* 0xA5 */
uint8_t type; /* OFFLOG_EVT_xxx */
uint8_t len; /* payload 有效字节数 (0..12) */
uint8_t flags; /* bit0: unix_ts 有效 */
uint32_t seq; /* 全局序号 (跨 boot 递增) */
uint32_t ts_ms; /* boot 内相对时间 mstick() */
uint32_t unix_ts; /* 已同步 Unix 秒 (flags.bit0=1 时有效) */
uint16_t boot_seq; /* 启动序号 */
uint16_t rsvd;
uint8_t payload[12]; /* 事件参数 */
} OfflogEvt; /* 32B, 无 padding */
#define OFFLOG_EVT_MAGIC 0xA5
#define OFFLOG_EVT_SIZE sizeof(OfflogEvt)
#define OFFLOG_UNIX_VALID_FLAG 0x01
/*===========================================================================
*
*===========================================================================*/
enum {
OFFLOG_EVT_BOOT = 0x01, /* 上电/复位; payload[0..3]=复位原因寄存器 RCC_RSTSCKR */
OFFLOG_EVT_IOT_CONNECT = 0x10, /* MQTT TCP 连接成功 (socket 中断) */
OFFLOG_EVT_IOT_READY = 0x11, /* MQTT SUBACK → 发 initialize (重复上线直接原因!) */
OFFLOG_EVT_IOT_DISCONN = 0x12, /* MQTT 断连; payload[0]=原因(1=断开 2=超时 3=CONNACK拒绝 4=连接超时) */
OFFLOG_EVT_IOT_RECONN = 0x13, /* 重连退避; payload[0..3]=退避 ms */
OFFLOG_EVT_EVT_RETRY = 0x30, /* event_report ACK 超时重发; payload[0..3]=msg_id, payload[4]=retry */
OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */
OFFLOG_EVT_COIL = 0x40, /* 线圈事件; payload[0]=sub(1=进 2=出 3=断开 4=恢复), payload[1]=ch, payload[2..5]=value(50ms) */
OFFLOG_EVT_TIME_ANCHOR = 0x50, /* 时钟同步锚点; unix_ts=同步值 */
OFFLOG_EVT_OTA_START = 0x60, /* 固件升级开始 (V1.08); payload[0]=slot, payload[1]=target, payload[2..5]=size BE */
OFFLOG_EVT_OTA_RESULT = 0x61, /* 固件升级结果 (V1.08); payload[0..3]=result code BE (0=成功, 非0=OTA_ERR_*) */
OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */
};
/* 复位原因寄存器 (CH32V20x RCC 基址 0x40021000, RSTSCKR 偏移 0x24,
STM32F1 ; , , ) */
#define OFFLOG_RCC_BASE 0x40021000UL
#define OFFLOG_RCC_RSTSCKR (*(volatile uint32_t *)(OFFLOG_RCC_BASE + 0x24UL))
/* 复位原因位 (RCC_RSTSCKR, CH32V20x 布局 — 2026-08-12 对照 WCH ch32v20x.h 修正,
: IWDG/SFT/POR , RMVF ) */
#define OFFLOG_RST_LPWR (1UL << 31) /* 低功耗复位 */
#define OFFLOG_RST_WWDG (1UL << 30) /* 窗口看门狗复位 */
#define OFFLOG_RST_IWDG (1UL << 29) /* 独立看门狗复位 (IWDG 超时 → 主循环卡死!) */
#define OFFLOG_RST_SFT (1UL << 28) /* 软件复位 NVIC_SystemReset */
#define OFFLOG_RST_POR (1UL << 27) /* 上电/掉电复位 (真断电!) */
#define OFFLOG_RST_PIN (1UL << 26) /* NRST 引脚复位 */
#define OFFLOG_RST_RMVF (1UL << 24) /* 清除复位标志 (写 1 清全部, WCH: RCC_RMVF) */
/*===========================================================================
* API
*===========================================================================*/
void offlog_init(void); /* 上电初始化 (扫描恢复) + 内部准备 BOOT 事件 */
void offlog_boot(uint32_t reset_reason); /* 记录 BOOT 事件 (带复位原因) */
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len);
void offlog_iot_connect(void);
void offlog_iot_ready(void);
void offlog_iot_disconnect(uint8_t reason);
void offlog_iot_reconn(uint32_t backoff_ms);
void offlog_evt_retry(uint32_t msg_id, uint8_t retry);
void offlog_evt_giveup(uint32_t msg_id);
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value);
void offlog_time_anchor(uint32_t unix_ts);
void offlog_ota_start(uint8_t slot, uint32_t size); /* 固件升级开始 (V1.08, 会话静默前写入) */
void offlog_ota_result(uint32_t code); /* 固件升级结果 (V1.08, 恢复后补记) */
uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */
uint16_t offlog_count(void); /* 有效记录数 */
int offlog_read_idx(uint16_t idx, OfflogEvt *out); /* 按逻辑序号读, 0=成功 -1=越界 */
void offlog_clear(void); /* 清空事件区 (清空后写 LOG_CLEAR 审计) */
/*===========================================================================
* (MQTT V1.06 / TCP JSON V1.02)
*===========================================================================*/
uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */
uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */
const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 (旧方案, 保留) */
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len); /* 32B → 64 hex (TCP/MQTT log_query, V1.03/V1.07) */
#define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */
#endif /* _OFFLOG_H__ */
@@ -0,0 +1,147 @@
/**
******************************************************************************
* @file ota_srv.h
* @author wangfq
* @version V1.0
* @date 2026-08-20
* @brief DLD960 Loop MCU OTA (MQTT V1.08, ROADMAP P1.4 )
*
* : MQTT W25Qxx (0x010000 512KB)
* CRC32 0x9F ISP Loop MCU
*
* : docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5
* 稿: docs/DLD960_MQTT_OTA协议.md V1.01
******************************************************************************
*/
#ifndef __OTA_SRV_H__
#define __OTA_SRV_H__
#include <stdint.h>
/*===========================================================================
* ( offlog.h: OFFLOG_PARAM_SIZE=64KB + OFFLOG_OTA_SIZE=512KB)
* OTA = 0x010000 ~ 0x090000 (512KB)
* : (4KB) + Slot A(100KB) + Slot B(100KB) + (308KB)
*===========================================================================*/
#define OTA_AREA_BASE 0x10000UL /* 0x010000 (参数区 64KB 后) */
#define OTA_AREA_SIZE 0x80000UL /* 512KB */
#define OTA_META_BASE (OTA_AREA_BASE + 0x0000UL) /* 元数据主备份 */
#define OTA_META_BACKUP (OTA_AREA_BASE + 0x0200UL) /* 元数据备份 (同扇区) */
#define OTA_META_SECTOR (OTA_AREA_BASE / 4096UL) /* 元数据所在扇区号 */
#define OTA_SLOT_A_BASE (OTA_AREA_BASE + 0x1000UL) /* 0x011000 */
#define OTA_SLOT_B_BASE (OTA_AREA_BASE + 0x1A000UL) /* 0x02A000 */
#define OTA_SLOT_SIZE (100UL * 1024UL) /* 100KB */
#define OTA_MAX_IMAGE (96UL * 1024UL) /* 镜像上限 96KB (留 4KB 余量) */
/* 下载单片 (协议常量, 256B 页对齐) */
#define OTA_CHUNK_SIZE 256
/* 刷写块 (0x9F LEN 为 uint8 → DATA ≤ 254B; 取 248B) */
#define OTA_FLASH_BLOCK 248
/*===========================================================================
*
*===========================================================================*/
enum {
OTA_STATE_IDLE = 0, /* 空闲 */
OTA_STATE_DOWNLOADING, /* 下载中 */
OTA_STATE_READY, /* 校验通过, 可刷写 */
OTA_STATE_FLASHING, /* 刷写中 */
OTA_STATE_FLASH_FAILED, /* 刷写失败 (保留镜像可重试) */
OTA_STATE_ABORTED, /* 已中止 */
};
/* 刷写子状态 (非阻塞 tick 驱动) */
enum {
OTA_FLASH_IDLE = 0,
OTA_FLASH_START, /* 发 A5 启动帧 */
OTA_FLASH_WAIT_READY, /* 等 pre_ok */
OTA_FLASH_ADDR, /* 发 A6 地址帧 */
OTA_FLASH_WAIT_ADDR, /* 等 addr_ok */
OTA_FLASH_DATA, /* 发 A7 数据块 */
OTA_FLASH_WAIT_ACK, /* 等 data ACK */
};
/* OTA 错误码 (event_report type=ota_error 的 value, 协议 §5.3) */
#define OTA_ERR_READY_TIMEOUT 0x1001 /* 启动帧无响应 (pre_ok 超时) */
#define OTA_ERR_ADDR_FAIL 0x1002 /* 地址帧错误 (addr_err / 超时) */
#define OTA_ERR_DATA_ACK_LIMIT 0x1003 /* 数据块 ACK 超限 (重试×3 耗尽) */
#define OTA_ERR_CRC_FAIL 0x1004 /* 全镜像校验失败 */
#define OTA_ERR_FORCE_FAIL 0x1005 /* 安全窗口拒绝后强制失败 */
/*===========================================================================
* (56B, 64B )
*===========================================================================*/
typedef struct {
uint32_t magic; /* OTA_MAGIC 'DLD9' */
uint32_t state; /* OTA_STATE_* */
uint8_t target; /* 0=loop (1=dbn 预留) */
uint8_t slot; /* 0=A, 1=B */
uint16_t rsv;
uint32_t size; /* 镜像字节数 */
uint32_t crc32; /* 全镜像 CRC32 */
uint32_t received; /* 已下载字节数 (片对齐, 断点续传依据) */
uint32_t last_result; /* 上次刷写结果: 0=无/成功, 非0=错误码 */
uint32_t begin_ts; /* 会话开始 (同步后 Unix 秒, 0=未同步) */
char version[16]; /* 目标固件版本 (审计) */
uint32_t flash_cnt; /* 刷写尝试次数 */
uint32_t rsv2;
} OtaMeta; /* 56B */
#define OTA_MAGIC 0x39444C44UL /* 'DLD9' */
/*===========================================================================
* API
*===========================================================================*/
/* 初始化: 读元数据 (双备份校验), 刷写状态机复位 */
void ota_init(void);
/* 主循环轮询: 刷写状态机 (非阻塞 tick 驱动, 绝不阻塞主循环) */
void ota_poll(void);
/* OTA 会话期间静默标志: downloading/flashing 时置位
event_report () + offlog/ */
uint8_t ota_silent_active(void);
/* 当前会话元数据 (只读, MQTT 响应组装用) */
const OtaMeta *ota_meta(void);
/* 状态字符串 (JSON state 字段) */
const char *ota_state_str(uint8_t state);
/*===========================================================================
* MQTT (iot_mqtt_srv.c , )
* 0=, 0= (1= 3= 5=; data.err_code OTA_ERR_*)
*===========================================================================*/
int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force);
int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex);
int ota_cmd_end(uint32_t crc32);
int ota_cmd_abort(void);
int ota_cmd_flash(uint8_t slot, uint8_t force);
/* 分片落盘用: hex → bin (n 字节), 成功 0 */
int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n);
/* 从 MQTT JSON 中提取 ota_data 的 data 字段 hex 值 (兼容 "data":"..." 与 "data": "...")
hex , 0=/ */
int ota_json_extract_hex(const char *json, char *out, int out_len);
/* CRC-32/ISO-HDLC (逐位实现, 零查表 RAM) */
uint32_t ota_crc32(const uint8_t *data, uint32_t len);
/*===========================================================================
* UART2 (usart_biz.c 使)
*===========================================================================*/
/* 1 = 本地刷写进行中: uart_srv OTA 分支把 0x9F ACK 交给 ota_flash_feed_ack,
BLE (extern ota_srv.c) */
extern uint8_t g_ota_flash_active;
/* usart_srv 收到 bootloader 0x9F ACK 帧时调用 (主循环上下文) */
void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len);
/* 刷写进度 (已送字节 / 总量, ota_report 用) */
uint32_t ota_flash_sent(void);
uint32_t ota_flash_total(void);
#endif /* __OTA_SRV_H__ */
@@ -0,0 +1,139 @@
/**
******************************************************************************
* @file snapshot.h
* @author wfq
* @version V1.0
* @date 2026-08-12
* @brief DBN (W25Qxx SPI NOR, , )
*
* : 0xC0 (4 线) , ;
* "发生了什么", "波形长什么样"
*
* (ROADMAP 2026-08-12 ):
* | 0x000000 64KB ()
* | OTA 0x010000 512KB ()
* | 0x090000 (offlog , g_offlog_part.area_size)
* | ( - - )
* JEDEC ID (0x9F) / (g_snap_part )
*
* ( offlog ):
* - 0 () = , (boot_seq//),
* - 64 x 64B (4096/64)
* - , (),
* - , = ()
*
* 线 (, 2026-08-12 ):
* - USART2 ISR lup_feed_byte(), uart_srv()
* lup_process_frame iot_sensor_ingest snap_enqueue()
* - snap_enqueue(): + RAM ( SPI, );
* - snap_flush(): iot_mqtt_publish_sensor , RAM SPI
* ( ~45ms , offlog 线)
* - : tail/count, head
*
* SnapRec (64B , padding):
* magic(1) len(1) flags(1) rsvd(1) | seq(4) ts_ms(4) boot_seq(2) rsvd2(2) | coils(48)
* coils = 4 x 12B, 0xC0 线线:
* [0] 1 freq_level(2)|direction(1)|freq_type(1)|sensitivity(4)
* [1] condition(4)|loop_state(1)|car_state(1)|misc_type(2)
* [2..4] 3B LE
* [5..7] 3B LE ()
* [8..11] 4B LE
******************************************************************************
*/
#ifndef _SNAPSHOT_H__
#define _SNAPSHOT_H__
#include <stdint.h>
#include "offlog.h" /* OFFLOG_CHIP_xxx + g_offlog_part (事件区大小) */
#include "loop_uart_proto.h" /* LUP_SensorReport / LUP_CoilSensor */
/*===========================================================================
*
*===========================================================================*/
/* 固定分区 (与 offlog 一致): 参数区 64KB + OTA 512KB */
#define SNAP_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */
#define SNAP_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */
#define SNAP_FIXED_SIZE (SNAP_PARAM_SIZE + SNAP_OTA_SIZE) /* 0x090000 */
#define SNAP_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
#define SNAP_REC_PER_SECTOR 64 /* 4096 / 64 */
/* 运行时分区表 (snap_init 由 JEDEC ID + g_offlog_part 填充; 默认 W25Q32) */
typedef struct {
uint8_t chip; /* OFFLOG_CHIP_xxx */
uint32_t area_base; /* 快照区起始 (事件区后) */
uint32_t area_size; /* 快照区大小 (剩余容量) */
uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */
uint32_t max_records; /* 最大记录数 = data_sectors * 64 */
} SnapPart;
extern SnapPart g_snap_part;
/* 兼容宏: 展开为运行时值 */
#define SNAP_DATA_BASE (g_snap_part.area_base + 4096UL)
#define SNAP_AREA_SIZE (g_snap_part.area_size)
#define SNAP_DATA_SECTOR_CNT (g_snap_part.data_sectors)
#define SNAP_DATA_SIZE ((g_snap_part.data_sectors) * 4096UL)
#define SNAP_MAX_RECORDS (g_snap_part.max_records)
/* 头扇区记录 (32B, 扇区切换时更新) */
typedef struct {
uint8_t magic[4]; /* "SNAP" */
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
uint32_t wr_sector; /* 上次写扇区号 (区内 1..n) */
uint32_t wr_off; /* 上次写偏移 (绝对地址) */
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
uint32_t count; /* 有效记录数 (0..max_records) */
uint32_t rsvd[2];
} SnapHead; /* 28B, 按 32B 对齐使用 */
#define SNAP_HEAD_MAGIC0 'S'
#define SNAP_HEAD_MAGIC1 'N'
#define SNAP_HEAD_MAGIC2 'A'
#define SNAP_HEAD_MAGIC3 'P'
/* 快照记录 (64B 定长, 字段重排保证自然对齐无 padding:
4×uint8 uint32×2 uint16×2 coils[48] = 16+48 = 64) */
typedef struct {
uint8_t magic; /* 0xA6 (区别于事件 0xA5) */
uint8_t len; /* 线圈数据有效长度 = coil_count*12 (0..48) */
uint8_t flags; /* bit0: 分包帧 (预留) */
uint8_t rsvd;
uint32_t seq; /* 全局序号 (跨 boot 递增, flush 时分配) */
uint32_t ts_ms; /* boot 内相对时间 mstick() (采集时刻) */
uint16_t boot_seq; /* 启动序号 */
uint16_t rsvd2;
uint8_t coils[48]; /* 4 × 12B 线圈数据 (0xC0 线上格式原样) */
} SnapRec; /* 64B, 无 padding */
#define SNAP_MAGIC 0xA6
#define SNAP_REC_SIZE sizeof(SnapRec)
#define SNAP_COIL_BYTES 12
#define SNAP_COILS_MAX 4
/* RAM 暂存深度 (中断 enqueue → 主循环 flush 之间的缓冲)
Loop 150ms, publish_sensor flush, 8 1.2s ;
CH32V208 SRAM : 64B × 8 = 512B, SPI_FLASH_BUF 4KB */
#define SNAP_RAM_DEPTH 8
#define SNAP_MAX_QUERY_RECORDS 2 /* QUERY 分页上限: BLE 原始 64B×2 (132B 缓冲); TCP/MQTT hex 上报 64B->128 字符×2 亦安全 (2026-08-18) */
/*===========================================================================
* API
*===========================================================================*/
void snap_init(void); /* 上电初始化 (扫描恢复) */
void snap_delayed_init(void); /* 主循环延后初始化 (开机 3s 后, 2026-08-17) */
uint8_t snap_enabled(void); /* Flash 初始化成功 */
void snap_enqueue(const LUP_SensorReport *sr); /* 主循环入队: 打包→RAM 暂存 (满丢新) */
void snap_flush(void); /* 主循环: RAM 暂存→SPI 落盘 */
uint32_t snap_count(void); /* 有效记录数 */
uint32_t snap_seq_last(void); /* 最新一条记录全局序号 (0=空) */
uint32_t snap_boot_seq(void); /* 当前 boot 序号 */
int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */
void snap_clear(void); /* 清空快照区 (审计写入事件流) */
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len); /* 记录 -> JSON (旧方案, 保留) */
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len); /* 64B -> 128 hex (TCP/MQTT log_query, V1.03/V1.07) */
#endif /* _SNAPSHOT_H__ */
@@ -32,7 +32,10 @@ void load_cfg_from_flash(void);
void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size); void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size);
void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size); void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size);
void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size);
void SPI_Flash_Erase_Sector(uint32_t Dst_Addr);
void storage_init(void); void storage_init(void);
uint8_t SPI_Flash_ReadJEDEC_ID(void); /* 0x9F JEDEC device id (W25Q32=0x16...) */
#endif #endif
@@ -17,7 +17,8 @@
* Protocol Constants * Protocol Constants
*===========================================================================*/ *===========================================================================*/
#define TCP_JSON_PORT 5960 // TCP JSON protocol default port #define TCP_JSON_PORT 5960 // TCP JSON protocol default port
#define TCP_JSON_MAX_FRAME 4096 // Max JSON frame length #define TCP_JSON_MAX_FRAME 800 //4096 // Max JSON frame length
#define TCP_JSON_DATA_BUF_LEN 1024 // 本地 JSON 组装缓冲长度 (data_json)
#define TCP_JSON_AUTH_TIMEOUT_MS 60000 // 60s auth timeout (ms) #define TCP_JSON_AUTH_TIMEOUT_MS 60000 // 60s auth timeout (ms)
#define TCP_JSON_RECV_BUF_LEN (TCP_JSON_MAX_FRAME * 2) #define TCP_JSON_RECV_BUF_LEN (TCP_JSON_MAX_FRAME * 2)
#define TCP_JSON_MAX_PWD_RETRY 3 // Max password retries before lockout #define TCP_JSON_MAX_PWD_RETRY 3 // Max password retries before lockout
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,7 @@
#include "CONFIG.h" #include "CONFIG.h"
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "cmcng.h" #include "cmcng.h"
#include "fault_diag.h"
#include <string.h> #include <string.h>
/*=========================================================================== /*===========================================================================
@@ -508,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];
@@ -654,6 +680,110 @@ int lup_feed_byte(uint8_t byte)
// 不应该到达这里,调用方应在 COMPLETE 后 reset // 不应该到达这里,调用方应在 COMPLETE 后 reset
lup_frame_reset(); lup_frame_reset();
break; break;
default:
/* 未知状态 (含 0x9F OTA 专用状态值): 防御性复位, 防状态机跑飞 */
lup_frame_reset();
break;
}
return 0;
}
/*===========================================================================
* 0x9F OTA (OTA , 2026-08-19)
*
* (DLD960LoopBootloader ):
* [9F] [SubL] [SubH] [LEN] [CMD] [DATA: LEN-1] [CHECK: SUM]
* - SubL/SubH: , 2 ()
* - LEN: CMD , DATA = LEN - 1
* - CHECK: SUM ( XOR), SubL DATA
* = LEN + 5 ( 0x7F )
*
* g_lup_parser (/), (LUP_FRAME_STATE_OTA_*)
* (uart2_dma_poll) OTA lup_frame_reset()
*===========================================================================*/
int lup_feed_byte_ota(uint8_t byte)
{
LUP_FrameParser *p = &g_lup_parser;
switch (p->state) {
case LUP_FRAME_STATE_IDLE:
if (byte == LUP_MAGIC_OTA) {
p->buf[0] = byte;
p->idx = 1;
p->state = LUP_FRAME_STATE_HEADER; /* 复用 HEADER: 收 SubL/SubH */
}
break;
case LUP_FRAME_STATE_HEADER:
p->buf[p->idx++] = byte;
if (p->idx == 3) { /* SubL + SubH 收完 */
p->state = LUP_FRAME_STATE_OTA_LEN;
}
break;
case LUP_FRAME_STATE_OTA_LEN:
p->buf[p->idx++] = byte;
{
uint8_t len_field = p->buf[3];
uint16_t data_bytes;
if (len_field < 1) {
data_bytes = 0; /* LEN<1 非法, 按无数据直接到 CHECK */
} else {
data_bytes = len_field - 1; /* LEN 含 CMD */
}
if (data_bytes > LUP_MAX_VALUE_LEN) {
lup_frame_reset(); /* 非法长度, 丢弃 */
break;
}
p->value_len = data_bytes;
p->value_idx = 0;
}
p->state = LUP_FRAME_STATE_OTA_CMD;
break;
case LUP_FRAME_STATE_OTA_CMD:
p->buf[p->idx++] = byte;
if (p->value_len > 0) {
p->state = LUP_FRAME_STATE_VALUE; /* 复用 VALUE: 收 DATA */
} else {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_VALUE:
p->buf[p->idx++] = byte;
p->value_idx++;
if (p->value_idx >= p->value_len) {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_OTA_CHECK:
p->buf[p->idx++] = byte;
{
/* SUM = SubL+SubH+LEN+CMD+DATA (不含 magic 0x9F, 不含本 CHECK) */
uint8_t sum = 0;
uint16_t i;
for (i = 1; i < (uint16_t)(p->idx - 1); i++) {
sum += p->buf[i];
}
if (sum == p->buf[p->idx - 1]) {
p->state = LUP_FRAME_STATE_COMPLETE;
return 1; /* 帧完成 */
}
lup_frame_reset(); /* 校验失败, 丢弃 */
}
break;
case LUP_FRAME_STATE_COMPLETE:
// 不应该到达这里,调用方应在 COMPLETE 后 reset
lup_frame_reset();
break;
default:
lup_frame_reset();
break;
} }
return 0; return 0;
@@ -681,14 +811,24 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
int csr; int csr;
uint8_t i; uint8_t i;
if (len < 6) return; FAULT_MARKER(MK_LUP_FRAME_IN);
// Debug: print raw if (len < 6) { FAULT_MARKER(MK_LUP_FRAME_OUT); return; }
PRINT("LUP Rx:");
for (i = 0; i < len; i++) { // Debug: print raw (2026-08-13: 缓冲一次性输出 — 逐字节 PRINT 关中断 ~350us/字节
PRINT(" %02X", pkg[i]); // 会屏蔽 UART2 ISR 丢帧, 且高频打印放大 printf 重入窗口。
// 再关闭: 190B 打印 @256000 = 7.4ms 关中断 → UART2(192000) 溢出丢字节 → 粘帧!
// 需要调试时打开 #if 1)
#if 0
{
char _hex[224];
int _p = snprintf(_hex, sizeof(_hex), "LUP Rx:");
for (i = 0; i < len && _p >= 0 && _p < (int)sizeof(_hex) - 5; i++) {
_p += snprintf(_hex + _p, sizeof(_hex) - (size_t)_p, " %02X", pkg[i]);
} }
PRINT("\n"); PRINT("%s\n", _hex);
}
#endif
// --- Checksum --- // --- Checksum ---
csr = lup_verify_checksum(pkg, len); csr = lup_verify_checksum(pkg, len);
@@ -712,4 +852,5 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
// Try matching with pending command // Try matching with pending command
lup_cmd_on_response(pkg, len); lup_cmd_on_response(pkg, len);
FAULT_MARKER(MK_LUP_FRAME_OUT);
} }
@@ -19,6 +19,7 @@
#include "simple_json.h" #include "simple_json.h"
#include "tcp_json_srv.h" #include "tcp_json_srv.h"
#include "iot_mqtt_srv.h" #include "iot_mqtt_srv.h"
#include "offlog.h"
uint32_t slen; uint32_t slen;
@@ -64,7 +65,7 @@ uint8_t MyBuf[RECE_BUF_LEN];
#define MAX_TMP_BUF_LEN 64 #define MAX_TMP_BUF_LEN 64
#define MAX_MQTTBUF_LEN 1024 //512: loop_data(4通道)≈604B > 512 → 序列化失败发全零垃圾包被broker RST (2026-07-15) #define MAX_MQTTBUF_LEN 800 // 2026-08-17: 1024→800 (loop_data~604B+头<800; 512仍不够勿改回) 省224B .bss
char mqtt_username[64] = {0}; char mqtt_username[64] = {0};
char mqtt_password[32] = {0}; char mqtt_password[32] = {0};
char mqtt_clientid[64] = {0}; char mqtt_clientid[64] = {0};
@@ -97,6 +98,7 @@ void dev_time_sync(uint32_t unix_ts)
if (unix_ts < 1600000000UL) return; // 合法性门槛(>2020-09), 挡掉上电秒数/0/异常 if (unix_ts < 1600000000UL) return; // 合法性门槛(>2020-09), 挡掉上电秒数/0/异常
_dev_time_base_unix = unix_ts; _dev_time_base_unix = unix_ts;
_dev_time_base_tick = mstick(); _dev_time_base_tick = mstick();
offlog_time_anchor(unix_ts); // 事件日志: 时钟同步锚点 (boot_seq↔unix 回算)
PRINT("TIME: synced unix=%lu\n", (unsigned long)unix_ts); PRINT("TIME: synced unix=%lu\n", (unsigned long)unix_ts);
} }
@@ -120,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)
@@ -344,6 +346,13 @@ void WCHNET_CreateTcpMqttSocket(void)
{ {
PRINT("WCHNET_MQTT_SocketCreate %d,desIP:%d.%d.%d.%d, des_port:%d, srcport:%d\r\n", SocketId_TCP, RemoteIP[0],RemoteIP[1],RemoteIP[2],RemoteIP[3], iot_net_info.mqtt_port, TmpSocketInf.SourPort); 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)
@@ -578,28 +587,20 @@ uint8_t i;
return; return;
} }
// IoT MQTT mode — inline MQTT handling (ref: DBN101GA) // IoT MQTT mode — delegate all socket events to iot_mqtt_srv
// (2026-07-23 修复: 旧 mqtt_connect/mqtt_data_manage 与 IoT 栈抢 socket, 致双 CONNECT → broker 踢线 → 频繁 initialize)
if (g_sub_code_enable.iot_enable) { if (g_sub_code_enable.iot_enable) {
if (intstat & SINT_STAT_RECV) {
mqtt_data_manage(socketid);
}
if (intstat & SINT_STAT_CONNECT) { if (intstat & SINT_STAT_CONNECT) {
#if KEEPLIVE_ENABLE #if KEEPLIVE_ENABLE
WCHNET_SocketSetKeepLive(socketid, ENABLE); WCHNET_SocketSetKeepLive(socketid, ENABLE);
#endif #endif
WCHNET_ModifyRecvBuf(socketid, (uint32_t)SocketRecvBuf[socketid], RECE_BUF_LEN); WCHNET_ModifyRecvBuf(socketid, (uint32_t)SocketRecvBuf[socketid], RECE_BUF_LEN);
PRINT("TCP Connect Success (MQTT)\n"); PRINT("TCP Connect Success (MQTT IoT)\n");
mqtt_connect();
} }
if (intstat & SINT_STAT_DISCONNECT) { if (intstat & SINT_STAT_DISCONNECT || intstat & SINT_STAT_TIM_OUT) {
PRINT("TCP Disconnect (MQTT)\n"); PRINT("TCP Disconnect/Timeout (MQTT IoT)\n");
g_iot_state = IOT_STATE_DISCONNECTED;
g_net_state.flag = 1;
}
if (intstat & SINT_STAT_TIM_OUT) {
PRINT("TCP Timeout (MQTT)\n");
g_net_state.flag = 1;
} }
iot_mqtt_handle_sock_int(socketid, intstat);
return; return;
} }
@@ -0,0 +1,529 @@
/**
******************************************************************************
* @file offlog.c
* @author wfq
* @version V1.0
* @date 2026-08-04
* @brief DBN W25Q32
*
* : storage.c (SPI_Flash_*), cmcng.h (mstick), net_srv.c (dev_time_now)
* (), SPI ~45ms
******************************************************************************
*/
#include "offlog.h"
#include "storage.h"
#include "cmcng.h"
#include "net_srv.h"
#include <string.h>
/* 编译期断言: 记录必须是 32B (定长环形索引依赖, 踩过 36B padding 的坑) */
typedef char offlog_evt_size_must_be_32[(sizeof(OfflogEvt) == 32) ? 1 : -1];
typedef char offlog_head_size_must_be_32[(sizeof(OfflogHead) == 32) ? 1 : -1];
/*===========================================================================
*
*===========================================================================*/
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..63) */
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
static uint16_t _boot_seq; /* 当前 boot 序号 */
static uint32_t _count; /* 有效记录数 (0..max_records) */
static uint8_t _ready; /* init 完成标志 */
/*===========================================================================
* (ROADMAP 2026-08-12: JEDEC ID )
*===========================================================================*/
OfflogPart g_offlog_part = {
OFFLOG_CHIP_W25Q32, /* 默认 */
0x80000UL, /* 512KB */
127, /* (512KB/4096) - 1 头扇区 */
16256, /* 127 * 128 */
};
/* 读 JEDEC ID → 填分区表。未知芯片按 W25Q32 兜底 (与 storage_init 打印对照) */
static void offlog_part_detect(void)
{
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
uint32_t size = 0x80000UL;
uint8_t chip = OFFLOG_CHIP_W25Q32;
switch (dev) {
case OFFLOG_CHIP_W25Q32: size = 0x80000UL; break; /* 512KB */
case OFFLOG_CHIP_W25Q64: size = 0x100000UL; break; /* 1MB */
case OFFLOG_CHIP_W25Q128: size = 0x200000UL; break; /* 2MB */
case OFFLOG_CHIP_W25Q256: size = 0x400000UL; break; /* 4MB */
default: chip = OFFLOG_CHIP_W25Q32; break; /* 未知→默认 */
}
g_offlog_part.chip = chip;
g_offlog_part.area_size = size;
g_offlog_part.data_sectors = (size / 4096UL) - 1;
g_offlog_part.max_records = g_offlog_part.data_sectors * OFFLOG_EVT_PER_SECTOR;
}
/*===========================================================================
*
*===========================================================================*/
static void offlog_head_write(const OfflogHead *h)
{
uint8_t buf[32];
memset(buf, 0xFF, sizeof(buf));
memcpy(buf, h, sizeof(OfflogHead));
SPI_Flash_Write(buf, OFFLOG_AREA_BASE, sizeof(buf));
}
static void offlog_head_read(OfflogHead *h)
{
uint8_t buf[32];
SPI_Flash_Read(buf, OFFLOG_AREA_BASE, sizeof(buf));
memcpy(h, buf, sizeof(OfflogHead));
}
static int offlog_evt_read_at(uint32_t abs_off, OfflogEvt *out)
{
SPI_Flash_Read((uint8_t *)out, abs_off, OFFLOG_EVT_SIZE);
return (out->magic == OFFLOG_EVT_MAGIC) ? 0 : -1;
}
/*===========================================================================
*
* , magic + seq ,
* () , ;
*===========================================================================*/
static void offlog_scan_recover(const OfflogHead *h)
{
uint32_t pos = h->wr_off;
uint32_t scanned = 0;
uint32_t expect_seq = h->wr_seq;
OfflogEvt e;
/* 防御: wr_off 必须在数据区范围内且 32B 对齐 */
if (pos < OFFLOG_DATA_BASE || pos >= OFFLOG_AREA_BASE + OFFLOG_AREA_SIZE) {
pos = OFFLOG_DATA_BASE;
}
if ((pos - OFFLOG_DATA_BASE) % OFFLOG_EVT_SIZE != 0) {
pos = OFFLOG_DATA_BASE; /* 未对齐 → 从头开始 */
}
while (scanned < OFFLOG_MAX_RECORDS) {
if (offlog_evt_read_at(pos, &e) != 0) {
break; /* 找到真实写位置 */
}
if (expect_seq != 0 && e.seq != expect_seq) {
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
}
pos += OFFLOG_EVT_SIZE;
if (pos >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
pos = OFFLOG_DATA_BASE; /* 回绕数据区 */
}
scanned++;
expect_seq++;
}
_wr_off = pos;
_wr_sector = (uint16_t)((pos - OFFLOG_DATA_BASE) / 4096) + 1; /* 区内 1..63 */
_wr_seq = expect_seq; /* 下一条 = 最后有效 seq + 1 */
_count = h->count + scanned;
if (_count > OFFLOG_MAX_RECORDS) _count = OFFLOG_MAX_RECORDS;
_boot_seq = (uint16_t)(h->boot_seq + 1);
if (_boot_seq == 0) _boot_seq = 1;
}
/*===========================================================================
*
*===========================================================================*/
void offlog_init(void)
{
OfflogHead h;
offlog_part_detect(); /* JEDEC ID → 事件区容量 (W25Q32/Q64/Q128/Q256) */
memset(&h, 0xFF, sizeof(h));
offlog_head_read(&h);
if (!(h.magic[0] == OFFLOG_HEAD_MAGIC0 && h.magic[1] == OFFLOG_HEAD_MAGIC1 &&
h.magic[2] == OFFLOG_HEAD_MAGIC2 && h.magic[3] == OFFLOG_HEAD_MAGIC3)) {
/* 全新区: 懒擦 — 只擦当前写扇区 ~45ms (原擦全部 127 扇区 ~5.7s,
IWDG 4s ), */
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
memset(&h, 0xFF, sizeof(h));
h.magic[0] = OFFLOG_HEAD_MAGIC0;
h.magic[1] = OFFLOG_HEAD_MAGIC1;
h.magic[2] = OFFLOG_HEAD_MAGIC2;
h.magic[3] = OFFLOG_HEAD_MAGIC3;
h.boot_seq = 0;
h.wr_sector = 1;
h.wr_off = OFFLOG_DATA_BASE;
h.wr_seq = 0;
h.count = 0;
offlog_head_write(&h);
_wr_off = OFFLOG_DATA_BASE;
_wr_sector = 1;
_wr_seq = 1;
_count = 0;
_boot_seq = 1;
} else {
offlog_scan_recover(&h);
}
/* 回写头: boot_seq 递增 */
h.magic[0] = OFFLOG_HEAD_MAGIC0;
h.magic[1] = OFFLOG_HEAD_MAGIC1;
h.magic[2] = OFFLOG_HEAD_MAGIC2;
h.magic[3] = OFFLOG_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
offlog_head_write(&h);
_ready = 1;
}
/*===========================================================================
* ()
*===========================================================================*/
static void offlog_flush_head(void)
{
OfflogHead h;
h.magic[0] = OFFLOG_HEAD_MAGIC0;
h.magic[1] = OFFLOG_HEAD_MAGIC1;
h.magic[2] = OFFLOG_HEAD_MAGIC2;
h.magic[3] = OFFLOG_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
offlog_head_write(&h);
}
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic。
, , 0xA5 */
static int offlog_sector_has_data(uint32_t abs_sec_off)
{
OfflogEvt e;
SPI_Flash_Read((uint8_t *)&e, abs_sec_off, OFFLOG_EVT_SIZE);
return (e.magic == OFFLOG_EVT_MAGIC) ? 1 : 0;
}
static void offlog_write_raw(const OfflogEvt *e)
{
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
if (_wr_off >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
_wr_off = OFFLOG_DATA_BASE;
}
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区。
: (sec_pos==0, )
(sec_pos + 32 > 4096) */
{
uint32_t sec_index = (_wr_off - OFFLOG_DATA_BASE) / 4096; /* 0..62 */
if ((uint32_t)(_wr_sector - 1) != sec_index) {
uint32_t target_abs = (OFFLOG_DATA_BASE / 4096 + sec_index) * 4096;
/* 仅环形覆盖 (目标扇区有旧数据) 才扣减 count;
*/
if (offlog_sector_has_data(target_abs)) {
uint32_t erased = (_count > OFFLOG_EVT_PER_SECTOR)
? OFFLOG_EVT_PER_SECTOR : _count;
_count -= erased;
}
SPI_Flash_Erase_Sector(target_abs / 4096);
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..63 */
offlog_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
}
}
SPI_Flash_Write_NoCheck((uint8_t *)e, _wr_off, OFFLOG_EVT_SIZE);
_wr_off += OFFLOG_EVT_SIZE;
_wr_seq++;
if (_count < OFFLOG_MAX_RECORDS) _count++;
}
/* 内部: 支持外部指定 unix_ts (TIME_ANCHOR 用平台下发值, 严格一致) */
static void offlog_evt_ts(uint8_t type, const uint8_t *payload, uint8_t len, uint32_t unix_ts)
{
OfflogEvt e;
uint32_t now = mstick();
if (!_ready) return;
memset(&e, 0, sizeof(e));
e.magic = OFFLOG_EVT_MAGIC;
e.type = type;
e.len = (len > 12) ? 12 : len;
e.seq = _wr_seq;
e.boot_seq= _boot_seq;
e.ts_ms = now;
e.unix_ts = unix_ts;
if (unix_ts >= 1600000000UL) { /* 与 dev_time_sync 合法性门槛一致 */
e.flags |= OFFLOG_UNIX_VALID_FLAG;
}
if (payload != NULL && e.len > 0) {
memcpy(e.payload, payload, e.len);
}
offlog_write_raw(&e);
}
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len)
{
offlog_evt_ts(type, payload, len, dev_time_now());
}
/*===========================================================================
* 便
*===========================================================================*/
void offlog_boot(uint32_t reset_reason)
{
uint8_t p[4];
p[0] = (uint8_t)(reset_reason >> 24);
p[1] = (uint8_t)(reset_reason >> 16);
p[2] = (uint8_t)(reset_reason >> 8);
p[3] = (uint8_t)reset_reason;
offlog_evt(OFFLOG_EVT_BOOT, p, 4);
}
void offlog_iot_connect(void)
{
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
}
void offlog_iot_ready(void)
{
offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0);
}
void offlog_iot_disconnect(uint8_t reason)
{
offlog_evt(OFFLOG_EVT_IOT_DISCONN, &reason, 1);
}
void offlog_iot_reconn(uint32_t backoff_ms)
{
uint8_t p[4];
p[0] = (uint8_t)(backoff_ms >> 24);
p[1] = (uint8_t)(backoff_ms >> 16);
p[2] = (uint8_t)(backoff_ms >> 8);
p[3] = (uint8_t)backoff_ms;
offlog_evt(OFFLOG_EVT_IOT_RECONN, p, 4);
}
void offlog_evt_retry(uint32_t msg_id, uint8_t retry)
{
uint8_t p[5];
p[0] = (uint8_t)(msg_id >> 24);
p[1] = (uint8_t)(msg_id >> 16);
p[2] = (uint8_t)(msg_id >> 8);
p[3] = (uint8_t)msg_id;
p[4] = retry;
offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5);
}
void offlog_evt_giveup(uint32_t msg_id)
{
uint8_t p[4];
p[0] = (uint8_t)(msg_id >> 24);
p[1] = (uint8_t)(msg_id >> 16);
p[2] = (uint8_t)(msg_id >> 8);
p[3] = (uint8_t)msg_id;
offlog_evt(OFFLOG_EVT_EVT_GIVEUP, p, 4);
}
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value)
{
uint8_t p[6];
p[0] = sub;
p[1] = ch;
p[2] = (uint8_t)(value >> 24);
p[3] = (uint8_t)(value >> 16);
p[4] = (uint8_t)(value >> 8);
p[5] = (uint8_t)value;
offlog_evt(OFFLOG_EVT_COIL, p, 6);
}
void offlog_ota_start(uint8_t slot, uint32_t size)
{
uint8_t p[6];
p[0] = slot;
p[1] = 0; /* target: 0=loop */
p[2] = (uint8_t)(size >> 24);
p[3] = (uint8_t)(size >> 16);
p[4] = (uint8_t)(size >> 8);
p[5] = (uint8_t)size;
offlog_evt(OFFLOG_EVT_OTA_START, p, 6);
}
void offlog_ota_result(uint32_t code)
{
uint8_t p[4];
p[0] = (uint8_t)(code >> 24);
p[1] = (uint8_t)(code >> 16);
p[2] = (uint8_t)(code >> 8);
p[3] = (uint8_t)code;
offlog_evt(OFFLOG_EVT_OTA_RESULT, p, 4);
}
void offlog_time_anchor(uint32_t unix_ts)
{
offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */
}
/*===========================================================================
*
*===========================================================================*/
uint32_t offlog_boot_seq(void)
{
return _boot_seq;
}
uint16_t offlog_count(void)
{
return (uint16_t)_count;
}
int offlog_read_idx(uint16_t idx, OfflogEvt *out)
{
uint32_t cnt = _count;
if (!_ready || idx >= cnt) return -1;
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
uint32_t phys = (_wr_off - OFFLOG_DATA_BASE); /* 0..DATA_SIZE */
uint32_t start = (phys + OFFLOG_DATA_SIZE - (uint32_t)cnt * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
uint32_t read_off = OFFLOG_DATA_BASE + (start + (uint32_t)idx * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
return offlog_evt_read_at(read_off, out);
}
/*===========================================================================
* (MQTT V1.06 / TCP JSON V1.02 )
*===========================================================================*/
uint8_t offlog_enabled(void)
{
return _ready;
}
uint32_t offlog_seq_last(void)
{
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
}
const char *offlog_type_str(uint8_t type)
{
switch (type) {
case OFFLOG_EVT_BOOT: return "boot";
case OFFLOG_EVT_IOT_CONNECT: return "iot_connect";
case OFFLOG_EVT_IOT_READY: return "iot_ready";
case OFFLOG_EVT_IOT_DISCONN: return "iot_disconnect";
case OFFLOG_EVT_IOT_RECONN: return "iot_reconn";
case OFFLOG_EVT_EVT_RETRY: return "evt_retry";
case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup";
case OFFLOG_EVT_COIL: return "coil";
case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor";
case OFFLOG_EVT_OTA_START: return "ota_start";
case OFFLOG_EVT_OTA_RESULT: return "ota_result";
case OFFLOG_EVT_LOG_CLEAR: return "log_clear";
default: return "unknown";
}
}
/* 线圈事件子类型: 1=进 2=出 3=断开 4=恢复
( iot_mqtt_srv.c offlog_coil ) */
static const char *offlog_coil_sub_str(uint8_t sub)
{
switch (sub) {
case 1: return "car_enter";
case 2: return "car_leave";
case 3: return "loop_cut";
case 4: return "loop_restore";
default: return "unknown";
}
}
/* payload 大端 uint32 (与 offlog_boot/offlog_coil 写入一致) */
static uint32_t offlog_payload_u32(const uint8_t *p)
{
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
/* 记录 → JSON 对象 (data 按事件类型组装; 无参数事件 data=null)
snprintf ; buf_len (130B) */
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len)
{
const char *type = offlog_type_str(e->type);
char data[96];
switch (e->type) {
case OFFLOG_EVT_BOOT:
snprintf(data, sizeof(data), "{\"rst\":%lu}",
(unsigned long)offlog_payload_u32(e->payload));
break;
case OFFLOG_EVT_IOT_DISCONN:
snprintf(data, sizeof(data), "{\"reason\":%u}", e->payload[0]);
break;
case OFFLOG_EVT_IOT_RECONN:
snprintf(data, sizeof(data), "{\"backoff_ms\":%lu}",
(unsigned long)offlog_payload_u32(e->payload));
break;
case OFFLOG_EVT_EVT_RETRY:
snprintf(data, sizeof(data), "{\"msg_id\":%lu,\"retry\":%u}",
(unsigned long)offlog_payload_u32(e->payload), e->payload[4]);
break;
case OFFLOG_EVT_EVT_GIVEUP:
snprintf(data, sizeof(data), "{\"msg_id\":%lu}",
(unsigned long)offlog_payload_u32(e->payload));
break;
case OFFLOG_EVT_COIL:
snprintf(data, sizeof(data), "{\"sub\":\"%s\",\"ch\":%u,\"value\":%lu}",
offlog_coil_sub_str(e->payload[0]), e->payload[1],
(unsigned long)offlog_payload_u32(e->payload + 2));
break;
default:
data[0] = '\0'; /* iot_connect/iot_ready/time_anchor/log_clear → null */
break;
}
if (data[0] == '\0') {
return snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":null}",
(unsigned long)e->seq, (unsigned)e->boot_seq,
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type);
}
return snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":%s}",
(unsigned long)e->seq, (unsigned)e->boot_seq,
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type, data);
}
/*===========================================================================
* (: LOG_CLEAR)
*===========================================================================*/
void offlog_clear(void)
{
if (!_ready) return;
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms, 原 2.8s 接近 IWDG 4s 风险):
count=0 使, */
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
_wr_off = OFFLOG_DATA_BASE;
_wr_sector = 1;
_count = 0;
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证日志全局唯一 */
offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0);
offlog_flush_head();
}
/* OfflogEvt -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §7 字段表解析)
Used by TCP JSON / MQTT log_query stream=event (protocol V1.03/V1.07).
Returns snprintf written length; 32B -> 64 hex chars. */
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)e;
for (i = 0; i < (int)sizeof(OfflogEvt); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
@@ -0,0 +1,633 @@
/**
******************************************************************************
* @file ota_srv.c
* @author wangfq
* @version V1.0
* @date 2026-08-20
* @brief DLD960 Loop MCU OTA (MQTT V1.08, ROADMAP P1.4 )
*
* :
* 1 : MQTT (256B/, + CRC32) W25Qxx
* 2 : ota_end CRC32 state=ready
* 3 : ota_flash 0x9F ISP (A5/A6/A7 ),
* tick ota_poll(), MQTT
*
* : downloading/flashing event_report (,
* ) + offlog/ ("升级开始",
* "升级结果")
*
* : docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5
* 稿: docs/DLD960_MQTT_OTA协议.md V1.01
******************************************************************************
*/
#include "CONFIG.h"
#include "cmcng.h"
#include "ota_srv.h"
#include "offlog.h"
#include "storage.h"
#include "loop_uart_proto.h"
#include <string.h>
/*===========================================================================
*
*===========================================================================*/
static OtaMeta _meta; /* 当前会话元数据 (RAM 副本) */
static uint32_t _slot_base; /* 当前 Slot 数据区基址 */
static uint32_t _meta_watermark; /* 上次落盘元数据时的 received (节流) */
static uint8_t _init_done; /* ota_init 完成 */
/* 刷写状态机 (非阻塞 tick 驱动) */
static uint8_t _fs; /* OTA_FLASH_* */
static uint8_t _fs_retry; /* 当前步重试计数 */
static uint32_t _fs_start_ms; /* 当前步起始时刻 (超时判断) */
static uint16_t _fs_sub; /* 当前块序号 (首=总块数, 末=1) */
static uint32_t _fs_sent; /* 已送 Loop 字节数 */
uint8_t g_ota_flash_active = 0; /* 本地刷写进行中 → uart_srv 把 ACK 交给 ota_srv */
/* 块缓冲 (static: 栈紧张教训; union: 下载片 256B 与 A7 帧 254B 互斥复用, 省 ~260B RAM) */
static union {
uint8_t chunk[OTA_CHUNK_SIZE]; /* 下载片缓冲 / 校验读缓冲 */
uint8_t frame[5 + OTA_FLASH_BLOCK + 1]; /* A7 帧: 9F+SubL+SubH+LEN+CMD+DATA+CHECK */
} _ota_buf;
/*===========================================================================
* CRC-32/ISO-HDLC ( §4.19.1; , RAM)
* poly=0x04C11DB7 (reflected 0xEDB88320), init=0xFFFFFFFF, xorout=0xFFFFFFFF
* : crc32("123456789") = 0xCBF43926
*===========================================================================*/
/* 增量更新: 调用方初始 crc=0xFFFFFFFF, 分块续算, 最后 ^0xFFFFFFFF
( 96KB , RAM) */
static uint32_t ota_crc32_update(uint32_t crc, const uint8_t *data, uint32_t len)
{
uint32_t i;
uint8_t b;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (b = 0; b < 8; b++) {
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
}
return crc;
}
uint32_t ota_crc32(const uint8_t *data, uint32_t len)
{
return ota_crc32_update(0xFFFFFFFFu, data, len) ^ 0xFFFFFFFFu;
}
/*===========================================================================
* hex
*===========================================================================*/
static uint8_t ota_hex_val(char c)
{
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10);
if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10);
return 0xFF;
}
int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n)
{
uint32_t i;
for (i = 0; i < n; i++) {
uint8_t hi = ota_hex_val(hex[i * 2]);
uint8_t lo = ota_hex_val(hex[i * 2 + 1]);
if (hi == 0xFF || lo == 0xFF) return -1;
out[i] = (uint8_t)((hi << 4) | lo);
}
return 0;
}
/* 从 MQTT JSON 提取 ota_data 的 data 字段 hex 值。
2026-08-20 : json.dumps ("data": "hex"),
strstr("\"data\":\"") CRC
"data" /, */
int ota_json_extract_hex(const char *json, char *out, int out_len)
{
const char *p = json;
int n = 0;
if (!json || !out || out_len <= 0) return 0;
out[0] = '\0';
while (*p) {
if (strncmp(p, "\"data\"", 6) == 0) {
const char *q = p + 6;
while (*q == ' ' || *q == '\t') q++;
if (*q == ':') {
q++;
while (*q == ' ' || *q == '\t') q++;
if (*q == '"') {
q++; /* 跳过起始引号 */
while (*q && *q != '"' && n < out_len - 1) {
out[n++] = *q;
q++;
}
out[n] = '\0';
return n;
}
}
}
p++;
}
return 0;
}
/*===========================================================================
* (, 0x010000~0x0103FF)
* : (45ms) + NoCheck +
* : magic , ; IDLE
*===========================================================================*/
static void ota_meta_write(const OtaMeta *m)
{
uint8_t buf[64];
memset(buf, 0xFF, sizeof(buf));
memcpy(buf, m, sizeof(OtaMeta));
SPI_Flash_Erase_Sector((uint32_t)OTA_META_SECTOR);
SPI_Flash_Write_NoCheck(buf, OTA_META_BASE, 64);
SPI_Flash_Write_NoCheck(buf, OTA_META_BACKUP, 64);
PRINT("OTA: meta write state=%lu received=%lu\n",
(unsigned long)m->state, (unsigned long)m->received);
}
static int ota_meta_read(OtaMeta *m)
{
uint8_t buf[64];
memset(buf, 0xFF, sizeof(buf));
SPI_Flash_Read(buf, OTA_META_BASE, 64);
memcpy(m, buf, sizeof(OtaMeta));
if (m->magic == OTA_MAGIC) return 0;
memset(buf, 0xFF, sizeof(buf));
SPI_Flash_Read(buf, OTA_META_BACKUP, 64);
memcpy(m, buf, sizeof(OtaMeta));
if (m->magic == OTA_MAGIC) return 0;
return -1;
}
/* 节流落盘: 每累计 4KB 或关键节点写元数据 (擦 45ms 不能每片做) */
static void ota_meta_flush(void)
{
if (_meta.received - _meta_watermark >= 4096u) {
ota_meta_write(&_meta);
_meta_watermark = _meta.received;
}
}
/*===========================================================================
*
*===========================================================================*/
const char *ota_state_str(uint8_t state)
{
static const char *names[] = {
"idle", "downloading", "ready", "flashing", "flash_failed", "aborted"
};
return (state < 6) ? names[state] : "idle";
}
uint8_t ota_silent_active(void)
{
return (_meta.state == OTA_STATE_DOWNLOADING || _meta.state == OTA_STATE_FLASHING);
}
const OtaMeta *ota_meta(void)
{
return &_meta;
}
/*===========================================================================
*
*===========================================================================*/
void ota_init(void)
{
OtaMeta m;
memset(&m, 0, sizeof(m));
if (ota_meta_read(&m) == 0) {
/* 恢复上次会话状态 (掉电/复位后) */
_meta = m;
if (_meta.state == OTA_STATE_DOWNLOADING) {
/* 下载中断: 保持 downloading, 平台 ota_begin 时续传 */
PRINT("OTA: resume downloading received=%lu\n", (unsigned long)_meta.received);
} else if (_meta.state == OTA_STATE_FLASHING) {
/* 刷写中断 (掉电): 置失败态, 保留镜像可重试 */
_meta.state = OTA_STATE_FLASH_FAILED;
_meta.last_result = OTA_ERR_READY_TIMEOUT;
PRINT("OTA: flash interrupted on boot, mark failed\n");
ota_meta_write(&_meta);
}
/* ready/aborted/flash_failed/idle: 原样保留 */
} else {
memset(&_meta, 0, sizeof(_meta));
_meta.magic = OTA_MAGIC;
_meta.state = OTA_STATE_IDLE;
_meta.slot = 0;
PRINT("OTA: fresh meta (idle)\n");
}
_slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE;
_meta_watermark = _meta.received;
_fs = OTA_FLASH_IDLE;
_init_done = 1;
PRINT("OTA: init done state=%s slot=%d received=%lu\n",
ota_state_str(_meta.state), _meta.slot, (unsigned long)_meta.received);
}
/*===========================================================================
* ota_begin ( / )
* : 0= (data.offset = _meta.received size), 0=
*===========================================================================*/
int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force)
{
(void)force; /* 版本冲突判断暂不校验 (bootloader 不校验版本), 预留 */
if (size == 0 || size > OTA_MAX_IMAGE) {
return 1; /* 参数错误 */
}
/* 已有镜像且 size+crc32 一致 → 续传或直接 ready */
if (_meta.magic == OTA_MAGIC && _meta.size == size && _meta.crc32 == crc32) {
_slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE;
if (_meta.state == OTA_STATE_READY) {
/* 镜像已就绪, 平台可直接 ota_flash */
return 0; /* data.offset = size */
}
if (_meta.state == OTA_STATE_IDLE) {
/* V1.09: 刷写完成后的 idle 且镜像一致 → 直接回 ready, 免下载可重刷 */
_meta.state = OTA_STATE_READY;
ota_meta_write(&_meta);
PRINT("OTA: begin idle+same image → ready (reflash)\n");
return 0; /* data.offset = size */
}
if (_meta.state == OTA_STATE_DOWNLOADING) {
_meta.state = OTA_STATE_DOWNLOADING;
PRINT("OTA: begin resume received=%lu\n", (unsigned long)_meta.received);
return 0; /* data.offset = received */
}
/* flash_failed / aborted: 重新走下载 */
}
/* 新会话: 始终用 Slot A (B 保留上一版镜像做回滚) */
_slot_base = OTA_SLOT_A_BASE;
memset(&_meta, 0, sizeof(_meta));
_meta.magic = OTA_MAGIC;
_meta.state = OTA_STATE_DOWNLOADING;
_meta.target = 0;
_meta.slot = 0;
_meta.size = size;
_meta.crc32 = crc32;
_meta.received = 0;
_meta.last_result = 0;
_meta.begin_ts = 0;
memset(_meta.version, 0, sizeof(_meta.version));
if (version) strncpy(_meta.version, version, sizeof(_meta.version) - 1);
_meta.flash_cnt = 0;
_meta_watermark = 0;
/* 先擦目标 Slot 数据区 (100KB = 25 扇区 ~1.1s, 主循环上下文可接受 < IWDG 4s) */
{
uint32_t sec;
for (sec = 0; sec < OTA_SLOT_SIZE / 4096UL; sec++) {
SPI_Flash_Erase_Sector((uint32_t)(_slot_base / 4096UL) + sec);
}
}
ota_meta_write(&_meta);
PRINT("OTA: begin new session size=%lu crc32=0x%08lX slot=%d\n",
(unsigned long)size, (unsigned long)crc32, _meta.slot);
return 0;
}
/*===========================================================================
* ota_data (, 256B/)
* : 0= (), 1=/CRC/ (err_code ), 3=
*===========================================================================*/
int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex)
{
uint32_t clen;
if (_meta.state != OTA_STATE_DOWNLOADING) {
return 3; /* 会话状态不允许 (err_code=2 由调用方映射) */
}
if (offset % OTA_CHUNK_SIZE != 0 || offset >= _meta.size) {
return 1; /* 参数错误 */
}
if (offset < _meta.received) {
return 0; /* 重复片: 幂等 */
}
if (offset > _meta.received) {
return 1; /* 乱序缺片: 调用方用 data.offset=received 指示续传 */
}
clen = _meta.size - offset;
if (clen > OTA_CHUNK_SIZE) clen = OTA_CHUNK_SIZE;
if (ota_hex_decode(hex, _ota_buf.chunk, clen) != 0) {
return 1; /* hex 格式错误 */
}
if (ota_crc32(_ota_buf.chunk, clen) != crc32) {
return 1; /* 单片 CRC 失败 */
}
/* 落盘: SPI_Flash_Write 自带"目标区非 0xFF 则读-改-写擦扇区" (256B 页对齐) */
SPI_Flash_Write(_ota_buf.chunk, _slot_base + offset, (uint16_t)clen);
_meta.received = offset + clen;
ota_meta_flush();
PRINT("OTA: data@%lu crc_ok received=%lu\n",
(unsigned long)offset, (unsigned long)_meta.received);
return 0;
}
/*===========================================================================
* ota_end ( CRC32 )
*===========================================================================*/
int ota_cmd_end(uint32_t crc32)
{
if (_meta.state != OTA_STATE_DOWNLOADING) {
return 3;
}
if (_meta.received != _meta.size) {
return 1; /* 不完整: data.offset=received 续传 */
}
if (crc32 != _meta.crc32) {
return 1; /* 声明值不一致 */
}
/* 读回暂存区计算全镜像 CRC32 (分块增量, 96KB ≈ 40ms @120MHz, 主循环上下文) */
{
uint32_t total = 0xFFFFFFFFu;
uint32_t i = 0;
while (i < _meta.size) {
uint32_t n = _meta.size - i;
if (n > OTA_CHUNK_SIZE) n = OTA_CHUNK_SIZE;
SPI_Flash_Read(_ota_buf.chunk, _slot_base + i, (uint16_t)n);
total = ota_crc32_update(total, _ota_buf.chunk, n);
i += n;
}
total ^= 0xFFFFFFFFu;
if (total != _meta.crc32) {
_meta.state = OTA_STATE_DOWNLOADING; /* 保留已下载数据, 可重发错片 */
PRINT("OTA: end CRC mismatch 0x%08lX != 0x%08lX\n",
(unsigned long)total, (unsigned long)_meta.crc32);
return 5; /* 内部错误 + data.crc_ok=false */
}
}
_meta.state = OTA_STATE_READY;
_meta.last_result = 0;
ota_meta_write(&_meta);
PRINT("OTA: end OK crc_ok → ready (slot %d)\n", _meta.slot);
return 0;
}
/*===========================================================================
* ota_abort
*===========================================================================*/
int ota_cmd_abort(void)
{
if (_meta.state == OTA_STATE_IDLE) {
return 0; /* 无会话: 幂等 */
}
_meta.state = OTA_STATE_ABORTED;
ota_meta_write(&_meta);
PRINT("OTA: abort → aborted\n");
return 0;
}
/*===========================================================================
* ota_flash ( ISP , + )
* : 4 (err_code=3); force
*===========================================================================*/
int ota_cmd_flash(uint8_t slot, uint8_t force)
{
uint8_t any_car;
extern uint8_t iot_any_car(void);
if (_meta.state != OTA_STATE_READY) {
return 3; /* 非 ready: 先 ota_end */
}
if (slot != 0 && slot != 1) {
return 1;
}
if (slot != _meta.slot) {
/* 指定了非当前 ready 槽: 读该槽元数据? 简化: 仅允许当前槽 */
return 1;
}
/* 与 BLE OTA 透传互斥: g_flag_counter_ota.flag=1 且非本地刷写 → 拒绝 */
if (g_flag_counter_ota.flag != 0 && !g_ota_flash_active) {
return 3;
}
/* 安全窗口: 有车压线圈拒绝 (force 跳过) */
any_car = iot_any_car();
if (!force && any_car) {
PRINT("OTA: flash rejected, car on loop\n");
return 3;
}
/* 启动刷写 (异步): offlog 开始事件先写 (会话静默前) */
offlog_ota_start(_meta.slot, _meta.size);
_meta.state = OTA_STATE_FLASHING;
_meta.flash_cnt++;
_meta.last_result = 0;
ota_meta_write(&_meta);
_fs = OTA_FLASH_START;
_fs_retry = 0;
_fs_sent = 0;
_fs_sub = 0;
g_ota_flash_active = 1;
g_flag_counter_ota.flag = 1; /* UART2 切 0x9F 解析 */
g_flag_counter_ota.tick = 0;
PRINT("OTA: flash start slot=%d size=%lu (force=%d)\n",
_meta.slot, (unsigned long)_meta.size, force);
return 0;
}
/*===========================================================================
* ( tick , ota_poll )
*
* A5 pre_ok A6 addr_ok A7 ×N (=1)
* 1s ×3; state=flash_failed + event_report ota_error
*===========================================================================*/
/* 0x9F 帧发送: 9F SubL SubH LEN CMD DATA... CHECK(SUM 不含 magic) */
static void ota_send_9f(uint8_t sub_l, uint8_t sub_h, uint8_t cmd,
const uint8_t *data, uint16_t datalen)
{
uint16_t i;
uint8_t sum = 0;
_ota_buf.frame[0] = 0x9F;
_ota_buf.frame[1] = sub_l;
_ota_buf.frame[2] = sub_h;
_ota_buf.frame[3] = (uint8_t)(1 + datalen); /* LEN 含 CMD */
_ota_buf.frame[4] = cmd;
if (data && datalen) memcpy(&_ota_buf.frame[5], data, datalen);
for (i = 1; i < 5 + datalen; i++) sum += _ota_buf.frame[i]; /* SubL..DATA */
_ota_buf.frame[5 + datalen] = sum;
UART2_SendString(_ota_buf.frame, (uint16_t)(5 + datalen + 1));
}
/* 刷写失败收尾 (V1.09): 状态落盘 + ota_report failed + event_report 告警 + 恢复 0x7F 模式 */
static void ota_flash_fail(uint32_t err)
{
PRINT("OTA: flash FAILED err=0x%lX\n", (unsigned long)err);
_meta.state = OTA_STATE_FLASH_FAILED;
_meta.last_result = err;
ota_meta_write(&_meta);
offlog_ota_result(err);
/* 恢复 UART2 0x7F 模式 */
g_ota_flash_active = 0;
g_flag_counter_ota.flag = 0;
lup_frame_reset();
_fs = OTA_FLASH_IDLE;
/* ota_report stage=failed (平台判定主依据, V1.09) */
extern void iot_ota_report_result(uint8_t ok, uint32_t err);
iot_ota_report_result(0, err);
/* event_report ota_error 告警 (平台必答, 由 iot_mqtt_srv 发送) */
extern void iot_evt_report_ota_error(uint32_t err);
iot_evt_report_ota_error(err);
}
/* 刷写成功收尾 (V1.09): 状态回 idle 但镜像保留 (可重刷, last_result=0),
ota_report stage=done (), 0x7F */
static void ota_flash_done(void)
{
PRINT("OTA: flash DONE sent=%lu\n", (unsigned long)_fs_sent);
_meta.state = OTA_STATE_IDLE; /* V1.09: idle+size>0 = 已刷写完成 (区别于 ready=待刷) */
_meta.last_result = 0;
ota_meta_write(&_meta);
offlog_ota_result(0);
g_ota_flash_active = 0;
g_flag_counter_ota.flag = 0;
lup_frame_reset();
_fs = OTA_FLASH_IDLE;
/* ota_report stage=done (平台判定主依据, V1.09) */
extern void iot_ota_report_result(uint8_t ok, uint32_t err);
iot_ota_report_result(1, 0);
}
/* UART2 收到 bootloader 0x9F ACK 帧 (uart_srv OTA 分支调用, 主循环上下文)
* ACK : 9F SubL SubH 02 CMD Status CHECK
* pre_ok = 9F 01 00 02 A5 00 A8
* addr_ok = 9F 01 00 02 A6 00 A9 | addr_err = 9F 01 00 02 A6 01 AA
* data ack= 9F SubL SubH 02 A7 00 CK | data err = 9F SubL SubH 02 A7 01 CK */
void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len)
{
uint8_t cmd, status;
if (len < 7 || pkg[0] != 0x9F) return;
if (pkg[3] != 0x02) return; /* ACK 帧 LEN=2 */
cmd = pkg[4];
status = pkg[5];
PRINT("OTA: ack cmd=0x%02X status=%d sub=%u\n", cmd, status, pkg[1]);
switch (_fs) {
case OTA_FLASH_WAIT_READY:
if (cmd == 0xA5 && status == 0x00) {
_fs = OTA_FLASH_ADDR;
_fs_retry = 0;
}
break;
case OTA_FLASH_WAIT_ADDR:
if (cmd == 0xA6 && status == 0x00) {
/* 地址 OK → 进入数据阶段: 首块序号 = 总块数 (末块=1) */
uint32_t blocks = (_meta.size + OTA_FLASH_BLOCK - 1) / OTA_FLASH_BLOCK;
_fs_sub = (uint16_t)(blocks > 0xFFFF ? 0xFFFF : blocks);
_fs_sent = 0;
_fs = OTA_FLASH_DATA;
_fs_retry = 0;
} else if (cmd == 0xA6 && status != 0x00) {
ota_flash_fail(OTA_ERR_ADDR_FAIL);
}
break;
case OTA_FLASH_WAIT_ACK:
if (cmd == 0xA7) {
if (status == 0x00) {
_fs_sent += OTA_FLASH_BLOCK;
if (_fs_sent >= _meta.size) _fs_sent = _meta.size;
_fs_retry = 0;
if (_fs_sub <= 1) {
/* 末块已 ACK → 完成 (bootloader 清 flag 复位跑新 APP) */
ota_flash_done();
} else {
_fs_sub--; /* 序号递减: 首=总块数, 末=1 */
_fs = OTA_FLASH_DATA;
}
} else {
/* 数据块错误: 重发同块 (序号不变) */
_fs_retry++;
if (_fs_retry > 3) {
ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT);
} else {
_fs = OTA_FLASH_DATA;
}
}
}
break;
default:
break;
}
}
uint32_t ota_flash_sent(void) { return _fs_sent; }
uint32_t ota_flash_total(void) { return _meta.size; }
/* 主循环轮询: 推进刷写状态机 (每轮最多发 1~2 块, 绝不阻塞) */
void ota_poll(void)
{
uint32_t now;
if (!_init_done || _fs == OTA_FLASH_IDLE) return;
now = mstick();
if (_fs == OTA_FLASH_START) {
/* 发 A5 启动帧: 9F 01 00 01 A5 A7 → Loop APP 写 flag 复位 → bootloader 回 pre_ok */
static const uint8_t start_frame[6] = {0x9F, 0x01, 0x00, 0x01, 0xA5, 0xA7};
UART2_SendString((uint8_t *)start_frame, 6);
_fs = OTA_FLASH_WAIT_READY;
_fs_start_ms = now;
/* 重试计数保持 (首发由 ota_cmd_flash 置 0; 重发不清零, 否则超时重试永不失败) */
PRINT("OTA: sent A5 start\n");
return;
}
if (_fs == OTA_FLASH_WAIT_READY) {
if (now - _fs_start_ms > 1000u) {
_fs_retry++;
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_READY_TIMEOUT); return; }
_fs = OTA_FLASH_START; /* 重发启动帧 */
}
return;
}
if (_fs == OTA_FLASH_ADDR) {
/* 发 A6 地址帧: 9F 01 00 05 A6 08 00 34 00 SUM (0x08003400 大端) */
uint8_t addr[4] = {0x08, 0x00, 0x34, 0x00}; /* APP_START_ADDR */
ota_send_9f(0x01, 0x00, 0xA6, addr, 4);
_fs = OTA_FLASH_WAIT_ADDR;
_fs_start_ms = now;
/* 重试计数保持 (ACK 成功路径在 ota_flash_feed_ack 清 0) */
return;
}
if (_fs == OTA_FLASH_WAIT_ADDR) {
if (now - _fs_start_ms > 1000u) {
_fs_retry++;
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_ADDR_FAIL); return; }
_fs = OTA_FLASH_ADDR; /* 重发地址帧 */
}
return;
}
if (_fs == OTA_FLASH_DATA) {
/* 从 W25Qxx 读一块 → 组 A7 帧 → 发 (停等 ACK) */
uint32_t read_off = _fs_sent;
uint32_t remain = _meta.size - read_off;
uint16_t datalen = (remain > OTA_FLASH_BLOCK) ? OTA_FLASH_BLOCK : (uint16_t)remain;
SPI_Flash_Read(_ota_buf.frame + 5, _slot_base + read_off, datalen);
ota_send_9f((uint8_t)(_fs_sub & 0xFF), (uint8_t)(_fs_sub >> 8), 0xA7,
_ota_buf.frame + 5, datalen);
_fs = OTA_FLASH_WAIT_ACK;
_fs_start_ms = now;
PRINT("OTA: sent A7 sub=%u off=%lu datalen=%u\n",
_fs_sub, (unsigned long)read_off, datalen);
return;
}
if (_fs == OTA_FLASH_WAIT_ACK) {
if (now - _fs_start_ms > 1000u) {
_fs_retry++;
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT); return; }
_fs = OTA_FLASH_DATA; /* 重发同块 */
}
return;
}
}
@@ -147,6 +147,11 @@ static uint8_t attDeviceName[GAP_DEVICE_NAME_LEN] = "DLD960GA"; // "Simple Perip
static peripheralConnItem_t peripheralConnList; static peripheralConnItem_t peripheralConnList;
static uint16_t peripheralMTU = ATT_MTU_SIZE; static uint16_t peripheralMTU = ATT_MTU_SIZE;
/* getter for dbn_ble_srv.c: dynamic notify chunk by negotiated MTU */
uint16_t peripheral_get_mtu(void)
{
return peripheralMTU;
}
/********************************************************************* /*********************************************************************
* LOCAL FUNCTIONS * LOCAL FUNCTIONS
*/ */
@@ -679,13 +684,30 @@ static void peripheralStateNotificationCB(gapRole_States_t newState, gapRoleEven
*/ */
static void performPeriodicTask(void) static void performPeriodicTask(void)
{ {
// uint8_t notiData[SIMPLEPROFILE_CHAR4_LEN] = {0x88}; /* 2026-08-17 cleanup: removed _dbg_cnt 500ms debug heartbeat print */
// peripheralChar4Notify(notiData, SIMPLEPROFILE_CHAR4_LEN);
if(g_flag_notify_temp){ if(g_flag_notify_temp){
g_flag_notify_temp = 0; g_flag_notify_temp = 0;
PRINT("BLE send: len=%d resp=%d amt=%d seq=%d off=%d\n",
g_notify_buftemp.len, g_buf_ble_response.flag,
g_buf_ble_response.pkg_amount, g_buf_ble_response.pkg_seq,
g_buf_ble_response.dat_offset);
peripheralChar4Notify(g_notify_buftemp.buf, g_notify_buftemp.len); peripheralChar4Notify(g_notify_buftemp.buf, g_notify_buftemp.len);
clear_ble_notify_buf(&g_notify_buftemp); clear_ble_notify_buf(&g_notify_buftemp);
} }
/* chunk continuation: when notify buf idle and response queue still
has more fragments, actively pull next packet here (50ms period),
do NOT rely on poll_dbn_ble rx events */
if(g_notify_buftemp.flag == 0)
{
uint8_t _pull = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
g_flag_notify_temp = _pull;
if(_pull == 0 && g_buf_ble_response.flag)
{
PRINT("BLE pull FAIL: resp flag=%d amt=%d seq=%d off=%d\n",
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
}
}
} }
/********************************************************************* /*********************************************************************
@@ -701,9 +723,12 @@ static void performPeriodicTask(void)
static void peripheralChar4Notify(uint8_t *pValue, uint16_t len) static void peripheralChar4Notify(uint8_t *pValue, uint16_t len)
{ {
attHandleValueNoti_t noti; attHandleValueNoti_t noti;
PRINT("N4 enter: len=%d resp=%d amt=%d seq=%d off=%d\n", len,
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
if(len > (peripheralMTU - 3)) if(len > (peripheralMTU - 3))
{ {
PRINT("Too large noti\n"); PRINT("Too large noti, len:%d, peripheralMTU:%d\n", len, peripheralMTU);
return; return;
} }
noti.len = len; noti.len = len;
@@ -713,8 +738,22 @@ static void peripheralChar4Notify(uint8_t *pValue, uint16_t len)
tmos_memcpy(noti.pValue, pValue, noti.len); tmos_memcpy(noti.pValue, pValue, noti.len);
if(simpleProfile_Notify(peripheralConnList.connHandle, &noti) != SUCCESS) if(simpleProfile_Notify(peripheralConnList.connHandle, &noti) != SUCCESS)
{ {
PRINT("BLE notify FAIL, len:%d, MTU:%d\n", len, peripheralMTU);
GATT_bm_free((gattMsg_t *)&noti, ATT_HANDLE_VALUE_NOTI); GATT_bm_free((gattMsg_t *)&noti, ATT_HANDLE_VALUE_NOTI);
} }
else
{
uint8_t _ti = 0;
PRINT("N4 after simpleProfile_Notify: resp=%d amt=%d seq=%d off=%d\n",
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
PRINT("BLE notify OK, len:%d, MTU:%d\n", len, peripheralMTU);
for(_ti = 0; _ti < len; _ti++)
{
PRINT(" %02X", pValue[_ti]);
}
PRINT("\n");
}
} }
} }
@@ -22,9 +22,13 @@
#include "eth_driver.h" #include "eth_driver.h"
#include "net_srv.h" #include "net_srv.h"
#include "storage.h" #include "storage.h"
#include "offlog.h"
#include "snapshot.h"
#include "tcp_json_srv.h" #include "tcp_json_srv.h"
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "iot_mqtt_srv.h" #include "iot_mqtt_srv.h"
#include "ota_srv.h"
#include "fault_diag.h"
/********************************************************************* /*********************************************************************
* GLOBAL TYPEDEFS * GLOBAL TYPEDEFS
@@ -229,6 +233,9 @@ void Main_Circulation(void)
uint32_t _counter = 0; uint32_t _counter = 0;
while(1) while(1)
{ {
FAULT_MARKER(MK_LOOP_TOP);
iot_watchdog_kick(); /* 无条件喂狗 (2026-08-13): 原只在 iot_mqtt_poll, tcp_json 分支不喂 */
snap_delayed_init(); /* 快照区延后初始化: 开机 3s 后首次进入执行 (2026-08-17) */
TMOS_SystemProcess(); TMOS_SystemProcess();
if(g_net_state.flag < 2) if(g_net_state.flag < 2)
@@ -265,13 +272,18 @@ void Main_Circulation(void)
WCHNET_HandleGlobalInt(); WCHNET_HandleGlobalInt();
} }
FAULT_MARKER(MK_UART_SRV_IN);
uart_srv(); uart_srv();
FAULT_MARKER(MK_UART_SRV_OUT);
uart1_dma_poll(); /* 4G 通道下行收帧 (协议 §6.1): 置于 fault marker 之外, 不改变 MK_UART_SRV 语义 */
ota_poll(); /* OTA 刷写状态机 (V1.08): 非阻塞 tick 驱动, 先消费 UART2 ACK 再推进 */
snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */
FAULT_MARKER(MK_POLL_BLE_IN);
poll_dbn_ble(); poll_dbn_ble();
if (g_sub_code_enable.iot_enable) { if (g_sub_code_enable.iot_enable) {
iot_mqtt_publish_sensor(); // Push 0xC0 sensor data to MQTT broker iot_mqtt_publish_sensor(); // Push 0xC0 sensor data to MQTT broker
poll_mqtt(); // MQTT PINGREQ heartbeat (in net_srv.c) iot_mqtt_poll(); // IoT MQTT state machine + PINGREQ keepalive
} else { } else {
tcp_json_push_sensor(); // Push 0xC0 sensor data to TCP JSON client tcp_json_push_sensor(); // Push 0xC0 sensor data to TCP JSON client
tcp_json_poll(); tcp_json_poll();
@@ -298,12 +310,49 @@ int main(void)
// USART_Printf_Init( 115200 ); // USART_Printf_Init( 115200 );
USART_Printf_Init( 256000 ); USART_Printf_Init( 256000 );
#endif #endif
/* 诊断: 复位原因寄存器 (bit31=IWDG bit30=WWDG bit29=LPWR bit26=NRST bit25=POR bit24=软复位)
, vs vs */
PRINT("RST_REASON: 0x%08lx\n", (unsigned long)OFFLOG_RCC_RSTSCKR);
fault_diag_init(); /* 打印上次复位现场 (HardFault mcause/mepc + 执行轨迹 marker) */
iot_watchdog_init(); /* 无条件 IWDG (2026-08-13): iot_enable=0 时 iot_mqtt_init 不调,
IWDG 4s + */
PRINT("%s\n", VER_LIB); PRINT("%s\n", VER_LIB);
PRINT("SystemCoreClock:%d\n", SystemCoreClock); PRINT("SystemCoreClock:%d\n", SystemCoreClock);
/* ===== 实验: 禁用全部 Flash/SPI 操作 (2026-08-12) =====
: SPI +
: storage.c SPI + GPIO 沿 ( SPI_Flash_Init)
: NRST 100nF / SPI 线 NRST / 沿
#if 0 = 恢复全部 SPI; 保留本块便于回退实验 */
#if 0
PRINT("ISOLATE: flash/SPI disabled (flash isolation test)\n");
#else
GetMacAddr(gMacAddr); GetMacAddr(gMacAddr);
storage_init(); storage_init();
PRINT("INIT: storage ok\n");
offlog_init();
PRINT("INIT: offlog ok\n");
ota_init(); /* OTA 元数据恢复 (V1.08): 下载中断续传 / 刷写中断标记失败 */
PRINT("INIT: ota ok\n");
/* 快照区: 延后初始化 (2026-08-17) — snap_init 移到主循环 3s 后
(snap_delayed_init), SPI (+ ~80ms);
3s snap_enqueue/snap_flush !_ready */
/* offlog_boot: 记录本次复位原因到事件日志。
A(2026-08-17, offlog_boot仍崩) */
{
uint32_t rcc_rst = OFFLOG_RCC_RSTSCKR;
offlog_boot(rcc_rst);
OFFLOG_RCC_RSTSCKR |= OFFLOG_RST_RMVF; /* RMVF (bit24): 清复位标志 */
}
/* ===== 栈溢出修复闭环 (2026-08-17) =====
: .bss46KB ~1.9KB; load_cfg/output_cfg printf
.noinit+ PC
: D定位触发点 RAM (ETH 768/payload 800/MQTT buf 800,
1.9KB~4.75KB) load_cfg 0 */
load_cfg_from_flash(); load_cfg_from_flash();
output_cfg_from_flash(); output_cfg_from_flash(); /* 2026-08-17: 栈瘦身后恢复 (栈~4.75KB, printf 峰值安全) */
#endif
PRINT("MAC: %02X %02X %02X %02X %02X %02X\r\n", gMacAddr[0],gMacAddr[1], gMacAddr[2], gMacAddr[3],gMacAddr[4],gMacAddr[5]); PRINT("MAC: %02X %02X %02X %02X %02X %02X\r\n", gMacAddr[0],gMacAddr[1], gMacAddr[2], gMacAddr[3],gMacAddr[4],gMacAddr[5]);
PRINT("net version:%x\n", WCHNET_GetVer()); PRINT("net version:%x\n", WCHNET_GetVer());
@@ -311,6 +360,24 @@ int main(void)
HAL_Init(); HAL_Init();
uart_init(); uart_init();
/* ===== 实验: 禁用 UART2 (Loop 口) 隔离测试 (2026-08-12) =====
: UART2 Loop ( SPI )
#if 0 = 恢复 UART2 正常 */
#if 0
{
GPIO_InitTypeDef gpio;
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
USART_Cmd(USART2, DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
gpio.GPIO_Pin = GPIO_Pin_3;
gpio.GPIO_Mode = GPIO_Mode_IPU; /* PA3 脱离 UART2, 上拉输入 */
gpio.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio);
PRINT("ISOLATE: UART2 disabled (Loop isolation test)\n");
}
#endif
TIM3_Init(); TIM3_Init();
TIM2_Init(); TIM2_Init();
@@ -0,0 +1,496 @@
/**
******************************************************************************
* @file snapshot.c
* @author wfq
* @version V1.0
* @date 2026-08-12
* @brief DBN ( offlog , )
*
* : storage.c (SPI_Flash_*), cmcng.h (mstick), offlog.c (g_offlog_part + )
* 线: enqueue (USART2 ISR), flush/write_raw/clear
* SPI ( ~45ms , offlog 线)
******************************************************************************
*/
#include "snapshot.h"
#include "storage.h"
#include "cmcng.h"
#include <string.h>
#include <stdio.h>
/* 编译期断言: 记录必须是 64B (定长环形索引依赖) */
typedef char snap_rec_size_must_be_64[(sizeof(SnapRec) == 64) ? 1 : -1];
typedef char snap_head_size_must_be_32[(sizeof(SnapHead) == 32) ? 1 : -1];
/*===========================================================================
* (访: init/flush/clear/)
*===========================================================================*/
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..n) */
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
static uint16_t _boot_seq; /* 当前 boot 序号 */
static uint32_t _count; /* 有效记录数 (0..max_records) */
static uint8_t _ready; /* init 完成标志 */
/* 中断安全 RAM 暂存 (单生产者=中断 enqueue / 单消费者=主循环 flush, 无锁)
g_snap_wr / g_snap_count, g_snap_rd; count */
static SnapRec g_snap_pending[SNAP_RAM_DEPTH];
static volatile uint8_t g_snap_rd; /* 消费者 (主循环) 读指针 */
static volatile uint8_t g_snap_wr; /* 生产者 (中断) 写指针 */
static volatile uint8_t g_snap_count; /* 暂存条数 (容量 = SNAP_RAM_DEPTH) */
static volatile uint32_t g_snap_drop; /* 满时丢弃计数 (调试用) */
/*===========================================================================
* (ROADMAP 2026-08-12: JEDEC ID + = )
*===========================================================================*/
SnapPart g_snap_part = {
OFFLOG_CHIP_W25Q32, /* 默认 */
0x110000UL, /* W25Q32: 0x090000 + 512KB 事件区 */
0x2F0000UL, /* 3008KB = 4MB - 576KB - 512KB */
751, /* 3008KB/4096 - 1 头扇区 */
48064, /* 751 * 64 */
};
/* 芯片总容量 (W25Q32 4MB → W25Q256 32MB) */
static uint32_t snap_chip_total(uint8_t chip)
{
switch (chip) {
case OFFLOG_CHIP_W25Q32: return 0x00400000UL;
case OFFLOG_CHIP_W25Q64: return 0x00800000UL;
case OFFLOG_CHIP_W25Q128: return 0x01000000UL;
case OFFLOG_CHIP_W25Q256: return 0x02000000UL;
default: return 0x00400000UL; /* 未知→默认 4MB */
}
}
/* 读 JEDEC ID → 填分区表。事件区大小与 offlog 同一事实来源 (g_offlog_part),
= - - W25Q32 */
static void snap_part_detect(void)
{
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
uint8_t chip = OFFLOG_CHIP_W25Q32;
uint32_t evt_size;
uint32_t total;
switch (dev) {
case OFFLOG_CHIP_W25Q32: chip = dev; break;
case OFFLOG_CHIP_W25Q64: chip = dev; break;
case OFFLOG_CHIP_W25Q128: chip = dev; break;
case OFFLOG_CHIP_W25Q256: chip = dev; break;
default: chip = OFFLOG_CHIP_W25Q32; break;
}
evt_size = g_offlog_part.area_size; /* 与事件区严格一致 */
total = snap_chip_total(chip);
g_snap_part.chip = chip;
g_snap_part.area_base = SNAP_FIXED_SIZE + evt_size;
g_snap_part.area_size = total - SNAP_FIXED_SIZE - evt_size;
g_snap_part.data_sectors = (g_snap_part.area_size / 4096UL) - 1;
g_snap_part.max_records = g_snap_part.data_sectors * SNAP_REC_PER_SECTOR;
}
/*===========================================================================
*
*===========================================================================*/
static void snap_head_write(const SnapHead *h)
{
uint8_t buf[32];
memset(buf, 0xFF, sizeof(buf));
memcpy(buf, h, sizeof(SnapHead));
SPI_Flash_Write(buf, g_snap_part.area_base, sizeof(buf));
}
static void snap_head_read(SnapHead *h)
{
uint8_t buf[32];
SPI_Flash_Read(buf, g_snap_part.area_base, sizeof(buf));
memcpy(h, buf, sizeof(SnapHead));
}
static int snap_rec_read_at(uint32_t abs_off, SnapRec *out)
{
SPI_Flash_Read((uint8_t *)out, abs_off, SNAP_REC_SIZE);
return (out->magic == SNAP_MAGIC) ? 0 : -1;
}
/*===========================================================================
* ( offlog : , magic + seq )
*===========================================================================*/
static void snap_scan_recover(const SnapHead *h)
{
uint32_t pos = h->wr_off;
uint32_t scanned = 0;
uint32_t expect_seq = h->wr_seq;
SnapRec r;
/* 防御: wr_off 必须在数据区范围内且 64B 对齐 */
if (pos < SNAP_DATA_BASE || pos >= g_snap_part.area_base + g_snap_part.area_size) {
pos = SNAP_DATA_BASE;
}
if ((pos - SNAP_DATA_BASE) % SNAP_REC_SIZE != 0) {
pos = SNAP_DATA_BASE;
}
while (scanned < SNAP_MAX_RECORDS) {
if (snap_rec_read_at(pos, &r) != 0) {
break; /* 找到真实写位置 */
}
if (expect_seq != 0 && r.seq != expect_seq) {
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
}
pos += SNAP_REC_SIZE;
if (pos >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
pos = SNAP_DATA_BASE; /* 回绕数据区 */
}
scanned++;
expect_seq++;
}
_wr_off = pos;
_wr_sector = (uint16_t)((pos - SNAP_DATA_BASE) / 4096) + 1; /* 区内 1..n */
_wr_seq = expect_seq;
_count = h->count + scanned;
if (_count > SNAP_MAX_RECORDS) _count = SNAP_MAX_RECORDS;
_boot_seq = (uint16_t)(h->boot_seq + 1);
if (_boot_seq == 0) _boot_seq = 1;
}
/*===========================================================================
*
*===========================================================================*/
void snap_init(void)
{
SnapHead h;
snap_part_detect(); /* JEDEC ID + 事件区大小 → 快照区容量 */
memset(&h, 0xFF, sizeof(h));
snap_head_read(&h);
if (!(h.magic[0] == SNAP_HEAD_MAGIC0 && h.magic[1] == SNAP_HEAD_MAGIC1 &&
h.magic[2] == SNAP_HEAD_MAGIC2 && h.magic[3] == SNAP_HEAD_MAGIC3)) {
/* 全新区: 懒擦 — 只擦当前写扇区 (数据扇区1) ~45ms, 其余扇区由环形写
751 ~34s IWDG 4s */
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
memset(&h, 0xFF, sizeof(h));
h.magic[0] = SNAP_HEAD_MAGIC0;
h.magic[1] = SNAP_HEAD_MAGIC1;
h.magic[2] = SNAP_HEAD_MAGIC2;
h.magic[3] = SNAP_HEAD_MAGIC3;
h.boot_seq = 0;
h.wr_sector = 1;
h.wr_off = SNAP_DATA_BASE;
h.wr_seq = 0;
h.count = 0;
snap_head_write(&h);
_wr_off = SNAP_DATA_BASE;
_wr_sector = 1;
_wr_seq = 1;
_count = 0;
_boot_seq = 1;
} else {
snap_scan_recover(&h);
}
/* 回写头: boot_seq 递增 */
h.magic[0] = SNAP_HEAD_MAGIC0;
h.magic[1] = SNAP_HEAD_MAGIC1;
h.magic[2] = SNAP_HEAD_MAGIC2;
h.magic[3] = SNAP_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
snap_head_write(&h);
g_snap_rd = 0;
g_snap_wr = 0;
g_snap_count = 0;
g_snap_drop = 0;
_ready = 1;
}
/*===========================================================================
* (2026-08-17): SNAP_START_DELAY_MS
* : SPI ( + ~80ms),
* VDD / (08-17 : BOOT_CNT =1, RSTSCKR )
* : 3s snap_enqueue/snap_flush !_ready ,
* ()
* , (/ return)
*===========================================================================*/
#define SNAP_START_DELAY_MS 3000
void snap_delayed_init(void)
{
if (_ready) return; /* 已初始化 */
if (mstick() < SNAP_START_DELAY_MS) return; /* 未到延时窗口 */
snap_init();
}
/*===========================================================================
* ()
*===========================================================================*/
static void snap_flush_head(void)
{
SnapHead h;
h.magic[0] = SNAP_HEAD_MAGIC0;
h.magic[1] = SNAP_HEAD_MAGIC1;
h.magic[2] = SNAP_HEAD_MAGIC2;
h.magic[3] = SNAP_HEAD_MAGIC3;
h.boot_seq = _boot_seq;
h.wr_sector = _wr_sector;
h.wr_off = _wr_off;
h.wr_seq = _wr_seq;
h.count = _count;
snap_head_write(&h);
}
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic */
static int snap_sector_has_data(uint32_t abs_sec_off)
{
SnapRec r;
SPI_Flash_Read((uint8_t *)&r, abs_sec_off, SNAP_REC_SIZE);
return (r.magic == SNAP_MAGIC) ? 1 : 0;
}
/* 写一条记录 (仅主循环; rec->seq 已由 snap_flush 分配) */
static void snap_write_raw(const SnapRec *rec)
{
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
if (_wr_off >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
_wr_off = SNAP_DATA_BASE;
}
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区 */
{
uint32_t sec_index = (_wr_off - SNAP_DATA_BASE) / 4096; /* 0..n-2 */
if ((uint32_t)(_wr_sector - 1) != sec_index) {
uint32_t target_abs = (SNAP_DATA_BASE / 4096 + sec_index) * 4096;
if (snap_sector_has_data(target_abs)) {
uint32_t erased = (_count > SNAP_REC_PER_SECTOR)
? SNAP_REC_PER_SECTOR : _count;
_count -= erased;
}
SPI_Flash_Erase_Sector(target_abs / 4096);
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..n */
snap_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
}
}
SPI_Flash_Write_NoCheck((uint8_t *)rec, _wr_off, SNAP_REC_SIZE);
_wr_off += SNAP_REC_SIZE;
_wr_seq++;
if (_count < SNAP_MAX_RECORDS) _count++;
}
/*===========================================================================
* ( 0xC0 线线 12B )
*===========================================================================*/
static void snap_pack_coil(const LUP_CoilSensor *cs, uint8_t *o)
{
o[0] = (uint8_t)((cs->freq_level << 6) | (cs->direction << 5)
| (cs->freq_type << 4) | (cs->sensitivity & 0x0F));
o[1] = (uint8_t)((cs->condition << 4) | (cs->loop_state << 3)
| (cs->car_state << 2) | (cs->misc_type & 0x03));
o[2] = (uint8_t)(cs->freq & 0xFF);
o[3] = (uint8_t)((cs->freq >> 8) & 0xFF);
o[4] = (uint8_t)((cs->freq >> 16) & 0xFF);
o[5] = (uint8_t)(cs->variation & 0xFF);
o[6] = (uint8_t)((cs->variation >> 8) & 0xFF);
o[7] = (uint8_t)((cs->variation >> 16) & 0xFF);
o[8] = (uint8_t)(cs->misc.passtime_ms & 0xFF);
o[9] = (uint8_t)((cs->misc.passtime_ms >> 8) & 0xFF);
o[10] = (uint8_t)((cs->misc.passtime_ms >> 16) & 0xFF);
o[11] = (uint8_t)((cs->misc.passtime_ms >> 24) & 0xFF);
}
/* 打包传感帧 → SnapRec (seq 留待 flush 时分配; ts_ms/boot_seq 用采集时刻) */
static void snap_pack(const LUP_SensorReport *sr, SnapRec *rec)
{
uint8_t i, n;
memset(rec, 0, sizeof(*rec));
rec->magic = SNAP_MAGIC;
n = (sr->coil_count > SNAP_COILS_MAX) ? SNAP_COILS_MAX : sr->coil_count;
rec->len = n * SNAP_COIL_BYTES;
rec->ts_ms = mstick();
rec->boot_seq = _boot_seq;
for (i = 0; i < n; i++) {
snap_pack_coil(&sr->coils[i], rec->coils + i * SNAP_COIL_BYTES);
}
}
/*===========================================================================
* (: uart_srv lup_process_frame iot_sensor_ingest)
* USART2 ISR lup_feed_byte, uart_srv
* , RAM + flush : SPI
* - g_snap_wr / g_snap_pending / g_snap_count
* - g_snap_rd; count volatile ,
* - : count >= DEPTH ( = SNAP_RAM_DEPTH)
*===========================================================================*/
void snap_enqueue(const LUP_SensorReport *sr)
{
SnapRec rec;
if (!_ready || sr == NULL) return;
snap_pack(sr, &rec);
if (g_snap_count >= SNAP_RAM_DEPTH) {
g_snap_drop++; /* 暂存满 → 丢新帧 (快照环形, 可接受) */
return;
}
g_snap_pending[g_snap_wr] = rec;
g_snap_wr = (uint8_t)((g_snap_wr + 1) & (SNAP_RAM_DEPTH - 1));
g_snap_count++;
}
/*===========================================================================
* (iot_mqtt_publish_sensor ; /使)
* - seq ( _wr_seq, )
*===========================================================================*/
void snap_flush(void)
{
if (!_ready) return;
while (g_snap_count > 0) {
SnapRec *rec = &g_snap_pending[g_snap_rd];
rec->seq = _wr_seq; /* 主循环独占分配 */
snap_write_raw(rec);
g_snap_rd = (uint8_t)((g_snap_rd + 1) & (SNAP_RAM_DEPTH - 1));
g_snap_count--;
}
if (g_snap_drop != 0) {
g_snap_drop = 0; /* 丢弃计数清零 (调试观察点) */
}
}
/*===========================================================================
*
*===========================================================================*/
uint8_t snap_enabled(void)
{
return _ready;
}
uint32_t snap_count(void)
{
return _count;
}
uint32_t snap_seq_last(void)
{
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
}
uint32_t snap_boot_seq(void)
{
return _boot_seq;
}
int snap_read_idx(uint32_t idx, SnapRec *out)
{
uint32_t cnt = _count;
if (!_ready || idx >= cnt) return -1;
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
uint32_t phys = (_wr_off - SNAP_DATA_BASE); /* 0..DATA_SIZE */
uint32_t start = (phys + SNAP_DATA_SIZE - cnt * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
uint32_t read_off = SNAP_DATA_BASE + (start + idx * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
return snap_rec_read_at(read_off, out);
}
/*===========================================================================
* (: LOG_CLEAR, payload[0]=2=)
*===========================================================================*/
void snap_clear(void)
{
if (!_ready) return;
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms): count=0 使旧数据不可读,
751 ~34s > IWDG 4s */
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
_wr_off = SNAP_DATA_BASE;
_wr_sector = 1;
_count = 0;
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证快照全局唯一 */
{
uint8_t p[1] = {2}; /* 2 = 快照流 (审计区分) */
offlog_evt(OFFLOG_EVT_LOG_CLEAR, p, 1);
}
snap_flush_head();
}
/* SnapRec -> JSON object (channels aligned with 0xC0 wire format).
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
Returns snprintf written length; caller must ensure buf_len enough. */
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len)
{
int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES))
? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES);
if (coil_count < 0) coil_count = 0;
int pos = snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[",
(unsigned long)r->seq, (unsigned)r->boot_seq,
(unsigned long)r->ts_ms, (unsigned)coil_count);
int c;
for (c = 0; c < coil_count; c++) {
const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES];
uint8_t cfg = p[0];
uint8_t cond = p[1];
uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16);
int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16));
uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8)
| ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24);
uint8_t freq_level = (cfg >> 6) & 0x03;
uint8_t direction = (cfg >> 5) & 0x01;
uint8_t freq_type = (cfg >> 4) & 0x01;
uint8_t sensitivity = cfg & 0x0F;
uint8_t condition = (cond >> 4) & 0x0F;
uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */
uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */
uint8_t misc_type = cond & 0x03;
const char *fl = "high";
const char *mt = "time";
if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */
if (freq_level == 1) fl = "mid_high";
else if (freq_level == 2) fl = "mid_low";
else if (freq_level == 3) fl = "low";
if (misc_type == 1) mt = "cut_count";
else if (misc_type == 2) mt = "flow_count";
else if (misc_type == 3) mt = "relay_count";
if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ",");
pos += snprintf(buf + pos, buf_len - pos,
"{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u,"
"\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s,"
"\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}",
(unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type,
(unsigned)sensitivity, (unsigned)condition,
loop_state ? "false" : "true", car_state ? "true" : "false",
mt, (unsigned long)freq, (long)variation, (unsigned long)misc);
}
pos += snprintf(buf + pos, buf_len - pos, "]}");
return pos;
}
/* SnapRec -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §6.4 字段表解析)
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
Returns snprintf written length; 64B -> 128 hex chars. */
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)r;
for (i = 0; i < (int)sizeof(SnapRec); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
@@ -19,11 +19,11 @@
/* Winbond SPIFalsh ID */ /* Winbond SPIFalsh ID */
#define W25Q80 0XEF13 #define W25Q80 0X13 // device ID only, no vendor code
#define W25Q16 0XEF14 #define W25Q16 0X14
#define W25Q32 0XEF15 #define W25Q32 0X15
#define W25Q64 0XEF16 //64Mbit, 8MByte #define W25Q64 0X16 //64Mbit, 8MByte
#define W25Q128 0XEF17 #define W25Q128 0X17
/* Winbond SPIFalsh Instruction List */ /* Winbond SPIFalsh Instruction List */
#define W25X_WriteEnable 0x06 #define W25X_WriteEnable 0x06
@@ -90,13 +90,13 @@ void SPI_Flash_Init(void)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4); GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
@@ -105,7 +105,7 @@ void SPI_Flash_Init(void)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
@@ -114,7 +114,7 @@ void SPI_Flash_Init(void)
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure); SPI_Init(SPI1, &SPI_InitStructure);
@@ -225,6 +225,33 @@ u16 SPI_Flash_ReadID(void)
} }
/*********************************************************************
* @fn SPI_Flash_ReadJEDEC_ID
*
* @brief Read JEDEC ID (0x9F). Return 3rd byte (capacity code).
* W25Q32=0x16 Q64=0x17 Q128=0x18 Q256=0x19.
* 2026-08-19: no vendor code check (id[0]/id[1]) - compatible
* with other vendors same-capacity parts.
*
* @return device id byte
*/
uint8_t SPI_Flash_ReadJEDEC_ID(void)
{
uint8_t id[3] = {0, 0, 0};
GPIO_WriteBit(GPIOA, GPIO_Pin_4, 0);
SPI1_ReadWriteByte(W25X_JedecDeviceID);
id[0] = SPI1_ReadWriteByte(0xFF);
id[1] = SPI1_ReadWriteByte(0xFF);
id[2] = SPI1_ReadWriteByte(0xFF);
GPIO_WriteBit(GPIOA, GPIO_Pin_4, 1);
/* 2026-08-19: no vendor code check - JEDEC capacity code (id[2])
is standardized across vendors (e.g. 0x16=32Mbit for ISSI/GD too) */
return id[2];
}
/********************************************************************* /*********************************************************************
* @fn SPI_Flash_Erase_Sector * @fn SPI_Flash_Erase_Sector
* *
@@ -481,7 +508,7 @@ void storage_init(void)
SPI_Flash_Init(); SPI_Flash_Init();
uint16_t Flash_Model = SPI_Flash_ReadID(); uint16_t Flash_Model = SPI_Flash_ReadID();
switch(Flash_Model) switch(Flash_Model & 0xFF) /* device ID low byte only, no vendor check */
{ {
case W25Q80: PRINT("W25Q80 OK!\r\n"); break; case W25Q80: PRINT("W25Q80 OK!\r\n"); break;
case W25Q16: PRINT("W25Q16 OK!\r\n"); break; case W25Q16: PRINT("W25Q16 OK!\r\n"); break;
@@ -18,6 +18,9 @@
#include "loop_uart_proto.h" #include "loop_uart_proto.h"
#include "simple_json.h" #include "simple_json.h"
#include "storage.h" #include "storage.h"
#include "offlog.h"
#include "snapshot.h"
#include "fault_diag.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
@@ -636,7 +639,129 @@ static void handle_report_config(uint8_t socket, uint32_t msg_id, const char *js
g_report_cfg.enable, g_report_cfg.interval, g_report_cfg.sensor_type); g_report_cfg.enable, g_report_cfg.interval, g_report_cfg.sensor_type);
} }
/* 4.16 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */ /* 4.16 log_stat — 查询脱机日志统计 (event/snapshot 流, 协议 V1.03) */
static void handle_log_stat(uint8_t socket, uint32_t msg_id, const char *json) {
char data_json[256];
char stream_buf[16];
uint32_t count, seq_last, seq_first, capacity;
const char *stream = "event";
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
if (strcmp(stream_buf, "snapshot") == 0) stream = "snapshot";
if (strcmp(stream, "snapshot") == 0) {
count = snap_count();
seq_last = snap_seq_last();
seq_first = (count > 0) ? (seq_last - count + 1) : 0;
capacity = SNAP_MAX_RECORDS;
snprintf(data_json, sizeof(data_json),
"{\"stream\":\"snapshot\",\"enabled\":%s,\"boot_seq\":%lu,"
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}",
snap_enabled() ? "true" : "false",
(unsigned long)snap_boot_seq(),
(unsigned long)count, (unsigned long)capacity,
(unsigned long)seq_first, (unsigned long)seq_last);
PRINT("JSON: log_stat(snapshot) count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last);
} else {
count = offlog_count();
seq_last = offlog_seq_last();
seq_first = (count > 0) ? (seq_last - count + 1) : 0;
capacity = OFFLOG_MAX_RECORDS;
snprintf(data_json, sizeof(data_json),
"{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu,"
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}",
offlog_enabled() ? "true" : "false",
(unsigned long)offlog_boot_seq(),
(unsigned long)count, (unsigned long)capacity,
(unsigned long)seq_first, (unsigned long)seq_last);
PRINT("JSON: log_stat count=%lu seq_first=%lu seq_last=%lu\n",
(unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last);
}
json_send_ok(socket, msg_id, "log_stat", data_json);
}
/* 4.17 log_query — 分页拉取脱机日志 (event/snapshot 流, hex 原始字节上报, 协议 V1.03) */
static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json) {
char data_json[TCP_JSON_DATA_BUF_LEN];
char stream_buf[16];
uint32_t start_seq = json_get_uint_field(json, "\"start_seq\"");
uint32_t req_count = json_get_uint_field(json, "\"count\"");
int is_snap = 0;
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
if (strcmp(stream_buf, "snapshot") == 0) is_snap = 1;
int pos = snprintf(data_json, sizeof(data_json),
"{\"start_seq\":%lu,\"records\":[",
(unsigned long)start_seq);
uint32_t fetched = 0;
if (is_snap) {
uint32_t total = snap_count();
uint32_t seq_last = snap_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
if (req_count > SNAP_MAX_QUERY_RECORDS) req_count = SNAP_MAX_QUERY_RECORDS;
if (req_count == 0) req_count = SNAP_MAX_QUERY_RECORDS;
if (snap_enabled() && total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
SnapRec rec;
char hex[132]; /* 64B -> 128 hex + NUL */
if (snap_read_idx(idx + fetched, &rec) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
snap_rec_to_hex(&rec, hex, sizeof(hex));
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)rec.seq, hex);
fetched++;
}
}
PRINT("JSON: log_query(snapshot) start_seq=%lu req=%lu fetched=%lu\n",
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
} else {
uint32_t total = offlog_count();
uint32_t seq_last = offlog_seq_last();
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS;
if (req_count == 0) req_count = OFFLOG_MAX_QUERY_RECORDS;
if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
OfflogEvt evt;
char hex[68]; /* 32B -> 64 hex + NUL */
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
offlog_evt_to_hex(&evt, hex, sizeof(hex));
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)evt.seq, hex);
fetched++;
}
}
PRINT("JSON: log_query start_seq=%lu req=%lu fetched=%lu\n",
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
}
snprintf(data_json + pos, sizeof(data_json) - pos, "]}");
json_send_ok(socket, msg_id, "log_query", data_json);
}
/* 4.18 log_clear — 清除脱机日志 (event/snapshot 流, 审计留痕, 协议 V1.03) */
static void handle_log_clear(uint8_t socket, uint32_t msg_id, const char *json) {
char stream_buf[16];
memset(stream_buf, 0, sizeof(stream_buf));
simple_parse_json(json, "\"stream\"", stream_buf);
if (strcmp(stream_buf, "snapshot") == 0) {
snap_clear(); /* 阻塞 ~45ms (逻辑清除 + 当前写扇区) */
PRINT("JSON: log_clear(snapshot) done\n");
} else {
offlog_clear(); /* 阻塞 ~2.8s (63 扇区擦除) */
PRINT("JSON: log_clear done\n");
}
json_send_ok(socket, msg_id, "log_clear", NULL);
}
/* 4.19 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */
static void handle_loop_version_query(uint8_t socket, uint32_t msg_id, const char *json) { static void handle_loop_version_query(uint8_t socket, uint32_t msg_id, const char *json) {
lup_send_get_version(); lup_send_get_version();
g_json_pending.active = 1; g_json_pending.active = 1;
@@ -754,6 +879,9 @@ static const JsonCmdEntry g_cmd_table[] = {
{"loop_sens_read", handle_loop_sens_read, 1}, {"loop_sens_read", handle_loop_sens_read, 1},
{"loop_sens_write", handle_loop_sens_write, 1}, {"loop_sens_write", handle_loop_sens_write, 1},
{"report_config", handle_report_config, 1}, {"report_config", handle_report_config, 1},
{"log_stat", handle_log_stat, 1},
{"log_query", handle_log_query, 1},
{"log_clear", handle_log_clear, 1},
}; };
#define JSON_CMD_COUNT (sizeof(g_cmd_table) / sizeof(g_cmd_table[0])) #define JSON_CMD_COUNT (sizeof(g_cmd_table) / sizeof(g_cmd_table[0]))
@@ -813,13 +941,23 @@ static int format_loop_param_json(char *buf, uint16_t buf_size, const LUP_ParamG
static void json_sensor_callback(const uint8_t *pkg, uint16_t len) static void json_sensor_callback(const uint8_t *pkg, uint16_t len)
{ {
FAULT_MARKER(MK_EVT_CB_IN);
/* 2026-08-13 实验: enter PRINT 是 LUP 帧后卡死嫌疑点 (printf %d 处理死循环),
printf , _write/ */
#if 0
PRINT("JSON: sensor_cb enter socket=%d auth=%d report=%d\n", PRINT("JSON: sensor_cb enter socket=%d auth=%d report=%d\n",
g_json_socket_listen, g_json_auth_state, g_report_cfg.enable); g_json_socket_listen, g_json_auth_state, g_report_cfg.enable);
#endif
// Check: socket active, authed, report enabled // Check: socket active, authed, report enabled
if (g_json_socket_listen == 0xFF) { PRINT("JSON: sensor_cb skip: no socket\n"); return; } if (g_json_socket_listen == 0xFF) { PRINT("JSON: sensor_cb skip: no socket\n"); return; }
/* 2026-08-13: not authed 每帧打印太吵 (Loop MCU 高频上报), 且占用主循环
UART2 */
#if 0
if (g_json_auth_state != JSON_STATE_AUTHED) { PRINT("JSON: sensor_cb skip: not authed\n"); return; } if (g_json_auth_state != JSON_STATE_AUTHED) { PRINT("JSON: sensor_cb skip: not authed\n"); return; }
if (!g_report_cfg.enable) { PRINT("JSON: sensor_cb skip: report disabled\\n"); return; } #endif
if (g_json_auth_state != JSON_STATE_AUTHED) { return; }
if (!g_report_cfg.enable) { PRINT("JSON: sensor_cb skip: report disabled\n"); return; }
LUP_SensorReport sr; LUP_SensorReport sr;
memset(&sr, 0, sizeof(sr)); memset(&sr, 0, sizeof(sr));
@@ -829,7 +967,7 @@ static void json_sensor_callback(const uint8_t *pkg, uint16_t len)
return; return;
} }
char data_json[2048]; char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) { if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
char *out = (char *)malloc(TCP_JSON_MAX_FRAME); char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
if (out) { if (out) {
@@ -933,13 +1071,13 @@ void tcp_json_handle_sock_int(uint8_t socketid, uint8_t intstat) {
uint16_t space = TCP_JSON_RECV_BUF_LEN - g_json_recv_len; uint16_t space = TCP_JSON_RECV_BUF_LEN - g_json_recv_len;
if (recv_len > space) recv_len = space; if (recv_len > space) recv_len = space;
uint32_t rd_len = recv_len; uint32_t rd_len = recv_len;
uint8_t tmp_buf[RECE_BUF_LEN]; static uint8_t tmp_buf[RECE_BUF_LEN]; /* 2026-08-17: static — 中断上下文 1KB 栈数组 (防溢出) */
WCHNET_SocketRecv(socketid, tmp_buf, &rd_len); WCHNET_SocketRecv(socketid, tmp_buf, &rd_len);
g_json_last_comm_ts = mstick(); // 有数据交互 g_json_last_comm_ts = mstick(); // 有数据交互
memcpy(g_json_recv_buf + g_json_recv_len, tmp_buf, (uint16_t)rd_len); memcpy(g_json_recv_buf + g_json_recv_len, tmp_buf, (uint16_t)rd_len);
g_json_recv_len += (uint16_t)rd_len; g_json_recv_len += (uint16_t)rd_len;
char frame[TCP_JSON_MAX_FRAME]; static char frame[TCP_JSON_MAX_FRAME]; /* 2026-08-17: static — 中断上下文 800B 栈数组 (防溢出) */
while (json_extract_frame(g_json_recv_buf, &g_json_recv_len, frame, sizeof(frame))) { while (json_extract_frame(g_json_recv_buf, &g_json_recv_len, frame, sizeof(frame))) {
PRINT("JSON recv: %s\n", frame); PRINT("JSON recv: %s\n", frame);
json_process_frame(socketid, frame); json_process_frame(socketid, frame);
@@ -1102,7 +1240,7 @@ static void json_check_pending(void) {
memset(&pg, 0, sizeof(pg)); memset(&pg, 0, sizeof(pg));
int ret = lup_parse_param_get(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &pg); int ret = lup_parse_param_get(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &pg);
if (ret == 0) { if (ret == 0) {
char data_json[2048]; char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_loop_param_json(data_json, sizeof(data_json), &pg) == 0) { if (format_loop_param_json(data_json, sizeof(data_json), &pg) == 0) {
json_send_ok(g_json_pending.socket, g_json_pending.msg_id, json_send_ok(g_json_pending.socket, g_json_pending.msg_id,
g_json_pending.cmd, data_json); g_json_pending.cmd, data_json);
@@ -1319,7 +1457,7 @@ void tcp_json_push_sensor(void) {
return; return;
} }
char data_json[2048]; char data_json[TCP_JSON_DATA_BUF_LEN];
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) { if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
char *out = (char *)malloc(TCP_JSON_MAX_FRAME); char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
if (out) { if (out) {
@@ -8,14 +8,352 @@
#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")));
/*==================== UART2 RX DMA (2026-08-17 方案A) ====================
* : PRINT ( ~7.4ms) USART2 RXNE checksum fail
* : DMA1_Ch6 ( CPU ),
* - 512B : ~7 (70B), (SPI 45ms)
* - ( RXNE, DMA ) ; WCHNET ETH DMA,
* BLE DMA1 Ch6
* - lup_feed_byte ()
*======================================================================*/
#define UART2_DMA_BUF_LEN 512
static uint8_t uart2_dma_buf[UART2_DMA_BUF_LEN] __attribute__((aligned(4)));
static uint16_t uart2_dma_last = 0; /* 主循环消费位置 (DMA 写指针由硬件维护) */
static uint32_t uart2_dma_drop = 0; /* 溢出丢弃计数 (主循环消费不及时) */
static uint8_t uart2_dma_ota_mode = 0xFF; /* 上次 OTA 模式 (0xFF=未初始化, 首轮强制 reset) */
void uart2_dma_init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel6);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DATAR);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart2_dma_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = UART2_DMA_BUF_LEN;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel6, &DMA_InitStructure);
/* DMA 接管 USART2 RX: 硬件收字节, 打印关中断不丢 */
USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE);
DMA_Cmd(DMA1_Channel6, ENABLE);
/* 关闭 RXNE 中断 (DMA 接管后逐字节中断不再需要) */
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
uart2_dma_last = 0;
}
/* 主循环每轮调用: 消费 DMA 环形缓冲新字节 → 批量喂 lup_feed_byte 状态机 */
void uart2_dma_poll(void)
{
uint16_t cur = (uint16_t)(UART2_DMA_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel6));
uint16_t n = (uint16_t)((cur + UART2_DMA_BUF_LEN - uart2_dma_last) & (UART2_DMA_BUF_LEN - 1));
if (n == 0) return;
/* 溢出保护: 未消费 > 半缓冲 (256B) → 消费太慢被 DMA 覆盖, 重置解析器丢帧计数
2026-08-19: OTA 0x9F ACK 7B, Loop 1 ,
256B TX ACK */
{
uint16_t drop_thr = g_flag_counter_ota.flag ? UART2_DMA_BUF_LEN : (UART2_DMA_BUF_LEN / 2);
if (n > drop_thr) {
uart2_dma_drop++;
lup_frame_reset();
uart2_dma_last = cur;
return;
}
}
/* OTA 模式切换 (0x7F ↔ 0x9F) 时重置帧解析器, 防旧模式残留状态污染 */
if (g_flag_counter_ota.flag != uart2_dma_ota_mode) {
lup_frame_reset();
uart2_dma_ota_mode = g_flag_counter_ota.flag;
}
while (n--) {
uint8_t b = uart2_dma_buf[uart2_dma_last];
uart2_dma_last = (uint16_t)((uart2_dma_last + 1) & (UART2_DMA_BUF_LEN - 1));
/* OTA 模式走 0x9F 帧解析 (bootloader 的 pre_ok/addr_ok/data ACK),
OTA 0x7F */
int done = g_flag_counter_ota.flag ? lup_feed_byte_ota(b) : lup_feed_byte(b);
if (done) {
/* 帧接收完成 → 复制到 g_pkg_uart_2 (与中断版同逻辑, 主循环无竞争) */
const uint8_t *frame = lup_frame_data();
uint16_t frame_len = lup_frame_len();
if (frame_len <= BUFF_STACK_SIZE) {
memcpy(g_pkg_uart_2.pkg, frame, frame_len);
g_pkg_uart_2.offset = frame_len;
g_pkg_uart_2.flag = 1;
g_pkg_uart_2.tick = 0;
}
lup_frame_reset();
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
/* 收帧中 → tick 归零 (对齐中断版行为, 防 TIM3 半帧兜底误判) */
g_pkg_uart_2.tick = 0;
}
}
}
/*==================== UART1 <-> Air780 4G 通道 (协议 §6.1) ====================
* : 4G vd960DBN "字节流透传",
* : Loop 0x7F (UART2) ----> UART1 --> Air780 JSON
* : Air780 (UART1) ----> 0x7F: UART2( vd960Loop)
* 0x8F: DBN(CH32V208GBU6)
* ,
*
* :
* 1) lup_verify_checksum() Air780 loop_state 沿,
* =
* 2) DMA ( UART2 A ): PRINT / BLE / SPI
* RXNE -> -> "不丢帧"
* 3) g_lup_parser: UART2(Loop) ,
* ()
*
* ( Loop/BLE ):
* [7F][Addr][LEN][CMD][Value: LEN-1B][XOR][SUM] = LEN + 5
* Addr..Value ( magic), lup_verify_checksum()
*
* :
* (DEBUG != 0): printf USART1/PB6, ->
* 4G (DEBUG == 0): -DDEBUG=0 (PB6/PB7 )
* -DUART1_AIR780_EN=1/0
*===========================================================================*/
#ifndef UART1_AIR780_EN
#define UART1_AIR780_EN (DEBUG == 0)
#endif
#define UART1_MAGIC_LOOP LUP_MAGIC /* 0x7F: 转发给 vd960Loop */
#define UART1_MAGIC_LOCAL MAGIC_BYTE_DBN_DEFAULT /* 0x8F: DBN 私有指令(自身配置读写), 不转发 */
#define UART1_DMA_BUF_LEN 512
#if (UART1_AIR780_EN)
static uint8_t uart1_dma_buf[UART1_DMA_BUF_LEN] __attribute__((aligned(4)));
static uint16_t uart1_dma_last = 0; /* 主循环消费位置 (DMA 写指针由硬件维护) */
/* 下行帧装配器 (独立实例, 不与 g_lup_parser 共享) */
static uint8_t uart1_rx_frame[LUP_MAX_PKG_LEN];
static uint16_t uart1_rx_idx = 0; /* 已收字节数 */
static uint16_t uart1_rx_need = 0; /* 本帧总长, 0 = 尚未定长 */
/* 通道计数: 现场核对"上行转发数 == Air780 实收数", 比翻日志可靠 */
uint32_t g_uart1_fwd_to_air = 0; /* 上行: UART2 -> UART1 转发帧数 */
uint32_t g_uart1_fwd_to_loop = 0; /* 下行: UART1 -> UART2 转发帧数 */
uint32_t g_uart1_local_cnt = 0; /* 下行: DBN 私有指令帧数 (0x8F) */
uint32_t g_uart1_badchk = 0; /* 下行: 校验失败帧数 */
uint32_t g_uart1_drop = 0; /* 下行: DMA 溢出丢弃次数 */
/* ---- 下行分流 ---- */
static void uart1_dispatch_frame(uint8_t *pkg, uint16_t len)
{
if (pkg[0] == UART1_MAGIC_LOOP) {
/* 0x7F -> 原样转发给 vd960Loop */
g_uart1_fwd_to_loop++;
UART2_SendString(pkg, len);
} else {
/* 0x8F -> DBN(CH32V208GBU6) 私有指令: 只作用在自身读写配置上, 不转发给 Loop/Air780。
* BLE manage_dbn_ble_default()
* (pkg[1]=Addr, pkg[2]=LEN, pkg[3]=CMD, pkg[4..]=Value),
* OTA : ( flash), OTA */
g_uart1_local_cnt++;
if (g_flag_counter_ota.flag == 0) {
manage_dbn_ble_default(pkg, (uint8_t)len);
}
}
}
/* ---- 坏帧回找魔数: 把 [1..len) 里第一个魔数起的残余搬到头部继续装配 ----
* "单帧损坏 -> 后续帧全部失步" */
static void uart1_resync(const uint8_t *buf, uint16_t len)
{
uint16_t i;
for (i = 1; i < len; i++) {
if (buf[i] == UART1_MAGIC_LOOP || buf[i] == UART1_MAGIC_LOCAL) {
uint16_t k, n = 0;
for (k = i; k < len; k++) {
uart1_rx_frame[n++] = buf[k];
}
uart1_rx_idx = n;
uart1_rx_need = 0;
if (n >= 3) {
uint8_t lf = uart1_rx_frame[2];
uint16_t need = (uint16_t)lf + 5;
if (lf >= 1 && need <= LUP_MAX_PKG_LEN) {
uart1_rx_need = need;
} else {
uart1_rx_idx = 0; /* LEN 非法: 残余作废 */
}
}
return;
}
}
/* 残余中没有魔数 -> 全部丢弃 */
}
/* ---- 定长收满 -> 校验 -> 分流 ---- */
static void uart1_frame_ready(void)
{
uint16_t len = uart1_rx_idx;
uart1_rx_idx = 0;
uart1_rx_need = 0;
if (lup_verify_checksum(uart1_rx_frame, len) == 0) {
uart1_dispatch_frame(uart1_rx_frame, len);
} else {
g_uart1_badchk++;
uart1_resync(uart1_rx_frame, len);
}
}
/* ---- UART1 字节流装配 ---- */
static void uart1_feed_byte(uint8_t b)
{
uint8_t guard = 0;
if (uart1_rx_idx == 0) {
if (b == UART1_MAGIC_LOOP || b == UART1_MAGIC_LOCAL) {
uart1_rx_frame[0] = b;
uart1_rx_idx = 1;
}
return; /* 非魔数字节: 帧间空闲, 直接丢 */
}
if (uart1_rx_idx >= LUP_MAX_PKG_LEN) { /* 防御: 正常流程到不了 */
uart1_rx_idx = 0;
uart1_rx_need = 0;
return;
}
uart1_rx_frame[uart1_rx_idx++] = b;
if (uart1_rx_idx == 3) { /* [7F][Addr][LEN] 到手 -> 定长 */
uint8_t lf = uart1_rx_frame[2];
uint16_t need = (uint16_t)lf + 5;
if (lf < 1 || need > LUP_MAX_PKG_LEN) {
uart1_rx_idx = 0; /* LEN 非法 -> 重新找魔数 */
uart1_rx_need = 0;
return;
}
uart1_rx_need = need;
}
/* 校验失败后 resync 可能让残余已够长, 故用 while (guard 防跑飞) */
while (uart1_rx_need != 0 && uart1_rx_idx >= uart1_rx_need && guard++ < 8) {
uart1_frame_ready();
}
}
void uart1_dma_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0};
DMA_InitTypeDef DMA_InitStructure;
/* PB6 = USART1_TX / PB7 = USART1_RX (USART1 重映射) */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; /* Tx */
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; /* Rx */
GPIO_Init(GPIOB, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200; /* 与 Air780 侧 uart1 一致 */
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
/* RX 用 DMA1_Ch5 循环缓冲 (UART2 占用 Ch6/Ch7, 通道不冲突) */
DMA_DeInit(DMA1_Channel5);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DATAR);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart1_dma_buf;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = UART1_DMA_BUF_LEN;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
DMA_Cmd(DMA1_Channel5, ENABLE);
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE); /* DMA 接管, 逐字节中断不再需要 */
uart1_dma_last = 0;
uart1_rx_idx = 0;
uart1_rx_need = 0;
}
/* 主循环每轮调用: 消费 DMA 环形缓冲新字节 -> 装配 -> 分流 */
void uart1_dma_poll(void)
{
uint16_t cur = (uint16_t)(UART1_DMA_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel5));
uint16_t n = (uint16_t)((cur + UART1_DMA_BUF_LEN - uart1_dma_last) & (UART1_DMA_BUF_LEN - 1));
if (n == 0) return;
/* 溢出保护: 未消费超过半缓冲 -> 已被 DMA 覆盖, 复位装配器重同步 */
if (n > (UART1_DMA_BUF_LEN / 2)) {
g_uart1_drop++;
uart1_rx_idx = 0;
uart1_rx_need = 0;
uart1_dma_last = cur;
return;
}
while (n--) {
uint8_t b = uart1_dma_buf[uart1_dma_last];
uart1_dma_last = (uint16_t)((uart1_dma_last + 1) & (UART1_DMA_BUF_LEN - 1));
uart1_feed_byte(b);
}
}
#else /* !UART1_AIR780_EN: 调试版 printf 占用 USART1 -> 空实现, 保证链接一致 */
void uart1_dma_init(void) { }
void uart1_dma_poll(void) { }
uint32_t g_uart1_fwd_to_air = 0;
uint32_t g_uart1_fwd_to_loop = 0;
uint32_t g_uart1_local_cnt = 0;
uint32_t g_uart1_badchk = 0;
uint32_t g_uart1_drop = 0;
#endif /* UART1_AIR780_EN */
void uart_init(void){ void uart_init(void){
GPIO_InitTypeDef GPIO_InitStructure = {0}; GPIO_InitTypeDef GPIO_InitStructure = {0};
USART_InitTypeDef USART_InitStructure = {0}; USART_InitTypeDef USART_InitStructure = {0};
@@ -44,8 +382,7 @@ void uart_init(void){
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART2, &USART_InitStructure); USART_Init(USART2, &USART_InitStructure);
// USART_ITConfig(USART2, USART_IT_IDLE, ENABLE); /* RXNE 中断由 uart2_dma_init 关闭 (DMA 接管, 2026-08-17) */
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
@@ -55,6 +392,9 @@ void uart_init(void){
USART_Cmd(USART2, ENABLE); USART_Cmd(USART2, ENABLE);
uart2_dma_init(); /* DMA 循环接收接管 (2026-08-17) */
uart1_dma_init(); /* 4G 通道 UART1↔Air780 初始化 (协议 §6.1); DEBUG!=0 时为空实现 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
@@ -90,27 +430,9 @@ void USART1_IRQHandler(void)
*/ */
void USART2_IRQHandler(void) void USART2_IRQHandler(void)
{ {
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) /* 2026-08-17: RXNE 中断已关 (DMA 接管), 本 handler 不再触发。
{ + IDLE : ,
uint8_t _dat = USART_ReceiveData(USART2); USART_IT_IDLE ( SR + DR , ) */
// 喂给帧解析器
if (lup_feed_byte(_dat)) {
// 帧接收完成,复制到 g_pkg_uart_2
const uint8_t *frame = lup_frame_data();
uint16_t frame_len = lup_frame_len();
if (frame_len <= BUFF_STACK_SIZE) {
memcpy(g_pkg_uart_2.pkg, frame, frame_len);
g_pkg_uart_2.offset = frame_len;
g_pkg_uart_2.flag = 1;
g_pkg_uart_2.tick = 0;
}
lup_frame_reset();
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
// 正在接收帧中,tick 归零
g_pkg_uart_2.tick = 0;
}
}
} }
@@ -151,6 +473,8 @@ void uart_srv(void)
uint8_t i; uint8_t i;
uint8_t _report_flag = 0; uint8_t _report_flag = 0;
uart2_dma_poll(); /* DMA 环形缓冲 → 帧解析 (2026-08-17) */
// 检查命令超时 // 检查命令超时
lup_cmd_check_timeout(); lup_cmd_check_timeout();
@@ -165,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)
@@ -172,43 +504,65 @@ void uart_srv(void)
if(g_dbn_ble_state_acs_enable.flag != 0){ if(g_dbn_ble_state_acs_enable.flag != 0){
// BLE ACS 已连接 → 改 Magic 为 0x8F 发给 BLE // BLE ACS 已连接 → 改 Magic 为 0x8F 发给 BLE
g_pkg_uart_2.pkg[0] = 0x8F; g_pkg_uart_2.pkg[0] = 0x8F;
_report_flag = 1; // 保留给 BLE g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp); // 保留给 BLE
}
/* 2026-08-13 修复: 0xC0 帧消费后必须清 flag —
, auth tcp_json_push_sensor return
/ (checksum fail ) */
InitPkgUart(&g_pkg_uart_2);
}
else {
if(g_flag_bt_state){
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
InitPkgUart(&g_pkg_uart_2);
}
else {
/* 2026-08-13 修复: 非 0xC0 + 无 BLE 连接也清 flag (原缺失) */
InitPkgUart(&g_pkg_uart_2);
} }
// else: 回调已推送 TCP,直接清理
} }
// 调试打印 // 调试打印 (2026-08-13: LUP Rx 已在 lup_process_frame 缓冲打印,
// 此处重复逐字节打印冗余且高频 — 暂时关闭)
#if 0
for(i = 0; i < g_pkg_uart_2.offset; i++){ for(i = 0; i < g_pkg_uart_2.offset; i++){
PRINT(" %02X", g_pkg_uart_2.pkg[i]); PRINT(" %02X", g_pkg_uart_2.pkg[i]);
} }
PRINT("\n"); PRINT("\n");
#endif
if(_report_flag){ }
// 传感器帧保留在 pkg 中供上层 (tcp_json_srv) 处理 else {
// 不 InitPkgUart — 由 tcp_json_push_sensor 消费后清理 // 非 0x7F 魔法字节 — hex 打印 (原 %s 会把二进制当字符串 → 乱码)
PRINT("Rcv_len:%d,dat:", g_pkg_uart_2.offset);
for(i = 0; i < g_pkg_uart_2.offset; i++){
PRINT(" %02X", g_pkg_uart_2.pkg[i]);
}
PRINT("\n");
} }
} }
else{ else{
// 非 0x7F 魔法字节 /* OTA 模式: Loop bootloader 回的 0x9F ACK 帧 (pre_ok/addr_ok/data ACK) 分发:
PRINT("Rcv_len:%d,dat: %s\n", g_pkg_uart_2.offset, g_pkg_uart_2.pkg); - OTA (g_ota_flash_active, V1.08) ota_srv
- BLE (, V1.02.03) BLE
(0xA7 WITH_BACK) ACK */
if(g_ota_flash_active){
ota_flash_feed_ack(g_pkg_uart_2.pkg, g_pkg_uart_2.offset);
} }
else if(g_flag_bt_state){
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
} }
else{ /* OTA 模式同样清 flag — 残留帧会在 OTA 结束后被误处理 */
// OTA 模式 — 忽略 Loop MCU 数据 InitPkgUart(&g_pkg_uart_2);
} }
if(g_flag_bt_state){ if(g_flag_bt_state){
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
} }
else{ else{
g_dbn_ble_state_acs_enable.flag = 0; g_dbn_ble_state_acs_enable.flag = 0;
} }
// 只有非 _report_flag 时才立即清空
// _report_flag 的帧由 tcp_json_push_sensor 消费后清理
if (!_report_flag) {
InitPkgUart(&g_pkg_uart_2);
}
} }
} }
@@ -180,6 +180,14 @@ SECTIONS
PROVIDE( _ebss = .); PROVIDE( _ebss = .);
} >RAM AT>FLASH } >RAM AT>FLASH
/* 故障诊断现场: 复位不清零 (startup 只清 .bss, 不碰 .noinit) */
.noinit (NOLOAD) :
{
. = ALIGN(4);
*(.noinit*)
. = ALIGN(4);
} >RAM
PROVIDE( _end = _ebss); PROVIDE( _end = _ebss);
PROVIDE( end = . ); PROVIDE( end = . );
+10 -1
View File
@@ -46,7 +46,16 @@ void USART_Printf_Init(uint32_t baudrate);
void SDI_Printf_Enable(void); void SDI_Printf_Enable(void);
#if(DEBUG) #if(DEBUG)
#define PRINT(format, ...) printf(format, ##__VA_ARGS__) /* 2026-08-13 printf 重入修复: BLE 协议栈回调(peripheral.c, BB 中断上下文)
PRINT, printf + / HardFault
/ MSTATUS: ,
: PRINT ( LUP Rx hex) , */
#define PRINT(format, ...) do { \
uint32_t _print_ms = __get_MSTATUS(); \
__disable_irq(); \
printf(format, ##__VA_ARGS__); \
__set_MSTATUS(_print_ms); \
} while(0)
#else #else
#define PRINT(X...) #define PRINT(X...)
#endif #endif
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""从 dbn_ble_srv.c 提取脱机日志 3 case 文本, 供 test_ble_offlog.c 隔离验证。
用法: python3 extract_offlog_cases.py
输出: ./offlog_cases_embedded.c ( tests/ 目录下运行)
原理: 测的是源文件真实代码而非拷贝 提取 `case CMD_DBN_OFFLOG_STAT`
`default:` 之间的文本 ( STAT/QUERY/CLEAR 3 case), 嵌入测试框架编译
"""
import os
SRC = os.path.join(os.path.dirname(__file__),
"../BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c")
OUT = os.path.join(os.path.dirname(__file__), "offlog_cases_embedded.c")
with open(SRC, "rb") as f:
content = f.read()
text = content.decode("utf-8", errors="replace").replace("\r\n", "\n")
start = text.find("case CMD_DBN_OFFLOG_STAT")
end = text.find("default:\n", start)
if start == -1 or end == -1:
raise SystemExit("ERROR: 未找到 offlog 3 case (确认 dbn_ble_srv.c 已修改)")
cases = text[start:end]
for c in ("CMD_DBN_OFFLOG_STAT", "CMD_DBN_OFFLOG_QUERY", "CMD_DBN_OFFLOG_CLEAR"):
if c not in cases:
raise SystemExit(f"ERROR: 提取内容缺少 {c}")
with open(OUT, "w") as f:
f.write(cases)
print(f"OK: {OUT} ({len(cases)} bytes, 3 cases)")
+5
View File
@@ -0,0 +1,5 @@
/* mock CONFIG.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
#ifndef __MOCK_CONFIG_H__
#define __MOCK_CONFIG_H__
#endif
+22
View File
@@ -0,0 +1,22 @@
/* mock cmcng.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
#ifndef __MOCK_CMCNG_H__
#define __MOCK_CMCNG_H__
#include <stdint.h>
#include <stdio.h>
typedef struct {
uint32_t flag;
uint32_t timeout;
uint32_t tick;
} Flag_Counter;
extern Flag_Counter g_flag_counter_key;
extern Flag_Counter g_flag_counter_ota;
uint32_t mstick(void);
void UART2_SendString(uint8_t *buf, uint16_t len);
#define PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
#endif
+245
View File
@@ -0,0 +1,245 @@
/**
* test_ble_offlog.c dbn_ble_srv.c 3 case
*
* dbn_ble_srv.c case ,
* Mock: offlog_* / set_response_buf / PRINT / Buf_DBN_BLE
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \
* -o test_ble_offlog test_ble_offlog.c ( extract_offlog_cases.py)
* : ./test_ble_offlog
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ============ 类型/宏 (来自 dbn_ble_srv.h 编辑后, 避免 include BLE 栈) ============ */
#define MAGIC_BYTE_DBN_DEFAULT 0x8F
#define CMD_DBN_OFFLOG_STAT 0x25
#define CMD_DBN_OFFLOG_QUERY 0x26
#define CMD_DBN_OFFLOG_CLEAR 0x27
#define MAX_BLE_TMP_BUF_LEN 132
#define MAX_BLE_DAT_BUF_LEN 132
#define MAX_BLE_DAT_RESPONSE_LEN (100 - 4) /* 单包数据上限 96B */
typedef struct _BLE_Notify_Buf_ {
uint8_t flag;
uint8_t len;
uint8_t buf[100];
} BLE_Notify_Buf;
typedef struct _BUF_DBN_BLE_ {
uint8_t flag;
uint8_t magic;
uint8_t cmd;
uint8_t dat_len ;
uint8_t dat_offset;
uint8_t pkg_amount;
uint8_t pkg_seq;
uint8_t dat[MAX_BLE_DAT_BUF_LEN];
} Buf_DBN_BLE;
#include "offlog.h"
#define PRINT(...) ((void)0)
/* ============ 全局 (来自 dbn_ble_srv.c) ============ */
static uint8_t tmp_ble_buf[MAX_BLE_TMP_BUF_LEN];
static Buf_DBN_BLE g_buf_ble_response;
/* ============ offlog mock ============ */
static uint32_t mock_count = 0;
static uint32_t mock_seq_last = 0;
static uint32_t mock_boot = 0;
static uint8_t mock_enabled = 1;
uint16_t offlog_count(void) { return (uint16_t)mock_count; }
uint32_t offlog_seq_last(void) { return mock_seq_last; }
uint32_t offlog_boot_seq(void) { return mock_boot; }
uint8_t offlog_enabled(void) { return mock_enabled; }
void offlog_clear(void) { mock_count = 1; mock_seq_last++; } /* 审计留痕: count 归 1, seq 继续 */
int offlog_read_idx(uint16_t idx, OfflogEvt *out)
{
uint32_t seq_first;
if (idx >= mock_count) return -1;
seq_first = mock_seq_last - mock_count + 1;
memset(out, 0, sizeof(OfflogEvt));
out->magic = OFFLOG_EVT_MAGIC;
out->type = (idx % 3 == 0) ? OFFLOG_EVT_BOOT :
(idx % 3 == 1) ? OFFLOG_EVT_IOT_READY : OFFLOG_EVT_COIL;
out->seq = seq_first + idx;
out->boot_seq = 2;
out->ts_ms = 1000 + idx * 100;
out->unix_ts = 1784768575UL;
if (idx % 3 == 2) { /* COIL: sub=1 ch=1 value=97 */
out->payload[0] = 1; out->payload[1] = 1;
out->payload[2] = 0; out->payload[3] = 0; out->payload[4] = 0; out->payload[5] = 97;
}
return 0;
}
/* g_offlog_part: OFFLOG_MAX_RECORDS 宏展开为运行时值, 此处 mock W25Q32 (512KB) */
OfflogPart g_offlog_part = { OFFLOG_CHIP_W25Q32, 0x80000UL, 127, 16256 };
/* ============ set_response_buf mock: 只记录调用参数 ============ */
static uint8_t last_cmd = 0;
static uint8_t last_len = 0;
static uint8_t last_dat[160];
uint8_t set_response_buf(Buf_DBN_BLE *response_dst, uint8_t magic, uint8_t cmd,
uint8_t *dat, uint8_t dat_len)
{
uint8_t i;
memset(last_dat, 0, sizeof(last_dat));
last_cmd = cmd;
last_len = dat_len;
for (i = 0; i < dat_len; i++) last_dat[i] = dat[i];
return 0;
}
/* ============ 被测: 从 dbn_ble_srv.c 提取的 3 case (嵌入 switch) ============ */
static void run_case(uint8_t cmd, uint8_t *pkg, uint8_t len)
{
uint8_t _cmd = cmd;
switch (_cmd) {
#include "offlog_cases_embedded.c"
default: break;
}
}
/* ============ 断言 ============ */
static int failures = 0;
#define CHECK(cond) do { \
if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \
} while (0)
static uint8_t pkg[16];
static void test_stat_ok(void)
{
mock_enabled = 1; mock_count = 1234; mock_seq_last = 1333; mock_boot = 2;
memset(tmp_ble_buf, 0, sizeof(tmp_ble_buf));
run_case(0x25, NULL, 0);
CHECK(last_cmd == 0x25);
CHECK(last_len == 19);
/* status=0 */
CHECK(last_dat[0] == 0x00);
/* boot_seq=2 LE */
CHECK(last_dat[1] == 0x02 && last_dat[2] == 0x00);
/* count=1234 LE */
CHECK(last_dat[3] == 0xD2 && last_dat[4] == 0x04 && last_dat[5] == 0x00 && last_dat[6] == 0x00);
/* capacity=16256 LE (W25Q32 512KB: 127 数据扇区 x 128) */
CHECK(last_dat[7] == 0x80 && last_dat[8] == 0x3F && last_dat[9] == 0x00 && last_dat[10] == 0x00);
/* seq_first=100 LE */
CHECK(last_dat[11] == 0x64 && last_dat[12] == 0x00 && last_dat[13] == 0x00 && last_dat[14] == 0x00);
/* seq_last=1333 LE */
CHECK(last_dat[15] == 0x35 && last_dat[16] == 0x05 && last_dat[17] == 0x00 && last_dat[18] == 0x00);
printf("PASS test_stat_ok\n");
}
static void test_stat_disabled(void)
{
mock_enabled = 0;
run_case(0x25, NULL, 0);
CHECK(last_cmd == 0x25);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x01);
printf("PASS test_stat_disabled\n");
}
static void test_query_ok(void)
{
/* 10 条: seq 11..20, 请求 start_seq=13 count=4 → 返回 seq 13,14,15,16 */
mock_enabled = 1; mock_count = 10; mock_seq_last = 20; mock_boot = 2;
memset(pkg, 0, sizeof(pkg));
pkg[4] = 13; /* start_seq LE */
pkg[5] = 0; pkg[6] = 0; pkg[7] = 0;
pkg[8] = 4; /* count */
memset(tmp_ble_buf, 0, sizeof(tmp_ble_buf));
run_case(0x26, pkg, 11);
CHECK(last_cmd == 0x26);
CHECK(last_len == 2 + 4 * 32); /* status + count + 4×32B */
CHECK(last_dat[0] == 0x00); /* status ok */
CHECK(last_dat[1] == 4); /* fetched 4 */
/* 记录0 (idx=2, seq=13): type=coil (mock: idx%3==2 → COIL), 仅查 magic+seq */
CHECK(last_dat[2] == OFFLOG_EVT_MAGIC);
CHECK(last_dat[6] == 13); /* seq LE32 偏移4 */
CHECK(last_dat[7] == 0);
/* 记录1 (idx=3, seq=14): type=boot */
CHECK(last_dat[2+32+1] == OFFLOG_EVT_BOOT);
CHECK(last_dat[2+32+4] == 14);
/* 记录2 (idx=4, seq=15): type=iot_ready, payload 空 */
CHECK(last_dat[2+64+1] == OFFLOG_EVT_IOT_READY);
CHECK(last_dat[2+64+4] == 15);
/* 记录3 (idx=5, seq=16): type=coil, payload sub=1 ch=1 value=97 */
CHECK(last_dat[2+96+1] == OFFLOG_EVT_COIL);
CHECK(last_dat[2+96+4] == 16);
CHECK(last_dat[2+96+20] == 1); /* payload[0] sub */
CHECK(last_dat[2+96+21] == 1); /* payload[1] ch */
CHECK(last_dat[2+96+25] == 97); /* payload[5] value LSB */
printf("PASS test_query_ok\n");
}
static void test_query_range_error(void)
{
mock_enabled = 1; mock_count = 10; mock_seq_last = 20; mock_boot = 2;
memset(pkg, 0, sizeof(pkg));
pkg[4] = 100; /* 越界 > seq_last */
pkg[8] = 4;
run_case(0x26, pkg, 11);
CHECK(last_cmd == 0x26);
CHECK(last_len == 2);
CHECK(last_dat[0] == 0x00);
CHECK(last_dat[1] == 0); /* 空 records */
printf("PASS test_query_range_error\n");
}
static void test_query_short_frame(void)
{
mock_enabled = 1;
pkg[8] = 4;
run_case(0x26, pkg, 9); /* len < 11 */
CHECK(last_cmd == 0x26);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x02); /* bad request */
printf("PASS test_query_short_frame\n");
}
static void test_query_disabled(void)
{
mock_enabled = 0;
pkg[8] = 4;
run_case(0x26, pkg, 11);
CHECK(last_cmd == 0x26);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x01); /* disabled */
printf("PASS test_query_disabled\n");
}
static void test_clear(void)
{
mock_enabled = 1; mock_count = 5; mock_seq_last = 30;
run_case(0x27, NULL, 0);
CHECK(last_cmd == 0x27);
CHECK(last_len == 1);
CHECK(last_dat[0] == 0x00);
/* offlog_clear mock: count 归 1, seq_last 递增 */
CHECK(mock_count == 1);
CHECK(mock_seq_last == 31);
printf("PASS test_clear\n");
}
int main(void)
{
test_stat_ok();
test_stat_disabled();
test_query_ok();
test_query_range_error();
test_query_short_frame();
test_query_disabled();
test_clear();
if (failures == 0) printf("\nALL PASS\n");
else printf("\n%d FAILURES\n", failures);
return failures ? 1 : 0;
}
+463
View File
@@ -0,0 +1,463 @@
/*
* test_iot_loop_ver.c V1.10 : 0x4A + + MQTT
*
* :
* 1. lup_parse_version: 0x4A (/)
* 2. lup_cache_from_info / iot_loop_ver_str / iot_loop_hw_ver_str:
* 3. iot_verq_poll :
* - pending + RESPONSE_READY code=0 + +
* - pending + TIMEOUT code=5
* - : dirty=1 + IDLE 0x4A ( pending)
* - : !dirty + RESPONSE_READY +
* - : TIMEOUT IDLE
*
* : gcc -o test_iot_loop_ver test_iot_loop_ver.c && ./test_iot_loop_ver
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
/* ---- mock: 最小依赖 ---- */
#define LUP_MAGIC 0x7F
#define LUP_CMD_GET_VERSION 0x4A
#define LUP_MAX_PKG_LEN 70
#define IOT_MQTT_TOPIC_MAX_LEN 64
typedef struct {
uint8_t status;
uint8_t hard_main, hard_sub, hard_ssub;
uint8_t soft_main, soft_sub, soft_ssub;
char version_str[32];
} LUP_VersionInfo;
typedef struct {
uint8_t valid;
uint8_t hard_main, hard_sub, hard_ssub;
uint8_t soft_main, soft_sub, soft_ssub;
} LoopVerCache;
typedef enum {
LUP_STATE_IDLE = 0,
LUP_STATE_WAIT_RESPONSE,
LUP_STATE_RESPONSE_READY,
LUP_STATE_TIMEOUT
} LUP_CmdState;
typedef struct {
uint8_t pending_cmd;
uint32_t send_tick;
uint32_t timeout_ms;
LUP_CmdState state;
uint8_t resp_buf[LUP_MAX_PKG_LEN];
uint16_t resp_len;
} LUP_CmdTracker;
/* ---- mock 全局 ---- */
static LUP_CmdTracker g_lup_cmd;
static LoopVerCache g_lup_ver_cache;
static char g_iot_dev_serial[16] = "A1B2C3D4E5F6";
/* ---- mock 时间 ---- */
static uint32_t g_mock_ms = 100000;
static uint32_t mstick(void) { return g_mock_ms; }
static uint32_t dev_time_now(void) { return 1719000008UL; }
/* ---- mock 发布捕获 ---- */
static char g_pub_topic[64];
static char g_pub_payload[512];
static uint16_t g_pub_len;
static int g_pub_count;
static int g_getver_calls;
static int iot_mqtt_publish(const char *topic, const char *payload,
uint16_t payload_len, uint8_t qos)
{
(void)qos;
g_pub_count++;
snprintf(g_pub_topic, sizeof(g_pub_topic), "%s", topic ? topic : "");
if (payload_len < sizeof(g_pub_payload)) {
memcpy(g_pub_payload, payload, payload_len);
g_pub_payload[payload_len] = '\0';
} else {
memcpy(g_pub_payload, payload, sizeof(g_pub_payload) - 1);
g_pub_payload[sizeof(g_pub_payload) - 1] = '\0';
}
g_pub_len = payload_len;
return 0;
}
/* mock lup_send_get_version: 模拟 lup_cmd_begin 的状态推进 */
static void lup_send_get_version(void)
{
g_getver_calls++;
g_lup_cmd.pending_cmd = LUP_CMD_GET_VERSION;
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE;
g_lup_cmd.resp_len = 0;
memset(g_lup_cmd.resp_buf, 0, sizeof(g_lup_cmd.resp_buf));
}
/* mock lup_cmd_done: 释放命令通道 */
static void lup_cmd_done(void)
{
g_lup_cmd.pending_cmd = 0;
g_lup_cmd.state = LUP_STATE_IDLE;
g_lup_cmd.resp_len = 0;
}
#define PRINT(...) printf(__VA_ARGS__)
/* ---- 被测静态状态 (从 iot_mqtt_srv.c 提取的变量, 单测转为可访问全局) ---- */
static uint8_t _iot_verq_pending;
static uint32_t _iot_verq_msg_id;
static uint32_t _iot_verq_deadline;
static uint8_t _iot_ver_cache_dirty;
/* ================= 嵌入被测函数 (从真实源码提取) ================= */
int lup_parse_version(const uint8_t *pkg, uint16_t len, LUP_VersionInfo *info)
{
if (len < 10) return -1; // 1+3+7+2 = 13 min
// pkg layout: [7F][Addr=0][LEN=8][CMD=0x4A][Status...7bytes][XOR][SUM]
if (pkg[0] != LUP_MAGIC || pkg[3] != LUP_CMD_GET_VERSION) return -2;
if (pkg[2] < 7) return -3; // LEN should be >= 7
const uint8_t *val = pkg + 4; // skip magic + header(3)
info->status = val[0];
info->hard_main = val[1];
info->hard_sub = val[2];
info->hard_ssub = val[3];
info->soft_main = val[4];
info->soft_sub = val[5];
info->soft_ssub = val[6];
return 0;
}
void lup_cache_from_info(const LUP_VersionInfo *info)
{
if (!info) return;
g_lup_ver_cache.valid = 1;
g_lup_ver_cache.hard_main = info->hard_main;
g_lup_ver_cache.hard_sub = info->hard_sub;
g_lup_ver_cache.hard_ssub = info->hard_ssub;
g_lup_ver_cache.soft_main = info->soft_main;
g_lup_ver_cache.soft_sub = info->soft_sub;
g_lup_ver_cache.soft_ssub = info->soft_ssub;
}
static void iot_loop_ver_str(char *out, uint16_t out_len)
{
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
snprintf(out, out_len, "%d.%d.%d",
g_lup_ver_cache.soft_main, g_lup_ver_cache.soft_sub, g_lup_ver_cache.soft_ssub);
}
static void iot_loop_hw_ver_str(char *out, uint16_t out_len)
{
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
snprintf(out, out_len, "%d.%d.%d",
g_lup_ver_cache.hard_main, g_lup_ver_cache.hard_sub, g_lup_ver_cache.hard_ssub);
}
static void iot_verq_publish(const char *payload, uint16_t len)
{
char topic[IOT_MQTT_TOPIC_MAX_LEN];
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
iot_mqtt_publish(topic, payload, len, 1);
}
static void iot_verq_poll(void)
{
if (_iot_verq_pending) {
/* 命令查询在途: 等 0x4A 响应或超时 */
if (g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
LUP_VersionInfo info;
char resp[320];
char ver_str[16], hw_str[16], vs_str[64];
int ok = 0;
memset(&info, 0, sizeof(info));
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
lup_cache_from_info(&info); /* 同步刷新缓存 */
snprintf(ver_str, sizeof(ver_str), "%d.%d.%d",
info.soft_main, info.soft_sub, info.soft_ssub);
snprintf(hw_str, sizeof(hw_str), "%d.%d.%d",
info.hard_main, info.hard_sub, info.hard_ssub);
snprintf(vs_str, sizeof(vs_str), "V%s (HW:%s)", ver_str, hw_str);
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":0,"
"\"msg\":\"success\","
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"%s\"}}",
_iot_verq_msg_id, dev_time_now(), ver_str, hw_str, vs_str);
ok = 1;
} else {
char lv[16], lhv[16];
iot_loop_ver_str(lv, sizeof(lv));
iot_loop_hw_ver_str(lhv, sizeof(lhv));
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
"\"msg\":\"parse error\","
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
_iot_verq_msg_id, dev_time_now(), lv, lhv);
}
lup_cmd_done();
_iot_verq_pending = 0;
iot_verq_publish(resp, strlen(resp));
PRINT("IOT: loop_version_query resp %s\n", ok ? "ok" : "parse error");
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT || mstick() > _iot_verq_deadline) {
char resp[320];
char lv[16], lhv[16];
iot_loop_ver_str(lv, sizeof(lv));
iot_loop_hw_ver_str(lhv, sizeof(lhv));
snprintf(resp, sizeof(resp),
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
"\"msg\":\"no response\","
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
_iot_verq_msg_id, dev_time_now(), lv, lhv);
lup_cmd_done();
_iot_verq_pending = 0;
iot_verq_publish(resp, strlen(resp));
PRINT("IOT: loop_version_query timeout\n");
}
/* 其它 state (WAIT_RESPONSE) 继续等 */
} else {
/* 后台缓存刷新: 上电 / OTA done 后置 dirty, 命令通道空闲时发 0x4A */
if (_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_IDLE) {
lup_send_get_version();
_iot_ver_cache_dirty = 0;
PRINT("IOT: loop version cache refresh query\n");
} else if (!_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
/* 后台查询响应 (无命令 pending) → 更新缓存 + 释放通道 */
LUP_VersionInfo info;
memset(&info, 0, sizeof(info));
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
lup_cache_from_info(&info);
PRINT("IOT: loop version cache refreshed %d.%d.%d (HW:%d.%d.%d)\n",
info.soft_main, info.soft_sub, info.soft_ssub,
info.hard_main, info.hard_sub, info.hard_ssub);
}
lup_cmd_done();
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT) {
lup_cmd_done(); /* 清残留超时态 (后台查询失败或被其它命令覆盖) */
}
}
}
/* ================= 测试工具 ================= */
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, msg) do { \
if (cond) { g_pass++; printf(" PASS: %s\n", msg); } \
else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \
} while (0)
static void reset_state(void)
{
memset(&g_lup_cmd, 0, sizeof(g_lup_cmd));
memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache));
memset(&g_pub_payload, 0, sizeof(g_pub_payload));
g_pub_count = 0;
g_pub_len = 0;
g_getver_calls = 0;
_iot_verq_pending = 0;
_iot_verq_msg_id = 0;
_iot_verq_deadline = 0;
_iot_ver_cache_dirty = 0;
g_mock_ms = 100000;
}
/* 构造 0x4A 响应帧: 7F 00 LEN 4A [status][hard_m][hard_s][hard_ss][soft_m][soft_s][soft_ss] [xor][sum]
(checksum lup_parse_version , 0) */
static void build_version_frame(uint8_t *pkg, uint16_t *len,
uint8_t status, uint8_t hm, uint8_t hs, uint8_t hss,
uint8_t sm, uint8_t ss, uint8_t sss)
{
pkg[0] = LUP_MAGIC;
pkg[1] = 0x00;
pkg[2] = 0x08;
pkg[3] = LUP_CMD_GET_VERSION;
pkg[4] = status;
pkg[5] = hm; pkg[6] = hs; pkg[7] = hss;
pkg[8] = sm; pkg[9] = ss; pkg[10] = sss;
pkg[11] = 0; pkg[12] = 0; /* XOR/SUM 不校验 */
*len = 13;
}
static void test_parse_version(void)
{
printf("[1] lup_parse_version\n");
uint8_t pkg[16];
uint16_t len;
LUP_VersionInfo info;
build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3);
CHECK(lup_parse_version(pkg, len, &info) == 0, "valid frame returns 0");
CHECK(info.status == 0x00, "status=0");
CHECK(info.hard_main == 1 && info.hard_sub == 0 && info.hard_ssub == 0, "hard=1.0.0");
CHECK(info.soft_main == 1 && info.soft_sub == 2 && info.soft_ssub == 3, "soft=1.2.3");
/* 错误路径 */
CHECK(lup_parse_version(pkg, 9, &info) == -1, "len<10 -> -1");
pkg[0] = 0x00;
CHECK(lup_parse_version(pkg, len, &info) == -2, "bad magic -> -2");
pkg[0] = LUP_MAGIC;
pkg[3] = 0x55;
CHECK(lup_parse_version(pkg, len, &info) == -2, "bad cmd -> -2");
pkg[3] = LUP_CMD_GET_VERSION;
pkg[2] = 0x05;
CHECK(lup_parse_version(pkg, len, &info) == -3, "LEN<7 -> -3");
}
static void test_cache(void)
{
printf("[2] 版本缓存\n");
LUP_VersionInfo info;
char out[24];
memset(&info, 0, sizeof(info));
info.hard_main = 1; info.hard_sub = 0; info.hard_ssub = 0;
info.soft_main = 1; info.soft_sub = 2; info.soft_ssub = 3;
lup_cache_from_info(&info);
CHECK(g_lup_ver_cache.valid == 1, "cache valid after write");
CHECK(g_lup_ver_cache.soft_main == 1 && g_lup_ver_cache.soft_ssub == 3, "cache soft fields");
iot_loop_ver_str(out, sizeof(out));
CHECK(strcmp(out, "1.2.3") == 0, "loop_ver str = 1.2.3");
iot_loop_hw_ver_str(out, sizeof(out));
CHECK(strcmp(out, "1.0.0") == 0, "loop_hw_ver str = 1.0.0");
/* 无效缓存 → 空串 */
memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache));
iot_loop_ver_str(out, sizeof(out));
CHECK(strcmp(out, "") == 0, "invalid cache -> empty str");
iot_loop_hw_ver_str(out, sizeof(out));
CHECK(strcmp(out, "") == 0, "invalid hw cache -> empty str");
/* NULL/0 长度防护 */
iot_loop_ver_str(NULL, 0);
CHECK(1, "NULL guard no crash");
}
static void test_verq_success(void)
{
printf("[3] verq_poll: 命令成功回包\n");
reset_state();
uint8_t pkg[16];
uint16_t len;
build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3);
memcpy(g_lup_cmd.resp_buf, pkg, len);
g_lup_cmd.resp_len = len;
g_lup_cmd.state = LUP_STATE_RESPONSE_READY;
_iot_verq_pending = 1;
_iot_verq_msg_id = 407;
_iot_verq_deadline = g_mock_ms + 300;
iot_verq_poll();
CHECK(g_pub_count == 1, "publish once");
CHECK(strstr(g_pub_payload, "\"msg_id\":407") != NULL, "msg_id echoed");
CHECK(strstr(g_pub_payload, "\"code\":0") != NULL, "code=0");
CHECK(strstr(g_pub_payload, "\"loop_ver\":\"1.2.3\"") != NULL, "loop_ver=1.2.3");
CHECK(strstr(g_pub_payload, "\"loop_hw_ver\":\"1.0.0\"") != NULL, "loop_hw_ver=1.0.0");
CHECK(strstr(g_pub_payload, "\"version_str\":\"V1.2.3 (HW:1.0.0)\"") != NULL, "version_str format");
CHECK(strcmp(g_pub_topic, "dld960/A1B2C3D4E5F6/dev") == 0, "resp topic");
CHECK(_iot_verq_pending == 0, "pending cleared");
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd channel released");
CHECK(g_lup_ver_cache.valid == 1, "cache refreshed by command resp");
}
static void test_verq_timeout(void)
{
printf("[4] verq_poll: 命令超时\n");
reset_state();
/* 缓存先有效, 超时回包应携带缓存值 */
g_lup_ver_cache.valid = 1;
g_lup_ver_cache.soft_main = 9; g_lup_ver_cache.soft_sub = 9; g_lup_ver_cache.soft_ssub = 9;
g_lup_ver_cache.hard_main = 8; g_lup_ver_cache.hard_sub = 8; g_lup_ver_cache.hard_ssub = 8;
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE; /* 还挂着 */
_iot_verq_pending = 1;
_iot_verq_msg_id = 408;
_iot_verq_deadline = g_mock_ms + 300;
g_mock_ms += 400; /* 超过 deadline */
iot_verq_poll();
CHECK(g_pub_count == 1, "timeout publish once");
CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "code=5");
CHECK(strstr(g_pub_payload, "\"msg\":\"no response\"") != NULL, "msg=no response");
CHECK(strstr(g_pub_payload, "\"loop_ver\":\"9.9.9\"") != NULL, "cache value kept in resp");
CHECK(_iot_verq_pending == 0, "pending cleared");
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd released after timeout");
/* 直接 TIMEOUT 态 */
reset_state();
g_lup_cmd.state = LUP_STATE_TIMEOUT;
_iot_verq_pending = 1;
_iot_verq_msg_id = 409;
_iot_verq_deadline = g_mock_ms + 300;
iot_verq_poll();
CHECK(g_pub_count == 1, "TIMEOUT state publish once");
CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "TIMEOUT state code=5");
}
static void test_bg_refresh(void)
{
printf("[5] verq_poll: 后台缓存刷新\n");
reset_state();
/* 5a. dirty=1 + IDLE → 发起查询, 不设 pending */
_iot_ver_cache_dirty = 1;
g_lup_cmd.state = LUP_STATE_IDLE;
iot_verq_poll();
CHECK(g_getver_calls == 1, "bg query issued");
CHECK(_iot_ver_cache_dirty == 0, "dirty cleared");
CHECK(_iot_verq_pending == 0, "no command pending set");
CHECK(g_pub_count == 0, "no publish for bg query");
/* 5b. 通道忙 (WAIT_RESPONSE) 时 dirty 不消费 */
{
int base = g_getver_calls;
_iot_ver_cache_dirty = 1;
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE;
iot_verq_poll();
CHECK(g_getver_calls == base, "busy channel -> no query");
CHECK(_iot_ver_cache_dirty == 1, "dirty kept while busy");
}
/* 5c. 后台响应消费: !dirty + RESPONSE_READY → 更新缓存 + 释放, 不回包 */
reset_state();
uint8_t pkg[16];
uint16_t len;
build_version_frame(pkg, &len, 0x00, 2,0,0, 3,4,5);
memcpy(g_lup_cmd.resp_buf, pkg, len);
g_lup_cmd.resp_len = len;
g_lup_cmd.state = LUP_STATE_RESPONSE_READY;
_iot_ver_cache_dirty = 0;
iot_verq_poll();
CHECK(g_pub_count == 0, "bg resp no publish");
CHECK(g_lup_ver_cache.valid == 1, "bg resp cache valid");
CHECK(g_lup_ver_cache.soft_main == 3 && g_lup_ver_cache.soft_ssub == 5, "bg resp soft=3.4.5");
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "bg resp channel released");
/* 5d. 残留超时清理 */
reset_state();
g_lup_cmd.state = LUP_STATE_TIMEOUT;
iot_verq_poll();
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "stale timeout cleaned");
}
int main(void)
{
printf("=== test_iot_loop_ver: V1.10 版本缓存 + 异步回包 ===\n");
reset_state();
test_parse_version();
test_cache();
test_verq_success();
test_verq_timeout();
test_bg_refresh();
printf("\nRESULT: %d passed, %d failed\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+262
View File
@@ -0,0 +1,262 @@
/*
* test_lup_ota_parser.c lup_feed_byte_ota 0x9F
*
* DLD960LoopBootloader (back_9F_pre_ok/addr_ok/data):
* pre_ok : 9F 01 00 02 A5 00 A8 (CHECK = 01+00+02+A5+00 = A8)
* addr_ok: 9F 01 00 02 A6 00 A9
* addr_err:9F 01 00 02 A6 01 AA
* data : 9F <sub_l> <sub_h> 02 A7 <status> <sum>
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include -o test_lup_ota_parser test_lup_ota_parser.c
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ---- mock: 从 loop_uart_proto.h 复制的最小依赖 ---- */
#define LUP_MAGIC 0x7F
#define LUP_MAGIC_OTA 0x9F
#define LUP_MAX_VALUE_LEN 64
#define LUP_MAX_PKG_LEN (1 + 3 + LUP_MAX_VALUE_LEN + 2)
typedef enum {
LUP_FRAME_STATE_IDLE = 0,
LUP_FRAME_STATE_HEADER,
LUP_FRAME_STATE_VALUE,
LUP_FRAME_STATE_CHECK,
LUP_FRAME_STATE_COMPLETE,
LUP_FRAME_STATE_OTA_LEN,
LUP_FRAME_STATE_OTA_CMD,
LUP_FRAME_STATE_OTA_CHECK
} LUP_FrameState;
typedef struct {
LUP_FrameState state;
uint8_t buf[LUP_MAX_PKG_LEN];
uint16_t idx;
uint16_t value_len;
uint16_t value_idx;
} LUP_FrameParser;
static LUP_FrameParser g_lup_parser;
static void lup_frame_reset(void)
{
g_lup_parser.state = LUP_FRAME_STATE_IDLE;
g_lup_parser.idx = 0;
g_lup_parser.value_len = 0;
g_lup_parser.value_idx = 0;
memset(g_lup_parser.buf, 0, sizeof(g_lup_parser.buf));
}
static const uint8_t *lup_frame_data(void) { return g_lup_parser.buf; }
static uint16_t lup_frame_len(void) { return g_lup_parser.idx; }
/* ---- 嵌入被测函数体 (从 loop_uart_proto.c 提取) ---- */
int lup_feed_byte_ota(uint8_t byte)
{
LUP_FrameParser *p = &g_lup_parser;
switch (p->state) {
case LUP_FRAME_STATE_IDLE:
if (byte == LUP_MAGIC_OTA) {
p->buf[0] = byte;
p->idx = 1;
p->state = LUP_FRAME_STATE_HEADER;
}
break;
case LUP_FRAME_STATE_HEADER:
p->buf[p->idx++] = byte;
if (p->idx == 3) {
p->state = LUP_FRAME_STATE_OTA_LEN;
}
break;
case LUP_FRAME_STATE_OTA_LEN:
p->buf[p->idx++] = byte;
{
uint8_t len_field = p->buf[3];
uint16_t data_bytes;
if (len_field < 1) {
data_bytes = 0;
} else {
data_bytes = len_field - 1;
}
if (data_bytes > LUP_MAX_VALUE_LEN) {
lup_frame_reset();
break;
}
p->value_len = data_bytes;
p->value_idx = 0;
}
p->state = LUP_FRAME_STATE_OTA_CMD;
break;
case LUP_FRAME_STATE_OTA_CMD:
p->buf[p->idx++] = byte;
if (p->value_len > 0) {
p->state = LUP_FRAME_STATE_VALUE;
} else {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_VALUE:
p->buf[p->idx++] = byte;
p->value_idx++;
if (p->value_idx >= p->value_len) {
p->state = LUP_FRAME_STATE_OTA_CHECK;
}
break;
case LUP_FRAME_STATE_OTA_CHECK:
p->buf[p->idx++] = byte;
{
uint8_t sum = 0;
uint16_t i;
for (i = 1; i < (uint16_t)(p->idx - 1); i++) {
sum += p->buf[i];
}
if (sum == p->buf[p->idx - 1]) {
p->state = LUP_FRAME_STATE_COMPLETE;
return 1;
}
lup_frame_reset();
}
break;
case LUP_FRAME_STATE_COMPLETE:
lup_frame_reset();
break;
default:
lup_frame_reset();
break;
}
return 0;
}
/* ---- 测试辅助 ---- */
static int fails = 0;
/* 喂完整帧, 断言: 帧完成 且 len 正确 */
static void feed_frame(const uint8_t *frame, int len, const char *name)
{
int i, done = 0;
lup_frame_reset();
for (i = 0; i < len; i++) {
if (lup_feed_byte_ota(frame[i])) {
done = 1;
break;
}
}
if (!done) {
printf("FAIL %s: frame not completed\n", name);
fails++;
return;
}
if ((int)lup_frame_len() != len) {
printf("FAIL %s: len=%d expected %d\n", name, (int)lup_frame_len(), len);
fails++;
return;
}
if (memcmp(lup_frame_data(), frame, len) != 0) {
printf("FAIL %s: content mismatch\n", name);
fails++;
return;
}
printf("PASS %s\n", name);
}
/* 喂错误校验帧, 断言: 解析失败 (reset 回 IDLE) */
static void feed_bad(const uint8_t *frame, int len, const char *name)
{
int i, done = 0;
lup_frame_reset();
for (i = 0; i < len; i++) {
if (lup_feed_byte_ota(frame[i])) {
done = 1;
break;
}
}
if (done) {
printf("FAIL %s: bad checksum accepted\n", name);
fails++;
return;
}
if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
printf("FAIL %s: parser not reset to IDLE (state=%d)\n", name, g_lup_parser.state);
fails++;
return;
}
printf("PASS %s\n", name);
}
int main(void)
{
uint8_t f1[] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8}; /* pre_ok */
uint8_t f2[] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9}; /* addr_ok */
uint8_t f3[] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x01, 0xAA}; /* addr_err */
uint8_t f4[] = {0x9F, 0x05, 0x00, 0x02, 0xA7, 0x00, 0xAE}; /* data ACK sub=5 ok */
uint8_t f5[] = {0x9F, 0x34, 0x12, 0x02, 0xA7, 0x01, 0xF0}; /* data ACK sub=0x1234 err */
uint8_t bad[] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA9}; /* CHECK 错 */
uint8_t v7f[] = {0x7F, 0x00, 0x04, 0x4A, 0x00, 0x4E, 0x4E}; /* 0x7F 帧不应被解析 */
feed_frame(f1, sizeof(f1), "pre_ok (9F 01 00 02 A5 00 A8)");
feed_frame(f2, sizeof(f2), "addr_ok (9F 01 00 02 A6 00 A9)");
feed_frame(f3, sizeof(f3), "addr_err(9F 01 00 02 A6 01 AA)");
feed_frame(f4, sizeof(f4), "data ack sub=5 (9F 05 00 02 A7 00 AE)");
feed_frame(f5, sizeof(f5), "data ack sub=0x1234 err (9F 34 12 02 A7 01 F0)");
feed_bad(bad, sizeof(bad), "bad checksum rejected");
/* 0x7F 帧: IDLE 不认 → 应无帧完成, 状态回 IDLE */
{
int i, done = 0;
lup_frame_reset();
for (i = 0; i < (int)sizeof(v7f); i++) {
if (lup_feed_byte_ota(v7f[i])) { done = 1; break; }
}
if (done) {
printf("FAIL 0x7F frame parsed by ota parser\n");
fails++;
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
printf("FAIL 0x7F frame left parser non-IDLE (state=%d)\n", g_lup_parser.state);
fails++;
} else {
printf("PASS 0x7F frame ignored by ota parser\n");
}
}
/* 粘帧: 两帧连续 → 各自解析成功 */
{
uint8_t glue[14];
int i, done_cnt = 0;
memcpy(glue, f1, 7);
memcpy(glue + 7, f2, 7);
lup_frame_reset();
for (i = 0; i < 14; i++) {
if (lup_feed_byte_ota(glue[i])) {
done_cnt++;
if (done_cnt == 1 && memcmp(lup_frame_data(), f1, 7) != 0) {
printf("FAIL glue frame1 mismatch\n"); fails++;
}
if (done_cnt == 2 && memcmp(lup_frame_data(), f2, 7) != 0) {
printf("FAIL glue frame2 mismatch\n"); fails++;
}
lup_frame_reset();
}
}
if (done_cnt != 2) {
printf("FAIL glue: %d frames completed, expected 2\n", done_cnt);
fails++;
} else {
printf("PASS glued frames parsed independently\n");
}
}
printf("\n%s (%d failures)\n", fails ? "FAILED" : "ALL PASS", fails);
return fails ? 1 : 0;
}
+312
View File
@@ -0,0 +1,312 @@
/**
* test_offlog.c offlog gcc
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \
* -o test_offlog test_offlog.c
* : ./test_offlog
*
* Mock: W25Q32 NOR (=AND, =0xFF), mstick/dev_time_now
* : / / / /
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* ============ mock W25Q32 (4MB, NOR 语义) ============ */
static uint8_t flash[4 * 1024 * 1024];
void SPI_Flash_Erase_Sector(uint32_t sec)
{
memset(flash + sec * 4096, 0xFF, 4096);
}
void SPI_Flash_Read(uint8_t *buf, uint32_t addr, uint16_t size)
{
memcpy(buf, flash + addr, size);
}
void SPI_Flash_Write_NoCheck(uint8_t *buf, uint32_t addr, uint16_t size)
{
uint16_t i;
for (i = 0; i < size; i++) flash[addr + i] &= buf[i]; /* NOR: 只能 1→0 */
}
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* mock W25Q32 */
void SPI_Flash_Write(uint8_t *buf, uint32_t addr, uint16_t size)
{
/* 模拟 storage.c 行为: 目标区域非全 FF 则擦整扇区再写 */
uint16_t i;
int need_erase = 0;
for (i = 0; i < size; i++) {
if (flash[addr + i] != 0xFF) { need_erase = 1; break; }
}
if (need_erase) SPI_Flash_Erase_Sector(addr / 4096);
SPI_Flash_Write_NoCheck(buf, addr, size);
}
/* ============ mock 时间 (已同步: >16亿) ============ */
static uint32_t mock_ms = 0;
uint32_t mstick(void) { return mock_ms; }
uint32_t dev_time_now(void) { return 1784768575UL; }
/* ch32v20x 库类型 (单测环境无 SDK) */
typedef unsigned char u8;
/* ============ 被测模块 (直接包含实现) ============ */
#include "../BLE/OnlyUpdateApp_Peripheral/APP/offlog.c"
/* ============ 断言 ============ */
static int failures = 0;
#define CHECK(cond) do { \
if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \
} while (0)
static void reset_flash(void)
{
memset(flash, 0xFF, sizeof(flash));
mock_ms = 0;
}
/* 测试1: 全新初始化 → BOOT 事件可读回 */
static void test_fresh_init(void)
{
OfflogEvt e;
uint8_t payload[4] = {0x18, 0, 0, 0}; /* 假复位原因 (大端序写入 payload) */
reset_flash();
offlog_init();
CHECK(offlog_boot_seq() == 1);
CHECK(offlog_count() == 0); /* init 不写事件 */
offlog_boot(0x18000000); /* bit27+bit24 之类 */
CHECK(offlog_count() == 1);
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.magic == OFFLOG_EVT_MAGIC);
CHECK(e.type == OFFLOG_EVT_BOOT);
CHECK(e.seq == 1);
CHECK(e.boot_seq == 1);
CHECK(e.flags & OFFLOG_UNIX_VALID_FLAG);
CHECK(e.len == 4);
CHECK(memcmp(e.payload, payload, 4) == 0);
CHECK(offlog_read_idx(1, &e) == -1); /* 越界 */
printf("PASS test_fresh_init\n");
}
/* 测试2: 环形回绕 — 写满 8064+5 条, count 封顶, 序号单调 */
static void test_ring_wrap(void)
{
OfflogEvt e;
uint32_t i, n = OFFLOG_MAX_RECORDS + 5;
reset_flash();
offlog_init();
for (i = 0; i < n; i++) {
offlog_evt(OFFLOG_EVT_IOT_RECONN, NULL, 0);
}
CHECK(offlog_count() == OFFLOG_MAX_RECORDS - OFFLOG_EVT_PER_SECTOR + 5); /* 8064-128+5 */
/* 最新一条是最后写入的, seq 应等于 n (从 1 开始) */
CHECK(offlog_read_idx(offlog_count() - 1, &e) == 0);
CHECK(e.seq == n);
/* 逻辑首: 回绕时整扇区(128条)被擦 + 写回5条 → 最旧 = 129 */
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.seq == n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4);
printf("PASS test_ring_wrap (seq %lu..%lu, count %u)\n",
(unsigned long)(n - OFFLOG_MAX_RECORDS + OFFLOG_EVT_PER_SECTOR - 4),
(unsigned long)n, offlog_count());
}
/* 测试3: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */
static void test_power_loss_recovery(void)
{
OfflogEvt e;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 10; i++) {
offlog_evt(OFFLOG_EVT_COIL, NULL, 0);
}
CHECK(offlog_count() == 10);
/* 模拟掉电重启 (flash 保留, RAM 全失) */
offlog_init();
CHECK(offlog_boot_seq() == 2);
CHECK(offlog_count() == 10); /* 上电恢复出之前 10 条 */
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
CHECK(offlog_count() == 11);
CHECK(offlog_read_idx(10, &e) == 0);
CHECK(e.type == OFFLOG_EVT_IOT_CONNECT);
CHECK(e.seq == 11); /* seq 跨 boot 连续 */
CHECK(e.boot_seq == 2);
printf("PASS test_power_loss_recovery\n");
}
/* 测试4: 扇区切换 — 写满 128 条后头扇区更新, 数据完整 */
static void test_sector_switch(void)
{
OfflogEvt e;
OfflogHead h;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 128; i++) {
offlog_evt(OFFLOG_EVT_BOOT, NULL, 0);
}
/* 第 129 条触发扇区切换 */
offlog_evt(OFFLOG_EVT_TIME_ANCHOR, NULL, 0);
CHECK(offlog_count() == 129);
SPI_Flash_Read((uint8_t *)&h, OFFLOG_AREA_BASE, sizeof(h));
CHECK(h.magic[0] == OFFLOG_HEAD_MAGIC0);
CHECK(h.wr_sector == 2);
/* 头在扇区切换时刷新: wr_off = 新扇区起点 (尚未写入第 129 条) */
CHECK(h.wr_off == OFFLOG_DATA_BASE + 4096);
CHECK(offlog_read_idx(127, &e) == 0);
CHECK(e.type == OFFLOG_EVT_BOOT);
CHECK(e.seq == 128);
CHECK(offlog_read_idx(128, &e) == 0);
CHECK(e.type == OFFLOG_EVT_TIME_ANCHOR);
CHECK(e.seq == 129);
printf("PASS test_sector_switch\n");
}
/* 测试5: 清空 — 擦数据区 + LOG_CLEAR 审计 */
static void test_clear(void)
{
OfflogEvt e;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 5; i++) {
offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0);
}
offlog_clear();
CHECK(offlog_count() == 1); /* 只剩审计 */
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.type == OFFLOG_EVT_LOG_CLEAR);
CHECK(e.seq == 6); /* seq 不重置 */
printf("PASS test_clear\n");
}
/* 测试6: 掉电恢复跨扇区 — 写到扇区 2 中间掉电 */
static void test_power_loss_mid_sector(void)
{
OfflogEvt e;
uint32_t i;
reset_flash();
offlog_init();
for (i = 0; i < 130; i++) { /* 已跨入扇区 2 */
offlog_evt(OFFLOG_EVT_COIL, NULL, 0);
}
CHECK(offlog_count() == 130);
offlog_init(); /* 掉电重启 */
CHECK(offlog_boot_seq() == 2);
CHECK(offlog_count() == 130);
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
CHECK(offlog_count() == 131);
CHECK(offlog_read_idx(130, &e) == 0);
CHECK(e.seq == 131);
printf("PASS test_power_loss_mid_sector\n");
}
/* 测试7: TIME_ANCHOR 严格使用平台下发 unix_ts (不走 dev_time_now) */
static void test_time_anchor_exact(void)
{
OfflogEvt e;
reset_flash();
offlog_init();
offlog_time_anchor(1800000000UL); /* 平台下发值, 与 mock dev_time_now() 不同 */
CHECK(offlog_count() == 1);
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(e.type == OFFLOG_EVT_TIME_ANCHOR);
CHECK(e.unix_ts == 1800000000UL); /* 严格等于传参 */
CHECK(e.flags & OFFLOG_UNIX_VALID_FLAG);
printf("PASS test_time_anchor_exact\n");
}
/* 测试8: 协议导出 — enabled/seq_last/type_str/evt_to_json (log_stat/log_query 数据源) */
static void test_export_json(void)
{
OfflogEvt e;
char buf[256];
reset_flash();
offlog_init();
CHECK(offlog_enabled() == 1);
CHECK(offlog_seq_last() == 0); /* 尚无记录 */
offlog_boot(0x80000000UL); /* IWDG 复位 → rst=2147483648 */
CHECK(offlog_count() == 1);
CHECK(offlog_seq_last() == 1);
CHECK(offlog_read_idx(0, &e) == 0);
CHECK(strcmp(offlog_type_str(e.type), "boot") == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"type\":\"boot\"") != NULL);
CHECK(strstr(buf, "\"data\":{\"rst\":2147483648}") != NULL);
/* coil: payload = sub(1=进) ch(2) value(97, 大端) */
{
uint8_t p[6] = {1, 2, 0, 0, 0, 97};
offlog_evt(OFFLOG_EVT_COIL, p, 6);
}
CHECK(offlog_read_idx(1, &e) == 0);
CHECK(strcmp(offlog_type_str(e.type), "coil") == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"type\":\"coil\"") != NULL);
CHECK(strstr(buf, "\"sub\":\"car_enter\"") != NULL);
CHECK(strstr(buf, "\"ch\":2") != NULL);
CHECK(strstr(buf, "\"value\":97") != NULL);
/* evt_retry: msg_id=5 retry=2 */
{
uint8_t p[5] = {0, 0, 0, 5, 2};
offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5);
}
CHECK(offlog_read_idx(2, &e) == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"msg_id\":5") != NULL);
CHECK(strstr(buf, "\"retry\":2") != NULL);
/* 无 payload 事件 → data:null */
offlog_iot_ready();
CHECK(offlog_read_idx(3, &e) == 0);
offlog_evt_to_json(&e, buf, sizeof(buf));
CHECK(strstr(buf, "\"type\":\"iot_ready\"") != NULL);
CHECK(strstr(buf, "\"data\":null") != NULL);
/* seq_first 公式: seq_last - count + 1 (4 条: seq 1..4) */
CHECK(offlog_seq_last() == 4);
CHECK(offlog_count() == 4);
printf("PASS test_export_json (seq_first=%lu seq_last=%lu)\n",
(unsigned long)(offlog_seq_last() - offlog_count() + 1),
(unsigned long)offlog_seq_last());
}
int main(void)
{
test_fresh_init();
test_ring_wrap();
test_power_loss_recovery();
test_sector_switch();
test_clear();
test_power_loss_mid_sector();
test_time_anchor_exact();
test_export_json();
if (failures) {
printf("\n%d FAILURE(S)\n", failures);
return 1;
}
printf("\nALL PASS\n");
return 0;
}
+547
View File
@@ -0,0 +1,547 @@
/*
* ota_srv.c (gcc, WCH )
*
* : mock CONFIG.h/cmcng.h + mock (NOR flash / UART2 /
* / offlog ) #include ../APP/ota_srv.c (,
* static ) + +
*
* :
* gcc -I tests/ota_mock -I APP/include -I APP -o /tmp/test_ota_srv \
* tests/test_ota_srv.c
* : /tmp/test_ota_srv
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
#include "cmcng.h" /* mock: Flag_Counter / PRINT / mstick / UART2_SendString */
/* ============================================================================
* mock
* ============================================================================*/
Flag_Counter g_flag_counter_key = {0, 0, 0};
Flag_Counter g_flag_counter_ota = {0, 0, 0};
static uint32_t _mock_ms = 0;
uint32_t mstick(void) { return _mock_ms; }
static void mock_advance(uint32_t ms) { _mock_ms += ms; }
/* UART2 发送记录: 保存最近一帧 */
static uint8_t _tx_buf[512];
static uint16_t _tx_len = 0;
static uint16_t _tx_count = 0;
void UART2_SendString(uint8_t *buf, uint16_t len)
{
_tx_len = (len <= sizeof(_tx_buf)) ? len : (uint16_t)sizeof(_tx_buf);
memcpy(_tx_buf, buf, _tx_len);
_tx_count++;
}
static void tx_reset(void) { _tx_len = 0; _tx_count = 0; }
/* ---- mock NOR flash (W25Q32 语义: 写=AND, 擦=0xFF) ---- */
#define MOCK_FLASH_SIZE (4 * 1024 * 1024)
static uint8_t flash[MOCK_FLASH_SIZE];
static void flash_init(void) { memset(flash, 0xFF, sizeof(flash)); }
void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size)
{
assert(ReadAddr + size <= MOCK_FLASH_SIZE);
memcpy(pBuffer, &flash[ReadAddr], size);
}
void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size)
{
uint16_t i;
assert(WriteAddr + size <= MOCK_FLASH_SIZE);
for (i = 0; i < size; i++) flash[WriteAddr + i] &= pBuffer[i];
}
void SPI_Flash_Erase_Sector(uint32_t Dst_Addr)
{
assert(Dst_Addr * 4096UL + 4096UL <= MOCK_FLASH_SIZE);
memset(&flash[Dst_Addr * 4096UL], 0xFF, 4096);
}
/* 与 storage.c 语义一致: 目标区非 0xFF → 擦扇区 → 读-改-写保留同扇区其他数据 */
void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size)
{
uint32_t secpos, secoff, secremain;
uint16_t i;
while (size > 0) {
secpos = WriteAddr / 4096;
secoff = WriteAddr % 4096;
secremain = 4096 - secoff;
if (size <= secremain) secremain = size;
/* 目标区是否全 0xFF */
{
uint8_t need_erase = 0;
for (i = 0; i < secremain; i++) {
if (flash[secpos * 4096 + secoff + i] != 0xFF) { need_erase = 1; break; }
}
if (need_erase) {
uint8_t sector[4096];
memcpy(sector, &flash[secpos * 4096], 4096);
for (i = 0; i < secremain; i++) sector[secoff + i] = pBuffer[i];
memset(&flash[secpos * 4096], 0xFF, 4096);
for (i = 0; i < 4096; i++) flash[secpos * 4096 + i] &= sector[i];
} else {
for (i = 0; i < secremain; i++) flash[WriteAddr + i] &= pBuffer[i];
}
}
pBuffer += secremain;
WriteAddr += (uint32_t)secremain;
size -= (uint16_t)secremain;
}
}
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* W25Q32 */
/* ---- mock offlog (记录最近 OTA 事件) ---- */
static uint32_t _ota_start_cnt = 0, _ota_result_cnt = 0;
static uint32_t _last_ota_result = 0;
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) { (void)type; (void)payload; (void)len; }
void offlog_ota_start(uint8_t slot, uint32_t size) { _ota_start_cnt++; (void)slot; (void)size; }
void offlog_ota_result(uint32_t code) { _ota_result_cnt++; _last_ota_result = code; }
void offlog_boot(uint32_t r) { (void)r; }
/* ---- mock loop_uart_proto ---- */
void lup_frame_reset(void) { }
/* ---- mock iot_mqtt_srv ---- */
static uint8_t _mock_any_car = 0;
static uint32_t _ota_err_report = 0, _ota_err_cnt = 0;
static uint8_t _ota_rep_ok = 0xFF;
static uint32_t _ota_rep_err = 0, _ota_rep_cnt = 0;
uint8_t iot_any_car(void) { return _mock_any_car; }
void iot_evt_report_ota_error(uint32_t err) { _ota_err_report = err; _ota_err_cnt++; }
/* V1.09: ota_report 结果上报 mock (ok=1 done, ok=0 failed) */
void iot_ota_report_result(uint8_t ok, uint32_t err) { _ota_rep_ok = ok; _ota_rep_err = err; _ota_rep_cnt++; }
/* ============================================================================
* ota_srv.c (, static )
* ============================================================================*/
#include "../APP/ota_srv.c"
/* ============================================================================
*
* ============================================================================*/
#define CHK(cond) do { if (!(cond)) { \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; } } while (0)
/* 构造测试镜像 (5120B = 20 片) */
static void make_image(uint8_t *buf, uint32_t size)
{
uint32_t i;
for (i = 0; i < size; i++) buf[i] = (uint8_t)(i * 7 + 3);
}
static int test_crc32(void)
{
/* CRC-32/ISO-HDLC 标准向量 */
CHK(ota_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL);
CHK(ota_crc32(NULL, 0) == 0x00000000UL);
printf("PASS test_crc32\n");
return 0;
}
static int test_hex_decode(void)
{
uint8_t out[4];
CHK(ota_hex_decode("0102aBff", out, 4) == 0);
CHK(out[0] == 0x01 && out[1] == 0x02 && out[2] == 0xAB && out[3] == 0xFF);
CHK(ota_hex_decode("01z2", out, 2) != 0); /* 非法字符 */
CHK(ota_hex_decode("010", out, 2) != 0); /* 长度不足 */
printf("PASS test_hex_decode\n");
return 0;
}
static int test_json_extract_hex(void)
{
char out[520];
/* 无空格 (紧凑格式) */
CHK(ota_json_extract_hex(
"{\"msg_id\":402,\"cmd\":\"ota_data\",\"ts\":1,\"data\":{\"target\":\"loop\",\"offset\":0,\"crc32\":1,\"data\":\"0102aBff\"}}",
out, sizeof(out)) == 8);
CHK(strcmp(out, "0102aBff") == 0);
/* 带空格 (json.dumps 默认) — 2026-08-20 事故场景 */
CHK(ota_json_extract_hex(
"{\"msg_id\": 402, \"cmd\": \"ota_data\", \"ts\": 1, \"data\": {\"target\": \"loop\", \"offset\": 0, \"crc32\": 1, \"data\": \"0102aBff\"}}",
out, sizeof(out)) == 8);
CHK(strcmp(out, "0102aBff") == 0);
/* 512 hex 满片 */
{
char json[700];
char hex[513];
int i, n;
for (i = 0; i < 256; i++) sprintf(hex + i * 2, "%02x", i);
snprintf(json, sizeof(json),
"{\"msg_id\":1,\"cmd\":\"ota_data\",\"data\":{\"data\":\"%s\"}}", hex);
n = ota_json_extract_hex(json, out, sizeof(out));
CHK(n == 512);
CHK(strcmp(out, hex) == 0);
}
/* 找不到 data 字段 */
CHK(ota_json_extract_hex("{\"cmd\":\"ota_status\"}", out, sizeof(out)) == 0);
CHK(out[0] == '\0');
printf("PASS test_json_extract_hex\n");
return 0;
}
static int test_meta_restore(void)
{
flash_init();
tx_reset();
ota_init();
CHK(_meta.state == OTA_STATE_IDLE);
/* 写一份 downloading 元数据 → 重新 init → 恢复续传状态 */
_meta.magic = OTA_MAGIC;
_meta.state = OTA_STATE_DOWNLOADING;
_meta.size = 512;
_meta.received = 256;
ota_meta_write(&_meta);
ota_init();
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 256);
printf("PASS test_meta_restore\n");
return 0;
}
static int test_session_flow(void)
{
uint8_t img[5120];
uint32_t i;
make_image(img, sizeof(img));
flash_init();
tx_reset();
ota_init();
/* begin */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 0);
/* 20 片下发 */
for (i = 0; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
CHK(_meta.received == sizeof(img));
/* end → ready */
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
/* 暂存区与镜像一致 (读回比对) */
{
uint8_t back[5120];
SPI_Flash_Read(back, OTA_SLOT_A_BASE, sizeof(back));
CHK(memcmp(back, img, sizeof(img)) == 0);
}
printf("PASS test_session_flow\n");
return 0;
}
static int test_begin_resume(void)
{
uint8_t img[5120];
uint32_t i;
make_image(img, sizeof(img));
flash_init();
ota_init();
/* 下载 16 片 (4096B, 触发元数据节流 flush) 后"断电重启" */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
for (i = 0; i < 16 * 256; i += 256) {
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, 256), hex) == 0);
}
CHK(_meta.received == 16 * 256); /* 4096B, 已 flush */
/* 重启 */
ota_init();
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 16 * 256); /* 节流点精确续传 */
/* 重新 begin → 续传点 */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
CHK(_meta.received == 16 * 256);
/* 继续下载剩余 → ready */
for (i = 16 * 256; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
printf("PASS test_begin_resume\n");
return 0;
}
static int test_dup_and_gap(void)
{
uint8_t img[1024];
uint8_t chunk[256];
char hex[513];
uint32_t j;
make_image(img, sizeof(img));
flash_init();
ota_init();
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
memcpy(chunk, &img[0], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0);
/* 重复片幂等 */
CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0);
CHK(_meta.received == 256);
/* 乱序缺片 (跳 256 直接发 512) → 拒绝 */
memcpy(chunk, &img[512], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(512, ota_crc32(chunk, 256), hex) == 1);
CHK(_meta.received == 256);
/* 单片 CRC 错 → 拒绝 */
memcpy(chunk, &img[256], 256);
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(256, 0xDEADBEEFUL, hex) == 1);
CHK(_meta.received == 256);
printf("PASS test_dup_and_gap\n");
return 0;
}
/* 模拟 bootloader: 根据收到的 A7 块回 data ACK */
static void flash_step_until_ack(uint32_t *blocks_done, uint32_t total_blocks)
{
/* ota_poll 推进一帧发送 + 喂 ACK, 直到收到末块 */
uint32_t guard = 0;
while (*blocks_done < total_blocks && guard++ < 10000) {
ota_poll(); /* 发 A7 块 */
CHK(_tx_len >= 7 && _tx_buf[0] == 0x9F);
CHK(_tx_buf[4] == 0xA7);
/* 构造 data ACK: 9F SubL SubH 02 A7 00 CHECK */
{
uint8_t ack[7];
uint8_t sum = 0;
ack[0] = 0x9F; ack[1] = _tx_buf[1]; ack[2] = _tx_buf[2];
ack[3] = 0x02; ack[4] = 0xA7; ack[5] = 0x00;
sum = (uint8_t)(ack[1] + ack[2] + ack[3] + ack[4] + ack[5]);
ack[6] = sum;
ota_flash_feed_ack(ack, 7);
}
(*blocks_done)++;
}
CHK(*blocks_done == total_blocks);
}
static int test_flash_flow(void)
{
uint8_t img[5120];
make_image(img, sizeof(img));
flash_init();
tx_reset();
_ota_err_cnt = 0;
_ota_rep_cnt = 0;
_ota_rep_ok = 0xFF;
ota_init();
/* 下载 → ready */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
{
uint32_t i;
for (i = 0; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
}
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
/* flash: 无车 → 启动 */
_mock_any_car = 0;
tx_reset();
CHK(ota_cmd_flash(0, 0) == 0);
CHK(_meta.state == OTA_STATE_FLASHING);
CHK(g_ota_flash_active == 1);
CHK(g_flag_counter_ota.flag == 1);
CHK(_ota_start_cnt >= 1); /* offlog 升级开始已写 */
/* START → 发 A5 启动帧 */
ota_poll();
CHK(_tx_len == 6 && _tx_buf[0] == 0x9F && _tx_buf[4] == 0xA5 && _tx_buf[5] == 0xA7);
/* pre_ok → ADDR */
{
uint8_t pre_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8};
ota_flash_feed_ack(pre_ok, 7);
}
ota_poll();
/* A6 地址帧: 9F 01 00 05 A6 08 00 34 00 CHECK */
CHK(_tx_len == 10 && _tx_buf[4] == 0xA6);
CHK(_tx_buf[5] == 0x08 && _tx_buf[6] == 0x00 && _tx_buf[7] == 0x34 && _tx_buf[8] == 0x00);
/* 校验和: SubL+SubH+LEN+CMD+ADDR = 01+00+05+A6+08+00+34+00 */
CHK(_tx_buf[9] == (uint8_t)(0x01 + 0x00 + 0x05 + 0xA6 + 0x08 + 0x00 + 0x34 + 0x00));
/* addr_ok → DATA 阶段 (总块数 = ceil(5120/248) = 21) */
{
uint8_t addr_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9};
ota_flash_feed_ack(addr_ok, 7);
}
CHK(_fs_sub == 21); /* 首块序号 = 总块数 */
/* 逐块 A7 + ACK 直到完成 */
flash_step_until_ack(&(uint32_t){0}, 21);
/* 完成后 (V1.09): 状态回 idle (镜像保留可重刷), 恢复 0x7F 模式, 结果已记,
ota_report stage=done */
CHK(_meta.state == OTA_STATE_IDLE);
CHK(_meta.last_result == 0);
CHK(g_ota_flash_active == 0);
CHK(g_flag_counter_ota.flag == 0);
CHK(_ota_result_cnt >= 1);
CHK(_last_ota_result == 0);
CHK(_ota_err_cnt == 0); /* 无失败告警 */
CHK(_ota_rep_cnt == 1); /* ota_report done 上报一次 */
CHK(_ota_rep_ok == 1);
CHK(_ota_rep_err == 0);
printf("PASS test_flash_flow (21 blocks)\n");
return 0;
}
static int test_flash_reject_car(void)
{
flash_init();
ota_init();
/* 直接构造 ready 态 */
_meta.state = OTA_STATE_READY;
_meta.size = 512;
_mock_any_car = 1;
CHK(ota_cmd_flash(0, 0) == 3); /* 有车 → 拒绝 */
CHK(g_ota_flash_active == 0);
_mock_any_car = 1;
CHK(ota_cmd_flash(0, 1) == 0); /* force → 跳过 */
CHK(g_ota_flash_active == 1);
printf("PASS test_flash_reject_car\n");
return 0;
}
static int test_flash_ready_timeout(void)
{
flash_init();
tx_reset();
_ota_err_cnt = 0;
_ota_rep_cnt = 0;
_ota_rep_ok = 0xFF;
ota_init();
_meta.state = OTA_STATE_READY;
_meta.size = 512;
_mock_any_car = 0;
CHK(ota_cmd_flash(0, 0) == 0);
ota_poll(); /* 发 A5 (首发) */
/* 不喂 ACK: 首发后 3 次超时重发 + 第 4 个超时周期 → 失败
: 1(START) A5(2) 2(START) A5(3) 3(START) A5(4) 4(fail) */
{
int k;
for (k = 0; k < 7; k++) { mock_advance(1001); ota_poll(); }
}
CHK(_meta.state == OTA_STATE_FLASH_FAILED);
CHK(_meta.last_result == OTA_ERR_READY_TIMEOUT);
CHK(_ota_err_cnt == 1); /* event_report ota_error 告警 */
CHK(_ota_err_report == OTA_ERR_READY_TIMEOUT);
CHK(g_ota_flash_active == 0);
CHK(g_flag_counter_ota.flag == 0);
CHK(_ota_result_cnt >= 1);
CHK(_last_ota_result == OTA_ERR_READY_TIMEOUT);
CHK(_ota_rep_cnt == 1); /* ota_report failed 上报一次 */
CHK(_ota_rep_ok == 0);
CHK(_ota_rep_err == OTA_ERR_READY_TIMEOUT);
printf("PASS test_flash_ready_timeout\n");
return 0;
}
/* V1.09: 刷写完成后 idle+镜像一致 → ota_begin 直接回 ready (免下载重刷) */
static int test_begin_reflash(void)
{
uint8_t img[5120];
make_image(img, sizeof(img));
flash_init();
tx_reset();
ota_init();
/* 下载 → ready */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
{
uint32_t i;
for (i = 0; i < sizeof(img); i += 256) {
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
uint8_t chunk[256];
char hex[513];
uint32_t j;
memcpy(chunk, &img[i], clen);
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
}
}
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
CHK(_meta.state == OTA_STATE_READY);
/* 模拟刷写完成 → idle (镜像保留) */
_meta.state = OTA_STATE_IDLE;
CHK(_meta.size == sizeof(img));
/* 重刷: begin 同镜像 → 直接 ready, offset=size (免下载) */
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
CHK(_meta.state == OTA_STATE_READY);
CHK(_meta.received == sizeof(img));
/* 不同镜像 → 新会话重下 */
CHK(ota_cmd_begin(sizeof(img) + 256, ota_crc32(img, sizeof(img)), "1.2.0", 0) == 0);
CHK(_meta.state == OTA_STATE_DOWNLOADING);
CHK(_meta.received == 0);
printf("PASS test_begin_reflash\n");
return 0;
}
int main(void)
{
int fails = 0;
fails += test_crc32();
fails += test_hex_decode();
fails += test_json_extract_hex();
fails += test_meta_restore();
fails += test_session_flow();
fails += test_begin_resume();
fails += test_dup_and_gap();
fails += test_flash_flow();
fails += test_flash_reject_car();
fails += test_flash_ready_timeout();
fails += test_begin_reflash();
if (fails == 0) {
printf("\nALL PASS\n");
return 0;
}
printf("\n%d TEST(S) FAILED\n", fails);
return 1;
}
+76
View File
@@ -0,0 +1,76 @@
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* mock SnapRec (与 snapshot.h 一致, 64B) */
typedef struct {
uint8_t magic;
uint8_t len;
uint8_t flags;
uint8_t rsvd;
uint32_t seq;
uint32_t ts_ms;
uint16_t boot_seq;
uint16_t rsvd2;
uint8_t coils[48];
} SnapRec;
#define SNAP_COILS_MAX 4
#define SNAP_COIL_BYTES 12
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)r;
for (i = 0; i < (int)sizeof(SnapRec); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
#define CHECK(cond) do { if (!(cond)) { printf("FAIL %d: %s\n", __LINE__, #cond); fails++; } else { passes++; } } while (0)
static int passes = 0, fails = 0;
int main(void)
{
char buf[256];
SnapRec r;
/* 用例1: 与 test_snap_to_json case2 相同数据, 验证 hex 与 JSON 语义一致
ch1: cfg=0xE8(11 1 0 0000: ///0) cond=0xFF(cond15///relay)
freq=0x00C383=50051, variation=-10, misc=128 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 48; r.seq = 999; r.ts_ms = 1234; r.boot_seq = 3;
r.coils[0] = 0xE8; r.coils[1] = 0xFF;
r.coils[2] = 0x83; r.coils[3] = 0xC3; r.coils[4] = 0x00; /* 50051 */
r.coils[5] = 0xF6; r.coils[6] = 0xFF; r.coils[7] = 0xFF; /* -10 */
r.coils[8] = 0x80; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00; /* 128 */
int n = snap_rec_to_hex(&r, buf, sizeof(buf));
printf("hex(%d): %s\n", n, buf);
CHECK(n == 128);
CHECK(strlen(buf) == 128);
/* 头部: magic=0xA6 len=0x30(48) flags=0 rsvd=0 seq=0xE7030000(999 LE) */
CHECK(strncmp(buf, "a6300000e7030000d204", 20) == 0);
/* ch1 12B: e8 ff 83 c3 00 f6 ff ff 80 00 00 00 */
CHECK(strncmp(buf + 32, "e8ff83c300f6ffff80000000", 24) == 0);
/* 全部小写 */
CHECK(strspn(buf, "0123456789abcdef") == 128);
/* 用例2: 空记录 len=0 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6;
n = snap_rec_to_hex(&r, buf, sizeof(buf));
CHECK(n == 128);
CHECK(strncmp(buf, "a60000000000000000000000", 24) == 0);
/* 用例3: 缓冲不足截断安全 (buf_len=10 不越界) */
memset(&r, 0, sizeof(r));
n = snap_rec_to_hex(&r, buf, 10);
CHECK(n == 8);
CHECK(strlen(buf) == 8);
printf("PASS=%d FAIL=%d\n", passes, fails);
return fails ? 1 : 0;
}
+164
View File
@@ -0,0 +1,164 @@
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* mock SnapRec (与 snapshot.h 一致, 64B) */
typedef struct {
uint8_t magic;
uint8_t len;
uint8_t flags;
uint8_t rsvd;
uint32_t seq;
uint32_t ts_ms;
uint16_t boot_seq;
uint16_t rsvd2;
uint8_t coils[48];
} SnapRec;
#define SNAP_COILS_MAX 4
#define SNAP_COIL_BYTES 12
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len)
{
int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES))
? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES);
if (coil_count < 0) coil_count = 0;
int pos = snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[",
(unsigned long)r->seq, (unsigned)r->boot_seq,
(unsigned long)r->ts_ms, (unsigned)coil_count);
int c;
for (c = 0; c < coil_count; c++) {
const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES];
uint8_t cfg = p[0];
uint8_t cond = p[1];
uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16);
int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16));
uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8)
| ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24);
uint8_t freq_level = (cfg >> 6) & 0x03;
uint8_t direction = (cfg >> 5) & 0x01;
uint8_t freq_type = (cfg >> 4) & 0x01;
uint8_t sensitivity = cfg & 0x0F;
uint8_t condition = (cond >> 4) & 0x0F;
uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */
uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */
uint8_t misc_type = cond & 0x03;
const char *fl = "high";
const char *mt = "time";
if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */
if (freq_level == 1) fl = "mid_high";
else if (freq_level == 2) fl = "mid_low";
else if (freq_level == 3) fl = "low";
if (misc_type == 1) mt = "cut_count";
else if (misc_type == 2) mt = "flow_count";
else if (misc_type == 3) mt = "relay_count";
if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ",");
pos += snprintf(buf + pos, buf_len - pos,
"{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u,"
"\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s,"
"\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}",
(unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type,
(unsigned)sensitivity, (unsigned)condition,
loop_state ? "false" : "true", car_state ? "true" : "false",
mt, (unsigned long)freq, (long)variation, (unsigned long)misc);
}
pos += snprintf(buf + pos, buf_len - pos, "]}");
return pos;
}
#define CHECK(cond) do { if (!(cond)) { printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); fails++; } else { passes++; } } while (0)
static int passes = 0, fails = 0;
int main(void)
{
char buf[2048];
SnapRec r;
/* 用例1: 单线圈, 高频(00)/触发/实时频率/灵敏度2, 正常无车, 时间量
freq=0x010F2A=69418, variation=+7, misc=0 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 12; r.seq = 12345; r.ts_ms = 456789; r.boot_seq = 2;
r.coils[0] = 0x12; /* freq_level=00 direction=0 freq_type=1 sens=0010 */
r.coils[1] = 0x00; /* condition=0 loop_state=0 car_state=0 misc_type=00 */
r.coils[2] = 0x2A; r.coils[3] = 0x0F; r.coils[4] = 0x01; /* 69418 */
r.coils[5] = 0x07; r.coils[6] = 0x00; r.coils[7] = 0x00; /* +7 */
r.coils[8] = 0x00; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00;
snap_rec_to_json(&r, buf, sizeof(buf));
printf("case1: %s\n", buf);
CHECK(strstr(buf, "\"seq\":12345") != NULL);
CHECK(strstr(buf, "\"boot_seq\":2") != NULL);
CHECK(strstr(buf, "\"ts_ms\":456789") != NULL);
CHECK(strstr(buf, "\"coil_count\":1") != NULL);
CHECK(strstr(buf, "\"ch\":1") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"high\"") != NULL);
CHECK(strstr(buf, "\"direction\":0") != NULL);
CHECK(strstr(buf, "\"freq_type\":1") != NULL);
CHECK(strstr(buf, "\"sensitivity\":2") != NULL);
CHECK(strstr(buf, "\"condition\":0") != NULL);
CHECK(strstr(buf, "\"loop_ok\":true") != NULL);
CHECK(strstr(buf, "\"has_car\":false") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"time\"") != NULL);
CHECK(strstr(buf, "\"freq\":69418") != NULL);
CHECK(strstr(buf, "\"variation\":7") != NULL);
CHECK(strstr(buf, "\"misc\":0") != NULL);
/* 用例2: 4线圈全量 + 负 variation + 低频 + relay_count + 有车/断开
ch1: (11)/(1)/(0)/0, condition=0xF, loop_state=1(), car_state=1(), misc_type=11(relay)
freq=0x00C383=50051, variation=-10 (F6 FF FF), misc=128 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 48; r.seq = 999; r.ts_ms = 1234; r.boot_seq = 3;
r.coils[0] = 0xE8; /* 11 1 0 0000 */
r.coils[1] = 0xFF; /* 1111 1 1 11 */
r.coils[2] = 0x83; r.coils[3] = 0xC3; r.coils[4] = 0x00; /* 50051 */
r.coils[5] = 0xF6; r.coils[6] = 0xFF; r.coils[7] = 0xFF; /* -10 */
r.coils[8] = 0x80; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00; /* 128 */
/* ch2: 中高(01), 正常无车, cut_count */
r.coils[12] = 0x50; /* 01 0 1 0000 */
r.coils[13] = 0x01; /* 0000 0 0 01 */
r.coils[14] = 0x00; r.coils[15] = 0x02; r.coils[16] = 0x00; /* 512 */
r.coils[17] = 0x01; r.coils[18] = 0x00; r.coils[19] = 0x00; /* +1 */
/* ch3: 中低(10), flow_count */
r.coils[24] = 0xA0; /* 10 1 0 0000 */
r.coils[25] = 0x02; /* 0000 0 0 10 */
r.coils[26] = 0x00; r.coils[27] = 0x01; r.coils[28] = 0x00; /* 256 */
/* ch4: 高频, 空 */
snap_rec_to_json(&r, buf, sizeof(buf));
printf("case2: %s\n", buf);
CHECK(strstr(buf, "\"coil_count\":4") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"low\"") != NULL);
CHECK(strstr(buf, "\"direction\":1") != NULL);
CHECK(strstr(buf, "\"freq_type\":0") != NULL);
CHECK(strstr(buf, "\"sensitivity\":0") != NULL);
CHECK(strstr(buf, "\"condition\":15") != NULL);
CHECK(strstr(buf, "\"loop_ok\":false") != NULL);
CHECK(strstr(buf, "\"has_car\":true") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"relay_count\"") != NULL);
CHECK(strstr(buf, "\"freq\":50051") != NULL);
CHECK(strstr(buf, "\"variation\":-10") != NULL);
CHECK(strstr(buf, "\"misc\":128") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"mid_high\"") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"cut_count\"") != NULL);
CHECK(strstr(buf, "\"freq_level\":\"mid_low\"") != NULL);
CHECK(strstr(buf, "\"misc_type\":\"flow_count\"") != NULL);
/* 用例3: len=0 边界 (空记录) */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 0;
snap_rec_to_json(&r, buf, sizeof(buf));
printf("case3: %s\n", buf);
CHECK(strstr(buf, "\"coil_count\":0") != NULL);
CHECK(strstr(buf, "\"channels\":[]") != NULL);
printf("\nPASS=%d FAIL=%d\n", passes, fails);
return fails ? 1 : 0;
}
+389
View File
@@ -0,0 +1,389 @@
/**
* test_snapshot.c gcc
*
* : gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \
* -o test_snapshot test_snapshot.c
* : ./test_snapshot
*
* Mock: W25Q32 NOR (=AND, =0xFF), mstick ,
* g_offlog_part ( 512KB), offlog_evt ( mock)
* : include offlog.c .c static ( _wr_off)
* (: snap_clear )
* : / (enqueue ) / flush /
* / / / / /
*/
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include "../BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h"
/* ============ mock W25Q32 (4MB, NOR 语义) ============ */
static uint8_t flash[4 * 1024 * 1024];
void SPI_Flash_Erase_Sector(uint32_t sec)
{
memset(flash + sec * 4096, 0xFF, 4096);
}
void SPI_Flash_Read(uint8_t *buf, uint32_t addr, uint16_t size)
{
memcpy(buf, flash + addr, size);
}
void SPI_Flash_Write_NoCheck(uint8_t *buf, uint32_t addr, uint16_t size)
{
uint16_t i;
for (i = 0; i < size; i++) flash[addr + i] &= buf[i]; /* NOR: 只能 1→0 */
}
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* mock W25Q32 */
void SPI_Flash_Write(uint8_t *buf, uint32_t addr, uint16_t size)
{
/* 模拟 storage.c 行为: 目标区域非全 FF 则擦整扇区再写 */
uint16_t i;
int need_erase = 0;
for (i = 0; i < size; i++) {
if (flash[addr + i] != 0xFF) { need_erase = 1; break; }
}
if (need_erase) SPI_Flash_Erase_Sector(addr / 4096);
SPI_Flash_Write_NoCheck(buf, addr, size);
}
/* ============ mock 时间 ============ */
static uint32_t mock_ms = 0;
uint32_t mstick(void) { return mock_ms; }
/* ============ mock offlog 依赖 (不 include offlog.c, 避免 static 冲突) ============ */
OfflogPart g_offlog_part = {
OFFLOG_CHIP_W25Q32, /* W25Q32 */
0x80000UL, /* 事件区 512KB */
127,
16256,
};
static uint8_t g_audit_type;
static uint8_t g_audit_payload[4];
static uint8_t g_audit_len;
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len)
{
g_audit_type = type;
g_audit_len = len;
if (payload && len > 0 && len <= 4) memcpy(g_audit_payload, payload, len);
}
/* ch32v20x 库类型 (单测环境无 SDK) */
typedef unsigned char u8;
/* ============ 被测模块 (直接包含实现) ============ */
#include "../BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c"
/* ============ 断言 ============ */
static int failures = 0;
#define CHECK(cond) do { \
if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \
} while (0)
static void reset_flash(void)
{
memset(flash, 0xFF, sizeof(flash));
mock_ms = 0;
}
/* 构造一帧传感报告: 4 线圈, 各线圈填可辨识值 */
static void make_report(LUP_SensorReport *sr, uint32_t base_freq, int32_t base_var)
{
uint8_t i;
memset(sr, 0, sizeof(*sr));
sr->sens_type = 0x0C;
sr->coil_count = 4;
for (i = 0; i < 4; i++) {
sr->coils[i].freq_level = 2; /* 中低 66nF */
sr->coils[i].direction = 1;
sr->coils[i].freq_type = 0;
sr->coils[i].sensitivity = 9;
sr->coils[i].condition = 5;
sr->coils[i].loop_state = 1;
sr->coils[i].car_state = (i & 1); /* 0/1 交替 */
sr->coils[i].misc_type = 0;
sr->coils[i].freq = base_freq + i * 1000;
sr->coils[i].variation = base_var + i * 100;
sr->coils[i].misc.passtime_ms = 0x10000000 + i;
}
}
/* 测试1: 全新初始化 → enqueue 不落盘(中断安全), flush 落盘可读回 */
static void test_fresh_init_and_flush(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
snap_init();
CHECK(snap_enabled() == 1);
CHECK(snap_count() == 0);
make_report(&sr, 0x123456, 1000);
mock_ms = 12345;
/* 中断路径: 只 enqueue, 不 flush → flash 不得有任何快照数据 */
snap_enqueue(&sr);
CHECK(snap_count() == 0); /* 未落盘 */
for (i = 0; i < 64; i++) {
CHECK(flash[SNAP_DATA_BASE + i] == 0xFF); /* 数据区仍全 FF */
}
/* 主循环: flush → 落盘 1 条 */
snap_flush();
CHECK(snap_count() == 1);
CHECK(snap_read_idx(0, &r) == 0);
CHECK(r.magic == SNAP_MAGIC);
CHECK(r.len == 48); /* 4 线圈 × 12B */
CHECK(r.seq == 1);
CHECK(r.boot_seq == 1);
CHECK(r.ts_ms == 12345);
CHECK(snap_seq_last() == 1);
printf("PASS test_fresh_init_and_flush\n");
}
/* 测试2: 打包格式与 0xC0 线上线圈单元 12B 一致 */
static void test_pack_format(void)
{
LUP_SensorReport sr;
SnapRec r;
reset_flash();
snap_init();
make_report(&sr, 0x123456, -12345); /* 负变化量 */
/* 覆盖值: freq_level=2 direction=1 freq_type=0 sens=9 → o[0]=0xA9
condition=5 loop=1 car=0 misc=3 o[1]=0x5B
freq=0x123456 56 34 12
variation=-12345 0xFFFFCFC7 24bit C7 CF FF
misc=0x10000000 00 00 00 10 */
sr.coils[0].freq = 0x123456;
sr.coils[0].variation = -12345;
sr.coils[0].misc_type = 3;
sr.coils[0].misc.relay_count = 0x10000000;
snap_enqueue(&sr);
snap_flush();
snap_read_idx(0, &r);
CHECK(r.coils[0] == 0xA9);
CHECK(r.coils[1] == 0x5B);
CHECK(r.coils[2] == 0x56 && r.coils[3] == 0x34 && r.coils[4] == 0x12);
CHECK(r.coils[5] == 0xC7 && r.coils[6] == 0xCF && r.coils[7] == 0xFF);
CHECK(r.coils[8] == 0x00 && r.coils[9] == 0x00 && r.coils[10] == 0x00 && r.coils[11] == 0x10);
/* 线圈 2: car_state=1 → o[1] bit2=1 → 0x5F (其他字段相同) */
CHECK((r.coils[12 + 1] & 0x04) == 0x04);
CHECK((r.coils[0 + 1] & 0x04) == 0x00);
printf("PASS test_pack_format\n");
}
/* 测试3: 环形回绕 — 写满 max+5 条, count 封顶, 序号单调 */
static void test_ring_wrap(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i, n = SNAP_MAX_RECORDS + 5;
reset_flash();
snap_init();
make_report(&sr, 0x100, 100);
for (i = 0; i < n; i++) {
snap_enqueue(&sr);
snap_flush();
}
CHECK(snap_count() == SNAP_MAX_RECORDS - SNAP_REC_PER_SECTOR + 5);
CHECK(snap_read_idx(snap_count() - 1, &r) == 0);
CHECK(r.seq == n);
CHECK(snap_seq_last() == n);
CHECK(snap_read_idx(snap_count(), &r) == -1); /* 越界 */
printf("PASS test_ring_wrap (count %lu, max %lu)\n",
(unsigned long)snap_count(), (unsigned long)SNAP_MAX_RECORDS);
}
/* 测试4: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */
static void test_power_loss_recovery(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
snap_init();
make_report(&sr, 0x200, 200);
for (i = 0; i < 10; i++) {
snap_enqueue(&sr);
snap_flush();
}
CHECK(snap_count() == 10);
snap_init(); /* 模拟掉电重启 */
CHECK(snap_boot_seq() == 2);
CHECK(snap_count() == 10);
snap_enqueue(&sr);
snap_flush();
CHECK(snap_count() == 11);
CHECK(snap_read_idx(10, &r) == 0);
CHECK(r.seq == 11);
CHECK(r.boot_seq == 2);
printf("PASS test_power_loss_recovery\n");
}
/* 测试5: 扇区切换 — 写满 64 条后头扇区更新, 数据完整 */
static void test_sector_switch(void)
{
LUP_SensorReport sr;
SnapRec r;
SnapHead h;
uint32_t i;
reset_flash();
snap_init();
make_report(&sr, 0x300, 300);
for (i = 0; i < 64; i++) {
snap_enqueue(&sr);
snap_flush();
}
snap_enqueue(&sr);
snap_flush(); /* 第 65 条触发扇区切换 */
CHECK(snap_count() == 65);
SPI_Flash_Read((uint8_t *)&h, g_snap_part.area_base, sizeof(h));
CHECK(h.magic[0] == SNAP_HEAD_MAGIC0);
CHECK(h.wr_sector == 2);
CHECK(h.wr_off == SNAP_DATA_BASE + 4096);
CHECK(snap_read_idx(63, &r) == 0);
CHECK(r.seq == 64);
CHECK(snap_read_idx(64, &r) == 0);
CHECK(r.seq == 65);
printf("PASS test_sector_switch\n");
}
/* 测试6: RAM 暂存满丢新 — 8 深, 第 9 条丢, flush 只落 8 条 */
static void test_ram_overflow_drop(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
snap_init();
make_report(&sr, 0x400, 400);
for (i = 0; i < SNAP_RAM_DEPTH + 3; i++) { /* 11 条, 深 8 */
snap_enqueue(&sr); /* 不 flush, 模拟中断风暴 */
}
CHECK(snap_count() == 0); /* 全部还在 RAM */
snap_flush();
CHECK(snap_count() == 8); /* 只落 8 条, 丢 3 条 */
CHECK(snap_read_idx(0, &r) == 0);
CHECK(r.seq == 1); /* 丢的是最新(尾部) */
CHECK(snap_read_idx(7, &r) == 0);
CHECK(r.seq == 8);
printf("PASS test_ram_overflow_drop\n");
}
/* 测试7: 清空 — 擦数据区 + 事件流审计 (payload[0]=2=快照流) */
static void test_clear_audit(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
g_audit_type = 0; g_audit_len = 0; memset(g_audit_payload, 0, sizeof(g_audit_payload));
snap_init();
make_report(&sr, 0x500, 500);
for (i = 0; i < 5; i++) {
snap_enqueue(&sr);
snap_flush();
}
CHECK(snap_count() == 5);
snap_clear();
CHECK(snap_count() == 0);
CHECK(snap_seq_last() == 5); /* seq 不重置 */
CHECK(snap_read_idx(0, &r) == -1); /* 数据区已空 */
/* 审计: offlog_evt 被调用, type=LOG_CLEAR, payload[0]=2 */
CHECK(g_audit_type == OFFLOG_EVT_LOG_CLEAR);
CHECK(g_audit_len == 1);
CHECK(g_audit_payload[0] == 2);
printf("PASS test_clear_audit\n");
}
/* 测试8: 运行时分区 — W25Q32 快照区 0x110000, 3008KB, 48064 条 */
static void test_part_table(void)
{
reset_flash();
snap_init();
CHECK(g_snap_part.area_base == 0x110000UL);
CHECK(g_snap_part.area_size == 0x2F0000UL); /* 3008KB */
CHECK(g_snap_part.data_sectors == 751);
CHECK(g_snap_part.max_records == 751 * 64);
CHECK(SNAP_DATA_BASE == 0x111000UL); /* base + 4KB 头扇区 */
printf("PASS test_part_table\n");
}
/* 测试9: 懒擦 — init 只擦当前写扇区, 其余扇区由环形写切扇区时覆盖
(: init 751 ~34s IWDG 4s ) */
static void test_lazy_erase(void)
{
LUP_SensorReport sr;
SnapRec r;
uint32_t i;
reset_flash();
/* 在数据扇区 2 预埋旧数据 (模拟 Flash 残留) */
flash[SNAP_DATA_BASE + 4096] = 0xAA;
flash[SNAP_DATA_BASE + 4096 + 1] = 0x55;
snap_init();
/* 懒擦: 只擦扇区 1, 扇区 2 保持旧数据 */
CHECK(flash[SNAP_DATA_BASE] == 0xFF); /* 扇区1 已擦 */
CHECK(flash[SNAP_DATA_BASE + 4096] == 0xAA); /* 扇区2 未动 */
CHECK(flash[SNAP_DATA_BASE + 4096 + 1] == 0x55);
make_report(&sr, 0x600, 600);
for (i = 0; i < 64; i++) { /* 写满扇区 1 */
snap_enqueue(&sr);
snap_flush();
}
CHECK(flash[SNAP_DATA_BASE + 4096] == 0xAA); /* 仍在扇区 1 内, 扇区2 未动 */
snap_enqueue(&sr); /* 第 65 条切扇区 */
snap_flush();
CHECK(flash[SNAP_DATA_BASE + 4096] != 0xAA); /* 扇区2 已擦并写入新数据 */
CHECK(snap_read_idx(64, &r) == 0);
CHECK(r.seq == 65);
CHECK(snap_count() == 65);
printf("PASS test_lazy_erase\n");
}
int main(void)
{
test_fresh_init_and_flush();
test_pack_format();
test_ring_wrap();
test_power_loss_recovery();
test_sector_switch();
test_ram_overflow_drop();
test_clear_audit();
test_part_table();
test_lazy_erase();
if (failures) {
printf("\n%d FAILURE(S)\n", failures);
return 1;
}
printf("\nALL PASS\n");
return 0;
}
+245
View File
@@ -0,0 +1,245 @@
# vd960Loop 自动分频(Auto Frequency Selection)技术分析与开发计划
> 分析日期:2026-08-25
> 场景:高速公路,每条车道 4 个线圈(前后串行),相邻车道也有线圈
> 目标:检测到干扰时,四通道各自独立调节 4 档电容频率(33/43/66/76nF)错开干扰源
---
## 0. 现状盘点
| 能力 | 状态 | 说明 |
|------|------|------|
| 硬件切换 | ✅ 已具备 | 每通道 2 个 GPIOFLPA1/2…FLPD1/2),2bit 编码 4 档电容 |
| 基础软件 | ✅ 已具备 | `set_flp_level(loop_num, freq_level)` 已实现(storage.c:117 |
| 配置字段 | ✅ 已具备 | `Loop_Cng_Unit.loopFreq_Level`(2bit),flash 持久化,协议字段 freq_level 已定义 |
| 频率测量 | ✅ 已具备 | CAPVD ∝ 周期,上报实际频率(main.c 转换公式) |
| 干扰检测 | ❌ 缺失 | 无"自动判断有干扰"的逻辑 |
| 切换决策 | ❌ 缺失 | 无"切哪档、何时切"的逻辑 |
| 切换时序 | ❌ 缺失 | 切档会打断检测,无软切换/基线重建流程 |
| 频率规划 | ❌ 缺失 | 无多线圈错频分配策略 |
**结论:硬件和基础软件齐了,缺的是"自动"三件套:检测 → 决策 → 软切换。**
---
## 1. 物理层分析:四档频率与错频裕量
### 1.1 四档频率(L₀≈59μH 时)
| 档位 | 电容 | 频率 | 相对上一档间隔 |
|------|------|------|---------------|
| 00 高频 | 33nF | ~114.1 kHz | — |
| 01 中高 | 43nF | ~100.0 kHz | 14.1 kHz14.1% |
| 10 中低 | 66nF | ~80.7 kHz | 19.3 kHz19.3% |
| 11 低频 | 76nF | ~75.2 kHz | **5.5 kHz7.3%** ⚠ |
**关键发现:四档电容间隔不均匀(+30%/+53%/+15%),66nF 与 76nF 之间频率间隔只有 7.3%**。f ∝ 1/√C,这个比例与线圈 L 无关(恒为 √(76/66)=1.073)。若按行业惯例"相邻线圈频率差 ≥10%"要求,**66/76 这一对不达标**。
### 1.2 但真正的敌人不是"差拍大不大",而是"注入锁定"
错频抑制串扰的物理机制分两层:
1. **异频耦合 → 差拍**:本道 f1、邻道 f2,互感耦合产生 |f1f2| 差拍。本道 10ms tick 采样,奈奎斯特 50Hz
- 差拍 >50Hz(如 5.5kHz):欠采样 → 随机高频抖动 → **IIR(τ≈32ms)天然压住**,无害
- 差拍 <50Hz(近频):慢拍,可穿透 IIR → 有害
- 差拍 →0(同频):注入锁定,最危险
2. **同频/近频 → 注入锁定**:f1≈f2 时振荡器互相牵引,频率被拉偏、相位锁定,CAPVD 出现慢周期扰动,直接污染检测。
**结论:错频目标不是"差拍尽量大",而是"避免同档/近档导致注入锁定"。** 只要相邻线圈不同档(≥1 档间隔,即 ≥7.3% 频率差),差拍欠采样抖动可被滤波抑制。**7.3% 的最小档间隔对避免注入锁定足够,但余量偏小**,规划时优先避免 66/76 相邻组合。
### 1.3 串扰方向性(衔接上一轮分析)
- 错频配置下串扰为差拍抖动(正负交替,无稳定极性)
- 单极性串扰只在同频/近频耦合时出现(错误配置)
- 负向串扰进滑动平均会污染基线(真实危害)
---
## 2. 信号层:干扰检测算法("怎么知道有干扰")
### 2.1 干扰特征(候选)
| 特征 | 检测量 | 灵敏度 | 可靠性 |
|------|--------|--------|--------|
| **差拍抖动** | 无车时窗口内 variation 峰谷差/σ(复用 §10 健康度) | 高(差拍泄漏直接可见) | 高 |
| **注入锁定慢拍** | CAPVD 出现 0.5~50Hz 周期性扰动(相邻样本差分谱) | 中 | 高(最隐蔽也最危险) |
| **基线冻结频率** | 无车时 dev ≥ 4×dlt_ORG 的次数/占比(§10 冻结占比) | 中 | 高 |
| **误触发率** | 无车误触发计数(单位时间) | 低(需要时间累积) | 中(最终后果指标) |
### 2.2 检测窗口约束(关键)
- **干扰检测必须在"本道无车"窗口做**——有车时 CAPVD 大变化是正常信号,不能当干扰
- 但邻道车经过时差拍增强(邻道线圈被车扰动)→ 检测窗口应**包含"本道无车 + 邻道有车"时段**(无需知道邻道状态,本道无车时抖动超阈值即视为干扰)
- 复用 §10 健康度框架:无车窗口峰谷差 > 阈值 × 连续 N 窗 → 判定"干扰存在"
### 2.3 干扰度评分(量化)
```
干扰度 D = α·(抖动能量超标度) + β·(冻结占比) + γ·(误触发率归一化)
D > D_HIGH → 需切换
D > D_MID → 可疑,加强监测
```
阈值需现场标定("待现场验证"),初值建议:无车窗口峰谷差 > 2× 正常噪声底(即 §10 健康度的漂移速率超基线 2 倍)。
---
## 3. 调节时机策略(核心决策点)
### 方案 A:上电自适应(一次性探测)
```
上电 → 各通道稳定期 → 分频探测(每档测 ~5s 噪声底)→ 选最优组合 → 固化 → 正常检测
```
| 维度 | 评价 |
|------|------|
| 实现 | 简单,无运行中切换风险 |
| 探测时长 | 4 通道 × 4 档 × 5s = 80s(交错并行可压到 ~20-30s|
| 缺点 1 | 探测期无检测功能(几十秒)|
| 缺点 2 | **邻道可能未同时上电**,探测结果基于不完整邻居信息 |
| 缺点 3 | 现场后期新增设备(护栏、新线圈、改造)不响应 |
### 方案 B:运行中自适应(动态切换)
```
运行中持续监测干扰度 D → 超阈值 → 软切换(无车时执行)→ 切换后评估(改善则保持,恶化则回滚)
```
| 维度 | 评价 |
|------|------|
| 实现 | 复杂,需软切换 + 回滚 |
| 响应 | 实时适应现场变化 |
| 风险 | 切换期间通道短暂不可用;切换震荡(需防抖:限次、评估、回滚)|
| 硬约束 | **必须有车才能切**(防丢检测);切换后重建基线 |
### 方案 C:混合策略(推荐)
```
上电探测建立初始分配(快速) + 运行中事件触发复测(慢速、保守) + 回滚保护
```
- 上电:扫描选档(几十秒,覆盖"邻道未上电"风险用保守选档:选次优但间隔安全的档)
- 运行中:干扰度 D 超阈值 或 定时(如每小时)复测 → 软切换 + 评估回滚
- 切换频率限制:1 小时内最多 N 次(防震荡)
**切换时机细节(任何方案都需要):**
```
前置条件: 该通道无车(VD_FLAG=0) 且 无脉冲输出中 且 非安全复位中
执行序列: 挂起检测 → GPIO 切档 → 等待振荡稳定(~100ms, 实测标定) → 重建基线(稳定期 128样本≈1.3s) → 恢复检测
安全约束: 切换期间继电器保持原状态; 四通道交错切换(防同时切换电源跌落)
```
> 切换期间该通道约 1.5s 不可用——**这是方案取舍的核心代价,需用户确认可接受**。
---
## 4. 频率规划:4 档怎么分给 8+ 个线圈
### 4.1 冲突定义
场景:每车道 4 线圈(前后)+ 相邻车道 4 线圈。冲突组合:
- **同车道前后相邻**:线圈间距 ~几米,磁耦合强
- **左右相邻车道最近线圈**:跨车道间距,耦合中等
- 斜向/隔一档:耦合弱,可复用
### 4.2 复用策略(蜂窝式频率规划)
```
同车道 4 线圈: 档位 1-2-3-4 顺序错开(每线圈独立档)
相邻车道错相位: 车道 2 用 2-3-4-1 循环 → 左右最近线圈档位差 ≥1
优先避免: 66/76(7.3% 间隔)相邻组合;同档复用距离 ≥2 个线圈位
```
### 4.3 关键矛盾:车检器不知道邻道的档位
无通信前提下可选:
| 方案 | 做法 | 评价 |
|------|------|------|
| **现场配置基准**(推荐起步) | 协议/拨码设置每通道初始档位,各车道按约定错相位 | 简单可靠,依赖施工规范;为后续自动调整提供基准 |
| **盲探测避让** | 扫描各档时测抖动能量,避开"差拍落入危险区"的档 | 不需邻道信息,但只能"避开坏的"不能"选最优" |
| **差拍频率反推** | 从抖动频谱估计 f_beat,反推邻道档位 | 理论可行,工程复杂(欠采样谱估计),暂缓 |
**建议:初始档位现场配置(保证基本错频)+ 自动扫描避让(动态优化)+ 复用规则(防同档相邻)。**
---
## 5. 工程约束与风险清单
| 项 | 约束/风险 | 对策 |
|----|----------|------|
| 检测中断 | 切换 ~1.5s 不可用 | 无车才切;交错切换;切换评估回滚 |
| 安全 | 有车时切档丢检测 → 砸车风险 | **硬约束:VD_FLAG=0 才可切**;继电器保持 |
| 看门狗 | 探测/切换流程不能阻塞主循环 | 全部状态机化,单步 ≤50ms |
| 基线 | 换档后 CAPVD 跳变 | 重建基线 + 稳定期(现有机制复用)|
| 灵敏度 | 换档后阈值变化? | **归一化红利:SensTable 是相对量(×Origin/65536),换档后 Origin 变了但相对阈值不变,灵敏度自动保持** ✓ |
| 持久化 | 新档位掉电丢失 | 写回 flash loopFreq_Level |
| 切换震荡 | 频繁切换 | 限次(1h≤N)、评估回滚、Hysteresis |
| 电源 | 多通道同时切电容冲击 | 交错切换 |
| 协议联动 | 运行中档位变化后台未知 | 新增自动分频状态上报(含当前档位/干扰度/切换事件)|
| 测试 | 无真实串扰环境难验证 | 实验室双线圈对测 + 工装注入模拟干扰 + 现场实测 |
---
## 6. 开发计划
### 阶段 0:硬件标定(前置,依赖实验室/现场)
- [ ] 0.1 实测四档频率、切换 GPIO 到频率稳定的时间(示波器)
- [ ] 0.2 标定振荡稳定等待时间(切档 → CAPVD 有效)
- [ ] 0.3 两线圈对测:同档/相邻档/隔档的差拍形态与注入锁定距离
- [ ] 0.4 标定干扰检测阈值初值(正常噪声底、抖动超标倍率)
- 输出:参数表(写回代码宏)
### 阶段 1:干扰检测模块(先会"看病",不切换)
- [ ] 1.1 新建 `freq_monitor.c/h`:无车窗口抖动能量统计(复用 §10 峰谷差逻辑)
- [ ] 1.2 注入锁定慢拍检测(相邻样本差分谱,0.5~50Hz 能量)
- [ ] 1.3 干扰度评分 D(抖动 + 冻结占比 + 误触发)
- [ ] 1.4 检测状态机:正常/可疑/干扰(带防抖、去程)
- [ ] 1.5 协议上报:干扰状态字段(可选,先本地 PRINT)
- [ ] 1.6 gcc 隔离单测(模拟差拍/锁定/车辆信号,验证不误报)
- 验证:模拟数据注入,检测准确率;实车现场对比人工判断
### 阶段 2:选档决策(freq_planner
- [ ] 2.1 候选档评估:每档噪声底 + 避让规则(差拍危险区)+ 复用约束(同车道不重复)
- [ ] 2.2 决策状态机:保持/切换/回滚(带 Hysteresis + 限次)
- [ ] 2.3 持久化:新档位写 flash
- [ ] 2.4 单测:给定干扰特征序列 → 期望档位序列
### 阶段 3:软切换执行(freq_switch
- [ ] 3.1 切档状态机:挂起检测 → GPIO 切档 → 等待稳定 → 重建基线 → 恢复
- [ ] 3.2 与 vd1_task 集成(切档期间跳过检测、继电器保持)
- [ ] 3.3 四通道交错切换调度
- [ ] 3.4 切换前后 CAPVD 突变防护(切档瞬间不进入 IIR)
- [ ] 3.5 单测:状态机全路径 + 异常分支(切换中有车进入 → 中止/回滚)
### 阶段 4:自适应策略整合(混合方案)
- [ ] 4.1 上电分频探测流程(4 通道交错扫描,~30s)
- [ ] 4.2 运行中事件触发复测(干扰度超阈值)
- [ ] 4.3 定时复测(如 1h,可配置)
- [ ] 4.4 切换评估:切后 5 分钟干扰度对比,恶化回滚
- [ ] 4.5 切换限次 + 防震荡
### 阶段 5:协议/后台联动 + 现场验证
- [ ] 5.1 协议新增:自动分频状态(当前档位/干扰度/切换事件),版本 bump
- [ ] 5.2 vd960DBN 解析同步(若走 0xC0 传感帧或独立命令)
- [ ] 5.3 现场测试方案:双车道对测、邻道干扰注入、长期稳定性(一周)
- [ ] 5.4 devlog/产品文档同步
---
## 7. 需要用户拍板的决策点
1. **切换时机**:方案 A(仅上电)/ B(仅运行中)/ C(混合,推荐)?
2. **初始档位来源**:现场配置基准(拨码/协议)vs 全自动扫描?(建议:配置基准 + 自动避让)
3. **切换代价**:每通道 ~1.5s 检测中断,可接受?(有车不切,只影响无车时段,实际影响很小)
4. **邻道协调**:相邻车道是否也是自家车检器?能否约定统一的错相位配置规范?
5. **范围**:v1 只做"干扰检测 + 手动/半自动提示"(后台建议换档),v2 再做全自动?还是一次到位?
+288
View File
@@ -4,6 +4,294 @@
--- ---
## 2026-09-02 — V1.03:上电自检基准判稳同步 V4B V4.23(版本号 V1.03
> **固件版本:V1.03**cmcng.hFIRMWARE_VER="1.03" + MAIN=1/SUB=3SSUB=1 保持)。devlog 修订。
### 背景(源自 DLD154V4B 现场反馈,V4B V4.23 已实测通过)
绿灯上电自检快闪:原逻辑固定 128 样本(≈1.3 s)即判稳,**与基准值是否真稳定无关**;
`stable_cnt` 只在 `init_vd_single` 清零,**有限存在超时 / 安全复位(LC_Reset=1)重学阶段不清零**
残留计数会让二次稳定期几乎瞬间判稳(绿灯自检形同虚设)。V4B V4.23 修复后本仓四路同步。
### 改动
- `TaskLoop.h`:新增 `STABLE_ORIGIN_PPT 1`(窗口 Origin 均值漂移 ≤ Origin×0.1%/
`STABLE_SETTLE_WINDOWS 2`(连续 2 窗判稳)/ `STABLE_MAX_SAMPLES 500`(≈5 s 硬兜底);
`Loop154_Unit` 新增 `settle_cnt`
- `TaskLoop.c`
- 稳定期:每窗(100 样本≈1 s)完成时比较 Origin 均值漂移,≤0.1% → `settle_cnt++` 否则清零;
判稳 = 最少 128 样本 + 连续 2 窗稳定,或 500 样本硬兜底;判稳退出时 `stable_cnt/settle_cnt` 清零
- `init_vd_single` / 有限存在超时 / 安全复位超时(重学路径)统一清零自检计数 → 二次稳定期从零开始
- 绿灯 `poll_green_led` 条件不变:该路绿灯闪至**本路基准稳定**才停(四路独立)
### 验证
- `tests/test_stable_settle.c`:常量 → 200 样本判稳;首窗尖峰 → 400(扰动后需 2 个干净窗口);
持续漂移 → 500 硬兜底;重学复位后 → 200(防呆:非瞬间判稳)ALL PASS
### 待现场验证
上电绿灯自检时长(干净启动 ≈2 s / 有扰动 3~4 s / 干扰大 5 s 兜底)与四路独立闪烁观感。
---
## 2026-09-02 — V1.02:基线跟踪双向对称保护同步 V4B V4.20(版本号 V1.02
> **固件版本:V1.02**cmcng.hFIRMWARE_VER="1.02" + MAIN=1/SUB=2SSUB=1 保持)。devlog 修订。
### 背景(现场问题,源自 DLD154V4B 同步)
特殊形状铁块(磁导率主导)靠近线圈 → variation 为**负**CAPVD 高于 Origin):
- 原基线跟踪**单边保护**(只挡 `dev > +4×dlt`),负 variation 时基线照常更新 → **Origin 被污染抬高**
- 铁块离开时 CAPVD 回落 → 跨过抬高的进入线 → **假进入** → 进入后 Origin 冻结 → 释放线抬高 → **锁死不释放**
- DLD154V4B V4.20 已修复并现场验证(三次铁块测试 0 锁死),本仓同步
### 改动(TaskLoop.c,四路 unit 结构同步)
```
原逻辑: dev < +4×dlt → 更新基线 ← 单边, 负variation漏网
修复后: |dev| < 4×dlt → 更新基线 ← 对称窗口 [-4×dlt, +4×dlt]
dev < 0 (正variation,疑似车) → 冻结 + FREEZE_TIMEOUT 超时兜底更新(保留)
dev > 0 (负variation,铁块) → 只冻结, 永不超时更新Origin ← 核心
```
- 对称窗口内正常跟踪时补 `loop_freeze_ref = 0`(残留清零,与 V4B 一致)
- 权衡:长时间负温漂基线不跟随(安全方向,正车检测仍正常)
- 逻辑与 DLD154V4B V4.20 `test_neg_variation.c` 6 项回归单测覆盖一致
### 版本
- cmcng.hFIRMWARE_VER "1.01"→"1.02"FIRMWARE_VER_SUB 1→2
- 同步仓:DLD154Pro5f7082c)、DLD110SV4fd6ee79)同期完成
---
## 2026-08-28 — 看门狗升级:双保险喂狗模式 + 超时对齐 V4B(1.64s)
### 背景
与 DLD154V4B(同款 AT32F421)看门狗实现对照后发现三个问题:
| 问题 | 说明 |
|------|------|
| **直喂模式缺 ISR 活性检测** | 原实现主循环每 10ms 无条件 `wdt_feed()`——TMR15 ISR 死掉但主循环活着时,看门狗照样被喂,系统"假活"(时序状态机全停但永不复位) |
| **超时 3s 偏长** | `div=64, rld=1875` → 3s;V4B 已用 1.64s,复位自愈更快 |
| **注释矛盾** | `main.c` 注释写"重载值=4687 超时≈3秒"4687 是 7.5s 的算法),实际 1875→3s |
### 改动(对齐 DLD154V4B V2.10
```c
// TaskLoop.h
#define WDT_COUNTER_LIMIT 100 // 喂狗周期 500ms (TMR15 5ms × 100)
#define WDT_DIV 2 // IWDT 分频枚举值: 2 = WDT_CLK_DIV_16 (实际分频 16)
#define WDT_RLD 4095 // 12bit 重装载 → 超时 ≈ 1.64s
```
| 文件 | 变更 |
|------|------|
| `src/main.c` | `wdt_init()``WDT_CLK_DIV_64/1875``WDT_CLK_DIV_16/4095`(超时 3s→1.64s);`wdt_feed` 定义删除(移至 TaskLoop.c);注释修正 |
| `src/TaskLoop.c` | +`g_wdg_counter` + `poll_wdg()`500ms 才 reload);TMR15 ISR 5ms 递增;调用点 `wdt_feed()``poll_wdg()` |
| `inc/TaskLoop.h` | +WDT 宏 + `poll_wdg` 声明 + `g_wdg_counter` extern(删 `wdt_feed` |
### 验证
- 隔离测试(host gcc mock 3bit 寄存器语义)3 项断言全过:分频 16/重载 4095/超时 1638ms > 500ms 喂狗周期、10s 喂狗 20 次(500ms 精确)、ISR 死不喂狗
- 双保险语义:ISR 死 → 计数不涨 → 永不喂狗;主循环死 → poll_wdg 不执行 → 超时复位
- 待现场验证:LSI 实际频率偏差对复位时间的影响
### ⚠ 踩坑提醒(V4B 同源事故)
`wdt_divider_set()` 参数是**枚举值**不是分频数值(`WDT_CLK_DIV_16=0x02`,寄存器只取低 3 位)——V4B 曾把 `WDT_DIV=16` 当数值传入 → 实际分频 4 → 超时 410ms < 500ms 喂狗周期 → **不断复位重启**。本次 vd960Loop 直接沿用枚举写法,避免同坑。
---
## 2026-08-27 — LOOP_MEASURE_BASE 归一化基数上报(协议 V1.09)
### 背景
跨产品 MEASUREMENT_BASE 对齐(2026-08-26 DLD110S ×4 事故:第三方小程序把 ΔL/L 报成 8.85%,真实 2.21%)。variation 是周期域计数值(`variation = Origin CAPVD`,CAPVD ∝ √L),归一化必须除**设备自身**的 MEASUREMENT_BASE。960Loop 是产品线中唯一未上报基数的型号——0x64 读参响应裸发参数,第三方平台无从换算(写死 131072 恰好蒙对,写 524288 就 ÷4 失真)。
### 改动
| 文件 | 内容 |
|------|------|
| `docs/DLD960Loop_串口通信协议.md`vd_960 仓库) | **V1.09**0x64 响应尾部追加 3B LE `LOOP_MEASURE_BASE`(响应 34B→37B)+ 换算公式 + 修订记录 |
| `inc/cmcng.h` | `#define LOOP_MEASURE_BASE 131072L`(注释标明与 TaskLoop.h MEASUREMENT_BASE 同步) |
| `src/main.c` | `CMD_DBN_GET_MCJQ_PARAM` (0x64) handler for 循环后追加 3B LE(小端,响应 2+8×4+3=37B |
### 换算公式(写入协议文档)
```
Δf/f ≈ variation / LOOP_MEASURE_BASE (131072, AT32@120MHz)
ΔL/L ≈ -2 × variation / LOOP_MEASURE_BASE (系数 2 来自 CAPVD ∝ √L 展开)
```
### 设计决策
- 常量放 **0x64 查询响应**(低频),不进 0xC0 高频帧(每线圈 12B 已贴边,加字段触发分包)
- 向后兼容:老客户端只读前 34B 不受影响;新客户端读 37B 拿基数
- ⚠ 联动待办:DBN 侧对外(BLE/TCP/MQTT)暴露基数未落地——平台走网络协议评估时仍需加字段
### 验证
- ✅ `check_c_balance.py` 语法平衡 OK;编码保持 UTF-8+CRLF
- [ ] 板级验证:0x64 响应长度 37B、尾部 3B = `00 00 02`131072 LE
---
## 2026-08-25 — 灵敏度 4 级制修复(协议 V1.08)
### 背景
现场用金属块测感应高度,设置不同灵敏度档位**感应高度无区别**。排查发现三个根因:
| # | 根因 | 位置 |
|---|------|------|
| 1 | **运行中配置命令不同步检测单元**`unpack_pkg_set_mcjq_param()` 只更新 `g_loop_cng_info.loop_cng[].sensitvity` + 写 flash**没有同步 `g_loop_states.loop_unit[].loop_SensLevel`** → 改灵敏度不重启不生效(对比上电路径 `para_store_init()` 有同步) | main.c:576 |
| 2 | **9级制映射 `& 0x03` 折叠**:协议 0~9 级(默认 7),`sensitvity & 0x03` 只取低 2 位 → 7→3、**3→3(从默认 7 改到 3 无效)**、8→0(反向折叠) | storage.c:184 |
| 3 | 测试陷阱:金属块过大/过近信号饱和,4 档阈值都在饱和区 → 感应高度差异不可见(建议小金属块/测临界距离) | 测试方法 |
### 修复(方案 B4 级制)
- 提取映射函数 `sens_level_from_config()`storage.c/h):**4级制 0~3 一一对应**,9 级制后续实现时只改这一处
- `unpack_pkg_set_mcjq_param()``loop_SensLevel` 同步 → 运行中配置立即生效
- `para_store_init()` 上电路径改用同一函数(消除双处映射漂移)
- 协议 V1.08Sensitivity 语义 0~9 级 → **4 级制 0~3**(默认 2
- 新增 `tests/test_sens_mapping.c` gcc 隔离单测(mock flash.h/FreeRTOS.h/task.h + FLP GPIO 宏):验证 0~3 无折叠、兼容旧默认 7→3、SensTable 单调
### 改动
| 文件 | 内容 |
|------|------|
| `src/storage.c` | 新增 `sens_level_from_config()``para_store_init` 改用 |
| `src/main.c` | `unpack_pkg_set_mcjq_param` 补 loop_SensLevel 同步(运行中生效) |
| `inc/storage.h` | sensitvity 注释 4 级制;函数声明 |
| `tests/test_sens_mapping.c` | 新增映射单测(gcc PASS |
| `tests/mock/` | flash.h/FreeRTOS.h/task.h 隔离测试 mock |
| `docs/DLD960Loop_串口通信协议.md` | V1.08:灵敏度 4 级制 |
### 验证
gcc 单测 PASS0→0, 1→1, 2→2, 3→3 无折叠;`7&0x0F → 3` 兼容旧默认。实机待金属块复测(建议小金属块测临界感应高度)。
### 9 级制灵敏度表设计(2026-08-25)
用户给行业典型范围:**0.01% ~ 5.00% ΔL/L**(跨度 546 倍)。9 级等比公比 2.198,方向 0=最低、8=最高(与 4 级一致)。
| SENS | sens_in | ΔL/L 进入 | sens_out | ΔL/L 离开 | out/in |
|------|---------|-----------|----------|-----------|--------|
| 0 | 1638 | 5.00% | 819 | 2.50% | 50% |
| 1 | 745 | 2.27% | 373 | 1.14% | 50% |
| 2 | 339 | 1.03% | 170 | 0.52% | 50% |
| 3 | 154 | 0.47% | 100 | 0.31% | 65% |
| 4 | 70 | 0.21% | 46 | 0.14% | 65% |
| 5 | 32 | 0.098% | 21 | 0.064% | 65% |
| 6 | 15 | 0.046% | 14 | 0.043% | 90% |
| 7 | 7 | 0.021% | 6 | 0.018% | 90% |
| 8 | 3 | 0.01% | 3 | 0.01% | 100% |
设计要点:
- 滞回渐变 50%→65%→90%/100%:高档 out 低于噪声底会不释放(对齐 DLD154Pro 最高档 86% 经验)
- 4 级→9 级迁移(就近):0→3、1→4、2→5、3→7(端点不再保留,需现场复测)
- 工程警示:SENS=8 dlt≈6 counts 贴噪声底(仅低噪声环境);SENS=0 dlt≈3275(只认重型车)
- 溢出检查:Origin×1638 = 214M < 2³² ✓
落地:
| 文件 | 内容 |
|------|------|
| `docs/variation-analysis.md` | §9.3.1 9 级制表(设计稿) |
| `tools/variation_calc.py` | `--sens-table 9` 支持 9 级显示(4 级默认兼容) |
验证:工具 9 级端点 0.0092%/4.9988% ≈ 0.01%/5.00% ✓。代码落地(set_factory_param/sens_level_from_config)待 9 级立项。
### 补充根因(同日):INIT_VDs 硬编码覆盖配置 + flash 坏数据
现场串口日志(`Read_Cng_Store`)显示:配置 4 通道 SENS=3/2/1/0,但检测时 `Car_In` 全部打印 `sens_in:36`(SENS=2 表值)——4 通道检测全用 SENS=2。定位两个根因:
**① `init_vd_single()` 硬编码 `loop_SensLevel = 2`TaskLoop.c:111**
时序:`main → para_store_init()`(同步 loop_SensLevel=配置)→ 任务启动 → `INIT_VDs()``init_vd_single()` **把 loop_SensLevel 重置为 2**,覆盖配置值 → 全通道 SENS=2。
修复:`init_vd_single` 改为从配置读——`sens_level_from_config(g_loop_cng_info.loop_cng[unit->loop_num].sensitvity)`unit->loop_num 由 INIT_VDs 先设置)。
**② flash 灵敏度表 sens[0]={512,400} 是 0x8A 配置值(更正:非坏数据)**
解析 `Read_Cng_Store` flash 数据(0x40 起小端 2B):`{512,400},{108,72},{36,18},{10,9}`。**512/400 是 `CMD_DBN_LOOP_SENS_LIST (0x8A)` 配置的自定义 SENS=0 档阈值**main.c:946:写 → `memcpy(g_loop_sens_list.sens)``storage_dev()` 持久化 → `para_store_init()` 重读),链路完整。值大 = 阈值高 = 更不灵敏(0.78% vs 出厂 0.33% Δf/f),属于用户自定义,**勿恢复出厂(会清掉配置)**。修复 ① 后 loop_3SENS=0)将使用 512/400 配置值,符合预期。
---
## 2026-08-25 — variation ↔ ΔL/ΔL-L 物理对应分析与工具
### 背景
后台需要把上报的 `variation`(CAPVD 计数域)换算成物理电感变化量,用于标定灵敏度、分析车辆信号强度。
### 核心推导
CAPVD ∝ 周期 T = 2π√(LC),即 **CAPVD ∝ √L 而非 L**
```
variation/Origin = 1 √(1 + ΔL/L₀) 精确
ΔL/L₀ ≈ 2 × variation/Origin 一阶(残差 <0.2%
Δf/f ≈ variation/Origin 频域对应
```
- 系数 2 来源于 √(1+x) ≈ 1+x/2 展开
- 符号:车辆进入 ΔL<0 → variation 恒为正,与 V1.05 协议语义一致
- **电容档无关性**Origin 与 CAPVD 均 ∝ √Cvariation/Origin 中 C 消掉 → 33/43/66/76nF 四档共用同一换算表,现场换档不换标定
### 灵敏度档位 → ΔL/L 触发阈值(SensTable {216,108,36,10}
| SENS | Δf/f 进入 (=SensTable/65536) | ΔL/L 进入 ≈ 2× |
|------|------------------------------|-----------------|
| 0 低 | 0.330% | 0.659% |
| 1 中 | 0.165% | 0.330% |
| 2 高 | 0.055% | 0.110% |
| 3 最高 | 0.015% | 0.031% |
交叉验证:DLD154Pro 技术文档 sens_in {108,54,28,14} 标注 ΔL/L = 0.330%/0.165%/0.085%/0.043%,恰为 SensTable/65536 的 2 倍——产品线口径一致。
### 改动
| 文件 | 内容 |
|------|------|
| `docs/variation-analysis.md` | 新增 §9(物理链路/公式/档位表/电容无关性/示例/注意事项) |
| `tools/variation_calc.py` | 新增换算工具:正向(freq+cap+variation+origin→L₀,ΔL,ΔL/L)、反推、3B LE 补码解析(V1.05)、灵敏度对照、交互模式 |
### 验证
工具四模式实测通过,正反算自洽(ΔL/L=0.33% ↔ variation=216 @ Origin=131072)。
### 协议文字勘误(协议 V1.07)
用户指出 §5.6.3 记录的口径差异应直接改协议:原文 `±8388607` 不对称,负向边界少了 1。统一为 **8388608 ~ +8388607**3B 补码标准值域 −2²³ ~ +2²³−1,对齐代码负向饱和边界 0x800000),后台按文档实现时不会对 0x800000 边界犯嘀咕。
| 文件 | 变更 |
|------|------|
| `DLD960Loop_串口通信协议.md` | variation 字段值域文字修正 + 修订记录 V1.07 |
| `variation-analysis.md` | 头部版本引用 → V1.07;§1 值域;§5.1 表格;§5.6.3 口径小注改写 |
代码无改动(`main.c` 饱和边界本就是 8388608 ~ +8388607)。
### 环境健康度算法(§10
用户提出零成本环境健康度方案:无车时 variation 锯齿的峰 = 漂移信号,后台逐窗统计画健康度曲线。模拟验证后修正两处口径并落地:
1. **绝对峰 ≠ 窗口漂移**:Origin 更新到窗口均值(滞后半窗 2.5s),稳态绝对峰 = **1.5×** 窗口漂移 → 改用**峰谷差**(max−min,误差 <1%
2. **600ms 采样系统性低估 ~12%**:峰恒在窗口末(500 tick),末采样点 480 tick 永远错过 → 单边偏差,趋势可用,绝对标定 ×1.14
3. **冻结态失效**:漂移 > 4×dlt_ORG(中档 0.13%/s)→ 锯齿消失,速率口径失效 → 双指标:`漂移速率 = 峰谷差/Origin/5s` + `冻结占比`
4. 冻结判定双判据:跨窗跳变消失(|jump|≈0)+ 峰谷差突变(>4×前 4 窗中位数),最后一窗也可判
| 文件 | 内容 |
|------|------|
| `docs/variation-analysis.md` | 新增 §10(口径修正表/双指标/冻结判定/工具用法) |
| `tools/drift_health.py` | 新增参考实现:CSV 输入 + 逐窗统计 + 冻结检测 + JSON 输出 + 自测 |
自测:前 6 窗慢漂移 0.02%/s 全正常,后 2 窗快漂移正确标冻结,平均速率/冻结占比准确。
---
## 2026-07-14 — variation 上报量 2B→3B 有符号 (协议 V1.05) ## 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;