Compare commits
102
Commits
v1.0.0
..
9d6f5ea412
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d6f5ea412 | ||
|
|
b20fc57954 | ||
|
|
d4ce0ba23a | ||
|
|
881a774c0c | ||
|
|
8e58d5fdd1 | ||
|
|
f1d9ac4e48 | ||
|
|
9209a9f034 | ||
|
|
832d2953b0 | ||
|
|
d67f955fd4 | ||
|
|
839d79b487 | ||
|
|
5a1893cd1c | ||
|
|
5e9b188c6a | ||
|
|
f2141976f0 | ||
|
|
d39ab5ae89 | ||
|
|
f1c9358aad | ||
|
|
e437dce556 | ||
|
|
55192523fe | ||
|
|
eee7d2459c | ||
|
|
845f16402c | ||
|
|
ba4f5e650c | ||
|
|
ae9f5eaf4c | ||
|
|
263dc4e05d | ||
|
|
8c0f135629 | ||
|
|
fed4335947 | ||
|
|
f274d70fbd | ||
|
|
cb63c581be | ||
|
|
89ba602e4f | ||
|
|
421334ec48 | ||
|
|
26f2326c96 | ||
|
|
452ff68bae | ||
|
|
8cf01544f6 | ||
|
|
5803255d89 | ||
|
|
0c717e4818 | ||
|
|
7894e67b36 | ||
|
|
8873b334d2 | ||
|
|
960d1e83a1 | ||
|
|
b493e10454 | ||
|
|
a36cc15cf2 | ||
|
|
e7389b95fb | ||
|
|
4bdf8a7993 | ||
|
|
ee3ade6f00 | ||
|
|
9f5102e9e8 | ||
|
|
1bbaac0046 | ||
|
|
87c8e01936 | ||
|
|
99ef6348d7 | ||
|
|
f766530609 | ||
|
|
8bb576111b | ||
|
|
9053831f27 | ||
|
|
22224416a3 | ||
|
|
06fb009773 | ||
|
|
eadc67584f | ||
|
|
f5b52d5bde | ||
|
|
3d4814bffe | ||
|
|
92887a3d92 | ||
|
|
a8120cc852 | ||
|
|
6c6ddf8b0f | ||
|
|
667b5376ab | ||
|
|
62c6133632 | ||
|
|
d7f9c29f25 | ||
|
|
193455e629 | ||
|
|
e8b7c6f70c | ||
|
|
67734d3b9a | ||
|
|
6582b93938 | ||
|
|
7a08e083d6 | ||
|
|
66f156105c | ||
|
|
60576b2b33 | ||
|
|
ef3316f2b3 | ||
|
|
070db9f383 | ||
|
|
5eef0756f2 | ||
|
|
bcf797285b | ||
|
|
62f7de0ca2 | ||
|
|
d6174b9d3f | ||
|
|
5ebd8247f0 | ||
|
|
1a01316c7f | ||
|
|
08893d5134 | ||
|
|
90755bf465 | ||
|
|
2b1e888a73 | ||
|
|
5155219c92 | ||
|
|
ac3fb512d5 | ||
|
|
a5ac54a2bf | ||
|
|
98d9b99cf6 | ||
|
|
7c0de6c566 | ||
|
|
29d1f6a783 | ||
|
|
bdc2720d96 | ||
|
|
8f1bbf75f8 | ||
|
|
127fed4b07 | ||
|
|
199cb5d23f | ||
|
|
9e830639d3 | ||
|
|
a9c36e4c9d | ||
|
|
67976eca8e | ||
|
|
a577537c38 | ||
|
|
32f0edb875 | ||
|
|
9521a5e667 | ||
|
|
3d017ccca9 | ||
|
|
086dbc6d7c | ||
|
|
e11a80c859 | ||
|
|
95a1d2b5d2 | ||
|
|
70e44e6241 | ||
|
|
ea29433633 | ||
|
|
579eeecb23 | ||
|
|
d61ff830aa | ||
|
|
c0bdcd956d |
@@ -3,6 +3,9 @@
|
||||
*.i
|
||||
*.old
|
||||
|
||||
# 测试生成物 (extract_offlog_cases.py 输出, 嵌入源文件真实代码)
|
||||
offlog_cases_embedded.c
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
@@ -89,3 +92,8 @@ wchreference/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
|
||||
# gcc 单测编译产物 (tests/*.c 保留)
|
||||
test_offlog
|
||||
test_ble_offlog
|
||||
test_snapshot
|
||||
|
||||
+172
-72
@@ -1,72 +1,172 @@
|
||||
# CHANGELOG — vd_960 (DLD960)
|
||||
|
||||
本文件记录 DLD960 整机(双 MCU 配套)的版本发布历史。
|
||||
单侧固件的细粒度演进见 `vd960Loop/docs/devlog.md` 与 `vd960DBN/docs/devlog.md`。
|
||||
|
||||
---
|
||||
|
||||
## V1.0.0 — 2026-07-16(首个正式发布)
|
||||
|
||||
**配套版本矩阵**
|
||||
|
||||
| 组件 | 版本 |
|
||||
|------|------|
|
||||
| vd960Loop 固件 (AT32F421) | 1.0 |
|
||||
| vd960DBN 固件 (CH32V208, DLD960GA) | 1.0 |
|
||||
| DLD960Loop 串口协议(MCU 间 0x7F) | V1.05 |
|
||||
| DLD960 串口通信协议(RS485/TTL) | V1.01 |
|
||||
| DLD960 TCP JSON 协议(:5960) | V1.01 |
|
||||
| DLD960 IoT MQTT 协议 | V1.05 |
|
||||
|
||||
> ⚠ 自协议 V1.05 起,Loop 固件与 DBN 固件必须同版本配套刷写,禁止混跑(variation 字段宽度变更导致帧格式不兼容)。
|
||||
|
||||
### vd960Loop(检测 MCU,AT32F421)
|
||||
|
||||
**检测算法**(自 DLD154V4B 移植并四通道化,针对 M4 优化)
|
||||
- 主循环 tick 50ms → **10ms**,单路 IIR(ALFA_CAP1=79,τ≈32ms,较 8051 时代快 5 倍)
|
||||
- 斜率限幅 MAX_SLOPE_RATE=5%(含低基值 floor=100 防锁死)
|
||||
- 进入确认 ENTRY_CONFIRM=3(连续 3 次低于阈值才判有车,抗尖峰误触发)
|
||||
- 基线冻结超时 10s + ±2% 稳定性检查:温漂/换线圈可自适应,慢速进车不误吸收
|
||||
- 稳定期(上电 128 样本)旁路 IIR/限幅,窗口 100 快速收敛;常态基线窗口 500(5s 更新,噪声抑制 22×)
|
||||
- 每通道可配置 hold_time(有限存在)与 relay_delay(继电器延时);有限存在超时执行**全通道重启**重建基线
|
||||
|
||||
**串口上报(0x7F 协议,UART @192000)**
|
||||
- 0xC0 传感数据主动上报:频率、variation(**3B 有符号**,`Origin − CAPVD`,V1.05)、车状态
|
||||
- 时间量(通过时间/车间距)统一 **50ms 单位**;上电 3 秒抑制主动上报
|
||||
- 继电器输出次数上报;0x4A 版本查询(CAPVD→实际频率 uint64 精确换算)
|
||||
|
||||
### vd960DBN(通信 MCU,CH32V208)
|
||||
|
||||
**TCP JSON 服务(:5960,协议 V1.01)**
|
||||
- 鉴权状态机 + 15 条命令;服务异常自动重启(3 条件 + 3 次限额 + 10min 冷却)
|
||||
- event_report 客户端必答:5s 超时重发同 msg_id/原始 ts ×3
|
||||
|
||||
**IoT MQTT(协议 V1.05)**
|
||||
- 双主题 `dld960/{sn}/srv` + `dld960/{sn}/dev`,消息类型由 `cmd` 区分
|
||||
- 上电 initialize 上线消息(dev_serial/model/hard_ver/soft_ver)
|
||||
- loop_data 三档调度:空闲按配置间隔 / 活动 300ms / **car_state 翻转沿立即上报**
|
||||
- event_report 平台必答 + 16 深环形队列,ACK 后出队;不受 report_config.enable 门控
|
||||
- 设备时钟同步方案B:平台经 report_config 下发 Unix ts,设备无 RTC 也能上报真实时间
|
||||
- 稳定性修复:mqtt_publish 缓冲溢出发垃圾包致 broker RST 风暴(现场 P0)、packet_id=0 断连、PINGREQ 保活、分批发送(MSS=576,Publish≤500B)
|
||||
- loop_data 陈旧快照修复:0xC0 帧双消费路径统一摄取,事件与缓存同源
|
||||
|
||||
**基础设施**
|
||||
- BLE 小程序配置 + OTA;Loop MCU ISP 串口透传升级
|
||||
- WCHNET TCP/IP(listen=N / data=N+1)、simple_json 解析器 6 项 bug 修复
|
||||
|
||||
### 文档与工具
|
||||
- 四份协议文档 + 硬件资源文档齐套(见 README 协议矩阵)
|
||||
- DBNetClient(TCP JSON 测试)、DBNMQTTool(MQTT 双主题测试)
|
||||
- 双 MCU devlog、验收标准、variation 分析报告、现场事故归档
|
||||
|
||||
### 已知约束
|
||||
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)
|
||||
- CH32V208 栈资源紧张:2KB 任务栈内勿连续 PRINT,勿随意扩大全局 BSS
|
||||
|
||||
---
|
||||
|
||||
## 版本号规则
|
||||
|
||||
- 整机发布 tag:`vX.Y.Z`(本仓库 git tag + Gitea Release)
|
||||
- 固件版本:两侧 `cmcng.h` 的 `FIRMWARE_VER`,随整机版本同步
|
||||
- 协议文档版本:各文档内修订记录表独立演进,发布时在配套矩阵中锁定
|
||||
# CHANGELOG — vd_960 (DLD960)
|
||||
|
||||
本文件记录 DLD960 整机(双 MCU 配套)的版本发布历史。
|
||||
单侧固件的细粒度演进见 `vd960Loop/docs/devlog.md` 与 `vd960DBN/docs/devlog.md`。
|
||||
|
||||
---
|
||||
|
||||
## vd960DBN V1.02.04 — 2026-08-19(DBN 通信板单侧更新)
|
||||
|
||||
> 本次为 **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-19(DBN 通信板单侧更新)
|
||||
|
||||
> 本次为 **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 MCU)OTA 升级失效
|
||||
- **根因**:DMA poll 帧解析 `lup_feed_byte()` 只认 0x7F,Loop 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 + 清 flag;OTA 溢出阈值放宽 + 收帧 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 hex,2 条快照 406B < 800B 缓冲)
|
||||
- MQTT 查询命令补齐:`ssc_net_query` / `iot_net_query` / `iot_topic_query`(协议命令表全对齐)
|
||||
|
||||
---
|
||||
|
||||
## vd960DBN V1.02.01 — 2026-08-17(DBN 通信板单侧更新)
|
||||
|
||||
> 本次为 **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 fail(1-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(全工程唯一,无冲突)
|
||||
|
||||
### 验证
|
||||
- ✅ 板级:无异常重启;MQTT(159.75.137.141:1883)、BLE 广播、配置加载正常
|
||||
- [ ] 待回归:UART2 DMA 长时间运行(checksum fail 消失)、TCP JSON 命令交互、快照 BLE 0x28/0x29/0x2A 查询
|
||||
|
||||
---
|
||||
|
||||
## V1.0.0 — 2026-07-16(首个正式发布)
|
||||
|
||||
**配套版本矩阵**
|
||||
|
||||
| 组件 | 版本 |
|
||||
|------|------|
|
||||
| vd960Loop 固件 (AT32F421) | 1.0 |
|
||||
| vd960DBN 固件 (CH32V208, DLD960GA) | 1.0 |
|
||||
| DLD960Loop 串口协议(MCU 间 0x7F) | V1.05 |
|
||||
| DLD960 串口通信协议(TTL) | V1.01 |
|
||||
| DLD960 TCP JSON 协议(:5960) | V1.02 |
|
||||
| DLD960 IoT MQTT 协议 | V1.06 |
|
||||
|
||||
> ⚠ 自协议 V1.05 起,Loop 固件与 DBN 固件必须同版本配套刷写,禁止混跑(variation 字段宽度变更导致帧格式不兼容)。
|
||||
|
||||
### vd960Loop(检测 MCU,AT32F421)
|
||||
|
||||
**检测算法**(自 DLD154V4B 移植并四通道化,针对 M4 优化)
|
||||
- 主循环 tick 50ms → **10ms**,单路 IIR(ALFA_CAP1=79,τ≈32ms,较 8051 时代快 5 倍)
|
||||
- 斜率限幅 MAX_SLOPE_RATE=5%(含低基值 floor=100 防锁死)
|
||||
- 进入确认 ENTRY_CONFIRM=3(连续 3 次低于阈值才判有车,抗尖峰误触发)
|
||||
- 基线冻结超时 10s + ±2% 稳定性检查:温漂/换线圈可自适应,慢速进车不误吸收
|
||||
- 稳定期(上电 128 样本)旁路 IIR/限幅,窗口 100 快速收敛;常态基线窗口 500(5s 更新,噪声抑制 22×)
|
||||
- 每通道可配置 hold_time(有限存在)与 relay_delay(继电器延时);有限存在超时执行**全通道重启**重建基线
|
||||
|
||||
**串口上报(0x7F 协议,UART @192000)**
|
||||
- 0xC0 传感数据主动上报:频率、variation(**3B 有符号**,`Origin − CAPVD`,V1.05)、车状态
|
||||
- 时间量(通过时间/车间距)统一 **50ms 单位**;上电 3 秒抑制主动上报
|
||||
- 继电器输出次数上报;0x4A 版本查询(CAPVD→实际频率 uint64 精确换算)
|
||||
|
||||
### vd960DBN(通信 MCU,CH32V208)
|
||||
|
||||
**TCP JSON 服务(:5960,协议 V1.02)**
|
||||
- 密码鉴权 + 18 条命令(新增脱机日志 log_stat/log_query/log_clear)
|
||||
- 鉴权状态机 + 15 条命令;服务异常自动重启(3 条件 + 3 次限额 + 10min 冷却)
|
||||
- event_report 客户端必答:5s 超时重发同 msg_id/原始 ts ×3
|
||||
|
||||
**IoT MQTT(协议 V1.06)**
|
||||
- 双主题 `dld960/{sn}/srv` + `dld960/{sn}/dev`,消息类型由 `cmd` 区分
|
||||
- 上电 initialize 上线消息(dev_serial/model/hard_ver/soft_ver)
|
||||
- loop_data 三档调度:空闲按配置间隔 / 活动 300ms / **car_state 翻转沿立即上报**
|
||||
- event_report 平台必答 + 16 深环形队列,ACK 后出队;不受 report_config.enable 门控
|
||||
- 设备时钟同步方案B:平台经 report_config 下发 Unix ts,设备无 RTC 也能上报真实时间
|
||||
- 稳定性修复:mqtt_publish 缓冲溢出发垃圾包致 broker RST 风暴(现场 P0)、packet_id=0 断连、PINGREQ 保活、分批发送(MSS=576,Publish≤500B)
|
||||
- loop_data 陈旧快照修复:0xC0 帧双消费路径统一摄取,事件与缓存同源
|
||||
- **脱机事件日志(W25Q32 环形 8064 条,V1.06 新增)**:log_stat / log_query / log_clear 三命令;BOOT 复位原因、MQTT 连接/断开、event ACK 超时、线圈事件、时钟锚点共 10 类;掉电不丢
|
||||
|
||||
**基础设施**
|
||||
- BLE 小程序配置 + OTA;Loop MCU ISP 串口透传升级
|
||||
- WCHNET TCP/IP(listen=N / data=N+1)、simple_json 解析器 6 项 bug 修复
|
||||
|
||||
### 文档与工具
|
||||
- 四份协议文档 + 硬件资源文档齐套(见 README 协议矩阵)
|
||||
- DBNetClient(TCP JSON 测试)、DBNMQTTool(MQTT 双主题测试)
|
||||
- 双 MCU devlog、验收标准、variation 分析报告、现场事故归档
|
||||
|
||||
### 已知约束
|
||||
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)
|
||||
- CH32V208 栈资源紧张:2KB 任务栈内勿连续 PRINT,勿随意扩大全局 BSS
|
||||
|
||||
---
|
||||
|
||||
## 版本号规则
|
||||
|
||||
- 整机发布 tag:`vX.Y.Z`(本仓库 git tag + Gitea Release)
|
||||
- 固件版本:两侧 `cmcng.h` 的 `FIRMWARE_VER`,随整机版本同步
|
||||
- 协议文档版本:各文档内修订记录表独立演进,发布时在配套矩阵中锁定
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.spec.bak
|
||||
build/
|
||||
dist/
|
||||
@@ -8,6 +8,7 @@ DLD960 IoT MQTT 协议定义
|
||||
|
||||
import json
|
||||
import time
|
||||
import struct
|
||||
from typing import Optional, Any
|
||||
from dataclasses import dataclass, field, asdict
|
||||
|
||||
@@ -61,6 +62,11 @@ CMD_LOOP_PARAM_SET = "loop_param_set"
|
||||
CMD_LOOP_PARAM_QUERY = "loop_param_query"
|
||||
CMD_REPORT_CONFIG = "report_config"
|
||||
|
||||
# V1.06: 脱机事件日志
|
||||
CMD_LOG_STAT = "log_stat"
|
||||
CMD_LOG_QUERY = "log_query"
|
||||
CMD_LOG_CLEAR = "log_clear"
|
||||
|
||||
# 设备上报
|
||||
CMD_LOOP_DATA = "loop_data"
|
||||
CMD_EVENT_REPORT = "event_report"
|
||||
@@ -71,6 +77,11 @@ CMD_INITIALIZE = "initialize" # V1.03: 设备上电初始化
|
||||
CONFIG_SET_COMMANDS = {CMD_DEV_SERIAL_SET, CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET, CMD_LOOP_PARAM_SET}
|
||||
CONFIG_QUERY_COMMANDS = {CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY, CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY}
|
||||
CTRL_COMMANDS = {CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET, CMD_REPORT_CONFIG}
|
||||
LOG_COMMANDS = {CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR} # V1.06 脱机日志
|
||||
|
||||
# V1.07: log_* 命令日志流 (与 BLE 0x28/0x2A 同语义)
|
||||
STREAM_EVENT = "event"
|
||||
STREAM_SNAPSHOT = "snapshot"
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -104,6 +115,29 @@ FREQ_LEVELS = ["high", "mid_high", "mid_low", "low"]
|
||||
OUTPUT_MODES = ["exist", "enter_pulse", "leave_pulse", "direction"]
|
||||
EVENT_TYPES = ["car_enter", "car_leave", "loop_cut", "loop_restore"]
|
||||
|
||||
# 脱机事件日志类型描述 (log_query 响应 records[].type, MQTT V1.06)
|
||||
LOG_EVENT_TYPE_DESC = {
|
||||
"boot": "上电/复位",
|
||||
"iot_connect": "MQTT TCP 连接成功",
|
||||
"iot_ready": "MQTT 订阅完成→发 initialize",
|
||||
"iot_disconnect": "MQTT 断连",
|
||||
"iot_reconn": "重连退避",
|
||||
"evt_retry": "event ACK 超时重发",
|
||||
"evt_giveup": "event 重试耗尽挂起",
|
||||
"coil": "线圈事件",
|
||||
"time_anchor": "时钟同步锚点",
|
||||
"log_clear": "日志清除(审计)",
|
||||
"unknown": "未知类型",
|
||||
}
|
||||
|
||||
# 传感快照 channels[].misc_type 描述 (log_query stream=snapshot, MQTT V1.07)
|
||||
SNAP_MISC_TYPE_DESC = {
|
||||
"time": "时间量(50ms)",
|
||||
"cut_count": "线圈断开次数",
|
||||
"flow_count": "车流量",
|
||||
"relay_count": "继电器输出次数",
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 消息构建
|
||||
@@ -214,6 +248,31 @@ def data_report_config(sensor_type: int = 12, enable: bool = True,
|
||||
}
|
||||
|
||||
|
||||
def data_log_query(start_seq: int = 1, count: int = 4, stream: str = STREAM_EVENT) -> dict:
|
||||
"""V1.07 log_query 分页拉取脱机日志
|
||||
stream=event: 事件流, count 上限 4 (设备侧超限按 4 处理)
|
||||
stream=snapshot: 快照流, count 上限 2 (hex 原始字节上报); 显式传 stream 字段
|
||||
"""
|
||||
d: dict[str, Any] = {
|
||||
"start_seq": max(1, int(start_seq)),
|
||||
"count": min(4, max(1, int(count))),
|
||||
}
|
||||
if stream == STREAM_SNAPSHOT:
|
||||
d["stream"] = STREAM_SNAPSHOT
|
||||
d["count"] = min(2, max(1, int(count)))
|
||||
return d
|
||||
|
||||
|
||||
def data_log_clear(stream: str = STREAM_EVENT) -> dict:
|
||||
"""V1.07 log_clear 清除脱机日志 (审计留痕)
|
||||
stream=snapshot: 快照流清除 (~45ms, 审计写事件流)
|
||||
event 流省略 data (设备缺省 event, 兼容老固件)
|
||||
"""
|
||||
if stream == STREAM_SNAPSHOT:
|
||||
return {"stream": STREAM_SNAPSHOT}
|
||||
return {}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 解析设备上报
|
||||
# ============================================================
|
||||
@@ -224,3 +283,91 @@ def parse_topic_dev_serial(topic: str) -> Optional[str]:
|
||||
if len(parts) >= 2 and parts[0] == "dld960":
|
||||
return parts[1]
|
||||
return None
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 脱机日志 hex 解析 (V1.07: log_query 响应 records[].hex 原始字节)
|
||||
# 字段表与《DLD960 BLE 协议》§6.4 (SnapRec) / §7 (OfflogEvt) 一致
|
||||
# ============================================================
|
||||
|
||||
# OfflogEvt 事件类型 (BLE 协议 §7 事件类型表)
|
||||
OFFLOG_TYPE_NAMES = {
|
||||
0x01: "boot", 0x10: "iot_connect", 0x11: "iot_ready", 0x12: "iot_disconnect",
|
||||
0x13: "iot_reconn", 0x30: "evt_retry", 0x31: "evt_giveup", 0x40: "coil",
|
||||
0x50: "time_anchor", 0x70: "log_clear",
|
||||
}
|
||||
_OFFLOG_SUB_NAMES = {1: "car_enter", 2: "car_leave", 3: "loop_cut", 4: "loop_restore"}
|
||||
_OFFLOG_DISC_REASONS = {1: "断开", 2: "超时", 3: "CONNACK拒绝", 4: "连接超时"}
|
||||
|
||||
|
||||
def parse_offlog_hex(h: str) -> dict:
|
||||
"""OfflogEvt 32B hex → dict (magic/type/seq/ts_ms/unix_ts/boot_seq/payload)"""
|
||||
b = bytes.fromhex(h)
|
||||
if len(b) != 32:
|
||||
return {"error": f"bad len {len(b)}"}
|
||||
magic, etype, elen, flags, seq, ts_ms, unix_ts, boot_seq, rsvd, payload = \
|
||||
struct.unpack('<BBBBIIIHH12s', b)
|
||||
return {"magic": magic, "type": etype, "len": elen, "flags": flags, "seq": seq,
|
||||
"ts_ms": ts_ms, "unix_ts": unix_ts, "boot_seq": boot_seq, "payload": payload}
|
||||
|
||||
|
||||
def offlog_payload_desc(etype: int, payload: bytes) -> str:
|
||||
"""事件 payload 人类可读描述 (BLE 协议 §7 payload 定义, 大端)"""
|
||||
if etype == 0x01: # boot: 复位原因寄存器
|
||||
rst = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
|
||||
bits = [n for n, b in [("LPWR", 31), ("WWDG", 30), ("IWDG", 29),
|
||||
("SFT", 28), ("POR", 27), ("NRST", 26)] if rst & (1 << b)]
|
||||
return f"rst=0x{rst:08X} ({'+'.join(bits) if bits else 'none'})"
|
||||
if etype == 0x12: # iot_disconnect
|
||||
r = payload[0] if payload else 0
|
||||
return f"reason={r} ({_OFFLOG_DISC_REASONS.get(r, '?')})"
|
||||
if etype == 0x13: # iot_reconn
|
||||
return f"backoff_ms={int.from_bytes(payload[0:4], 'big')}" if len(payload) >= 4 else ""
|
||||
if etype == 0x30: # evt_retry
|
||||
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
|
||||
retry = payload[4] if len(payload) >= 5 else 0
|
||||
return f"msg_id={mid} retry={retry}"
|
||||
if etype == 0x31: # evt_giveup
|
||||
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
|
||||
return f"msg_id={mid}"
|
||||
if etype == 0x40: # coil
|
||||
sub = _OFFLOG_SUB_NAMES.get(payload[0], payload[0]) if payload else "?"
|
||||
ch = payload[1] if len(payload) >= 2 else 0
|
||||
val = int.from_bytes(payload[2:6], 'big') if len(payload) >= 6 else 0
|
||||
return f"sub={sub} ch={ch} value={val}"
|
||||
if etype in (0x10, 0x11, 0x50, 0x70): # 无 payload
|
||||
return ""
|
||||
return payload.hex() if payload else ""
|
||||
|
||||
|
||||
def parse_snap_hex(h: str) -> dict:
|
||||
"""SnapRec 64B hex → dict (seq/boot_seq/ts_ms/coil_count/channels[])"""
|
||||
b = bytes.fromhex(h)
|
||||
if len(b) != 64:
|
||||
return {"error": f"bad len {len(b)}"}
|
||||
magic, slen, flags, rsvd, seq, ts_ms, boot_seq, rsvd2, coils = \
|
||||
struct.unpack('<BBBBIIHH48s', b)
|
||||
coil_count = min(slen // 12, 4)
|
||||
channels = []
|
||||
for c in range(coil_count):
|
||||
p = coils[c * 12:(c + 1) * 12]
|
||||
cfg, cond = p[0], p[1]
|
||||
freq = p[2] | (p[3] << 8) | (p[4] << 16)
|
||||
var = p[5] | (p[6] << 8) | (p[7] << 16)
|
||||
if var & 0x800000:
|
||||
var -= 0x1000000
|
||||
misc = int.from_bytes(p[8:12], 'little')
|
||||
channels.append({
|
||||
"ch": c + 1,
|
||||
"freq_level": ("high", "mid_high", "mid_low", "low")[(cfg >> 6) & 3],
|
||||
"direction": (cfg >> 5) & 1,
|
||||
"freq_type": (cfg >> 4) & 1,
|
||||
"sensitivity": cfg & 0x0F,
|
||||
"condition": (cond >> 4) & 0x0F,
|
||||
"loop_ok": not ((cond >> 3) & 1),
|
||||
"has_car": bool((cond >> 2) & 1),
|
||||
"misc_type": ("time", "cut_count", "flow_count", "relay_count")[cond & 3],
|
||||
"freq": freq, "variation": var, "misc": misc,
|
||||
})
|
||||
return {"seq": seq, "boot_seq": boot_seq, "ts_ms": ts_ms,
|
||||
"coil_count": coil_count, "channels": channels}
|
||||
|
||||
+241
-141
@@ -1,141 +1,241 @@
|
||||
# DBNMQTTool 开发日志
|
||||
|
||||
> DLD960 IoT MQTT 桌面工具 | Python 3.11+ | PySide6 | paho-mqtt v2 | 跨平台 (Windows/Linux/macOS)
|
||||
>
|
||||
> 定位: DLD960 车检器 MQTT 调试工具 — 设备管理、实时数据监控、模拟上报、主题订阅/发布
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465`
|
||||
|
||||
### 1. 模拟上报 Tab
|
||||
|
||||
- **loop_data / event_report / heartbeat** 三类上报携带独立可编辑 JSON 编辑区
|
||||
- **范例填充**:首次切换类型时自动填入协议定义的标准载荷
|
||||
- 单次发送 + **周期上报**(QSpinBox 可调间隔)
|
||||
- 周期开关由 `QPushButton` toggle 控制
|
||||
|
||||
### 2. 协议Topic Tab
|
||||
|
||||
- 预加载双主题:`dld960/{sn}/srv`(下发)、`dld960/{sn}/dev`(上报)
|
||||
- QListWidget 展示,双击自动填充到发布区
|
||||
- 发布区可编辑 JSON 载荷,一键发布
|
||||
- 主题列表受 SN 输入框驱动,实时更新
|
||||
|
||||
### 3. 自定义Topic Tab
|
||||
|
||||
- 自由输入 topic + JSON 载荷,QoS (0/1/2) 可调
|
||||
- **订阅 / 取消订阅**,接收消息实时展示在 QPlainTextEdit
|
||||
- `#` / `+` 通配符支持,`paho.mqtt.topic_matches_sub` 路由匹配
|
||||
- 自定义订阅的消息自动分发到接收区,附带 topic 前缀
|
||||
|
||||
### 4. 新增依赖
|
||||
|
||||
- `paho.mqtt.client` 导入新增 `topic_matches_sub`
|
||||
- UI 控件: QComboBox, QSpinBox, QPlainTextEdit
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 — paho-mqtt v2 回调修复 `213c0dd`
|
||||
|
||||
### 问题
|
||||
断开连接时报错:
|
||||
```
|
||||
TypeError: MqttClient._on_disconnect() missing 1 required positional argument: 'reason_code'
|
||||
```
|
||||
|
||||
### 根因
|
||||
paho-mqtt v2 默认 `callback_api_version=VERSION2`,`on_disconnect` 回调签名为 5 参数 `(client, userdata, flags, reason_code, properties)`,而 v1 为 3 参数 `(client, userdata, rc)`。
|
||||
|
||||
### 修复
|
||||
- `_on_disconnect` 改用 `*args` 兼容 v1/v2
|
||||
- `Client` 构造显式指定 `callback_api_version=CallbackAPIVersion.VERSION2`
|
||||
- `_on_connect` 保持 v2 签名不变
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 — tkinter → PySide6 迁移 `e1bf3dc`
|
||||
|
||||
### 决策
|
||||
放弃 tkinter,改用 PySide6 (Qt for Python),理由:
|
||||
- 跨平台原生外观 (Fusion 风格)
|
||||
- Qt 信号/槽机制替代 tkinter 回调,更易维护
|
||||
- 组件丰富 (QGroupBox, QSplitter, QTabWidget 等)
|
||||
|
||||
### 改动
|
||||
- `main.py` 全量重写为 PySide6 主窗口
|
||||
- 布局: QGroupBox 分组 → QSplitter 分栏 → QTabWidget 功能分区
|
||||
- `requirements.txt` 新增 `PySide6>=6.6.0`
|
||||
- tkinter → Qt 映射: `StringVar`→信号/槽, `Listbox`→`QListWidget`, `Text`→`QTextEdit`
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 — 项目初始化 `a2cfe46`
|
||||
|
||||
### 功能 (初始版本, tkinter)
|
||||
- MQTT Broker 连接管理
|
||||
- 设备自动发现 (`dld960/+/dev` 通配符订阅)
|
||||
- 设备信息查询 + 主题配置
|
||||
- 实时线圈数据 / 事件上报 / 心跳 监控
|
||||
- 控制命令: 密码验证/设置、出厂初始化、设备复位
|
||||
|
||||
### 项目结构
|
||||
```
|
||||
DBNMQTTool/
|
||||
├── main.py # 主窗口 (tkinter → 后迁移至 PySide6)
|
||||
├── dbn_mqtt_tool/
|
||||
│ ├── protocol.py # DLD960 IoT MQTT 协议定义 (主题/命令/载荷模板)
|
||||
│ ├── mqtt_client.py # MQTT 客户端封装 (paho-mqtt v2)
|
||||
│ └── device_manager.py # 设备发现与状态管理
|
||||
└── requirements.txt # paho-mqtt>=2.0.0 + PySide6>=6.6.0
|
||||
```
|
||||
|
||||
### 协议依据
|
||||
`DLD960_IoT_MQTT协议.md` V1.00
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 组件 | 状态 |
|
||||
|------|------|
|
||||
| MQTT 连接/断开 | ✅ 已实现,v1/v2 兼容 |
|
||||
| 设备发现 | ✅ 通配符订阅 |
|
||||
| 实时数据监控 | ✅ loop_data/event_report/heartbeat |
|
||||
| 控制命令 | ✅ 15 条命令 |
|
||||
| 模拟上报 | ✅ c19e465 |
|
||||
| 协议Topic订阅/发布 | ✅ c19e465 |
|
||||
| 自定义Topic订阅/发布 | ✅ c19e465 |
|
||||
| 固件联调 | ✅ vd960DBN IoT 模式稳定上报 |
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-07 — MQTT V1.01 适配 + 稳定性修复
|
||||
|
||||
### 1. MQTT 协议 V1.01 适配
|
||||
|
||||
- Topic 从多主题(`/loop_data`, `/event_report`, `/heartbeat`, `/cmd`…)压缩为双主题 `dld960/{sn}/dev`(上报)、`dld960/{sn}/srv`(下发)
|
||||
- `manage_mqtt_recv_message()` 实现 V1.01 协议命令分发,支持 `cmd`/`Method` 双格式
|
||||
- MQTT PUBLISH 接收修复:topic 过滤 + 通配符路由修正
|
||||
|
||||
### 2. MqttClient 重构:tkinter 回调 → QObject 信号/槽
|
||||
|
||||
**问题**: `paho.mqtt` C 层回调直接操作 Qt UI 控件 → `RuntimeError: wrapped C/C++ object has been deleted` 崩溃。
|
||||
|
||||
**修复**:
|
||||
- `MqttClient` 继承 `QObject`,使用 Qt 信号/槽机制
|
||||
- `on_message` → `message_received` 信号 → 主线程 slot 安全更新 UI
|
||||
- 所有 publish 路径增加通用异常捕获,防止未捕获异常导致静默崩溃
|
||||
- 回退 Qt 信号排队方案,改用 `QMetaObject.invokeMethod` + 日志追踪定位阻塞点
|
||||
|
||||
### 3. DeviceManager 死锁修复
|
||||
|
||||
`threading.Lock` 在单线程 Qt 事件循环中自锁 → `RLock`。`add_device()` 和 `remove_device()` 同线程互调时 `Lock` 阻塞自身。
|
||||
|
||||
### 4. UI/UX 改进
|
||||
|
||||
- **日志增强**: 收发打印 topic + payload 详情,调试信息更直观
|
||||
- **查询响应自动回填**: 设备信息查询后自动填充 SN 等输入框
|
||||
- **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关
|
||||
- `right` 局部变量 → `self._notebook`,修复 tab 引用丢失
|
||||
|
||||
# DBNMQTTool 开发日志
|
||||
|
||||
> DLD960 IoT MQTT 桌面工具 | Python 3.11+ | PySide6 | paho-mqtt v2 | 跨平台 (Windows/Linux/macOS)
|
||||
>
|
||||
> 定位: DLD960 车检器 MQTT 调试工具 — 设备管理、实时数据监控、模拟上报、主题订阅/发布
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-18 — log_query 响应改 hex 原始字节解析展示(协议 V1.07 修订)
|
||||
|
||||
### 背景
|
||||
|
||||
MQTT 快照流实测 `MQTTSerialize_publish failed`——JSON 化快照记录超 800B 发送缓冲。协议修订为**原始字节 hex 上报**(与 BLE 通道同语义),工具同步改为 hex 解析。
|
||||
|
||||
### 变更
|
||||
|
||||
- **protocol.py**:新增 `parse_offlog_hex`(OfflogEvt 32B,`<BBBBIIIHH12s`)、`parse_snap_hex`(SnapRec 64B,`<BBBBIIHH48s`)、`offlog_payload_desc`(boot 复位位/coil/evt_retry/evt_giveup/iot_disconnect/reconn)、`OFFLOG_TYPE_NAMES`——字段表与 BLE 协议 §6.4/§7 一致
|
||||
- `data_log_query` snapshot count 恢复 **2**(hex 体积可控)
|
||||
- **main.py**:`_apply_log_query_data` 解析 `records[].hex`(hex 长度 128=快照 / 64=事件);事件流展示 type 描述 + payload 可读化,快照流逐通道字段
|
||||
|
||||
### 验证
|
||||
|
||||
- 构造事件/快照二进制→hex→解析回断言全过(POR/SFT 复位位、coil payload、evt_retry、快照 4ch、负 variation 符号扩展)
|
||||
- 2 条快照 hex 响应 payload = 406B < 800B
|
||||
- offscreen UI:事件流(上电/复位、线圈事件、event 重发)+ 快照流(4ch/2ch)展示 + 翻页正常
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-18 — 脱机日志翻页:上一页/下一页 + 自动填充起始序号
|
||||
|
||||
### 变更
|
||||
|
||||
- 脱机日志区按钮行新增 **◀ 上一页 / 下一页 ▶**(初始禁用,拉取成功后启用)
|
||||
- **下一页**:起始序号 = 当前页最后一条 `seq` + 1(锚点法,不依赖请求条数,快照流 2 条/事件流 4 条均精确);空记录兜底按 `start + count` 步进
|
||||
- **上一页**:起始序号 = `max(1, 当前起始 - 条数)`,边界保护
|
||||
- 翻页后自动填充起始序号 SpinBox 并触发拉取;页锚点在 `_apply_log_query_data` 更新(`_log_page_last_seq`)
|
||||
|
||||
### 验证
|
||||
|
||||
- offscreen 实测 6 场景全过:初始禁用 / 锚点=103 / 下一页→104 / 上一页→100 / 边界→1 / 空记录步进→504 / 快照流 2 条→12
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-18 — 命令集对齐:禁用固件未实现的 MQTT 命令按钮
|
||||
|
||||
### 背景
|
||||
|
||||
实测发现 MQTT 通道 `ssc_net_query` / `iot_net_query` 返回 `code=4 unsupported command`——固件 MQTT 分发只实现 6 条命令,工具按协议文档命令表做了全按钮。
|
||||
|
||||
### 变更
|
||||
|
||||
- **固件同步补齐**(vd960DBN V4.2):`ssc_net_query` / `iot_net_query` / `iot_topic_query` 三条查询命令已实现,工具对应查询按钮保持可用
|
||||
- **工具禁用**固件仍未实现的命令按钮:`ssc_net_set` / `iot_net_set` / `iot_topic_set` / `pwd_set` / `factory_reset` / `device_reset`(`setEnabled(False)` + tooltip 引导走 TCP JSON/BLE 通道)
|
||||
- 设备刷新列表移除 `CMD_LOOP_PARAM_QUERY`(固件未实现,刷新时不再报 error)
|
||||
|
||||
### 验证
|
||||
|
||||
- offscreen 实例化:6 个按钮禁用 + tooltip 正确,查询类/验证密码/report_config/log_* 可用
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-18 — 脱机日志支持快照流 (stream=snapshot, 协议 V1.07)
|
||||
|
||||
### 变更
|
||||
|
||||
- **protocol.py**:新增 `STREAM_EVENT` / `STREAM_SNAPSHOT` 常量;`data_log_query` 加 `stream` 参数(快照流显式传 `stream` 字段 + count≤2);新增 `data_log_clear(stream)`(事件流省略 data 兼容老固件);新增 `SNAP_MISC_TYPE_DESC`(time/cut_count/flow_count/relay_count 中文描述)
|
||||
- **main.py**:脱机日志区加"日志流"选择器(事件日志/传感快照);统计/拉取/清除三按钮按当前流发请求(log_clear 确认框提示阻塞时长 2.8s/45ms);`log_stat`/`log_query` 响应展示区分流——快照记录解析 `channels[]` 逐字段(freq_level/direction/freq_type/sens/cond/loop/car/freq/variation/misc)
|
||||
|
||||
### 验证
|
||||
|
||||
- protocol 数据构建器断言 6 例全过(事件流不带 stream / 快照流 count≤2 / log_clear 流参数)
|
||||
- `QT_QPA_PLATFORM=offscreen` 实例化 MainWindow 实测:`log_stat(snapshot)` capacity=48064 展示、`log_query(snapshot)` 2 条快照记录逐字段展示(含负 variation、misc_type 中文)、事件流展示回归无损、空记录边界
|
||||
- 依赖:venv + PySide6 + paho-mqtt(`venv/` 已加 .gitignore)
|
||||
|
||||
### 配套
|
||||
|
||||
- 固件 vd960DBN `log_*` stream=snapshot 分支(同日实现,devlog V4.1)
|
||||
- 协议文档:TCP JSON V1.03 / IoT MQTT V1.07
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 (晚) — 模拟上报 + 协议Topic + 自定义Topic `c19e465`
|
||||
|
||||
### 1. 模拟上报 Tab
|
||||
|
||||
- **loop_data / event_report / heartbeat** 三类上报携带独立可编辑 JSON 编辑区
|
||||
- **范例填充**:首次切换类型时自动填入协议定义的标准载荷
|
||||
- 单次发送 + **周期上报**(QSpinBox 可调间隔)
|
||||
- 周期开关由 `QPushButton` toggle 控制
|
||||
|
||||
### 2. 协议Topic Tab
|
||||
|
||||
- 预加载双主题:`dld960/{sn}/srv`(下发)、`dld960/{sn}/dev`(上报)
|
||||
- QListWidget 展示,双击自动填充到发布区
|
||||
- 发布区可编辑 JSON 载荷,一键发布
|
||||
- 主题列表受 SN 输入框驱动,实时更新
|
||||
|
||||
### 3. 自定义Topic Tab
|
||||
|
||||
- 自由输入 topic + JSON 载荷,QoS (0/1/2) 可调
|
||||
- **订阅 / 取消订阅**,接收消息实时展示在 QPlainTextEdit
|
||||
- `#` / `+` 通配符支持,`paho.mqtt.topic_matches_sub` 路由匹配
|
||||
- 自定义订阅的消息自动分发到接收区,附带 topic 前缀
|
||||
|
||||
### 4. 新增依赖
|
||||
|
||||
- `paho.mqtt.client` 导入新增 `topic_matches_sub`
|
||||
- UI 控件: QComboBox, QSpinBox, QPlainTextEdit
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 — paho-mqtt v2 回调修复 `213c0dd`
|
||||
|
||||
### 问题
|
||||
断开连接时报错:
|
||||
```
|
||||
TypeError: MqttClient._on_disconnect() missing 1 required positional argument: 'reason_code'
|
||||
```
|
||||
|
||||
### 根因
|
||||
paho-mqtt v2 默认 `callback_api_version=VERSION2`,`on_disconnect` 回调签名为 5 参数 `(client, userdata, flags, reason_code, properties)`,而 v1 为 3 参数 `(client, userdata, rc)`。
|
||||
|
||||
### 修复
|
||||
- `_on_disconnect` 改用 `*args` 兼容 v1/v2
|
||||
- `Client` 构造显式指定 `callback_api_version=CallbackAPIVersion.VERSION2`
|
||||
- `_on_connect` 保持 v2 签名不变
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 — tkinter → PySide6 迁移 `e1bf3dc`
|
||||
|
||||
### 决策
|
||||
放弃 tkinter,改用 PySide6 (Qt for Python),理由:
|
||||
- 跨平台原生外观 (Fusion 风格)
|
||||
- Qt 信号/槽机制替代 tkinter 回调,更易维护
|
||||
- 组件丰富 (QGroupBox, QSplitter, QTabWidget 等)
|
||||
|
||||
### 改动
|
||||
- `main.py` 全量重写为 PySide6 主窗口
|
||||
- 布局: QGroupBox 分组 → QSplitter 分栏 → QTabWidget 功能分区
|
||||
- `requirements.txt` 新增 `PySide6>=6.6.0`
|
||||
- tkinter → Qt 映射: `StringVar`→信号/槽, `Listbox`→`QListWidget`, `Text`→`QTextEdit`
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-06 — 项目初始化 `a2cfe46`
|
||||
|
||||
### 功能 (初始版本, tkinter)
|
||||
- MQTT Broker 连接管理
|
||||
- 设备自动发现 (`dld960/+/dev` 通配符订阅)
|
||||
- 设备信息查询 + 主题配置
|
||||
- 实时线圈数据 / 事件上报 / 心跳 监控
|
||||
- 控制命令: 密码验证/设置、出厂初始化、设备复位
|
||||
|
||||
### 项目结构
|
||||
```
|
||||
DBNMQTTool/
|
||||
├── main.py # 主窗口 (tkinter → 后迁移至 PySide6)
|
||||
├── dbn_mqtt_tool/
|
||||
│ ├── protocol.py # DLD960 IoT MQTT 协议定义 (主题/命令/载荷模板)
|
||||
│ ├── mqtt_client.py # MQTT 客户端封装 (paho-mqtt v2)
|
||||
│ └── device_manager.py # 设备发现与状态管理
|
||||
└── requirements.txt # paho-mqtt>=2.0.0 + PySide6>=6.6.0
|
||||
```
|
||||
|
||||
### 协议依据
|
||||
`DLD960_IoT_MQTT协议.md` V1.00
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 组件 | 状态 |
|
||||
|------|------|
|
||||
| MQTT 连接/断开 | ✅ 已实现,v1/v2 兼容 |
|
||||
| 设备发现 | ✅ 通配符订阅 |
|
||||
| 实时数据监控 | ✅ loop_data/event_report/heartbeat |
|
||||
| 控制命令 | ✅ 15 条命令 |
|
||||
| 模拟上报 | ✅ c19e465 |
|
||||
| 协议Topic订阅/发布 | ✅ c19e465 |
|
||||
| 自定义Topic订阅/发布 | ✅ c19e465 |
|
||||
| 固件联调 | ✅ vd960DBN IoT 模式稳定上报 |
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-07 — MQTT V1.01 适配 + 稳定性修复
|
||||
|
||||
### 1. MQTT 协议 V1.01 适配
|
||||
|
||||
- Topic 从多主题(`/loop_data`, `/event_report`, `/heartbeat`, `/cmd`…)压缩为双主题 `dld960/{sn}/dev`(上报)、`dld960/{sn}/srv`(下发)
|
||||
- `manage_mqtt_recv_message()` 实现 V1.01 协议命令分发,支持 `cmd`/`Method` 双格式
|
||||
- MQTT PUBLISH 接收修复:topic 过滤 + 通配符路由修正
|
||||
|
||||
### 2. MqttClient 重构:tkinter 回调 → QObject 信号/槽
|
||||
|
||||
**问题**: `paho.mqtt` C 层回调直接操作 Qt UI 控件 → `RuntimeError: wrapped C/C++ object has been deleted` 崩溃。
|
||||
|
||||
**修复**:
|
||||
- `MqttClient` 继承 `QObject`,使用 Qt 信号/槽机制
|
||||
- `on_message` → `message_received` 信号 → 主线程 slot 安全更新 UI
|
||||
- 所有 publish 路径增加通用异常捕获,防止未捕获异常导致静默崩溃
|
||||
- 回退 Qt 信号排队方案,改用 `QMetaObject.invokeMethod` + 日志追踪定位阻塞点
|
||||
|
||||
### 3. DeviceManager 死锁修复
|
||||
|
||||
`threading.Lock` 在单线程 Qt 事件循环中自锁 → `RLock`。`add_device()` 和 `remove_device()` 同线程互调时 `Lock` 阻塞自身。
|
||||
|
||||
### 4. UI/UX 改进
|
||||
|
||||
- **日志增强**: 收发打印 topic + payload 详情,调试信息更直观
|
||||
- **查询响应自动回填**: 设备信息查询后自动填充 SN 等输入框
|
||||
- **主动上报配置**: `report_config` 面板支持完整 7 参数独立开关
|
||||
- `right` 局部变量 → `self._notebook`,修复 tab 引用丢失
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-05 — 脱机事件日志命令支持 (MQTT V1.06)
|
||||
|
||||
配合 vd960DBN 固件 P1.3(commit 1a01316)+ 协议 V1.06,工具新增脱机事件日志面板(参数配置 Tab 底部):
|
||||
|
||||
### 新增命令
|
||||
|
||||
| 命令 | 按钮 | 说明 |
|
||||
|------|------|------|
|
||||
| `log_stat` | 统计 | 日志统计/分页定位: enabled/boot_seq/count/capacity/seq_first/seq_last |
|
||||
| `log_query` | 拉取日志 | 按全局序号分页拉取, 起始序号 + 条数(1~4) 可调 |
|
||||
| `log_clear` | 清除日志 | 二次确认弹窗, 审计留痕不可撤销 |
|
||||
|
||||
### 实现要点
|
||||
|
||||
- `protocol.py`: 新增 `CMD_LOG_*` 常量、`LOG_COMMANDS` 集合、`data_log_query()` 构建器(start_seq≥1 / count 1~4 边界钳制)、`LOG_EVENT_TYPE_DESC` 事件类型中文描述(10 类 + unknown)
|
||||
- `main.py`: 参数配置 Tab 新增 g6 组(起始序号/条数 spinbox + 三按钮 + 只读显示区 QPlainTextEdit)
|
||||
- `_on_message` code==0 分支新增 `log_stat`/`log_query`/`log_clear` 响应处理
|
||||
- 显示优化: log_query 记录带事件类型中文描述; unix_ts 已同步显示真实时间, 未同步显示 `boot+ts_ms(未同步)`
|
||||
- 清除日志前 QMessageBox 二次确认(防误触, 与出厂初始化/设备复位同模式)
|
||||
|
||||
### 验证
|
||||
|
||||
- `py_compile` 四个模块通过
|
||||
- protocol 逻辑单测: data_log_query 边界钳制 (0→1, 99→4, 0→1)、命令枚举一致性、build_request 带 data
|
||||
|
||||
+201
-3
@@ -31,9 +31,12 @@ from dbn_mqtt_tool.protocol import (
|
||||
CMD_SSC_NET_SET, CMD_IOT_NET_SET, CMD_IOT_TOPIC_SET,
|
||||
CMD_PWD_VERIFY, CMD_PWD_SET, CMD_FACTORY_RESET, CMD_DEVICE_RESET,
|
||||
CMD_LOOP_DATA, CMD_EVENT_REPORT, CMD_HEARTBEAT, CMD_INITIALIZE,
|
||||
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES,
|
||||
CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR,
|
||||
STREAM_EVENT, STREAM_SNAPSHOT,
|
||||
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC, SNAP_MISC_TYPE_DESC,
|
||||
OFFLOG_TYPE_NAMES, parse_offlog_hex, offlog_payload_desc, parse_snap_hex,
|
||||
data_ssc_net_set, data_iot_net_set, data_iot_topic_set,
|
||||
data_pwd_verify, data_pwd_set, data_report_config,
|
||||
data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
|
||||
build_request, next_msg_id,
|
||||
topic_down, topic_up,
|
||||
)
|
||||
@@ -194,6 +197,8 @@ class MainWindow(QMainWindow):
|
||||
b1.clicked.connect(self._query_ssc_net)
|
||||
b2 = QPushButton("设置")
|
||||
b2.clicked.connect(self._set_ssc_net)
|
||||
b2.setEnabled(False)
|
||||
b2.setToolTip("MQTT 固件暂未实现 ssc_net_set,请用 TCP JSON 或蓝牙通道配置")
|
||||
btn_row.addWidget(b1)
|
||||
btn_row.addWidget(b2)
|
||||
btn_row.addStretch()
|
||||
@@ -222,6 +227,8 @@ class MainWindow(QMainWindow):
|
||||
b3.clicked.connect(self._query_iot_net)
|
||||
b4 = QPushButton("设置")
|
||||
b4.clicked.connect(self._set_iot_net)
|
||||
b4.setEnabled(False)
|
||||
b4.setToolTip("MQTT 固件暂未实现 iot_net_set,请用 TCP JSON 或蓝牙通道配置")
|
||||
btn_row2.addWidget(b3)
|
||||
btn_row2.addWidget(b4)
|
||||
btn_row2.addStretch()
|
||||
@@ -249,6 +256,8 @@ class MainWindow(QMainWindow):
|
||||
b5.clicked.connect(self._query_iot_topic)
|
||||
b6 = QPushButton("设置")
|
||||
b6.clicked.connect(self._set_iot_topic)
|
||||
b6.setEnabled(False)
|
||||
b6.setToolTip("MQTT 固件暂未实现 iot_topic_set,请用 TCP JSON 或蓝牙通道配置")
|
||||
btn_row3.addWidget(b5)
|
||||
btn_row3.addWidget(b6)
|
||||
btn_row3.addStretch()
|
||||
@@ -276,6 +285,9 @@ class MainWindow(QMainWindow):
|
||||
]:
|
||||
btn = QPushButton(label)
|
||||
btn.clicked.connect(slot)
|
||||
if label != "验证密码":
|
||||
btn.setEnabled(False)
|
||||
btn.setToolTip(f"MQTT 固件暂未实现 {label},请用 TCP JSON 或蓝牙通道")
|
||||
btn_row4.addWidget(btn)
|
||||
btn_row4.addStretch()
|
||||
g4_layout.addLayout(btn_row4, 1, 0, 1, 4)
|
||||
@@ -326,6 +338,61 @@ class MainWindow(QMainWindow):
|
||||
g5_layout.addLayout(btn_row5, 2, 0, 1, 6)
|
||||
layout.addWidget(g5)
|
||||
|
||||
# -- 脱机日志 (V1.06 事件流 / V1.07 快照流) --
|
||||
g6 = QGroupBox("脱机日志 (log_stat / log_query / log_clear)")
|
||||
g6_layout = QGridLayout(g6)
|
||||
|
||||
g6_layout.addWidget(QLabel("日志流:"), 0, 0)
|
||||
self._combo_log_stream = QComboBox()
|
||||
self._combo_log_stream.addItem("事件日志 (event)", STREAM_EVENT)
|
||||
self._combo_log_stream.addItem("传感快照 (snapshot)", STREAM_SNAPSHOT)
|
||||
self._combo_log_stream.setCurrentIndex(0)
|
||||
g6_layout.addWidget(self._combo_log_stream, 0, 1)
|
||||
|
||||
g6_layout.addWidget(QLabel("起始序号:"), 0, 2)
|
||||
self._edit_log_start_seq = QSpinBox()
|
||||
self._edit_log_start_seq.setRange(1, 2**31 - 1)
|
||||
self._edit_log_start_seq.setValue(1)
|
||||
g6_layout.addWidget(self._edit_log_start_seq, 0, 3)
|
||||
|
||||
g6_layout.addWidget(QLabel("条数:"), 0, 4)
|
||||
self._edit_log_count = QSpinBox()
|
||||
self._edit_log_count.setRange(1, 4)
|
||||
self._edit_log_count.setValue(4)
|
||||
g6_layout.addWidget(self._edit_log_count, 0, 5)
|
||||
|
||||
btn_row6 = QHBoxLayout()
|
||||
b9 = QPushButton("统计")
|
||||
b9.clicked.connect(self._query_log_stat)
|
||||
self._btn_log_prev = QPushButton("◀ 上一页")
|
||||
self._btn_log_prev.clicked.connect(self._page_log_prev)
|
||||
self._btn_log_prev.setEnabled(False)
|
||||
self._btn_log_prev.setToolTip("起始序号 = 当前页第一条 - 条数(下限 1)")
|
||||
b10 = QPushButton("拉取日志")
|
||||
b10.clicked.connect(self._query_log)
|
||||
self._btn_log_next = QPushButton("下一页 ▶")
|
||||
self._btn_log_next.clicked.connect(self._page_log_next)
|
||||
self._btn_log_next.setEnabled(False)
|
||||
self._btn_log_next.setToolTip("起始序号 = 当前页最后一条 seq + 1")
|
||||
b11 = QPushButton("清除日志")
|
||||
b11.clicked.connect(self._clear_log)
|
||||
btn_row6.addWidget(b9)
|
||||
btn_row6.addWidget(self._btn_log_prev)
|
||||
btn_row6.addWidget(b10)
|
||||
btn_row6.addWidget(self._btn_log_next)
|
||||
btn_row6.addWidget(b11)
|
||||
btn_row6.addStretch()
|
||||
g6_layout.addLayout(btn_row6, 1, 0, 1, 4)
|
||||
|
||||
self._log_rec_text = QPlainTextEdit()
|
||||
self._log_rec_text.setReadOnly(True)
|
||||
# 脱机日志翻页状态 (翻页锚点)
|
||||
self._log_page_last_seq = 0 # 当前页最后一条 seq; 0 = 无有效页
|
||||
self._log_rec_text.setMaximumHeight(150)
|
||||
self._log_rec_text.setFont(QFont("Consolas", 9))
|
||||
g6_layout.addWidget(self._log_rec_text, 2, 0, 1, 4)
|
||||
layout.addWidget(g6)
|
||||
|
||||
layout.addStretch()
|
||||
return w
|
||||
|
||||
@@ -873,6 +940,12 @@ class MainWindow(QMainWindow):
|
||||
self._show_json({cmd: data})
|
||||
elif cmd == CMD_REPORT_CONFIG:
|
||||
self._apply_report_config_data(data)
|
||||
elif cmd == CMD_LOG_STAT:
|
||||
self._apply_log_stat_data(data)
|
||||
elif cmd == CMD_LOG_QUERY:
|
||||
self._apply_log_query_data(data)
|
||||
elif cmd == CMD_LOG_CLEAR:
|
||||
self._log(f"脱机日志已清除 (审计留痕, 需重新统计确认)")
|
||||
else:
|
||||
self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
|
||||
|
||||
@@ -967,7 +1040,7 @@ class MainWindow(QMainWindow):
|
||||
if not sn:
|
||||
return
|
||||
for cmd in [CMD_DEV_INFO_QUERY, CMD_SSC_NET_QUERY, CMD_IOT_NET_QUERY,
|
||||
CMD_IOT_TOPIC_QUERY, CMD_LOOP_PARAM_QUERY]:
|
||||
CMD_IOT_TOPIC_QUERY]:
|
||||
try:
|
||||
self._mqtt.send_command(sn, cmd)
|
||||
except Exception:
|
||||
@@ -1078,6 +1151,131 @@ class MainWindow(QMainWindow):
|
||||
timeout=self._edit_report_timeout.value(),
|
||||
))
|
||||
|
||||
# ---- 脱机日志 (V1.06 事件流 / V1.07 快照流) ----
|
||||
|
||||
def _selected_log_stream(self) -> str:
|
||||
"""当前 UI 选择的日志流 (event/snapshot)"""
|
||||
return self._combo_log_stream.currentData() or STREAM_EVENT
|
||||
|
||||
def _query_log_stat(self):
|
||||
"""log_stat: 日志统计/分页定位 (按当前流)"""
|
||||
stream = self._selected_log_stream()
|
||||
data = {"stream": STREAM_SNAPSHOT} if stream == STREAM_SNAPSHOT else None
|
||||
self._send_cmd(CMD_LOG_STAT, data)
|
||||
|
||||
def _query_log(self):
|
||||
"""log_query: 按全局序号分页拉取 (事件流 count≤4 / 快照流 count≤2)"""
|
||||
self._send_cmd(CMD_LOG_QUERY, data_log_query(
|
||||
start_seq=self._edit_log_start_seq.value(),
|
||||
count=self._edit_log_count.value(),
|
||||
stream=self._selected_log_stream(),
|
||||
))
|
||||
|
||||
def _page_log_prev(self):
|
||||
"""上一页: 起始序号 = max(1, 当前起始 - 条数), 自动拉取"""
|
||||
start = self._edit_log_start_seq.value()
|
||||
count = self._edit_log_count.value()
|
||||
new_start = max(1, start - count)
|
||||
self._edit_log_start_seq.setValue(new_start)
|
||||
self._log(f"翻页: 上一页 → 起始序号 {new_start}")
|
||||
self._query_log()
|
||||
|
||||
def _page_log_next(self):
|
||||
"""下一页: 起始序号 = 当前页最后一条 seq + 1, 自动拉取"""
|
||||
if self._log_page_last_seq > 0:
|
||||
new_start = self._log_page_last_seq + 1
|
||||
else:
|
||||
# 无有效页锚点 (拉取返回空), 按页大小步进
|
||||
new_start = self._edit_log_start_seq.value() + self._edit_log_count.value()
|
||||
self._edit_log_start_seq.setValue(new_start)
|
||||
self._log(f"翻页: 下一页 → 起始序号 {new_start}")
|
||||
self._query_log()
|
||||
|
||||
def _clear_log(self):
|
||||
"""log_clear: 清除脱机日志 (按当前流, 审计留痕, 设备侧阻塞)"""
|
||||
stream = self._selected_log_stream()
|
||||
target = "传感快照" if stream == STREAM_SNAPSHOT else "脱机事件日志"
|
||||
block = "~45ms (逻辑清除+当前扇区)" if stream == STREAM_SNAPSHOT else "~2.8s (63 扇区擦除)"
|
||||
r = QMessageBox.question(self, "确认",
|
||||
f"确定要清除设备{target}吗?\n"
|
||||
f"清除动作本身会写入审计记录,不可撤销!\n"
|
||||
f"设备侧阻塞约 {block}。",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
if r == QMessageBox.Yes:
|
||||
self._send_cmd(CMD_LOG_CLEAR, data_log_clear(stream))
|
||||
|
||||
def _apply_log_stat_data(self, data: dict):
|
||||
stream = data.get("stream", "event")
|
||||
stream_name = "传感快照" if stream == "snapshot" else "事件日志"
|
||||
lines = [
|
||||
f"日志统计[{stream_name}]: enabled={data.get('enabled', False)} boot_seq={data.get('boot_seq', 0)}",
|
||||
f"记录数: {data.get('count', 0)} / {data.get('capacity', 0)}",
|
||||
f"全局序号范围: {data.get('seq_first', 0)} ~ {data.get('seq_last', 0)}",
|
||||
f"提示: 拉取日志用 log_query, 起始序号填 seq_first"
|
||||
+ (" (快照流 count 上限 2)" if stream == "snapshot" else ""),
|
||||
]
|
||||
self._log_rec_text.setPlainText("\n".join(lines))
|
||||
self._log(f"log_stat[{stream_name}]: count={data.get('count', 0)} seq={data.get('seq_first', 0)}~{data.get('seq_last', 0)}")
|
||||
|
||||
def _apply_log_query_data(self, data: dict):
|
||||
records = data.get("records", [])
|
||||
start_seq = data.get("start_seq", 0)
|
||||
# V1.07: 记录为 {"seq":N,"hex":"..."} 原始字节; hex 长度 128=快照(64B) / 64=事件(32B)
|
||||
is_snap = bool(records) and len(records[0].get("hex", "")) == 128
|
||||
stream_name = "传感快照" if is_snap else "事件日志"
|
||||
lines = [f"log_query[{stream_name}] start_seq={start_seq} → 返回 {len(records)} 条:"]
|
||||
for rec in records:
|
||||
hex_str = rec.get("hex", "")
|
||||
if is_snap:
|
||||
s = parse_snap_hex(hex_str)
|
||||
if "error" in s:
|
||||
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {s['error']}")
|
||||
continue
|
||||
lines.append(
|
||||
f" seq={s['seq']} boot={s['boot_seq']} "
|
||||
f"boot+{s['ts_ms']}ms ({s['coil_count']}ch)"
|
||||
)
|
||||
for ch in s["channels"]:
|
||||
mt = ch["misc_type"]
|
||||
misc_desc = SNAP_MISC_TYPE_DESC.get(mt, mt)
|
||||
lines.append(
|
||||
f" ch{ch['ch']}: {ch['freq_level']} "
|
||||
f"dir={ch['direction']} ftype={ch['freq_type']} "
|
||||
f"sens={ch['sensitivity']} cond={ch['condition']} "
|
||||
f"loop={'OK' if ch['loop_ok'] else '断'} "
|
||||
f"car={'有' if ch['has_car'] else '无'} "
|
||||
f"freq={ch['freq']}Hz Δ={ch['variation']} "
|
||||
f"[{misc_desc}] misc={ch['misc']}"
|
||||
)
|
||||
else:
|
||||
e = parse_offlog_hex(hex_str)
|
||||
if "error" in e:
|
||||
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {e['error']}")
|
||||
continue
|
||||
tname = OFFLOG_TYPE_NAMES.get(e["type"], f"0x{e['type']:02x}")
|
||||
desc = LOG_EVENT_TYPE_DESC.get(tname, tname)
|
||||
if e["unix_ts"]:
|
||||
ts_str = datetime.fromtimestamp(e["unix_ts"]).strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
ts_str = f"boot+{e['ts_ms']}ms (未同步)"
|
||||
pdesc = offlog_payload_desc(e["type"], e["payload"])
|
||||
lines.append(
|
||||
f" seq={e['seq']} boot={e['boot_seq']} "
|
||||
f"{ts_str} [{desc}] {pdesc}"
|
||||
)
|
||||
if not records:
|
||||
lines.append(" (无记录 / 起始序号越界, 可先用 log_stat 确认范围)")
|
||||
self._log_rec_text.setPlainText("\n".join(lines))
|
||||
self._log(f"log_query[{stream_name}]: start_seq={start_seq} got {len(records)}")
|
||||
|
||||
# 更新翻页状态: 锚点 = 当前页最后一条 seq; 拉取成功即启用翻页按钮
|
||||
if records:
|
||||
self._log_page_last_seq = records[-1].get("seq", 0)
|
||||
else:
|
||||
self._log_page_last_seq = 0
|
||||
self._btn_log_prev.setEnabled(True)
|
||||
self._btn_log_next.setEnabled(True)
|
||||
|
||||
# ================================================================
|
||||
# UI 更新
|
||||
# ================================================================
|
||||
|
||||
@@ -1,86 +1,94 @@
|
||||
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
|
||||
|
||||
> 当前发布版本:**V1.0.0**(2026-07-16)· 变更历史见 [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 产品概述
|
||||
|
||||
DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、RS485、蓝牙(小程序配置 + OTA),面向停车场出入口控制、流量计数、会车控制等场景。
|
||||
|
||||
产品型号:**DLD960GA**
|
||||
|
||||
## 硬件架构(双 MCU)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
线圈1~4 ──────────► AT32F421 (vd960Loop) │
|
||||
(TMR3 输入捕获) │ · 四路线圈驱动/频率采样 │
|
||||
继电器1~4 ◄───────│ · 车辆检测算法(IIR/斜率限幅/基线跟踪) │
|
||||
└───────────────┬─────────────────────────┘
|
||||
│ UART 0x7F 协议 @192000
|
||||
┌───────────────▼─────────────────────────┐
|
||||
蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │
|
||||
网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │
|
||||
RS485 ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │
|
||||
│ · 参数/日志外部存储 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 芯片 | 型号 | 主频 | Flash | SRAM | 职责 |
|
||||
|------|------|------|-------|------|------|
|
||||
| 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 |
|
||||
| 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz) | 128KB | 64KB | 蓝牙、网口、RS485、外部存储 |
|
||||
|
||||
- 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。
|
||||
- 支持 OTA:CH32V208 经蓝牙小程序 OTA;AT32F421 经 CH32V208 串口透传 ISP 升级。
|
||||
- 地感配置存储于 AT32F421,可经 小程序↔BLE↔CH32V208↔UART↔AT32F421 链路读写。
|
||||
|
||||
详见 [docs/DLD960硬件资源.md](docs/DLD960硬件资源.md)。
|
||||
|
||||
## 子项目
|
||||
|
||||
| 子项目 | 目录 | MCU | 固件版本 | 功能 |
|
||||
|--------|------|-----|---------|------|
|
||||
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
|
||||
| vd960DBN | `vd960DBN/` | CH32V208 | 1.0 | 蓝牙管理、TCP/IP(WCHNET)、MQTT、RS485、串口桥接 |
|
||||
|
||||
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
|
||||
|
||||
## 协议文档(docs/)
|
||||
|
||||
| 文档 | 版本 | 适用层 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 |
|
||||
| [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 RS485/TTL | 设备管理、参数配置、数据上报 |
|
||||
| [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.01 | 以太网 TCP :5960 | 鉴权 + 命令 + event_report 客户端必答 |
|
||||
| [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.05 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步 |
|
||||
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
|
||||
|
||||
## 开发文档
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [vd960Loop/docs/devlog.md](vd960Loop/docs/devlog.md) | 检测算法开发日志(IIR、斜率限幅、基线冻结、时间量单位演进) |
|
||||
| [vd960DBN/docs/devlog.md](vd960DBN/docs/devlog.md) | 通信固件开发日志(WCHNET、MQTT 稳定性、event_report 可靠上报) |
|
||||
| [vd960Loop/docs/acceptance-standard.md](vd960Loop/docs/acceptance-standard.md) | 检测算法验收标准 |
|
||||
| [vd960Loop/docs/variation-analysis.md](vd960Loop/docs/variation-analysis.md) | variation 上报量分析报告(基线 vs 当前值三时钟关系) |
|
||||
| [vd960DBN/docs/incidents/](vd960DBN/docs/incidents/) | 现场事故分析归档 |
|
||||
|
||||
## 配套工具
|
||||
|
||||
| 工具 | 目录 | 说明 |
|
||||
|------|------|------|
|
||||
| DBNetClient | `DBNetClient/` | TCP JSON 协议桌面测试工具(Python + tkinter,零第三方依赖) |
|
||||
| DBNMQTTool | `DBNMQTTool/` | IoT MQTT 协议测试工具(Python + PySide6 + paho-mqtt,双主题 UI) |
|
||||
|
||||
## 开发环境
|
||||
|
||||
- **vd960Loop**:Keil MDK / AT32 IDE(ARM Cortex-M4,ARMCC V5)、FreeRTOS V10.4.3
|
||||
- **vd960DBN**:MounRiver Studio(RISC-V)、WCH BLE 协议栈 + WCHNET
|
||||
- 调试口:Loop MCU TTL Tx(3.3V 9600 8N1);DBN 侧 PRINT 串口
|
||||
|
||||
## 版本发布流程
|
||||
|
||||
1. 确认 `vd960Loop` 与 `vd960DBN` 固件按同一协议版本联调通过;
|
||||
2. 更新两侧 `cmcng.h` 的 `FIRMWARE_VER`;
|
||||
3. 更新 `CHANGELOG.md` 与各 devlog 修订记录;
|
||||
4. 打 tag(`vX.Y.Z`)推送 Gitea,并在 Gitea Releases 挂发布说明与固件产物(hex/bin)。
|
||||
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
|
||||
|
||||
> 当前发布:**vd960DBN V1.02.04**(2026-08-19,DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 产品概述
|
||||
|
||||
DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支持网口(TCP JSON / IoT MQTT)、蓝牙(小程序配置 + OTA)、4G 扩展(RFU),面向停车场出入口控制、流量计数、会车控制等场景。
|
||||
|
||||
产品型号:**DLD960GA**
|
||||
|
||||
## 硬件架构(双 MCU)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
线圈1~4 ──────────► AT32F421 (vd960Loop) │
|
||||
(TMR3 输入捕获) │ · 四路线圈驱动/频率采样 │
|
||||
继电器1~4 ◄───────│ · 车辆检测算法(IIR/斜率限幅/基线跟踪) │
|
||||
└───────────────┬─────────────────────────┘
|
||||
│ UART 0x7F 协议 @192000
|
||||
┌───────────────▼─────────────────────────┐
|
||||
蓝牙(小程序) ◄────► CH32V208 (vd960DBN) │
|
||||
网口 ETH ◄────►│ · TCP JSON :5960 / IoT MQTT 双主题 │
|
||||
4G(RFU) ◄────►│ · BLE 配置 + OTA、Loop MCU ISP 透传 │
|
||||
│ · 参数/日志外部存储 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
| 芯片 | 型号 | 主频 | Flash | SRAM | 职责 |
|
||||
|------|------|------|-------|------|------|
|
||||
| 检测 MCU | AT32F421 (Cortex-M4) | 120MHz | 64KB | 16KB | 四路线圈信号采集与车辆检测算法 |
|
||||
| 通信 MCU | CH32V208 (RISC-V) | 144MHz(实际 120MHz) | 128KB | 64KB | 蓝牙、网口、双路 TTL 串口(内部互联 + 4G 扩展)、外部存储 |
|
||||
|
||||
- 四路线圈、四个双路继电器,每路线圈两级调频电容(33nF/10nF)。
|
||||
- 支持 OTA:CH32V208 经蓝牙小程序 OTA;AT32F421 经 CH32V208 串口透传 ISP 升级。
|
||||
- 地感配置存储于 AT32F421,可经 小程序↔BLE↔CH32V208↔UART↔AT32F421 链路读写。
|
||||
|
||||
详见 [docs/DLD960硬件资源.md](docs/DLD960硬件资源.md)。
|
||||
|
||||
## 子项目
|
||||
|
||||
| 子项目 | 目录 | MCU | 固件版本 | 功能 |
|
||||
|--------|------|-----|---------|------|
|
||||
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
|
||||
| vd960DBN | `vd960DBN/` | CH32V208 | 1.02.04 | 蓝牙管理、TCP/IP(WCHNET)、MQTT、串口桥接、4G 扩展(RFU) |
|
||||
|
||||
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
|
||||
|
||||
## 产品文档(docs/)
|
||||
|
||||
| 文档 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.02 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 |
|
||||
| [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.02 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 |
|
||||
|
||||
## 协议文档(docs/)
|
||||
|
||||
| 文档 | 版本 | 适用层 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| [DLD960Loop_串口通信协议.md](docs/DLD960Loop_串口通信协议.md) | V1.05 | Loop MCU ↔ DBN MCU | 0x7F 帧、0xC0 传感主动上报、variation 3B 有符号 |
|
||||
| [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 TTL 串口 | 设备管理、参数配置、数据上报 |
|
||||
| [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.03 | 以太网 TCP :5960 | 鉴权 + 命令 + event_report 客户端必答 + 脱机日志(事件/快照流) |
|
||||
| [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.07 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步、脱机日志(事件/快照流) |
|
||||
| [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.02 | 蓝牙 BLE | 帧格式 + 分包 + 脱机日志(OFFLOG_STAT/QUERY/CLEAR)+ 传感快照(SNAP_STAT/QUERY/CLEAR,0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
|
||||
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
|
||||
|
||||
## 开发文档
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [vd960Loop/docs/devlog.md](vd960Loop/docs/devlog.md) | 检测算法开发日志(IIR、斜率限幅、基线冻结、时间量单位演进) |
|
||||
| [vd960DBN/docs/devlog.md](vd960DBN/docs/devlog.md) | 通信固件开发日志(WCHNET、MQTT 稳定性、event_report 可靠上报) |
|
||||
| [vd960Loop/docs/acceptance-standard.md](vd960Loop/docs/acceptance-standard.md) | 检测算法验收标准 |
|
||||
| [vd960Loop/docs/variation-analysis.md](vd960Loop/docs/variation-analysis.md) | variation 上报量分析报告(基线 vs 当前值三时钟关系) |
|
||||
| [vd960DBN/docs/incidents/](vd960DBN/docs/incidents/) | 现场事故分析归档 |
|
||||
|
||||
## 配套工具
|
||||
|
||||
| 工具 | 目录 | 说明 |
|
||||
|------|------|------|
|
||||
| DBNetClient | `DBNetClient/` | TCP JSON 协议桌面测试工具(Python + tkinter,零第三方依赖) |
|
||||
| DBNMQTTool | `DBNMQTTool/` | IoT MQTT 协议测试工具(Python + PySide6 + paho-mqtt,双主题 UI) |
|
||||
|
||||
## 开发环境
|
||||
|
||||
- **vd960Loop**:Keil MDK / AT32 IDE(ARM Cortex-M4,ARMCC V5)、FreeRTOS V10.4.3
|
||||
- **vd960DBN**:MounRiver Studio(RISC-V)、WCH BLE 协议栈 + WCHNET
|
||||
- 调试口:Loop MCU TTL Tx(3.3V 9600 8N1);DBN 侧 PRINT 串口
|
||||
|
||||
## 版本发布流程
|
||||
|
||||
1. 确认 `vd960Loop` 与 `vd960DBN` 固件按同一协议版本联调通过;
|
||||
2. 更新两侧 `cmcng.h` 的 `FIRMWARE_VER`;
|
||||
3. 更新 `CHANGELOG.md` 与各 devlog 修订记录;
|
||||
4. 打 tag(`vX.Y.Z`)推送 Gitea,并在 Gitea Releases 挂发布说明与固件产物(hex/bin)。
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# DLD960 BLE 通信协议(脱机事件日志 + 传感快照)
|
||||
|
||||
> 版本: V1.02(2026-08-12,新增传感快照 3 命令;分包上限随协商 MTU 动态)
|
||||
> 适用: vd960DBN(CH32V208,WCH BLE 协议栈)
|
||||
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Qxx 环形日志(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
|
||||
|
||||
---
|
||||
|
||||
## 1 帧格式
|
||||
|
||||
与既有 BLE 配置命令一致:
|
||||
|
||||
```
|
||||
Magic | Header | Data | CheckByte
|
||||
| Addr/Sub Len CMD | | Xor Sum
|
||||
1 Byte | 1B 1B 1B | xx | 1B 1B
|
||||
```
|
||||
|
||||
- `Len = len(CMD) + len(Data)`(即 `pkg[2] = 1 + data_len`)
|
||||
- 校验:`Xor = XOR(pkg[1..Len+2])`,`Sum = SUM(pkg[1..Len+2])`,覆盖 header + cmd + data(不含 magic)
|
||||
- 本命令族 Magic = `0x8F`(MAGIC_BYTE_DBN_DEFAULT)
|
||||
|
||||
### 分包(长响应)
|
||||
|
||||
响应单包数据上限**随协商 MTU 动态变化**(V1.01,修复"Too large noti 丢包"):`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B)。分包时 header 字节 = `(pkg_amount << 4) | pkg_seq`,`pkg_seq` 从 1 递增;收端按 `pkg_amount`/`pkg_seq` 重组,最后一片 `pkg_amount == pkg_seq`。
|
||||
|
||||
| 协商 MTU | 单包最大 dat | 整包最大长度 |
|
||||
|---------|-------------|-------------|
|
||||
| 23(未协商) | 14 | 20 |
|
||||
| 96(常见 Android) | 87 | 93 |
|
||||
| 185(iOS 常见) | 94 | 100 |
|
||||
|
||||
脱机日志 QUERY 响应最大 130B:MTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
|
||||
|
||||
> 分包发送由 `performPeriodicTask`(50ms TMOS 周期)主动续传,两包间隔 ≤50ms,不依赖收包事件(2026-08-12 修复)。
|
||||
|
||||
---
|
||||
|
||||
## 2 命令码
|
||||
|
||||
| 命令码 | 名称 | 方向 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) |
|
||||
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
|
||||
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
|
||||
| `0x28` | `SNAP_STAT` | APP→设备 | 查询传感快照统计(分页定位) |
|
||||
| `0x29` | `SNAP_QUERY` | APP→设备 | 按全局序号分页拉取快照记录 |
|
||||
| `0x2A` | `SNAP_CLEAR` | APP→设备 | 清除快照(审计写入事件流) |
|
||||
|
||||
> 与 MQTT V1.06 / TCP JSON V1.02 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。
|
||||
|
||||
---
|
||||
|
||||
## 3 查询日志统计 `OFFLOG_STAT` (0x25)
|
||||
|
||||
**请求 data:** 无(`Len=1`,仅 cmd 字节)。
|
||||
|
||||
**响应 data(19B,全小端):**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=日志未启用(Flash 初始化失败) |
|
||||
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
|
||||
| 3 | 4 | `count` | 有效记录条数(0~8064,环形覆盖后 < capacity) |
|
||||
| 7 | 4 | `capacity` | 容量上限(8064) |
|
||||
| 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity) |
|
||||
| 7 | 4 | `capacity` | 容量上限(**随存储芯片动态**:W25Q32=16256 / W25Q64=32640 / W25Q128=65408 / W25Q256=130944) |
|
||||
|
||||
示例(boot_seq=2, count=1234, capacity=16256, seq_first=100, seq_last=1333):
|
||||
|
||||
```
|
||||
8F 00 14 25 00 02 00 D2 04 00 00 00 80 3F 00 00 00 64 00 00 00 35 05 00 00 00 XX XX
|
||||
-- -- -- -- -------------------- -------------------- -------------------- -----
|
||||
| | | | status count=1234 capacity=16256 seq_first=100
|
||||
| | | | cmd=0x25 boot_seq=2 seq_last=1333
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4 分页拉取日志 `OFFLOG_QUERY` (0x26)
|
||||
|
||||
**请求 data(5B):**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界(< seq_first 或 > seq_last)返回 0 条 |
|
||||
| 4 | 1 | `count` | 拉取条数,**上限 4**,超限按 4;0 按 4 处理 |
|
||||
|
||||
**响应 data:**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=日志未启用;`0x02`=请求帧过短 |
|
||||
| 1 | 1 | `count` | 本次实际返回记录条数(0~4) |
|
||||
| 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §6) |
|
||||
|
||||
记录顺序 = 逻辑序号升序(与 `start_seq` 一致)。越界/空日志:`count=0`。
|
||||
|
||||
请求示例(start_seq=1330, count=4):
|
||||
|
||||
```
|
||||
8F 00 06 26 32 05 00 00 04 XX XX
|
||||
-- -- -- -- -- -- -- -- ---
|
||||
| | | | | | | | count=4
|
||||
| | | | +--+--+--+ start_seq=1330 (LE)
|
||||
| | | cmd=0x26
|
||||
| | Len=5+1=6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5 清除日志 `OFFLOG_CLEAR` (0x27)
|
||||
|
||||
**请求 data:** 无。
|
||||
|
||||
**响应 data(1B):** `status`:`0x00`=OK。
|
||||
|
||||
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——留痕不可清除)。设备侧应做权限控制(与 MQTT `log_clear` 同语义)。
|
||||
>
|
||||
> ⚠ **阻塞 ~2.8s**:63 个数据扇区 SPI 擦除(~45ms/扇区),期间主循环阻塞。若设备 IoT MQTT 在线,此期间 WCHNET 无法轮询,可能导致断连重连(60s keepalive 内可恢复)。请勿高频调用。
|
||||
|
||||
成功后 `OFFLOG_STAT` 的 `count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。
|
||||
|
||||
---
|
||||
|
||||
## 6 传感快照日志(0x28/0x29/0x2A,V1.02 新增)
|
||||
|
||||
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
|
||||
|
||||
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
|
||||
|
||||
### 6.1 查询快照统计 `SNAP_STAT` (0x28)
|
||||
|
||||
**请求 data:** 无(`Len=1`)。
|
||||
|
||||
**响应 data(19B,全小端):** 与 §3 `OFFLOG_STAT` 布局相同:
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=快照未启用 |
|
||||
| 1 | 2 | `boot_seq` | 当前启动序号 |
|
||||
| 3 | 4 | `count` | 有效记录条数(0~capacity) |
|
||||
| 7 | 4 | `capacity` | 容量上限(**随芯片动态**:W25Q32=48064 / W25Q64=105408 / W25Q128=220096 / W25Q256=449472) |
|
||||
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
|
||||
| 15 | 4 | `seq_last` | 最新一条全局序号 |
|
||||
|
||||
### 6.2 分页拉取快照 `SNAP_QUERY` (0x29)
|
||||
|
||||
**请求 data(5B):**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界返回 0 条 |
|
||||
| 4 | 1 | `count` | 拉取条数,**上限 2**(64B×2+2=130B ≤ 单包缓冲),超限按 2;0 按 2 |
|
||||
|
||||
**响应 data:**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=快照未启用;`0x02`=请求帧过短 |
|
||||
| 1 | 1 | `count` | 本次实际返回记录条数(0~2) |
|
||||
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §6.4) |
|
||||
|
||||
记录顺序 = 逻辑序号升序。QUERY 响应最大 130B:MTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
|
||||
|
||||
### 6.3 清除快照 `SNAP_CLEAR` (0x2A)
|
||||
|
||||
**请求 data:** 无。**响应 data(1B):** `status`:`0x00`=OK。
|
||||
|
||||
> ⚠ **高风险操作**:清除动作本身写入**事件流** `log_clear` 审计(payload[0]=2=快照流,不可清除)。
|
||||
> 阻塞 ~45ms:逻辑清除(count=0 旧数据立即不可读)+ 只擦写指针起点扇区,其余扇区由环形写覆盖时自动擦(原擦全部 751 扇区 ~34s,会触发 IWDG 4s 复位——2026-08-12 修复)。
|
||||
|
||||
成功后 `SNAP_STAT` 的 `count` 归 0,`seq_last` 继续递增(序号不复位)。
|
||||
|
||||
### 6.4 快照记录格式(SnapRec,64B 定长,小端)
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `magic` | `0xA6`(区别于事件 `0xA5`) |
|
||||
| 1 | 1 | `len` | 线圈数据有效长度 = coil_count×12(0~48) |
|
||||
| 2 | 1 | `flags` | bit0=分包帧(预留) |
|
||||
| 3 | 1 | `rsvd` | 保留(0) |
|
||||
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
|
||||
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,采集时刻) |
|
||||
| 12 | 2 | `boot_seq` | 所属启动段 |
|
||||
| 14 | 2 | `rsvd2` | 保留(0) |
|
||||
| 16 | 48 | `coils` | 4×12B 线圈数据,与 0xC0 线上格式一致 |
|
||||
|
||||
**线圈 12B 单元**(与 0xC0 帧内每路线圈完全一致):
|
||||
|
||||
| 偏移(单元内) | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `cfg` | freq_level(2) + direction(1) + freq_type(1) + sensitivity(4) |
|
||||
| 1 | 1 | `cond` | condition(4) + loop_state(1) + car_state(1) + misc_type(2) |
|
||||
| 2 | 3 | `freq` | 频率,3B 小端无符号 |
|
||||
| 5 | 3 | `variation` | 变化量,3B 小端有符号补码(= Origin − CAPVD) |
|
||||
| 8 | 4 | `misc` | 杂项 4B 小端(misc_type=0 时=通过时间,50ms 单位) |
|
||||
|
||||
> 绝对时间回算:用事件流 `time_anchor`(boot_seq ↔ unix_ts 映射)+ 本记录 `boot_seq`/`ts_ms`。未同步段仅相对时间。
|
||||
|
||||
---
|
||||
|
||||
## 7 记录格式(OfflogEvt,32B 定长,小端)
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 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]` reason:1=断开 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 单位) | 线圈事件;sub:1=car_enter 2=car_leave 3=loop_cut 4=loop_restore |
|
||||
| `0x50` | time_anchor | —(`unix_ts` 即平台下发值,严格一致) | 时钟同步锚点 |
|
||||
| `0x70` | log_clear | — | 日志清除(审计,不可清除) |
|
||||
|
||||
> payload 内多字节为**大端**(与 offlog 写入侧一致),OfflogEvt 其余字段为**小端**(CPU 原生序,直接 memcpy)。
|
||||
|
||||
---
|
||||
|
||||
## 8 版本历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |
|
||||
| V1.01 | 2026-08-12 | 分包上限改为随协商 MTU 动态:`chunk = min(MTU-9, 94)`;修复 Too large noti 丢包 |
|
||||
| V1.02 | 2026-08-12 | 新增传感快照 3 命令:SNAP_STAT / SNAP_QUERY / SNAP_CLEAR(0x28/0x29/0x2A) |
|
||||
@@ -0,0 +1,238 @@
|
||||
# DLD960 BLE 日志查询交互过程(实测示例)
|
||||
|
||||
> 版本: V1.00(2026-08-12)
|
||||
> 适用: vd960DBN(CH32V208)+ 微信小程序
|
||||
> 目的: 以**真实串口日志**为样例,完整描述"小程序发指令 → 设备处理 → 设备回包"的交互过程,供固件/小程序两端排查对照
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| 185(iOS 常见) | 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` | ckb:Xor=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` = 16256(W25Q32 512KB:127 数据扇区 × 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 = 1(LE32,含) |
|
||||
| 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** > 单包上限 87B(MTU=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) |
|
||||
@@ -110,6 +110,12 @@ dld960/{dev_serial}/{direction}
|
||||
- 校准前(含首个 `initialize`),`ts` 为上电秒数;平台应能容忍并可按数量级区分。
|
||||
- 设备重启后需重新校准(无掉电保持)。平台在每次设备 `initialize` 后都应下发一次带 `ts` 的 `report_config`。
|
||||
|
||||
**脱机日志时间戳语义(V1.06 起)**:
|
||||
|
||||
- 设备侧事件日志(`log_query` 拉取)每条记录携带双时间戳:`ts_ms`(boot 内相对时间,单位 ms,断电归零)+ `unix_ts`(已同步 Unix 秒,**0 = 未同步**)。
|
||||
- 设备经本协议同步成功后,其后记录的 `unix_ts` 均为真实 Unix 时间;同步前(含每次上电的 `boot` 事件)`unix_ts = 0`,仅 `ts_ms` 相对时间。
|
||||
- 回算规则:`绝对时间 = 锚点.unix_ts + (记录.ts_ms - 锚点.ts_ms)/1000`,锚点取该 boot 段内第一条 `time_anchor` 事件(`unix_ts` 即平台下发值,严格一致)。从未同步过的 boot 段只有相对时间。
|
||||
|
||||
---
|
||||
|
||||
# 3 命令详表
|
||||
@@ -131,6 +137,9 @@ dld960/{dev_serial}/{direction}
|
||||
| `loop_param_set` | 设置车检器多路参数 | srv→dev | 0x63 |
|
||||
| `loop_param_query` | 读取车检器多路参数 | srv→dev | 0x64 |
|
||||
| `report_config` | 设置主动上报 | srv→dev | 0xC5 |
|
||||
| `log_stat` | 查询脱机日志统计(事件/快照流) | srv→dev | — |
|
||||
| `log_query` | 分页拉取脱机日志(事件/快照流) | srv→dev | — |
|
||||
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | srv→dev | — |
|
||||
| `initialize` | 设备上电初始化登陆 | dev→srv | — |
|
||||
| `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 |
|
||||
| `event_report` | 事件上报(**平台须应答**,见 §5.3) | dev→srv | — |
|
||||
@@ -639,6 +648,156 @@ dld960/{dev_serial}/{direction}
|
||||
|
||||
---
|
||||
|
||||
## 4.16 查询脱机日志统计 `log_stat`
|
||||
|
||||
> Topic: `dld960/{sn}/srv`
|
||||
> 设备本地 W25Qxx 环形日志(事件区/快照区容量随存储芯片动态,掉电不丢)。用于日志拉取前的分页定位。
|
||||
> 通过 `data.stream` 区分日志流:`event`(事件日志,缺省)/ `snapshot`(传感快照)。
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 16,
|
||||
"cmd": "log_stat",
|
||||
"ts": 1719000000,
|
||||
"data": {
|
||||
"stream": "event"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
|
||||
|
||||
**响应 data(stream=event):**
|
||||
|
||||
```json
|
||||
{
|
||||
"stream": "event",
|
||||
"enabled": true,
|
||||
"boot_seq": 2,
|
||||
"count": 1234,
|
||||
"capacity": 16256,
|
||||
"seq_first": 100,
|
||||
"seq_last": 1333
|
||||
}
|
||||
```
|
||||
|
||||
**响应 data(stream=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 协议》§7:magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
|
||||
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4:magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
|
||||
|
||||
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算(见 §2.3)。
|
||||
|
||||
---
|
||||
|
||||
## 4.18 清除脱机日志 `log_clear`
|
||||
|
||||
> Topic: `dld960/{sn}/srv`
|
||||
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——谁在何时清了日志,留痕不可清除)。平台侧应做权限控制。
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 18,
|
||||
"cmd": "log_clear",
|
||||
"ts": 1719000000,
|
||||
"data": {
|
||||
"stream": "event"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
|
||||
|
||||
**响应:** 标准成功/失败。
|
||||
|
||||
- `stream=event`:成功后 `log_stat` 的 `count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。**阻塞 ~2.8s**(63 个数据扇区 SPI 擦除),请勿高频调用。
|
||||
- `stream=snapshot`:成功后 `log_stat` 的 `count` 归 0,`seq_last` 继续递增;清除动作写入事件流审计(`log_clear`,payload 标记快照流)。**阻塞 ~45ms**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
|
||||
|
||||
---
|
||||
|
||||
# 5 设备主动上报
|
||||
|
||||
## 5.1 设备上电登陆信息 `initialize`
|
||||
@@ -893,4 +1052,6 @@ dld960/{dev_serial}/{direction}
|
||||
| V1.03 | 2026-07-09 | 增加设备上电初始化指令 | wangfq |
|
||||
| V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq |
|
||||
| V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq |
|
||||
| V1.06 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);§2.3 补充日志双时间戳语义(`ts_ms` 相对 + `unix_ts` 已同步,0=未同步,锚点回算规则) | wangfq |
|
||||
| V1.07 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤1(4 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32(W25Q256 事件流 130944 超 16bit) | wangfq |
|
||||
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
# DLD960 MQTT 远程 OTA 协议(Loop MCU 先存后刷)
|
||||
|
||||
> 文档版本 V1.01(设计稿·按修改意见修订)· 2026-08-20 · 适用范围:并入《DLD960 IoT 接口协议(MQTT + JSON)》V1.08 后生效
|
||||
> 依据:`docs/ROADMAP.md` P1.4 ① —— Loop MCU (AT32F421) 远程 OTA,v1.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 bootloader(2026-08-19 修复闭环,V1.02.03) |
|
||||
| 本轮目标 | MQTT → DBN 分片下载到 W25Qxx 暂存区 → 校验后**本地** ISP 刷写 Loop |
|
||||
| 暂存介质 | DBN 外挂 W25Qxx(SPI1),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) 自身 OTA(ROADMAP 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 ≤ 254(iap.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 true,xorout `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 原始字节小写 hex,512 字符 |
|
||||
|
||||
**设备行为:**
|
||||
1. `offset == received`(顺序片)→ 单片 CRC32 校验 → 写 W25Qxx(256B 页对齐)→ `received += 256`(≥size 时截断为 size)→ 回 `code=0`
|
||||
2. `offset < received`(重复片,平台重发)→ **幂等直接回 `code=0`**,不重写
|
||||
3. `offset > received`(缺片/乱序)→ 回 `code=1` + `data.offset=received`(指示平台从该处续传;协议不要求乱序重组,降低设备复杂度)
|
||||
4. 单片 CRC 失败 → 回 `code=1` + `data.err_code=1`(平台重发本片;连续失败由平台策略控制,可 abort)
|
||||
5. 会话未开始 / 状态非 downloading → `code=3`(先 `ota_begin`)
|
||||
6. `data` 非 512 hex 字符 / offset 非 256 对齐 / 超 size → `code=1` 参数错误
|
||||
|
||||
**响应:** 标准成功/失败 + data 回显 `{offset, received}`。
|
||||
|
||||
> 单片大小 256B 是协议常量,**不接受协商**(避免双方尺寸漂移)。若未来单片加大需协议升版。
|
||||
|
||||
## 5.3 结束下载 `ota_end`
|
||||
|
||||
> Topic: `dld960/{sn}/srv`
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 403,
|
||||
"cmd": "ota_end",
|
||||
"ts": 1719000003,
|
||||
"data": {
|
||||
"target": "loop",
|
||||
"crc32": 305419896
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**设备行为:**
|
||||
1. `received != size` → `code=1` + `data.offset=received`(不完整,续传)
|
||||
2. `received == size` → 读回暂存区全镜像计算 CRC32,与 ota_begin 声明值比对
|
||||
- 一致 → 元数据 `state=ready, last_result=0` → `code=0, data={crc_ok:true}`
|
||||
- 不一致 → 元数据 `state=downloading`(保留已下载数据,可重发错片)→ `code=5, data={crc_ok:false}`(msg 注明 CRC 不匹配)
|
||||
|
||||
## 5.4 中止会话 `ota_abort`
|
||||
|
||||
> Topic: `dld960/{sn}/srv`
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 404,
|
||||
"cmd": "ota_abort",
|
||||
"ts": 1719000004,
|
||||
"data": { "target": "loop" }
|
||||
}
|
||||
```
|
||||
|
||||
设备行为:元数据 `state=aborted`,Slot 标记可覆盖;正在刷写时 abort → 停止发送后续 A7 块(Loop 端由 bootloader 超时复位回 APP 兜底)。响应标准成功。
|
||||
|
||||
## 5.5 触发刷写 `ota_flash`
|
||||
|
||||
> Topic: `dld960/{sn}/srv`
|
||||
> ⚠ **会车安全关键命令**:平台应确认现场允许(无车压线圈、非高峰)再下发。
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 405,
|
||||
"cmd": "ota_flash",
|
||||
"ts": 1719000005,
|
||||
"data": {
|
||||
"target": "loop",
|
||||
"slot": "a",
|
||||
"force": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `slot` | string | `a` / `b`(缺省 = 当前 ready 的槽) |
|
||||
| `force` | bool | `true` = 跳过安全窗口检查(高风险,平台授权) |
|
||||
|
||||
**设备行为(同步检查 → 异步刷写):**
|
||||
|
||||
1. **安全窗口检查**(force=false 时):
|
||||
- 4 通道 Loop 有车(VD_FLAG 任一置位)→ `code=3` + `data.err_code=1`(有车,拒绝;平台可提示"车辆离开后重试")
|
||||
- 刷写期间会阻断检测与继电器控制 → 建议平台在低峰执行
|
||||
2. 元数据非 ready → `code=3`(先 `ota_end` 完成校验)
|
||||
3. 通过 → 立即回 `code=0`(异步),进入刷写:
|
||||
- 写 offlog 事件日志:`固件升级开始`(target/slot/version/size)
|
||||
- **暂停事件上报与脱机日志(会话期间)**:MQTT `event_report` 暂停发送(入队积压,16 深溢出丢最旧,会话结束恢复后补发);offlog/快照落盘暂停——理由:刷写期间 Loop 复位不产生检测事件,DBN 让出 SPI 总线/主循环给刷写状态机,保证保活与喂狗
|
||||
- 维持 Loop 现状:复位进 bootloader 后的 GPIO/继电器状态与现网 BLE OTA 升级完全一致(该通道已验证可用),不额外干预
|
||||
- 发 `9F 01 00 01 A5 A7` 启动帧 → Loop APP 写 flag 复位 → bootloader 回 pre_ok
|
||||
- 发 A6 地址帧(`0x08003400` 4 字节大端)→ addr_ok
|
||||
- 从 W25Qxx 读镜像,按 248B/块发 A7(停等 ACK,1s 超时重发 ×3)
|
||||
- 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP
|
||||
4. 进度经 `ota_report` 上行(见 §6);失败重试 ×3 仍失败 → 元数据 `state=flash_failed` + `event_report{type:ota_error}` 告警(平台必答)
|
||||
|
||||
**响应:** 标准成功/失败(`code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report`/`ota_status` 为准)。
|
||||
|
||||
## 5.6 查询状态 `ota_status`
|
||||
|
||||
> Topic: `dld960/{sn}/srv`
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{ "msg_id": 406, "cmd": "ota_status", "ts": 1719000006 }
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
|
||||
```json
|
||||
{
|
||||
"target": "loop",
|
||||
"state": "flashing",
|
||||
"slot": "a",
|
||||
"size": 46864,
|
||||
"received": 46864,
|
||||
"crc32": 305419896,
|
||||
"version": "1.1.0",
|
||||
"progress": { "sent": 42112, "total": 46864 },
|
||||
"last_result": 0,
|
||||
"last_error": 0
|
||||
}
|
||||
```
|
||||
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted` |
|
||||
| `progress.sent` | uint32 | 刷写阶段已送 Loop 的字节数 |
|
||||
| `last_result` | uint32 | 上次刷写结果:0=无/成功,非 0=错误码 |
|
||||
| `last_error` | uint32 | 上次失败细分错误码 |
|
||||
|
||||
**状态机(设备侧):**
|
||||
|
||||
```
|
||||
ota_begin(新会话) ota_data×N ota_end(CRC✓)
|
||||
IDLE ─────────────────▶ DOWNLOADING ────────────▶ READY
|
||||
▲ │ ▲ │
|
||||
│ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓)
|
||||
│ / flash_failed │ └──────────────┐ ▼
|
||||
└────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ (Loop 重启) ──▶ IDLE(清槽/保留)
|
||||
│
|
||||
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6 主动上报 `ota_report`
|
||||
|
||||
> Topic: `dld960/{sn}/dev` · QoS 1
|
||||
> 用途:刷写进度与结果主动推送(进度可丢,结果可经 `ota_status` 兜底查询;设备侧元数据持久化 last_result)
|
||||
|
||||
**上报:**
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 501,
|
||||
"cmd": "ota_report",
|
||||
"ts": 1719000007,
|
||||
"data": {
|
||||
"target": "loop",
|
||||
"stage": "flashing",
|
||||
"progress": { "sent": 42112, "total": 46864 },
|
||||
"code": 0,
|
||||
"msg": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| data 字段 | 说明 |
|
||||
|-----------|------|
|
||||
| `stage` | `begin`(会话开启)/ `downloading`(片落盘)/ `ready`(校验通过)/ `flashing`(刷写中)/ `done`(刷写成功,Loop 已重启)/ `failed`(刷写失败) |
|
||||
| `progress` | `sent`/`total` 字节(刷写阶段) |
|
||||
| `code` | stage 相关结果码 |
|
||||
|
||||
**上报节奏**:`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按块进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 msg_id/ts),平台去重窗口建议 10 分钟(与 event_report 同策略)。
|
||||
|
||||
## 6.1 失败告警(扩展 event_report)
|
||||
|
||||
刷写整体失败(重试 ×3 仍失败)时,设备经 `event_report` 上报(**平台必答**,复用 §5.3 ACK + 重发机制):
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 502,
|
||||
"cmd": "event_report",
|
||||
"ts": 1719000008,
|
||||
"data": {
|
||||
"events": [
|
||||
{ "type": "ota_error", "ch": 0, "value": 1003 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| type | value | 说明 |
|
||||
|------|-------|------|
|
||||
| `ota_error` | 0x1000 起 | `0x1001`=启动帧无响应 / `0x1002`=地址帧错误 / `0x1003`=数据块 ACK 超限 / `0x1004`=全镜像校验失败 / `0x1005`=安全窗口拒绝后强制失败 |
|
||||
|
||||
---
|
||||
|
||||
# 7 安全设计(会车产品底线)
|
||||
|
||||
| # | 措施 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | **升级窗口检查** | `ota_flash` 前检查 4 通道无车;有车 → `code=3`,平台提示延迟。`force=true` 可跳过(高风险,需平台权限控制) |
|
||||
| 2 | **维持 Loop 现状** | 刷写期间 Loop 行为与现网 BLE OTA 升级一致(复位进 bootloader → ISP 刷写 → 复位跑新 APP),继电器 GPIO 状态按当前硬件实测状态接受,不额外干预(BLE 通道已验证可用) |
|
||||
| 3 | **双重 CRC** | 单片 CRC32(传输层抓错)+ 全镜像 CRC32(落盘完整性);刷写前读回复核 |
|
||||
| 4 | **失败可重入** | 下载失败 → 断点续传;刷写失败 → 重试 ×3 → 保留镜像可重刷;AT32 ISP bootloader 兜底,不会真砖 |
|
||||
| 5 | **版本回滚** | Slot A/B 双槽;平台可 `ota_begin` 指定槽刷旧版(`force=true` 覆盖) |
|
||||
| 6 | **审计留痕** | 升级开始/结果写 offlog 事件日志(含目标版本、slot、结果码);平台侧 version 字段归档 |
|
||||
| 7 | **能力探测** | 老固件 `ota_*` 回 `code=4`;平台按 `ota_status`/`dev_info_query.soft_ver` 判断,不盲目下发 |
|
||||
| 8 | **幂等与防呆** | 重复片幂等 ACK;offset 乱序拒绝并要求续传;同版本默认拒绝重刷(force 绕过) |
|
||||
| 9 | **会话期间静默** | OTA 会话期间(begin ~ 结束)暂停 MQTT `event_report` 发送(队列积压,结束后补发)与 offlog/快照落盘("升级开始"日志在暂停前写入、"升级结果"在恢复后补记);避免刷写窗口与上报/日志抢 SPI 总线与主循环 |
|
||||
|
||||
---
|
||||
|
||||
# 8 错误码约定
|
||||
|
||||
顶层 `code` 沿用通用语义(0 成功 / 1 参数 / 2 密码 / 3 忙 / 4 不支持 / 5 内部 / 6 超长),OTA 细分错误经 `data.err_code` 表达:
|
||||
|
||||
| err_code | 场景 | 顶层 code |
|
||||
|----------|------|-----------|
|
||||
| 1 | 单片 CRC 失败 / 乱序缺片(data.offset 指示续传点) | 1 |
|
||||
| 2 | 会话状态不允许(未 begin / 非 downloading 收 ota_data) | 3 |
|
||||
| 3 | 安全窗口拒绝(有车压线圈) | 3 |
|
||||
| 4 | 版本冲突(同版本且非 force) | 1 |
|
||||
| 5 | 全镜像 CRC 不匹配(data.crc_ok=false) | 5 |
|
||||
| 6 | 暂存区写失败(SPI 异常/满) | 5 |
|
||||
|
||||
---
|
||||
|
||||
# 9 平台侧实现要点(edc_server / DBNMQTTool)
|
||||
|
||||
1. **hex↔bin**:`data` hex 解码(512 hex = 256B);bin 由 Intel HEX 转换(固定 0x08003400 偏移),转换在平台完成
|
||||
2. **CRC32**:`zlib.crc32(bin)` 全镜像;单片 `zlib.crc32(chunk)` 与设备逐字节一致(ISO-HDLC 即 Python 内置)
|
||||
3. **分片循环**:`for offset in range(0, size, 256)` 顺序下发,收到 `code=1 + data.offset` 时从该处续传;单片失败重发 ≤3 次后 abort
|
||||
4. **断点续传**:会话中断后重新 `ota_begin`,读 `data.offset` 续传
|
||||
5. **结果确认**:刷写启动后轮询 `ota_status`(或订阅 `ota_report`);`stage=done` 后经 dev_info_query 核对 Loop 版本(需 Loop 侧支持版本上报,见 §10 待办)
|
||||
6. **告警闭环**:收到 `event_report{type:ota_error}` 先落库后应答
|
||||
7. DBNMQTTool 增加 OTA 页签(工具先行惯例:先模拟分片下发,固件后到)
|
||||
|
||||
---
|
||||
|
||||
# 10 待板上验证项(实现前必须闭环)
|
||||
|
||||
| # | 项 | 影响 |
|
||||
|---|-----|------|
|
||||
| 1 | DBN UART2 TX 缓冲容量(能否容纳 254B A7 帧;现 BLE 透传块 ≤94B) | 刷写块大小 |
|
||||
| 2 | W25Qxx 写 256B 页 + 4KB 扇区擦除在 MQTT 接收回调内的耗时(阻塞窗口 vs MQTT 保活) | 单片落盘是否需移主循环 |
|
||||
| 3 | DBN RAM 预算:镜像块缓冲(248B)+ CRC 查表(1KB)是否挤占现有栈余量(历史 .bss 事故) | 内存方案 |
|
||||
| 4 | Loop APP 当前固件实际 bin 大小(验证 100KB 槽余量) | 槽位容量 |
|
||||
| 5 | 刷写期间 MQTT 保活验证(设计已保证非阻塞 tick 驱动,PINGREQ/心跳/IWDG 喂狗不受阻塞;板级抓包确认 6~8s 窗口内无 PINGREQ 超时断连、IWDG 不复位) | 刷写中断安全 |
|
||||
|
||||
---
|
||||
|
||||
# 11 与现有协议的关系
|
||||
|
||||
| 协议文档 | 变更 |
|
||||
|----------|------|
|
||||
| `DLD960_IoT_MQTT协议.md` | 本设计并入后升 **V1.08**:§3 命令表 + §4.19~4.24(ota_* 详情)+ §5.5(ota_report)+ §5.3 事件类型表补 `ota_error` + §8 错误码补 err_code 约定 + 修订记录 |
|
||||
| `DLD960_TCP_JSON协议.md` | 本版**不扩展**(ROADMAP 先走 MQTT);后续按"复用命令 + stream/字段"模式补(与 log_* 扩展同套路) |
|
||||
| `DLD960_BLE协议.md` | 不变(BLE OTA 维持流式透传现状;本地刷写状态机与 BLE 透传共用 `lup_feed_byte_ota`) |
|
||||
| `README.md` / `DLD960_技术规格书.md` | 协议矩阵同步 V1.08;规格书补 OTA 分区/安全约束(并入主文档时执行) |
|
||||
|
||||
**固件代码落点(协议拍板后动):**
|
||||
- `iot_mqtt_srv.c`:命令分发链加 6 个 ota_* 分支(现有 if-else 链)
|
||||
- 新增 `ota_srv.c/h`:会话状态机 + W25Qxx 暂存读写 + CRC32 查表
|
||||
- `usart_biz.c` / `loop_uart_proto.c`:本地刷写状态机(复用 `lup_feed_byte_ota` 解析 + `g_flag_counter_ota` 门控,注意退出机制——现 BLE 透传 flag 无退出点,本地刷写必须自管)
|
||||
- `offlog.c`:补"固件升级开始/结果"事件类型
|
||||
- 单测:`tests/test_ota_srv.c`(gcc 提取+嵌入模式,mock SPI/MQTT/Loop)
|
||||
|
||||
---
|
||||
|
||||
# 修订记录
|
||||
|
||||
| 版本 | 时间 | 说明 |
|
||||
|------|------|------|
|
||||
| V1.00 | 2026-08-20 | 设计稿:Loop MCU MQTT 远程 OTA 先存后刷协议(依据 ROADMAP P1.4 ①) |
|
||||
| V1.01 | 2026-08-20 | 按修改意见修订:①Slot A/B 容量 60KB→**100KB**(镜像上限 96KB,为 DBN 镜像预留)②删除"继电器 GPIO 默认态"待验证项,维持 Loop 现状(与 BLE OTA 行为一致,不额外干预)③新增"OTA 会话期间静默"约束:暂停 MQTT `event_report` 发送(队列积压结束后补发)+ offlog/快照落盘(开始/结果日志除外)④刷写调度明确**非阻塞 tick 驱动**,刷写窗口内 MQTT 保活(PINGREQ/心跳/IWDG 喂狗)不受影响 |
|
||||
@@ -119,6 +119,9 @@ Client Device (DLD960)
|
||||
| `loop_param_set` | 设置车检器多路参数 | 是 | 0x63 |
|
||||
| `loop_param_query` | 读取车检器多路参数 | 是 | 0x64 |
|
||||
| `report_config` | 设置主动上报 | 是 | 0xC5 |
|
||||
| `log_stat` | 查询脱机日志统计(事件/快照流) | 是 | — |
|
||||
| `log_query` | 分页拉取脱机日志(事件/快照流) | 是 | — |
|
||||
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | 是 | — |
|
||||
| 主动推送 | | | |
|
||||
| `loop_data` | 线圈传感数据(设备→客户端) | — | 0xC0 |
|
||||
| `event_report` | 事件上报(设备→客户端,**客户端须应答**,见 §5.2) | — | — |
|
||||
@@ -342,6 +345,106 @@ Client Device (DLD960)
|
||||
|
||||
---
|
||||
|
||||
## 4.16 查询脱机日志统计 `log_stat`
|
||||
|
||||
> 设备本地 W25Qxx 环形日志(事件区/快照区容量随存储芯片动态,掉电不丢)。用于日志拉取前的分页定位。
|
||||
> 通过 `data.stream` 区分日志流:`event`(事件日志,缺省)/ `snapshot`(传感快照)。
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{"msg_id":16,"cmd":"log_stat","ts":1719000000,"data":{"stream":"event"}}
|
||||
```
|
||||
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
|
||||
|
||||
**响应 data(stream=event):**
|
||||
|
||||
```json
|
||||
{"stream":"event","enabled":true,"boot_seq":2,"count":1234,"capacity":16256,"seq_first":100,"seq_last":1333}
|
||||
```
|
||||
|
||||
**响应 data(stream=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 协议》§7:magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
|
||||
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4:magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
|
||||
|
||||
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算。
|
||||
|
||||
---
|
||||
|
||||
## 4.18 清除脱机日志 `log_clear`
|
||||
|
||||
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——谁在何时清了日志,留痕不可清除)。客户端应做权限控制。
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{"msg_id":18,"cmd":"log_clear","ts":1719000000,"data":{"stream":"event"}}
|
||||
```
|
||||
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
|
||||
|
||||
**响应:** 标准成功/失败。
|
||||
|
||||
- `stream=event`:成功后 `log_stat` 的 `count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。**阻塞 ~2.8s**(63 个数据扇区 SPI 擦除),请勿高频调用。
|
||||
- `stream=snapshot`:成功后 `log_stat` 的 `count` 归 0,`seq_last` 继续递增;清除动作写入事件流审计(`log_clear`,payload 标记快照流)。**阻塞 ~45ms**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
|
||||
|
||||
---
|
||||
|
||||
# 5 设备主动推送
|
||||
|
||||
主动推送帧与请求-响应共用同一条 TCP 连接,由设备在任意时刻发出。
|
||||
@@ -457,3 +560,5 @@ Client Device (DLD960)
|
||||
|------|----------|----------|--------|
|
||||
| V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq |
|
||||
| V1.01 | 2026-07-15 | `event_report` 增加**客户端必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、客户端去重与先落库后应答要求(与 MQTT 协议 V1.04 对称) | wangfq |
|
||||
| V1.02 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);事件类型表与 MQTT 协议 V1.06 对齐 | wangfq |
|
||||
| V1.03 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤1(4 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32(W25Q256 事件流 130944 超 16bit) | wangfq |
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# DLD960 四通道车辆检测器 产品手册
|
||||
|
||||
> Product Manual · 产品型号:**DLD960GA**
|
||||
> 文档版本 V1.02 · 2026-08-19 · 适用固件:整机 V1.02.04(Loop 1.0 + DBN 1.02.04)
|
||||
|
||||
---
|
||||
|
||||
## 1. 产品简介
|
||||
|
||||
DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖四条车道/四个检测断面。采用双 MCU 架构:检测核心专注线圈信号的实时处理,通信核心提供蓝牙、以太网、4G 多种接入方式,可直连道闸控制器,也可对接云平台做流量统计与远程运维。
|
||||
|
||||
**典型应用场景**
|
||||
|
||||
- 停车场出入口道闸联动(存在检测、防砸车压线检测)
|
||||
- 分车道流量计数(进出计数、车间距/通过时间统计)
|
||||
- 单通道会车系统的车辆存在/方向输入
|
||||
- 传感数据上云(MQTT 接入业务平台)
|
||||
|
||||
**核心特性**
|
||||
|
||||
- 四路线圈独立检测,10ms 采样周期,进入确认 + 斜率限幅双重抗干扰
|
||||
- 四档灵敏度、四档工作频率,支持智能选频抑制相邻线圈串扰
|
||||
- 基线自适应跟踪:温漂、雨天频率漂移自动补偿,不误报不漏检
|
||||
- 四个双路继电器:存在/脉冲输出、方向判别、输出延时逐路可配
|
||||
- 蓝牙小程序现场配置,双 MCU 均可无线升级(OTA/ISP 透传)
|
||||
- 以太网双协议:TCP JSON(局域网对接)+ MQTT(云平台),关键事件确认送达
|
||||
- 通信可靠性加固:串口硬件 DMA 接收 + 固件内存优化,长时间运行稳定不丢帧、不异常重启
|
||||
- BLE OTA 升级链路加固:检测 MCU 升级响应帧(0x9F 协议)完整透传回小程序,停等 ACK 不再丢失,升级成功率提升
|
||||
|
||||
## 2. 硬件说明
|
||||
|
||||
### 2.1 接口一览
|
||||
|
||||
| 接口 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| 线圈接口 | 4 路 | 接环形地感线圈(LPA~LPD) |
|
||||
| 继电器输出 | 4 个双路 | RLY1~RLY4,默认关联线圈 1~4 |
|
||||
| 以太网口 | 1 路 | 10M,TCP JSON / MQTT |
|
||||
| TTL串口 | 1 路 | DLD960 内部 两个MCU之间的 串口通信协议 |
|
||||
| 蓝牙 | — | 小程序配置与升级 |
|
||||
| 按键 K1 | 1 个 | 多功能按键(复位) |
|
||||
|
||||
### 2.2 指示灯
|
||||
|
||||
| 指示灯 | 颜色 | 状态说明 |
|
||||
|--------|------|---------|
|
||||
| LED_PWR | 红色 | 呼吸闪烁 = 系统正常运行 |
|
||||
| LED_LP1~LP4 | 绿色 | 对应线圈 1~4:有车亮 / 无车灭 |
|
||||
| ETH_LED_G / ETH_LED_Y | 绿/黄 | 网口连接/活动指示 |
|
||||
|
||||
> 上电后 LED_PWR 呼吸、四路车检灯全灭(无车),约 1.3 秒完成基线建立进入检测状态。若某路车检灯常亮且现场无车,参见 §7 故障排查。
|
||||
|
||||
### 2.3 线圈施工要求(关键!)
|
||||
|
||||
线圈质量直接决定检测效果,施工务必达标:
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 线圈形状 | 矩形/八角形,典型 2m × 1m(依车道宽度调整) |
|
||||
| 匝数 | 4~6 匝(视周长,目标电感 80~300 μH) |
|
||||
| 线材 | ≥1.0mm² 耐高温多股软线(如硅橡胶线) |
|
||||
| 槽深 | 30~50mm,切槽后清理干净,线圈压实密封 |
|
||||
| 引线 | 双绞(≥20 绞/米),长度尽量短(≤100m),不与动力电缆同管 |
|
||||
| 相邻线圈间距 | ≥1m;无法保证时开启智能选频并错开频率档 |
|
||||
| 绝缘 | 线圈对地绝缘电阻 > 10 MΩ(500V 摇表) |
|
||||
|
||||
## 3. 快速上手
|
||||
|
||||
1. **接线**:接入线圈、继电器输出至道闸/控制器,需要联网则接网线。
|
||||
2. **上电**:LED_PWR 呼吸闪烁,约 1.3 秒后进入检测状态(上电 3 秒内不主动上报数据)。
|
||||
3. **配置**:手机小程序经蓝牙连接设备 → 验证密码 → 设置各路灵敏度/频率/输出模式。
|
||||
4. **验证**:车辆压线,对应绿色车检灯亮、继电器吸合;离开后灯灭、继电器释放。
|
||||
5. **联网(可选)**:小程序或 TCP 命令配置 IP/MQTT 参数,接入平台。
|
||||
|
||||
## 4. 参数配置
|
||||
|
||||
四种配置通道功能等价:**蓝牙小程序**(现场推荐)、**UART TTL 串口**、**TCP JSON**、**MQTT**(远程运维)。
|
||||
|
||||
### 4.1 每通道可配参数
|
||||
|
||||
| 参数 | 出厂默认 | 说明 |
|
||||
|------|---------|------|
|
||||
| 灵敏度 | 档位 1(中) | 4 档;大车/干扰大调低,摩托车/高底盘车调高 |
|
||||
| 工作频率 | 各路错开 | 4 档(高/中高/中低/低);相邻线圈必须错开档位 |
|
||||
| 输出模式 | 存在输出 | 存在 / 脉冲 |
|
||||
| 输出延时 | 0 | 继电器动作延时 |
|
||||
| 方向判别 | 触发模式 | 双线圈可组方向判别 |
|
||||
| 有限存在 | 0(永久) | 非 0 时,车辆持续占用超时后自动复位重建基线(单位 10 秒) |
|
||||
| 线圈安全模式 | 0(关闭) | 线圈异常保护超时(单位 10 秒) |
|
||||
|
||||
### 4.2 灵敏度选择建议
|
||||
|
||||
| 场景 | 建议档位 | 说明 |
|
||||
|------|---------|------|
|
||||
| 标准轿车出入口 | 1~2 | 兼顾灵敏与抗干扰 |
|
||||
| 大型车/货车通道 | 0~1 | 高底盘需要覆盖,但防相邻车道干扰 |
|
||||
| 摩托车/非机动车混行 | 2~3 | 高灵敏度,注意误触发风险 |
|
||||
| 强电磁干扰环境(变频器等) | 0~1 + 错开频率档 | 灵敏度让位于稳定性 |
|
||||
|
||||
### 4.3 网络配置要点
|
||||
|
||||
- **TCP JSON**:默认端口 **5960**,连接后须先 `pwd_verify` 鉴权,未鉴权命令一律拒绝。
|
||||
- **MQTT**:配置 Broker 地址与设备 Topic 后,设备上电自动发布 `initialize` 上线消息;平台应答 `report_config` 下发时间戳完成时钟同步并配置上报间隔。
|
||||
- 设备序列码(dev_serial)是 MQTT Topic 的组成部分(`dld960/{sn}/srv|dev`),出厂前须唯一设置。
|
||||
|
||||
## 5. 数据上报(联网模式)
|
||||
|
||||
| 消息 | 触发 | 内容 |
|
||||
|------|------|------|
|
||||
| `initialize` | 上电联网 | 序列码、型号、软硬件版本 |
|
||||
| `loop_data` | 周期性(空闲按配置间隔 / 有车活动 300ms / 车状态翻变立即) | 各通道频率、变化量、车状态 |
|
||||
| `event_report` | 事件发生 | 进车/出车/线圈断开,**带确认重发**(5s 超时 ×3 次,平台须应答) |
|
||||
| `heartbeat` | 周期 | 在线保活、运行时长 |
|
||||
|
||||
时间量(通过时间、车间距)分辨率 50ms,可用于速度估计与车型粗分类。
|
||||
|
||||
### 5.1 脱机数据取证(蓝牙 BLE)
|
||||
|
||||
设备本地 W25Q32 环形存储**事件日志**(容量 8064 条,掉电不丢)与**传感快照**(0xC0 传感波形按上报节奏落盘)。断网/故障期间事件照常记录,可事后经蓝牙小程序或 APP 读取:
|
||||
|
||||
- **事件日志**:上电/复位原因、MQTT 连接/断开/重连、event_report 重发/放弃、线圈事件、时钟同步锚点、日志清除审计——用于区分"设备真复位"与"MQTT 断连重连",故障溯源
|
||||
- **传感快照**:断网期间 4 路线圈波形回放,用于信号质量分析
|
||||
- **BLE 命令**:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR(0x25~0x27)+ SNAP_STAT / SNAP_QUERY / SNAP_CLEAR(0x28~0x2A),与 MQTT/TCP 的 log_stat / log_query / log_clear 同语义
|
||||
- **审计留痕**:日志/快照清除操作本身写入事件流,不可清除
|
||||
|
||||
详见《DLD960 BLE 协议》V1.02。
|
||||
|
||||
## 6. 固件升级
|
||||
|
||||
| 对象 | 方式 | 操作 |
|
||||
|------|------|------|
|
||||
| 通信 MCU | 蓝牙 OTA | 小程序选择固件直接升级 |
|
||||
| 检测 MCU | ISP 透传 | 小程序经蓝牙→通信 MCU 串口透传升级 |
|
||||
|
||||
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.04。
|
||||
>
|
||||
> ⚠ **检测 MCU 升级完成后,若小程序无响应,请将设备断电重启**(DBN OTA 透传模式需重启恢复,V1.02.03 已知约束)。
|
||||
|
||||
## 7. 故障排查
|
||||
|
||||
| 现象 | 可能原因 | 处理 |
|
||||
|------|---------|------|
|
||||
| LED_PWR 不亮/不呼吸 | 供电异常 / 主板故障 | 查电源电压,重新上电 |
|
||||
| 无车但车检灯常亮 | 线圈附近有大金属体移入;基线未收敛 | 移除金属物;等待 10s 冻结超时自适应,或断电重启重建基线 |
|
||||
| 有车不触发 | 灵敏度过低;线圈匝数不足/电感异常;底盘过高 | 提高灵敏度档位;实测线圈电感;摩托车场景用 3 档 |
|
||||
| 车检灯随机闪烁(无车误触发) | 相邻线圈串扰;变频器等电磁干扰;线圈绝缘劣化 | 错开频率档+开智能选频;降低灵敏度;摇表测绝缘 |
|
||||
| 雨天频繁误报 | 线圈密封失效,频率漂移超阈值 | 基线自适应可吸收慢漂移;快速漂移需重新密封线圈 |
|
||||
| 继电器输出正常但平台无数据 | 网络配置错误;MQTT Topic/序列码不符 | 查 IP/Broker 配置;核对 dev_serial 与 Topic |
|
||||
| 设备频繁自动重启 / 偶发数据丢失 | 通信 MCU 旧固件资源不足(V1.02.01 已修复) | 升级整机固件至 V1.02.01 及以上版本 |
|
||||
| BLE OTA 升级失败/进度卡住 | 通信 MCU 旧固件 0x9F 透传缺陷(V1.02.03 已修复) | 升级整机固件至 V1.02.03 及以上版本 |
|
||||
| 配置无法保存/每次上电恢复出厂 | 旧固件 factory 写入被禁用;换新存储芯片后参数区无出厂配置(V1.02.04 已修复) | 升级整机固件至 V1.02.04 及以上版本 |
|
||||
| 事件重复上报 | 平台未按协议应答 event_report | 平台须回显 msg_id 应答(先落库后应答) |
|
||||
| 线圈断开告警 | 线圈开路/引线断裂 | 万用表量线圈回路电阻(正常几欧姆以内) |
|
||||
|
||||
## 8. 技术支持信息
|
||||
|
||||
排障时请提供以下信息,可大幅加快定位:
|
||||
|
||||
1. 整机固件版本(小程序设备信息页 / `dev_info_query`)
|
||||
2. 现场线圈参数:尺寸、匝数、实测电感、引线长度
|
||||
3. 干扰源情况:变频器、大功率设备、相邻车道线圈间距
|
||||
4. 联网问题附 `loop_data` 中该通道的频率与变化量数值(后台可据此判断信号质量)
|
||||
|
||||
**相关文档**
|
||||
|
||||
| 文档 | 用途 |
|
||||
|------|------|
|
||||
| 《DLD960 技术规格书》 | 完整技术参数 |
|
||||
| 《DLD960 串口通信协议》V1.01 | 串口 对接开发 |
|
||||
| 《DLD960 TCP JSON 协议》V1.03 | 局域网对接开发 |
|
||||
| 《DLD960 IoT MQTT 协议》V1.07 | 云平台对接开发 |
|
||||
| 《DLD960 BLE 协议》V1.02 | 蓝牙小程序/APP 对接开发(脱机日志与传感快照读写) |
|
||||
| 《环路车辆检测器验收标准》 | 采购/部署验收 |
|
||||
|
||||
---
|
||||
|
||||
## 修订记录
|
||||
|
||||
| 版本 | 修订时间 | 修订说明 | 修订人 |
|
||||
|------|----------|----------|--------|
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04:BLE 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 |
|
||||
@@ -0,0 +1,191 @@
|
||||
# DLD960 四通道车辆检测器 技术规格书
|
||||
|
||||
> Technical Specification · 产品型号:**DLD960GA**
|
||||
> 文档版本 V1.02 · 2026-08-19 · 配套整机发布 V1.02.04
|
||||
|
||||
---
|
||||
|
||||
## 1. 产品概述
|
||||
|
||||
DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆检测器,采用双 MCU 架构:检测 MCU 专注线圈信号采集与检测算法的实时性,通信 MCU 承载蓝牙、以太网、4G 多通道接入与云平台对接。适用于停车场出入口控制、道闸联动、分车道流量计数、会车控制等场景。
|
||||
|
||||
## 2. 系统架构
|
||||
|
||||
```
|
||||
线圈1~4 ──► AT32F421(检测 MCU)──► 继电器1~4(存在/脉冲输出)
|
||||
│ UART 0x7F 私有协议 @192000bps
|
||||
▼
|
||||
CH32V208(通信 MCU)──► ETH / BLE / UART TTL / SPI Flash
|
||||
```
|
||||
|
||||
| 项目 | 检测 MCU | 通信 MCU |
|
||||
|------|---------|---------|
|
||||
| 型号 | 雅特力 AT32F421(ARM Cortex-M4) | 沁恒 CH32V208(RISC-V) |
|
||||
| 主频 | 120 MHz | 120 MHz(芯片最高 144 MHz,启用网络时限定 120 MHz) |
|
||||
| Flash / SRAM | 64 KB / 16 KB | 128 KB / 64 KB |
|
||||
| 职责 | 四路线圈驱动、频率采样、车辆检测算法、继电器输出 | BLE、以太网(TCP JSON / MQTT)、UART TTL(RX DMA)、参数与日志外部存储 |
|
||||
| RTOS/协议栈 | FreeRTOS V10.4.3 | WCH BLE 协议栈 + WCHNET TCP/IP |
|
||||
|
||||
双 MCU 间通过 TTL 串口互联(0x7F 私有帧协议,波特率 192000bps,UART2 RX 硬件 DMA 循环接收,抗打印关中断丢帧),传感数据主动上推、配置命令透传。OTA 升级时 UART2 RX 切换 0x9F 帧协议(DMA poll 按 `g_flag_counter_ota` 模式选择解析器),检测 MCU bootloader 的响应帧(pre_ok/addr_ok/data ACK)经 BLE 完整透传回小程序(V1.02.03)。
|
||||
|
||||
## 3. 检测规格
|
||||
|
||||
### 3.1 检测通道
|
||||
|
||||
| 参数 | 规格 |
|
||||
|------|------|
|
||||
| 检测通道数 | 4 路独立线圈通道 |
|
||||
| 检测原理 | LC 振荡频率法(TMR3 四通道输入捕获) |
|
||||
| 检测采样周期 | 10 ms/通道(算法主循环 tick) |
|
||||
| 工作频率档位 | 4 档(高 / 中高 / 中低 / 低),每路独立两级调频电容(33nF / 10nF)组合切换 |
|
||||
| 智能选频 | 支持(smart_mode,自动错开相邻通道频率,抑制串扰) |
|
||||
| 线圈电感适应范围 | 待确认(典型环形线圈 2~6 匝,需现场实测电感与振荡频率) |
|
||||
|
||||
### 3.2 灵敏度
|
||||
|
||||
进入阈值 = 基线 × SensTable[档位] / 65536,释放阈值带 ~50% 滞回(防止临界抖动)。
|
||||
|
||||
| 灵敏度档位 | 进入阈值(Δf/f) | 释放阈值(Δf/f) | 说明 |
|
||||
|-----------|-----------------|-----------------|------|
|
||||
| 0 | ≈ 0.330% | ≈ 0.165% | 最低灵敏度(大车/高底盘场景抗干扰) |
|
||||
| 1 | ≈ 0.165% | ≈ 0.110% | |
|
||||
| 2 | ≈ 0.055% | ≈ 0.027% | |
|
||||
| 3 | ≈ 0.015% | ≈ 0.014% | 最高灵敏度(摩托车/高底盘车检出) |
|
||||
|
||||
### 3.3 检测算法(Loop 固件 1.0)
|
||||
|
||||
| 机制 | 参数 | 作用 |
|
||||
|------|------|------|
|
||||
| IIR 低通滤波 | α = 79/256,τ ≈ 32 ms @10ms tick | 平滑高频噪声,保留车辆信号 |
|
||||
| 斜率限幅 | 单步最大变化 5%(低基值下限 100) | 削除瞬态尖峰(继电器动作、电磁干扰) |
|
||||
| 进入确认 | 连续 3 个采样周期低于阈值 | 抗尖峰误触发 |
|
||||
| 基线跟踪 | 滑动平均窗口 500 样本(5s 更新周期,噪声抑制 22×) | 跟随温漂/环境慢变 |
|
||||
| 基线冻结保护 | 偏差超限即冻结基线更新 | 防止过车信号污染基线 |
|
||||
| 冻结超时自适应 | 10 s 超时 + ±2% 稳定性窗校验 | 温漂/换线圈后自动重建基线;慢速进车不误吸收 |
|
||||
| 上电稳定期 | 128 样本(≈1.3 s)快速收敛,上电 3 s 内抑制主动上报 | 快速可用且不上报无效数据 |
|
||||
| 线圈安全模式 | 可配置超时(单位 10 s,0=关闭) | 异常长时间占用强制复位重建基线 |
|
||||
|
||||
### 3.4 检测性能指标(设计目标,依据《环路车辆检测器验收标准》v1.0)
|
||||
|
||||
| 指标 | 设计目标 | 验收优秀档 |
|
||||
|------|---------|-----------|
|
||||
| 存在检测响应时间 T_on(V2~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. 通信接口
|
||||
|
||||
| 接口 | 规格 | 用途 |
|
||||
|------|------|------|
|
||||
| 以太网 | 10M(CH32V208 内置 PHY),支持 DHCP/静态 IP | TCP JSON 服务(默认端口 5960)、IoT MQTT 客户端 |
|
||||
| 蓝牙 | BLE(WCH 低功耗蓝牙协议栈) | 小程序配置、OTA 升级、Loop MCU ISP 透传 |
|
||||
| TTL 串口 | 1 路 | DLD960 内部两个MCU之间 的串口通信协议(Modbus 风格私有帧);UART2 RX 硬件 DMA 循环接收(抗打印关中断丢帧);OTA 升级态支持 0x9F 帧透传(V1.02.03) |
|
||||
| TTL 串口 | 1 路(RFU) | 预留 4G 模块扩展 |
|
||||
| 外部存储 | SPI NOR Flash | 参数存储 + 日志存储 |
|
||||
| 按键 | K1 多功能按键 | 复位等 |
|
||||
|
||||
### 5.1 协议矩阵(V1.02.04 配套)
|
||||
|
||||
| 协议 | 版本 | 通道 | 要点 |
|
||||
|------|------|------|------|
|
||||
| DLD960 串口通信协议 | V1.01 | TTL | 设备管理、参数配置、数据上报 |
|
||||
| DLD960 TCP JSON 协议 | V1.03 | ETH :5960 | 密码鉴权 + 18 条命令;event_report 客户端必答(5s×3 重发);脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
|
||||
| DLD960 IoT MQTT 协议 | V1.07 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`;initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
|
||||
| DLD960Loop 串口协议 | V1.05 | MCU 间(内部) | 0x7F 帧、0xC0 传感上报(variation 3B 有符号) |
|
||||
| DLD960 BLE 协议 | V1.02 | 蓝牙 BLE | 帧格式 + 分包;脱机日志 OFFLOG_STAT/QUERY/CLEAR + 传感快照 SNAP_STAT/QUERY/CLEAR(0x28/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 → AT32F421;UART2 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~4(PB4~PB7)、LED_PWR 红色呼吸灯(PA8)+ 4 路绿色车检灯、USART1(PA9/PA10 ↔ CH32V208)
|
||||
- **CH32V208**:内置 ETH PHY(PC6~PC9)+ 双色 ETH LED、USART2(PA2/PA3 ↔ AT32F421)、扩展 USART1(PB6/PB7,RFU 4G)、K1 按键(PA0)、SPI1 外部 Flash(PA4~PA7)
|
||||
|
||||
## 10. 版本配套要求
|
||||
|
||||
> ⚠ **自内部协议 V1.05 起,vd960Loop 与 vd960DBN 固件必须同版本配套刷写**(variation 字段 2B→3B 帧格式变更,混刷导致数据静默错位)。
|
||||
|
||||
| 组件 | V1.02.04 配套版本 |
|
||||
|------|-------------------|
|
||||
| vd960Loop 固件 | 1.0(不变) |
|
||||
| vd960DBN 固件 | 1.02.04 |
|
||||
|
||||
### 10.1 已知约束(V1.02.01)
|
||||
|
||||
- 固件版本三段式:`FIRMWARE_VER="1.02.04"`(MAIN=1 / SUB=2 / SUBSUB=4);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。
|
||||
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)。
|
||||
- CH32V208 RAM 预算:BLE 协议栈固定占用低 16KB(不可裁剪),用户可用 48KB;固件经 RAM 瘦身优化,栈余量约 6KB,现场升级务必使用 V1.02.01 及以上版本。
|
||||
- 传感快照区延后 3s 初始化(开机 3s 内传感数据不落盘,设计接受)。
|
||||
- UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)。
|
||||
- **OTA 透传模式无自动退出机制**:进入 OTA 态(`g_flag_counter_ota.flag=1`)后需断电重启才恢复 0x7F 正常通信(V1.02.03 已知约束,升级完成后重启设备)。
|
||||
|
||||
---
|
||||
|
||||
## 修订记录
|
||||
|
||||
| 版本 | 修订时间 | 修订说明 | 修订人 |
|
||||
|------|----------|----------|--------|
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04:UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出;存储适配(SPI 识别去厂商代码 + factory 写入恢复) | wangfq |
|
||||
| V1.01 | 2026-08-18 | 配套整机发布 V1.02.01:版本矩阵更新、UART2 RX DMA 通信可靠性说明、已知约束补充 | wangfq |
|
||||
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
|
||||
# 概述
|
||||
|
||||
vd\_960是一个四通道车辆检测器,支持网口、RS485、蓝牙等。功能说明:
|
||||
vd\_960是一个四通道车辆检测器,支持网口、蓝牙、4G扩展(RFU)等。功能说明:
|
||||
|
||||
* 双核心(双MCU),雅特力芯片AT32F421(120MHz,64KB Flash,16KB SRAM)负责线圈功能,沁恒CH32V208(144MHz,实际采用的是120MHz, 128KB Flash, 64KB SRAM)负责通信(蓝牙、网口,一路RS485和一路TTL串口)和外部存储(参数存储和日志存储)
|
||||
* 双核心(双MCU),雅特力芯片AT32F421(120MHz,64KB Flash,16KB SRAM)负责线圈功能,沁恒CH32V208(144MHz,实际采用的是120MHz, 128KB Flash, 64KB SRAM)负责通信(蓝牙、网口,两路TTL串口:一路内部对接AT32F421,一路预留4G模块上云)和外部存储(参数存储和日志存储)
|
||||
|
||||
* 四路线圈,四个双路继电器。
|
||||
|
||||
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
# vd960 (DLD960) 开发计划 — V1.0.0 之后
|
||||
|
||||
> 制定日期:2026-07-17 · 基线:整机 V1.0.0(2026-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 NOR(SPI1)。默认 **W25Q32(32Mbit = 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()` 同源出口。**已实现(snapshot,2026-08-12)**:中断只入 RAM 暂存(8 深满丢新),主循环 flush 落盘;BLE 新增 SNAP_STAT/QUERY/CLEAR(0x28/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→4MB(16384→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_report,loop_data 仅做展示——两通道职责不混
|
||||
- **异常分支优先设计**:设备离线(心跳超时)→ 会车灯降级策略;继电器粘连检测(relay_count 与命令数比对);车辆长时间停留(hold_time 超时全通道重启的平台侧感知)
|
||||
- **单机版会车评估**:DLD960 四通道 + 继电器本地联动能否覆盖小型场景(无平台依赖,可靠性更高)——出对比方案:纯平台 / 纯单机 / 混合
|
||||
- **雷达/相机融合接口预留**:扩展 UART1 或网口接入目标列表,融合仲裁放平台侧
|
||||
|
||||
---
|
||||
|
||||
## 版本映射与节奏
|
||||
|
||||
| 版本 | 内容 | 前置 |
|
||||
|------|------|------|
|
||||
| v1.1.0 | P0 全部(BLE 收尾 + TODO 清剿 + 回归全绿) | — |
|
||||
| v1.2.x | P1 设备侧(脱机日志 + Loop 远程 OTA + 协议 V1.06) | edc_server 对接联调 |
|
||||
| v2.0.0 | P2 算法深化(抗干扰 + 测速分类 + 方向) | 现场干扰数据采集 |
|
||||
| v2.x | P3 会车控制 | P1 平台 + P2 方向判别 |
|
||||
|
||||
## 风险与待现场验证
|
||||
|
||||
| # | 风险 | 应对 |
|
||||
|---|------|------|
|
||||
| 1 | BLE 重构改动帧消费时序,复发"不同源"类 bug | P0.1 真值表 + 长车复测强制过 |
|
||||
| 2 | 变频器干扰形态未知,算法方案可能推倒重来 | 先采数据后设计,不拍脑袋 |
|
||||
| 3 | DBN 自身远程 OTA 无安全落点(128KB 无 A/B、bootloader 只认 BLE) | 过渡态:DBN 走 BLE、Loop 走远程;bootloader 改造单独评审 |
|
||||
| 4 | 协议再升版(V1.06)又是双侧死绑定 | 沿用 Len 字节区分格式的兼容方案,发布走配套矩阵 |
|
||||
| 5 | 无 RTC,脱机日志绝对时间不可靠 | boot_seq+mstick+时钟锚点回算;协议明示"未同步段仅相对时间" |
|
||||
| 6 | 日志高频写与 MQTT/BLE 共抢主循环与 SPI 总线 | 快照落盘按 page 缓写、擦除放空闲窗口;实测不达标就降快照档频率 |
|
||||
@@ -241,11 +241,60 @@ void load_cfg_from_flash(void)
|
||||
if(strncmp(mBuff, DEV_USER_FLASH_MAGIC, sizeof(DEV_USER_FLASH_MAGIC)) != 0)
|
||||
{
|
||||
free(mBuff);
|
||||
PRINT("Will_Factory_dev_____\n");
|
||||
|
||||
/* ===== 恢复 factory 写入 (2026-08-19) =====
|
||||
8-13 止血背景: factory SPI 写触发硬件复位死循环 — 根因已在 8-17 闭环
|
||||
(.bss 挤占 RAM → 栈溢出 + printf 重入, 非 SPI 硬件问题);
|
||||
offlog/snapshot 自 8-18 起持续 SPI 写入稳定。
|
||||
换新空片 (W25Q128) 参数区无 magic → 每次上电 memory defaults,
|
||||
且 write_net_config 保存配置不写 magic → 配置永久丢失。
|
||||
恢复: 首次 mismatch 写出厂默认 (magic+参数) 到 flash, 后续上电正常加载。 */
|
||||
factory_dev_info();
|
||||
|
||||
NVIC_SystemReset();
|
||||
PRINT("CFG: magic mismatch - factory defaults written\n");
|
||||
memset(g_dev_number, 0, 6);
|
||||
memcpy(g_dev_number, gMacAddr, 6);
|
||||
sprintf(g_dev_number_str, "%02X%02X%02X%02X%02X%02X",
|
||||
g_dev_number[0], g_dev_number[1], g_dev_number[2],
|
||||
g_dev_number[3], g_dev_number[4], g_dev_number[5]);
|
||||
g_sub_code_enable.code_set = 0x0001;
|
||||
g_max_counter_bt_min = BT_DISABLE_IDLE_TIMEOUT;
|
||||
g_max_counter_bt_timeout = g_max_counter_bt_min * 60 * 1000;
|
||||
g_storage_uart_baud = UART_BAUD_DEFAULT_PORT_1;
|
||||
g_storage_uart_baud_2 = UART_BAUD_DEFAULT_PORT_2;
|
||||
memcpy(g_dev_password, "123456", 6);
|
||||
memcpy(local_net_cfg.mac, g_dev_number, 6);
|
||||
local_net_cfg.lip[0] = 192; local_net_cfg.lip[1] = 168; local_net_cfg.lip[2] = 1; local_net_cfg.lip[3] = 188;
|
||||
local_net_cfg.sub[0] = 255; local_net_cfg.sub[1] = 255; local_net_cfg.sub[2] = 255; local_net_cfg.sub[3] = 0;
|
||||
local_net_cfg.gw[0] = 192; local_net_cfg.gw[1] = 168; local_net_cfg.gw[2] = 1; local_net_cfg.gw[3] = 1;
|
||||
local_net_cfg.dns[0] = 192; local_net_cfg.dns[1] = 168; local_net_cfg.dns[2] = 1; local_net_cfg.dns[3] = 1;
|
||||
local_net_cfg.port_ssc_tcp = 5550;
|
||||
local_net_cfg.port_ssc_udp = NET_REPORT_INTERVAL;
|
||||
local_net_cfg.port_ssc_udp_message = 5505;
|
||||
local_net_cfg.port_dev_tcp = 5550;
|
||||
local_net_cfg.port_dev_udp = 4900;
|
||||
net_center_info.lssc_ip[0] = 192; net_center_info.lssc_ip[1] = 168; net_center_info.lssc_ip[2] = 1; net_center_info.lssc_ip[3] = 222;
|
||||
net_center_info.msg_port = 4999;
|
||||
net_center_info.tcp_port = 5550;
|
||||
net_center_info.udp_port = NET_REPORT_INTERVAL;
|
||||
net_center_info.sw_ver[0] = 1; net_center_info.sw_ver[1] = 2;
|
||||
memset(iot_net_info.remote_addr, 0, 64);
|
||||
memcpy(iot_net_info.remote_addr, "121.37.20.199", 13);
|
||||
iot_net_info.mqtt_port = 1883;
|
||||
memset(iot_net_info.client_id, 0, 64);
|
||||
memset(iot_net_info.username, 0, 64); memcpy(iot_net_info.username, "admin", 5);
|
||||
memset(iot_net_info.password, 0, 32); memcpy(iot_net_info.password, "password", 8);
|
||||
iot_net_info.mode = 0;
|
||||
g_iot_topic.clientid_enable = 0;
|
||||
memset(g_iot_topic.topic_pub, 0, 64);
|
||||
memset(g_iot_topic.topic_sub, 0, 64);
|
||||
g_sub_code_enable.net_enable = 1;
|
||||
g_sub_code_enable.iot_enable = 0;
|
||||
g_sub_code_enable.custom_enable = 0;
|
||||
g_sub_code_enable.loop_enable = 0;
|
||||
g_sub_code_enable.dgdus_enable = 0;
|
||||
g_sub_code_enable.wbdus_enable = 0;
|
||||
g_sub_code_enable.radar_enable = 0;
|
||||
PRINT("CFG: memory defaults OK (factory written)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
_offset = DEV_NUMER_ADDR_OFFSET;
|
||||
@@ -305,6 +354,16 @@ void load_cfg_from_flash(void)
|
||||
|
||||
|
||||
|
||||
|
||||
/* 2026-08-17: 字符串 0 终止保险 — Flash 中字符串若不足定长且后续 0xFF,
|
||||
%s 打印会越界(历史乱码根因之一)。强制定长缓冲末字节为 '\0' */
|
||||
iot_net_info.remote_addr[63] = '\0';
|
||||
iot_net_info.client_id[63] = '\0';
|
||||
iot_net_info.username[63] = '\0';
|
||||
iot_net_info.password[31] = '\0';
|
||||
g_iot_topic.topic_pub[63] = '\0';
|
||||
g_iot_topic.topic_sub[63] = '\0';
|
||||
|
||||
free(mBuff);
|
||||
|
||||
g_sub_code_enable.net_enable = g_sub_code_enable.code_set & 0x01;
|
||||
@@ -315,6 +374,7 @@ void load_cfg_from_flash(void)
|
||||
g_sub_code_enable.wbdus_enable = (g_sub_code_enable.code_set >> 5) & 0x01;
|
||||
g_sub_code_enable.radar_enable = (g_sub_code_enable.code_set >> 6) & 0x01;
|
||||
|
||||
/* 2026-08-17 栈溢出修复后恢复 */
|
||||
PRINT("\nSub_Code: 0x%04X, net_enable:%d, iot_enable:%d\n", g_sub_code_enable.code_set, g_sub_code_enable.net_enable, g_sub_code_enable.iot_enable);
|
||||
|
||||
// g_sub_code_enable.laser_enable = (g_sub_code_enable.code_set >> 9) & 0x01;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "ch32v20x_it.h"
|
||||
#include "eth_driver.h"
|
||||
#include "CONFIG.h"
|
||||
#include "debug.h"
|
||||
#include "fault_diag.h"
|
||||
|
||||
/*********************************************************************
|
||||
* LOCAL FUNCTIONS
|
||||
@@ -38,6 +40,40 @@ void NMI_Handler(void)
|
||||
{
|
||||
}
|
||||
|
||||
/* 故障诊断现场: .noinit 段 (复位不清零) */
|
||||
FaultDiag g_fault_diag __attribute__((section(".noinit")));
|
||||
|
||||
/*********************************************************************
|
||||
* @fn fault_diag_init
|
||||
*
|
||||
* @brief main 开头调用: 打印上次复位现场 + 重置 + 递增 boot_cnt
|
||||
*
|
||||
* @return none
|
||||
*/
|
||||
void fault_diag_init(void)
|
||||
{
|
||||
if (g_fault_diag.magic == 0xFA57FA57UL) {
|
||||
PRINT("FAULT_DIAG: boot_cnt=%lu mcause=0x%08lx mepc=0x%08lx mtval=0x%08lx marker=0x%08lx\n",
|
||||
(unsigned long)g_fault_diag.boot_cnt,
|
||||
(unsigned long)g_fault_diag.mcause,
|
||||
(unsigned long)g_fault_diag.mepc,
|
||||
(unsigned long)g_fault_diag.mtval,
|
||||
(unsigned long)g_fault_diag.marker);
|
||||
} else if (g_fault_diag.marker != 0) {
|
||||
/* 非 HardFault 复位 (IWDG 超时/卡死/掉电): marker 仍记录执行轨迹 */
|
||||
PRINT("FAULT_DIAG: no-hardfault boot_cnt=%lu marker=0x%08lx (卡死/看门狗复位)\n",
|
||||
(unsigned long)g_fault_diag.boot_cnt,
|
||||
(unsigned long)g_fault_diag.marker);
|
||||
}
|
||||
/* 重置现场, boot_cnt 保留递增 */
|
||||
g_fault_diag.magic = 0;
|
||||
g_fault_diag.mcause = 0;
|
||||
g_fault_diag.mepc = 0;
|
||||
g_fault_diag.mtval = 0;
|
||||
g_fault_diag.marker = 0;
|
||||
g_fault_diag.boot_cnt++;
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* @fn HardFault_Handler
|
||||
*
|
||||
@@ -47,6 +83,16 @@ void NMI_Handler(void)
|
||||
*/
|
||||
void HardFault_Handler(void)
|
||||
{
|
||||
/* 纯 RAM 写现场 (不依赖 UART/printf): 栈坏/中断禁用也能留痕 */
|
||||
g_fault_diag.magic = 0xFA57FA57UL;
|
||||
g_fault_diag.mcause = __get_MCAUSE();
|
||||
g_fault_diag.mepc = __get_MEPC();
|
||||
g_fault_diag.mtval = __get_MTVAL();
|
||||
/* 尽力打印 (UART 可用时), 失败不影响 RAM 现场 */
|
||||
PRINT("HARDFAULT: mcause=0x%08lx mepc=0x%08lx mtval=0x%08lx\n",
|
||||
(unsigned long)g_fault_diag.mcause,
|
||||
(unsigned long)g_fault_diag.mepc,
|
||||
(unsigned long)g_fault_diag.mtval);
|
||||
NVIC_SystemReset();
|
||||
while(1)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include "storage.h"
|
||||
#include <string.h>
|
||||
#include "net_srv.h"
|
||||
|
||||
#include "offlog.h"
|
||||
#include "snapshot.h"
|
||||
extern uint16_t peripheral_get_mtu(void);
|
||||
|
||||
uint8_t g_flag_notify_temp = 0; //ÁÙʱ֪ͨnotify flag, 0 disable, 1 enable
|
||||
|
||||
@@ -29,6 +31,12 @@ extern uint8_t loop1_FLAG_CUT;
|
||||
|
||||
void clear_ble_notify_buf(BLE_Notify_Buf * buf)
|
||||
{
|
||||
if(buf->flag || buf->len)
|
||||
{
|
||||
PRINT("CLR_NTF: buf=%08lx flag=%d len=%d ret=%08lx\n",
|
||||
(unsigned long)buf, buf->flag, buf->len,
|
||||
(unsigned long)__builtin_return_address(0));
|
||||
}
|
||||
buf->len = 0;
|
||||
buf->flag = 0;
|
||||
memset(buf, 0, MAX_BLE_Notify_Buf_LEN);
|
||||
@@ -38,6 +46,12 @@ void clear_buf_dbn_ble(Buf_DBN_BLE * buf)
|
||||
{
|
||||
uint8_t i = 0;
|
||||
|
||||
if(buf->flag)
|
||||
{
|
||||
PRINT("CLEAR busy resp: amt=%d seq=%d off=%d ret=%08lx\n",
|
||||
buf->pkg_amount, buf->pkg_seq, buf->dat_offset,
|
||||
(unsigned long)__builtin_return_address(0));
|
||||
}
|
||||
|
||||
buf->flag = 0;
|
||||
buf->magic = 0;
|
||||
@@ -65,7 +79,7 @@ void clear_buf_dbn_ble_all(Buf_DBN_BLE * buf)
|
||||
buf->flag = 0;
|
||||
buf->magic = 0;
|
||||
buf->cmd = 0;
|
||||
memset(buf->dat, 0, MAX_BLE_BUF_LEN);
|
||||
memset(buf->dat, 0, MAX_BLE_DAT_BUF_LEN);
|
||||
buf->dat_len = 0;
|
||||
buf->dat_offset = 0;
|
||||
buf->pkg_amount = 0;
|
||||
@@ -181,6 +195,25 @@ static uint8_t set_net_info_to_ready(uint8_t * dat_dst)
|
||||
return i;
|
||||
}
|
||||
|
||||
/* =============================================================
|
||||
* ble_notify_chunk_max: max dat bytes per notify packet
|
||||
* follow negotiated MTU to avoid 'Too large noti' drop:
|
||||
* whole pkt = 4B header(magic/header/len/cmd) + dat + 2B ckb
|
||||
* must satisfy whole pkt <= peripheralMTU - 3 (ATT opcode+handle)
|
||||
* and whole pkt <= MAX_BLE_Notify_Buf_LEN (local buf, no overflow)
|
||||
* MTU=23 -> 14, MTU=96 -> 87, MTU>=103 -> 94
|
||||
* =============================================================
|
||||
*/
|
||||
static uint16_t ble_notify_chunk_max(void)
|
||||
{
|
||||
uint16_t _mtu = peripheral_get_mtu();
|
||||
if (_mtu < 23) _mtu = ATT_MTU_SIZE; /* fallback before negotiation */
|
||||
uint16_t _limit = _mtu - 9; /* pkt limit - header4 - ckb2 */
|
||||
if (_limit > (MAX_BLE_Notify_Buf_LEN - 6))
|
||||
_limit = MAX_BLE_Notify_Buf_LEN - 6;
|
||||
return _limit;
|
||||
}
|
||||
|
||||
void set_response_iot_net(Buf_DBN_BLE *response_dst)
|
||||
{ // config iot_net
|
||||
uint8_t ret = 0;
|
||||
@@ -205,10 +238,13 @@ void set_response_iot_net(Buf_DBN_BLE *response_dst)
|
||||
memcpy(&(response_dst->dat[i]), iot_net_info.password, strlen(iot_net_info.password));
|
||||
i += strlen(iot_net_info.password);
|
||||
response_dst->dat_len = i;
|
||||
response_dst->pkg_amount = i / MAX_BLE_DAT_RESPONSE_LEN;
|
||||
if((i % MAX_BLE_DAT_RESPONSE_LEN) > 0)
|
||||
{
|
||||
response_dst->pkg_amount += 1;
|
||||
uint16_t _chunk = ble_notify_chunk_max();
|
||||
response_dst->pkg_amount = i / _chunk;
|
||||
if((i % _chunk) > 0)
|
||||
{
|
||||
response_dst->pkg_amount += 1;
|
||||
}
|
||||
}
|
||||
response_dst->pkg_seq = 0;
|
||||
|
||||
@@ -232,10 +268,13 @@ void set_response_iot_topic(Buf_DBN_BLE *response_dst)
|
||||
memcpy(&(response_dst->dat[i]), g_iot_topic.topic_sub, strlen(g_iot_topic.topic_sub));
|
||||
i += strlen(g_iot_topic.topic_sub);
|
||||
response_dst->dat_len = i;
|
||||
response_dst->pkg_amount = i / MAX_BLE_DAT_RESPONSE_LEN;
|
||||
if((i % MAX_BLE_DAT_RESPONSE_LEN) > 0)
|
||||
{
|
||||
response_dst->pkg_amount += 1;
|
||||
uint16_t _chunk = ble_notify_chunk_max();
|
||||
response_dst->pkg_amount = i / _chunk;
|
||||
if((i % _chunk) > 0)
|
||||
{
|
||||
response_dst->pkg_amount += 1;
|
||||
}
|
||||
}
|
||||
response_dst->pkg_seq = 0;
|
||||
response_dst->flag = 1;
|
||||
@@ -246,12 +285,14 @@ uint8_t set_response_buf(Buf_DBN_BLE *response_dst, uint8_t magic, uint8_t cmd,
|
||||
{
|
||||
uint8_t ret = 0;
|
||||
uint8_t i = 0;
|
||||
if(dat_len > MAX_BLE_DAT_BUF_LEN) dat_len = MAX_BLE_DAT_BUF_LEN; /* overflow guard */
|
||||
clear_buf_dbn_ble(response_dst);
|
||||
response_dst->magic = magic;
|
||||
response_dst->cmd = cmd;
|
||||
|
||||
uint8_t _amount = dat_len / MAX_BLE_DAT_RESPONSE_LEN;
|
||||
if((dat_len % MAX_BLE_DAT_RESPONSE_LEN) > 0)
|
||||
uint16_t _chunk = ble_notify_chunk_max();
|
||||
uint8_t _amount = dat_len / _chunk;
|
||||
if((dat_len % _chunk) > 0)
|
||||
{
|
||||
_amount++;
|
||||
}
|
||||
@@ -308,6 +349,7 @@ uint8_t set_response_tran_to_notify(uint8_t *dat_ori, uint8_t dat_len, BLE_Notif
|
||||
}
|
||||
notify_dst->len = dat_len;
|
||||
notify_dst->flag = 1;
|
||||
return notify_dst->flag;
|
||||
}
|
||||
|
||||
|
||||
@@ -368,9 +410,12 @@ uint8_t set_response_to_notify(Buf_DBN_BLE *response_ori, BLE_Notify_Buf * notif
|
||||
uint8_t _pkg_amount = response_ori->pkg_amount;
|
||||
uint8_t _pkg_seq = response_ori->pkg_seq;
|
||||
uint8_t _remain_len = response_ori->dat_len - response_ori->dat_offset;
|
||||
if(_remain_len > MAX_BLE_DAT_RESPONSE_LEN)
|
||||
{
|
||||
_remain_len = MAX_BLE_DAT_RESPONSE_LEN;
|
||||
uint16_t _chunk = ble_notify_chunk_max();
|
||||
if(_remain_len > _chunk)
|
||||
{
|
||||
_remain_len = (uint8_t)_chunk;
|
||||
}
|
||||
}
|
||||
_pkg_seq += 1;
|
||||
notify_dst->buf[1] = (_pkg_amount << 4)|(_pkg_seq);
|
||||
@@ -875,6 +920,190 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len)
|
||||
}
|
||||
PRINT("Rcv_acs_enable_flag: %d\n", g_dbn_ble_state_acs_enable.flag);
|
||||
} break;
|
||||
case CMD_DBN_OFFLOG_STAT: {
|
||||
/* offlog stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */
|
||||
uint8_t _i = 0;
|
||||
uint32_t _total = offlog_count();
|
||||
uint32_t _seq_last = offlog_seq_last();
|
||||
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
|
||||
uint32_t _boot = offlog_boot_seq();
|
||||
uint32_t _cap = OFFLOG_MAX_RECORDS;
|
||||
|
||||
if (!offlog_enabled()) {
|
||||
tmp_ble_buf[_i++] = 0x01; /* disabled */
|
||||
} else {
|
||||
tmp_ble_buf[_i++] = 0x00; /* ok */
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF);
|
||||
}
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
PRINT("BLE: offlog_stat count=%lu seq_first=%lu seq_last=%lu\n",
|
||||
(unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last);
|
||||
} break;
|
||||
case CMD_DBN_OFFLOG_QUERY: {
|
||||
/* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..4)
|
||||
resp: status(1) count(1) + N x 32B OfflogEvt raw (LE) */
|
||||
uint32_t _start_seq = 0;
|
||||
uint32_t _req_count = 0;
|
||||
uint32_t _total = offlog_count();
|
||||
uint32_t _seq_last = offlog_seq_last();
|
||||
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
|
||||
uint8_t _i = 0, _j = 0;
|
||||
|
||||
if (!offlog_enabled()) {
|
||||
tmp_ble_buf[_i++] = 0x01; /* disabled */
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
break;
|
||||
}
|
||||
if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */
|
||||
tmp_ble_buf[_i++] = 0x02; /* bad request */
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
break;
|
||||
}
|
||||
_start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8)
|
||||
| ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24);
|
||||
_req_count = pkg[8];
|
||||
if (_req_count > OFFLOG_MAX_QUERY_RECORDS) _req_count = OFFLOG_MAX_QUERY_RECORDS;
|
||||
if (_req_count == 0) _req_count = OFFLOG_MAX_QUERY_RECORDS;
|
||||
/* overflow guard: resp dat = 2 + N*32 must fit tmp buf.
|
||||
history: MAX_BLE_TMP_BUF_LEN was 100, 130B write overflowed
|
||||
into g_notify_buftemp and erased pending chunk 2 */
|
||||
{ uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ?
|
||||
((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(OfflogEvt)) : 0;
|
||||
if (_req_count > _max_rec) _req_count = _max_rec; }
|
||||
|
||||
tmp_ble_buf[_i++] = 0x00; /* status ok */
|
||||
tmp_ble_buf[_i++] = 0; /* count placeholder */
|
||||
if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) {
|
||||
uint32_t _idx = _start_seq - _seq_first;
|
||||
while (_j < _req_count && (_idx + _j) < _total) {
|
||||
OfflogEvt _evt;
|
||||
if (offlog_read_idx((uint16_t)(_idx + _j), &_evt) != 0) break;
|
||||
memcpy(&tmp_ble_buf[_i], &_evt, sizeof(OfflogEvt));
|
||||
_i += sizeof(OfflogEvt);
|
||||
_j++;
|
||||
}
|
||||
}
|
||||
tmp_ble_buf[1] = _j;
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
PRINT("BLE: offlog_query start_seq=%lu req=%lu fetched=%u\n",
|
||||
(unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j);
|
||||
} break;
|
||||
case CMD_DBN_OFFLOG_CLEAR: {
|
||||
/* clear offlog (audit trail). blocking ~45ms (logical clear + current sector), main-loop ctx ok */
|
||||
uint8_t _i = 0;
|
||||
offlog_clear();
|
||||
tmp_ble_buf[_i++] = 0x00; /* ok */
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
PRINT("BLE: offlog_clear done\n");
|
||||
} break;
|
||||
case CMD_DBN_SNAP_STAT: {
|
||||
/* snap stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */
|
||||
uint8_t _i = 0;
|
||||
uint32_t _total = snap_count();
|
||||
uint32_t _seq_last = snap_seq_last();
|
||||
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
|
||||
uint32_t _boot = snap_boot_seq();
|
||||
uint32_t _cap = SNAP_MAX_RECORDS;
|
||||
|
||||
if (!snap_enabled()) {
|
||||
tmp_ble_buf[_i++] = 0x01; /* disabled */
|
||||
} else {
|
||||
tmp_ble_buf[_i++] = 0x00; /* ok */
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF);
|
||||
tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF);
|
||||
}
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
PRINT("BLE: snap_stat count=%lu seq_first=%lu seq_last=%lu\n",
|
||||
(unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last);
|
||||
} break;
|
||||
case CMD_DBN_SNAP_QUERY: {
|
||||
/* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..2)
|
||||
resp: status(1) count(1) + N x 64B SnapRec raw (LE) */
|
||||
uint32_t _start_seq = 0;
|
||||
uint32_t _req_count = 0;
|
||||
uint32_t _total = snap_count();
|
||||
uint32_t _seq_last = snap_seq_last();
|
||||
uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0;
|
||||
uint8_t _i = 0, _j = 0;
|
||||
|
||||
if (!snap_enabled()) {
|
||||
tmp_ble_buf[_i++] = 0x01; /* disabled */
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
break;
|
||||
}
|
||||
if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */
|
||||
tmp_ble_buf[_i++] = 0x02; /* bad request */
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
break;
|
||||
}
|
||||
_start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8)
|
||||
| ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24);
|
||||
_req_count = pkg[8];
|
||||
if (_req_count > SNAP_MAX_QUERY_RECORDS) _req_count = SNAP_MAX_QUERY_RECORDS;
|
||||
if (_req_count == 0) _req_count = SNAP_MAX_QUERY_RECORDS;
|
||||
/* overflow guard: resp dat = 2 + N*64 must fit tmp buf (ODR lesson) */
|
||||
{ uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ?
|
||||
((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(SnapRec)) : 0;
|
||||
if (_req_count > _max_rec) _req_count = _max_rec; }
|
||||
|
||||
tmp_ble_buf[_i++] = 0x00; /* status ok */
|
||||
tmp_ble_buf[_i++] = 0; /* count placeholder */
|
||||
if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) {
|
||||
uint32_t _idx = _start_seq - _seq_first;
|
||||
while (_j < _req_count && (_idx + _j) < _total) {
|
||||
SnapRec _rec;
|
||||
if (snap_read_idx(_idx + _j, &_rec) != 0) break;
|
||||
memcpy(&tmp_ble_buf[_i], &_rec, sizeof(SnapRec));
|
||||
_i += sizeof(SnapRec);
|
||||
_j++;
|
||||
}
|
||||
}
|
||||
tmp_ble_buf[1] = _j;
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
PRINT("BLE: snap_query start_seq=%lu req=%lu fetched=%u\n",
|
||||
(unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j);
|
||||
} break;
|
||||
case CMD_DBN_SNAP_CLEAR: {
|
||||
/* clear snapshot (audit trail). blocking ~45ms (logical clear + current sector) */
|
||||
uint8_t _i = 0;
|
||||
snap_clear();
|
||||
tmp_ble_buf[_i++] = 0x00; /* ok */
|
||||
set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i);
|
||||
PRINT("BLE: snap_clear done\n");
|
||||
} break;
|
||||
default:
|
||||
{
|
||||
} break;
|
||||
@@ -893,6 +1122,7 @@ void manage_dbn_ble_transparent(uint8_t *pkg, uint8_t len)
|
||||
|
||||
void poll_dbn_ble(void)
|
||||
{
|
||||
static uint8_t _last_ntf_flag = 0;
|
||||
uint8_t i = 0;
|
||||
uint8_t magic = 0;
|
||||
if(g_ble_rcv_buf.flag)
|
||||
@@ -935,8 +1165,24 @@ void poll_dbn_ble(void)
|
||||
}
|
||||
if(g_notify_buftemp.flag == 0)
|
||||
{
|
||||
// report_sens_acs();
|
||||
g_flag_notify_temp = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
|
||||
static uint8_t _last_resp_flag = 0;
|
||||
uint8_t _pull = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
|
||||
g_flag_notify_temp = _pull;
|
||||
if(g_buf_ble_response.flag != _last_resp_flag)
|
||||
{
|
||||
PRINT("BLE resp flag %d->%d (pull=%d amt=%d seq=%d off=%d)\n",
|
||||
_last_resp_flag, g_buf_ble_response.flag, _pull,
|
||||
g_buf_ble_response.pkg_amount, g_buf_ble_response.pkg_seq,
|
||||
g_buf_ble_response.dat_offset);
|
||||
_last_resp_flag = g_buf_ble_response.flag;
|
||||
}
|
||||
}
|
||||
if(g_notify_buftemp.flag != _last_ntf_flag)
|
||||
{
|
||||
PRINT("BLE ntf flag %d->%d (len=%d temp=%d)\n",
|
||||
_last_ntf_flag, g_notify_buftemp.flag, g_notify_buftemp.len,
|
||||
g_flag_notify_temp);
|
||||
_last_ntf_flag = g_notify_buftemp.flag;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,45 +11,46 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define PRODUCT_MODEL "DLD960GA"
|
||||
#define FIRMWARE_VER "1.0"
|
||||
#define FIRMWARE_VER "1.02.04"
|
||||
#define HARDWARE_VER "1.0"
|
||||
#define FIRMWARE_VER_MAIN 1
|
||||
#define FIRMWARE_VER_SUB 1
|
||||
#define FIRMWARE_VER_SUB 2
|
||||
#define FIRMWARE_VER_SUBSUB 4
|
||||
#define HARDWARE_VER_MAIN 1
|
||||
#define HARDWARE_VER_SUB 1
|
||||
|
||||
|
||||
#define BUFF_STACK_SIZE 512 //128
|
||||
#define BUFF_STACK_SIZE 64
|
||||
|
||||
#define MAX_UART_TXRX_LEN 32
|
||||
|
||||
#define MAX_TOUCHUAN_DISABLE_TIME 15
|
||||
#define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min
|
||||
#define BT_DISABLE_IDLE_TIMEOUT 0 //10 // 蓝牙空闲超时 关闭时间,默认10 min
|
||||
|
||||
|
||||
typedef enum
|
||||
{
|
||||
DDType_DNT900 = 1, //1 ???????? DNT900
|
||||
DDType_WireDUS, //2???ù?¨???????? DG
|
||||
DDType_DNT900 = 1, //1 区位有线 DNT900
|
||||
DDType_WireDUS, //2超声波车位有线 DG
|
||||
DDType_4G_1,
|
||||
DDType_WireDus_Wb, //4 ???ù?¨???????? ?ò??
|
||||
DDType_LoraAreaMain, //5 Lora ?????÷?ú
|
||||
DDType_LoraAreaSub, //6 Lora ???????ú
|
||||
DDType_LoraLotMain, //7 Lora ?????÷?ú
|
||||
DDType_LoraLotSub, //8 Lora ???????ú
|
||||
DDType_LoraBroadcast, //9 Lora ????????
|
||||
DDType_LoraRelay, //10 Lora ?????? ???? DLR101
|
||||
DDType_Relay, //11 ?????????? ?¨??RS485-2 ??·?
|
||||
DDType_4G_IOT, //12 4G ??±¨??????????????MQTT
|
||||
DDType_Wire_Collect_IOT, //13 ????????±¨??????????????MQTT
|
||||
DDType_Wire_Display_IOT, //14 ????????·?????????MQTT
|
||||
DDType_Radar_Lot, //15 ?×??????
|
||||
|
||||
DDType_WireDus_Wb, //4 超声波车位有线 万泊
|
||||
DDType_LoraAreaMain, //5 Lora 区位主机
|
||||
DDType_LoraAreaSub, //6 Lora 区位从机
|
||||
DDType_LoraLotMain, //7 Lora 车位主机
|
||||
DDType_LoraLotSub, //8 Lora 车位从机
|
||||
DDType_LoraBroadcast, //9 Lora 广播模式
|
||||
DDType_LoraRelay, //10 Lora 中继, 使用 DLR101
|
||||
DDType_Relay, //11 中继模式, 通过RS485-2 收发
|
||||
DDType_4G_IOT, //12 4G 上报传感数据,使用MQTT
|
||||
DDType_Wire_Collect_IOT, //13 有线,上报传感数据,使用MQTT
|
||||
DDType_Wire_Display_IOT, //14 有线显示发布,使用MQTT
|
||||
DDType_Radar_Lot, //15 雷达车位
|
||||
|
||||
DDType_DLD950 = 20, //DLD950
|
||||
DDType_DBN101 = 21,
|
||||
DDType_DNT910 = 22, //DNT910,DNT920
|
||||
DDType_DNT920 = 23,
|
||||
DDType_DLD950V4 = 24,
|
||||
DDType_DLD950V4 = 24, // DLD960
|
||||
}DG_Device_Type;
|
||||
|
||||
typedef enum
|
||||
@@ -67,12 +68,12 @@ typedef struct _SUB_CODE_ENABLE_
|
||||
uint8_t net_enable;
|
||||
uint8_t iot_enable;
|
||||
uint8_t custom_enable;
|
||||
uint8_t loop_enable; // ?÷??????
|
||||
uint8_t loop_enable; // 流量计数
|
||||
uint8_t dgdus_enable;
|
||||
uint8_t wbdus_enable;
|
||||
uint8_t radar_enable;
|
||||
uint8_t laser_enable; // lora?¤???×??????
|
||||
uint8_t lora_enable; // Lora ????
|
||||
uint8_t laser_enable; // lora激光雷达车位
|
||||
uint8_t lora_enable; // Lora 车位
|
||||
//uint8_t input_enable;
|
||||
} Sub_Code_Enable;
|
||||
|
||||
@@ -91,21 +92,12 @@ extern Pkg_Uart g_pkg_uart_1;
|
||||
extern Pkg_Uart g_pkg_uart_2;
|
||||
|
||||
|
||||
#define RX_BUFFER_LEN BUFF_STACK_SIZE
|
||||
|
||||
typedef struct
|
||||
{
|
||||
volatile uint8_t DMA_USE_BUFFER;
|
||||
uint8_t Rx_Buffer[2][RX_BUFFER_LEN];
|
||||
|
||||
} USART_DMA_UNIT;
|
||||
|
||||
|
||||
|
||||
typedef struct _DBN_BLE_STATE_
|
||||
{
|
||||
uint8_t flag; // if need to report
|
||||
uint8_t enable; // 是否使能
|
||||
uint8_t enable; // 使能
|
||||
uint8_t send_flag;
|
||||
uint8_t obj_amount;
|
||||
uint8_t cmd;
|
||||
|
||||
@@ -49,6 +49,13 @@
|
||||
#define CMD_DBN_SET_CJQ_PARAM 0x23 // 设置 车检器参数
|
||||
#define CMD_DBN_GET_CJQ_PARAM 0x24 // 读取 车检器参数
|
||||
|
||||
#define CMD_DBN_OFFLOG_STAT 0x25 // query offlog stat
|
||||
#define CMD_DBN_OFFLOG_QUERY 0x26 // query offlog records by seq
|
||||
#define CMD_DBN_OFFLOG_CLEAR 0x27 // clear offlog (audit trail)
|
||||
#define CMD_DBN_SNAP_STAT 0x28 // query snapshot stat
|
||||
#define CMD_DBN_SNAP_QUERY 0x29 // query snapshot records by seq
|
||||
#define CMD_DBN_SNAP_CLEAR 0x2A // clear snapshot (audit trail)
|
||||
|
||||
|
||||
#define CMD_DBN_RW_UART_BAUD 0x31
|
||||
|
||||
@@ -73,7 +80,8 @@
|
||||
#define MAX_BLE_DAT_RESPONSE_LEN (BLE_BUFF_MAX_LEN - 4) // 14 //返回 的所有字节数不能超过20个字节,除去 magic、header和ckb校验字节,剩下数据的字节数为14
|
||||
#define MAX_BLE_Notify_Buf_LEN BLE_BUFF_MAX_LEN //24
|
||||
#define MAX_BLE_BUF_LEN BLE_BUFF_MAX_LEN //128
|
||||
#define MAX_BLE_TMP_BUF_LEN BLE_BUFF_MAX_LEN
|
||||
#define MAX_BLE_TMP_BUF_LEN 132 // offlog QUERY: 1 status + 4x32B records
|
||||
#define MAX_BLE_DAT_BUF_LEN 132 // response data buffer (align tmp buf)
|
||||
|
||||
typedef struct _BLE_Notify_Buf_
|
||||
{
|
||||
@@ -91,7 +99,7 @@ typedef struct _BUF_DBN_BLE_
|
||||
uint8_t dat_offset;
|
||||
uint8_t pkg_amount;
|
||||
uint8_t pkg_seq;
|
||||
uint8_t dat[MAX_BLE_BUF_LEN];
|
||||
uint8_t dat[MAX_BLE_DAT_BUF_LEN];
|
||||
} Buf_DBN_BLE;
|
||||
|
||||
extern Buf_DBN_BLE g_buf_ble_response;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/********************************** (C) COPYRIGHT *******************************
|
||||
* File Name : fault_diag.h
|
||||
* Author : wfq
|
||||
* Version : V1.0
|
||||
* Date : 2026-08-13
|
||||
* Description : 故障诊断 — .noinit RAM 现场 + 执行轨迹 marker
|
||||
*
|
||||
* 背景: HardFault 时 UART/printf 可能不可用 (栈坏/中断禁用), 打印丢现场;
|
||||
* RST=0x00000000 的复位无任何标志, 无法从 RSTSCKR 判断复位源。
|
||||
* 方案: 全局记录放 .noinit 段 (复位不清零, 链接脚本已加):
|
||||
* - HardFault_Handler 纯 RAM 写 mcause/mepc/mtval (不依赖 UART)
|
||||
* - 关键路径 FAULT_MARKER() 记录执行轨迹
|
||||
* - main 开头 fault_diag_init() 打印上次现场 + 递增 boot_cnt
|
||||
* 用法: FAULT_MARKER(MK_XXX) 插在嫌疑路径各阶段; 复位后看 marker 停在哪。
|
||||
*******************************************************************************/
|
||||
|
||||
#ifndef __FAULT_DIAG_H
|
||||
#define __FAULT_DIAG_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t magic; /* 0xFA57FA57 = 现场有效 */
|
||||
uint32_t mcause; /* RISC-V 异常原因 (5/7=野指针 2=非法指令 4/6=未对齐) */
|
||||
uint32_t mepc; /* 异常指令 PC (对照 .map 反查函数) */
|
||||
uint32_t mtval; /* 错误访问地址 */
|
||||
uint32_t marker; /* 最后执行位置标记 */
|
||||
uint32_t boot_cnt; /* 复位计数 */
|
||||
} FaultDiag;
|
||||
|
||||
/* 定义在 ch32v20x_it.c (.noinit 段) */
|
||||
extern FaultDiag g_fault_diag;
|
||||
|
||||
/* main 开头调用: 打印上次现场 + 重置 + boot_cnt++ */
|
||||
void fault_diag_init(void);
|
||||
|
||||
/* 执行轨迹标记 */
|
||||
#define FAULT_MARKER(s) (g_fault_diag.marker = (s))
|
||||
|
||||
#define MK_MAIN_ENTRY 0xA0000001UL /* main 入口 */
|
||||
#define MK_INIT_DONE 0xA0000002UL /* 初始化完成, 进主循环前 */
|
||||
#define MK_LOOP_TOP 0xA0000010UL /* 主循环 while 开头 */
|
||||
#define MK_UART_SRV_IN 0xA0000011UL /* uart_srv 调用前 */
|
||||
#define MK_UART_SRV_OUT 0xA0000012UL /* uart_srv 返回后 */
|
||||
#define MK_SIM_SPI_IN 0xA0000013UL /* sim_snap_spi 调用前 */
|
||||
#define MK_POLL_BLE_IN 0xA0000014UL /* poll_dbn_ble 调用前 */
|
||||
#define MK_LUP_FRAME_IN 0xA0000020UL /* lup_process_frame 入口 */
|
||||
#define MK_LUP_FRAME_OUT 0xA0000021UL /* lup_process_frame 出口 */
|
||||
#define MK_INGEST_IN 0xA0000030UL /* iot_sensor_ingest 入口 */
|
||||
#define MK_INGEST_OUT 0xA0000031UL /* iot_sensor_ingest 出口 */
|
||||
#define MK_EVT_FEED_IN 0xA0000032UL /* iot_evt_feed 入口 */
|
||||
#define MK_EVT_FEED_OUT 0xA0000033UL /* iot_evt_feed 出口 */
|
||||
#define MK_EVT_CB_IN 0xA0000034UL /* iot_evt_sensor_cb 入口 */
|
||||
#define MK_EVT_CB_OUT 0xA0000035UL /* iot_evt_sensor_cb 出口 */
|
||||
|
||||
#endif /* __FAULT_DIAG_H */
|
||||
@@ -22,7 +22,7 @@
|
||||
#define IOT_MQTT_RECONNECT_MAX_MS 60000 // 最大重连间隔 (指数退避上限)
|
||||
#define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s)
|
||||
#define IOT_MQTT_RECV_BUF_LEN 1024 // 接收缓冲区 (单次交互 ≤1024)
|
||||
#define IOT_MQTT_SEND_BUF_LEN 1024 // 发送缓冲区 (单次交互 ≤1024)
|
||||
#define IOT_MQTT_SEND_BUF_LEN 800 // 发送缓冲区 (2026-08-18: hex 上报方案, 快照 2 条 128hex×2+头 ~420B; loop_data ~604B; 不扩容)
|
||||
#define IOT_MQTT_TOPIC_MAX_LEN 128 // Topic 最大长度
|
||||
|
||||
/*===========================================================================
|
||||
@@ -52,5 +52,7 @@ void iot_mqtt_poll(void); // 主循环轮询 (状态机 + 心跳 +
|
||||
void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理
|
||||
void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT
|
||||
void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04)
|
||||
void iot_watchdog_init(void); // 初始化硬件 IWDG (无条件, 主循环卡死兜底)
|
||||
void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用)
|
||||
|
||||
#endif /* __IOT_MQTT_SRV_H__ */
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* @brief DLD960Loop 串口通信协议 (7F) — CH32V208 ↔ DLD960Loop MCU
|
||||
*
|
||||
* 帧格式: 7F | Addr | LEN | CMD | Data... | XOR | SUM
|
||||
* UART: USART2, 115200bps, PA2(Tx)/PA3(Rx)
|
||||
* UART: USART2, 192000bps, PA2(Tx)/PA3(Rx)
|
||||
*
|
||||
* 该协议是通信MCU(CH32V208)与地感MCU(DLD960Loop/AT32F421)之间的接口。
|
||||
* 网络接口(TCP JSON/5960)通过本模块向地感MCU下发命令并获取数据。
|
||||
@@ -212,7 +212,11 @@ typedef enum {
|
||||
LUP_FRAME_STATE_HEADER, // 接收 Header (Addr, LEN, CMD)
|
||||
LUP_FRAME_STATE_VALUE, // 接收 Value
|
||||
LUP_FRAME_STATE_CHECK, // 接收 Checksum
|
||||
LUP_FRAME_STATE_COMPLETE // 帧完成
|
||||
LUP_FRAME_STATE_COMPLETE, // 帧完成
|
||||
/* 0x9F OTA 专用状态 (2026-08-19): 帧格式 9F SubL SubH LEN CMD DATA CHECK */
|
||||
LUP_FRAME_STATE_OTA_LEN, // 0x9F: 收 LEN (含 CMD)
|
||||
LUP_FRAME_STATE_OTA_CMD, // 0x9F: 收 CMD
|
||||
LUP_FRAME_STATE_OTA_CHECK // 0x9F: 收 CHECK (仅 SUM, 无 XOR)
|
||||
} LUP_FrameState;
|
||||
|
||||
typedef struct {
|
||||
@@ -228,6 +232,11 @@ extern LUP_FrameParser g_lup_parser;
|
||||
/* 喂一个字节给帧解析器,返回 1 表示帧接收完成 */
|
||||
int lup_feed_byte(uint8_t byte);
|
||||
|
||||
/* 0x9F OTA 帧解析器 (OTA 模式专用, g_flag_counter_ota.flag=1 时使用)
|
||||
* 帧格式: 9F SubL SubH LEN CMD DATA(LEN-1) CHECK(SUM)
|
||||
* 与 0x7F 共用 g_lup_parser 缓冲/索引, 状态值独立 (LUP_FRAME_STATE_OTA_*) */
|
||||
int lup_feed_byte_ota(uint8_t byte);
|
||||
|
||||
/* 取完整帧的指针和数据长度 */
|
||||
const uint8_t *lup_frame_data(void);
|
||||
uint16_t lup_frame_len(void);
|
||||
|
||||
@@ -36,7 +36,7 @@ extern "C" {
|
||||
/* The number of sockets, the maximum is 31 */
|
||||
#define WCHNET_MAX_SOCKET_NUM (WCHNET_NUM_IPRAW+WCHNET_NUM_UDP+WCHNET_NUM_TCP+WCHNET_NUM_TCP_LISTEN)
|
||||
|
||||
#define WCHNET_TCP_MSS 576 //1024 //1460 /* Size of TCP MSS*/
|
||||
#define WCHNET_TCP_MSS 700 //576 //1024 //1460 /* Size of TCP MSS*/
|
||||
|
||||
#define WCHNET_NUM_POOL_BUF (WCHNET_NUM_TCP*2+2) /* The number of POOL BUFs, the number of receive queues */
|
||||
|
||||
@@ -47,6 +47,12 @@ extern "C" {
|
||||
|
||||
#define ETH_RXBUFNB 4 /* Number of MAC received descriptors */
|
||||
|
||||
/* 2026-08-17 RAM 瘦身: ETH_MAX_PACKET_SIZE 1520 → 768.
|
||||
最大 TCP 帧 (MSS=700) = 700+20+20+14+4 = 758B ≤ 768 ✓;
|
||||
停车场景 (TCP JSON/MQTT/SSC UDP 小包) 无 >728B UDP 数据。
|
||||
省 .bss: (1520-768)×(RX 4 + TX 1) = 3.76KB (栈空间 +3.76KB)。
|
||||
若现场出现大 UDP 包截断, 改回 1024 (省 2.48KB)。 */
|
||||
#define ETH_MAX_PACKET_SIZE 768
|
||||
#ifndef ETH_MAX_PACKET_SIZE
|
||||
#define ETH_RX_BUF_SZE 1520 /* MAC receive buffer length, an integer multiple of 4 */
|
||||
#define ETH_TX_BUF_SZE 1520 /* MAC send buffer length, an integer multiple of 4 */
|
||||
@@ -84,7 +90,10 @@ extern "C" {
|
||||
/* If you want to achieve a higher transmission speed,
|
||||
* try to increase RECE_BUF_LEN to (WCHNET_TCP_MSS*4)
|
||||
* and increase WCHNET_NUM_TCP_SEG to (WCHNET_NUM_TCP*4)*/
|
||||
#define RECE_BUF_LEN (WCHNET_TCP_MSS*2) /* socket receive buffer size */
|
||||
/* 2026-08-17 RAM 瘦身: MSS×2(1400)→1024 — MSS=700 时完整 TCP 段+头≈740B,
|
||||
1024 足够; 5 个 RECE_BUF_LEN 数组 (SocketRecvBuf/MyBuf/tmp/_iot_wchnet_buf/
|
||||
g_json_wchnet_buf) 合计省 ~2.6KB .bss (栈空间 +2.6KB) */
|
||||
#define RECE_BUF_LEN 1024 /* socket receive buffer size */
|
||||
|
||||
#define WCHNET_NUM_PBUF WCHNET_NUM_POOL_BUF /* Number of PBUF structures */
|
||||
|
||||
@@ -92,7 +101,7 @@ extern "C" {
|
||||
|
||||
#define WCHNET_MEM_HEAP_SIZE (((WCHNET_TCP_MSS+0x10+54+8)*WCHNET_NUM_TCP_SEG)+ETH_TX_BUF_SZE+64+2*0x18) /* memory heap size */
|
||||
|
||||
#define WCHNET_NUM_ARP_TABLE 50 /* Number of ARP lists */
|
||||
#define WCHNET_NUM_ARP_TABLE 16 /* 2026-08-17: 50→16 (停车设备同时连接 IP 少, 省 ~0.8KB .bss) */
|
||||
|
||||
#define WCHNET_MEM_ALIGNMENT 4 /* 4 byte alignment */
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file offlog.h
|
||||
* @author wfq
|
||||
* @version V1.0
|
||||
* @date 2026-08-04
|
||||
* @brief DBN 脱机事件日志 (W25Qxx 外部 SPI NOR, 环形)
|
||||
*
|
||||
* 目的: 解决"平台重复上线/异常重启但现场无日志"的取证问题。
|
||||
* 区分「设备真复位」vs「MQTT 断连重连」(两者都会导致平台重收 initialize):
|
||||
* - 真复位: 会有 OFFLOG_EVT_BOOT 事件, boot_seq 递增
|
||||
* - 断连重连: boot_seq 不变, 只有 IOT_* 网络事件
|
||||
*
|
||||
* 分区 (2026-08-12 ROADMAP 新规划):
|
||||
* | 参数区 0x000000 64KB (固定)
|
||||
* | OTA 区 0x010000 512KB (固定)
|
||||
* | 事件区 0x090000 动态: W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB
|
||||
* | 快照区 事件区后 剩余容量 (snapshot.c, 2026-08-12 已实现)
|
||||
* 上电读 JEDEC ID (0x9F) 决定事件区容量 (g_offlog_part 运行时分区表)
|
||||
*
|
||||
* 环形实现:
|
||||
* - 扇区 0 (区内) = 头扇区, 存元数据 (boot_seq/写位置/条数), 扇区切换时更新
|
||||
* - 扇区 1..63 = 数据扇区, 每扇区 128 条 x 32B 定长记录
|
||||
* - 顺序写, 写满擦下一扇区 (环形覆盖), 天然磨损均衡
|
||||
* - 上电从头部记录的写位置向后扫描, 找第一条无效记录 = 真实写位置 (掉电恢复)
|
||||
*
|
||||
* 记录格式 (32B 定长):
|
||||
* magic(1) type(1) len(1) flags(1) seq(4) boot_seq(2) rsvd(2)
|
||||
* ts_ms(4) unix_ts(4) payload(14)
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef _OFFLOG_H__
|
||||
#define _OFFLOG_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*===========================================================================
|
||||
* 分区与容量
|
||||
*===========================================================================*/
|
||||
/* 固定分区 (ROADMAP 2026-08-12): 参数区 64KB + OTA 512KB, 事件区紧随其后 */
|
||||
#define OFFLOG_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */
|
||||
#define OFFLOG_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */
|
||||
#define OFFLOG_AREA_BASE (OFFLOG_PARAM_SIZE + OFFLOG_OTA_SIZE) /* 0x090000 */
|
||||
|
||||
#define OFFLOG_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
|
||||
#define OFFLOG_EVT_PER_SECTOR 128 /* 4096 / 32 */
|
||||
|
||||
/* 芯片 JEDEC ID (0x9F 命令第 3 字节, EF 40 xx) */
|
||||
#define OFFLOG_CHIP_W25Q32 0x16
|
||||
#define OFFLOG_CHIP_W25Q64 0x17
|
||||
#define OFFLOG_CHIP_W25Q128 0x18
|
||||
#define OFFLOG_CHIP_W25Q256 0x19
|
||||
|
||||
/* 运行时分区表 (offlog_init 由 JEDEC ID 填充; 默认 W25Q32) */
|
||||
typedef struct {
|
||||
uint8_t chip; /* OFFLOG_CHIP_xxx */
|
||||
uint32_t area_size; /* 事件日志区大小 */
|
||||
uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */
|
||||
uint32_t max_records; /* 最大记录数 = data_sectors * 128 */
|
||||
} OfflogPart;
|
||||
extern OfflogPart g_offlog_part;
|
||||
|
||||
/* 兼容宏: 展开为运行时值 (offlog.c 主体引用不变) */
|
||||
#define OFFLOG_AREA_SIZE (g_offlog_part.area_size)
|
||||
#define OFFLOG_DATA_SECTOR_CNT (g_offlog_part.data_sectors)
|
||||
#define OFFLOG_DATA_SIZE ((g_offlog_part.data_sectors) * 4096UL)
|
||||
#define OFFLOG_MAX_RECORDS (g_offlog_part.max_records)
|
||||
#define OFFLOG_DATA_BASE (OFFLOG_AREA_BASE + 4096UL)
|
||||
|
||||
/* 头扇区记录 (32B, 扇区切换时更新) */
|
||||
typedef struct {
|
||||
uint8_t magic[4]; /* "OFFL" */
|
||||
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
|
||||
uint32_t wr_sector; /* 上次写扇区号 (区内 1..63) */
|
||||
uint32_t wr_off; /* 上次写偏移 (相对 OFFLOG_AREA_BASE) */
|
||||
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
|
||||
uint32_t count; /* 有效记录数 (0..max_records) */
|
||||
uint32_t rsvd[2];
|
||||
} OfflogHead; /* 28B, 按 32B 对齐使用 */
|
||||
|
||||
#define OFFLOG_HEAD_MAGIC0 'O'
|
||||
#define OFFLOG_HEAD_MAGIC1 'F'
|
||||
#define OFFLOG_HEAD_MAGIC2 'L'
|
||||
#define OFFLOG_HEAD_MAGIC3 'L'
|
||||
|
||||
/* 事件记录 (32B 定长, 字段重排保证自然对齐无 padding:
|
||||
4×uint8 → uint32×3 → uint16×2 → payload[12] = 4+12+4+12 = 32) */
|
||||
typedef struct {
|
||||
uint8_t magic; /* 0xA5 */
|
||||
uint8_t type; /* OFFLOG_EVT_xxx */
|
||||
uint8_t len; /* payload 有效字节数 (0..12) */
|
||||
uint8_t flags; /* bit0: unix_ts 有效 */
|
||||
uint32_t seq; /* 全局序号 (跨 boot 递增) */
|
||||
uint32_t ts_ms; /* boot 内相对时间 mstick() */
|
||||
uint32_t unix_ts; /* 已同步 Unix 秒 (flags.bit0=1 时有效) */
|
||||
uint16_t boot_seq; /* 启动序号 */
|
||||
uint16_t rsvd;
|
||||
uint8_t payload[12]; /* 事件参数 */
|
||||
} OfflogEvt; /* 32B, 无 padding */
|
||||
|
||||
#define OFFLOG_EVT_MAGIC 0xA5
|
||||
#define OFFLOG_EVT_SIZE sizeof(OfflogEvt)
|
||||
#define OFFLOG_UNIX_VALID_FLAG 0x01
|
||||
|
||||
/*===========================================================================
|
||||
* 事件类型
|
||||
*===========================================================================*/
|
||||
enum {
|
||||
OFFLOG_EVT_BOOT = 0x01, /* 上电/复位; payload[0..3]=复位原因寄存器 RCC_RSTSCKR */
|
||||
OFFLOG_EVT_IOT_CONNECT = 0x10, /* MQTT TCP 连接成功 (socket 中断) */
|
||||
OFFLOG_EVT_IOT_READY = 0x11, /* MQTT SUBACK → 发 initialize (重复上线直接原因!) */
|
||||
OFFLOG_EVT_IOT_DISCONN = 0x12, /* MQTT 断连; payload[0]=原因(1=断开 2=超时 3=CONNACK拒绝 4=连接超时) */
|
||||
OFFLOG_EVT_IOT_RECONN = 0x13, /* 重连退避; payload[0..3]=退避 ms */
|
||||
OFFLOG_EVT_EVT_RETRY = 0x30, /* event_report ACK 超时重发; payload[0..3]=msg_id, payload[4]=retry */
|
||||
OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */
|
||||
OFFLOG_EVT_COIL = 0x40, /* 线圈事件; payload[0]=sub(1=进 2=出 3=断开 4=恢复), payload[1]=ch, payload[2..5]=value(50ms) */
|
||||
OFFLOG_EVT_TIME_ANCHOR = 0x50, /* 时钟同步锚点; unix_ts=同步值 */
|
||||
OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */
|
||||
};
|
||||
|
||||
/* 复位原因寄存器 (CH32V20x RCC 基址 0x40021000, RSTSCKR 偏移 0x24,
|
||||
STM32F1 兼容布局; 若实际硬件布局不同, 读出的原因位会异常, 待板上验证) */
|
||||
#define OFFLOG_RCC_BASE 0x40021000UL
|
||||
#define OFFLOG_RCC_RSTSCKR (*(volatile uint32_t *)(OFFLOG_RCC_BASE + 0x24UL))
|
||||
|
||||
/* 复位原因位 (RCC_RSTSCKR, CH32V20x 布局 — 2026-08-12 对照 WCH ch32v20x.h 修正,
|
||||
原定义错位: IWDG/SFT/POR 位置全错, 且 RMVF 清标志写错位导致标志从未清除) */
|
||||
#define OFFLOG_RST_LPWR (1UL << 31) /* 低功耗复位 */
|
||||
#define OFFLOG_RST_WWDG (1UL << 30) /* 窗口看门狗复位 */
|
||||
#define OFFLOG_RST_IWDG (1UL << 29) /* 独立看门狗复位 (IWDG 超时 → 主循环卡死!) */
|
||||
#define OFFLOG_RST_SFT (1UL << 28) /* 软件复位 NVIC_SystemReset */
|
||||
#define OFFLOG_RST_POR (1UL << 27) /* 上电/掉电复位 (真断电!) */
|
||||
#define OFFLOG_RST_PIN (1UL << 26) /* NRST 引脚复位 */
|
||||
#define OFFLOG_RST_RMVF (1UL << 24) /* 清除复位标志 (写 1 清全部, WCH: RCC_RMVF) */
|
||||
|
||||
/*===========================================================================
|
||||
* API
|
||||
*===========================================================================*/
|
||||
void offlog_init(void); /* 上电初始化 (扫描恢复) + 内部准备 BOOT 事件 */
|
||||
void offlog_boot(uint32_t reset_reason); /* 记录 BOOT 事件 (带复位原因) */
|
||||
|
||||
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len);
|
||||
|
||||
void offlog_iot_connect(void);
|
||||
void offlog_iot_ready(void);
|
||||
void offlog_iot_disconnect(uint8_t reason);
|
||||
void offlog_iot_reconn(uint32_t backoff_ms);
|
||||
void offlog_evt_retry(uint32_t msg_id, uint8_t retry);
|
||||
void offlog_evt_giveup(uint32_t msg_id);
|
||||
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value);
|
||||
void offlog_time_anchor(uint32_t unix_ts);
|
||||
|
||||
uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */
|
||||
uint16_t offlog_count(void); /* 有效记录数 */
|
||||
int offlog_read_idx(uint16_t idx, OfflogEvt *out); /* 按逻辑序号读, 0=成功 -1=越界 */
|
||||
void offlog_clear(void); /* 清空事件区 (清空后写 LOG_CLEAR 审计) */
|
||||
|
||||
/*===========================================================================
|
||||
* 协议导出 (MQTT V1.06 / TCP JSON V1.02)
|
||||
*===========================================================================*/
|
||||
uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */
|
||||
uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */
|
||||
const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */
|
||||
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 (旧方案, 保留) */
|
||||
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len); /* 32B → 64 hex (TCP/MQTT log_query, V1.03/V1.07) */
|
||||
#define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */
|
||||
|
||||
#endif /* _OFFLOG_H__ */
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file snapshot.h
|
||||
* @author wfq
|
||||
* @version V1.0
|
||||
* @date 2026-08-12
|
||||
* @brief DBN 传感快照日志 (W25Qxx 外部 SPI NOR, 环形, 与事件日志区分区独立)
|
||||
*
|
||||
* 用途: 0xC0 传感帧 (4 线圈) 按上报节奏落盘, 断网期间也有现场波形可查;
|
||||
* 事件流管"发生了什么", 快照流管"波形长什么样"。
|
||||
*
|
||||
* 分区 (ROADMAP 2026-08-12 新规划):
|
||||
* | 参数区 0x000000 64KB (固定)
|
||||
* | OTA 区 0x010000 512KB (固定)
|
||||
* | 事件区 0x090000 动态 (offlog 持有, g_offlog_part.area_size)
|
||||
* | 快照区 事件区后 剩余容量 (总容量 - 固定区 - 事件区)
|
||||
* 上电读 JEDEC ID (0x9F) 决定事件区/快照区容量 (g_snap_part 运行时分区表)
|
||||
*
|
||||
* 环形实现 (与 offlog 同模式):
|
||||
* - 扇区 0 (区内) = 头扇区, 存元数据 (boot_seq/写位置/条数), 扇区切换时更新
|
||||
* - 数据扇区每扇区 64 条 x 64B 定长记录 (4096/64)
|
||||
* - 顺序写, 写满擦下一扇区 (环形覆盖), 天然磨损均衡
|
||||
* - 上电从头部记录的写位置向后扫描, 找第一条无效记录 = 真实写位置 (掉电恢复)
|
||||
*
|
||||
* 线程模型 (关键, 2026-08-12 修正):
|
||||
* - USART2 ISR 只逐字节喂 lup_feed_byte(), 完整帧由主循环 uart_srv() 调
|
||||
* lup_process_frame → iot_sensor_ingest → snap_enqueue() — 全程主循环
|
||||
* - snap_enqueue(): 打包 + RAM 环形暂存 (无 SPI, 无阻塞); 满时丢新不覆盖
|
||||
* - snap_flush(): iot_mqtt_publish_sensor 每轮调用, RAM 暂存 → SPI 落盘
|
||||
* (扇区切换擦除 ~45ms 阻塞发生在主循环, 与 offlog 同底线)
|
||||
* - 单生产者单消费者无锁环形: 生产者只写 tail/count, 消费者只读 head
|
||||
*
|
||||
* 记录格式 SnapRec (64B 定长, 无 padding):
|
||||
* magic(1) len(1) flags(1) rsvd(1) | seq(4) ts_ms(4) boot_seq(2) rsvd2(2) | coils(48)
|
||||
* coils = 4 x 12B, 与 0xC0 线上线圈单元完全一致:
|
||||
* [0] 配置1 freq_level(2)|direction(1)|freq_type(1)|sensitivity(4)
|
||||
* [1] 条件 condition(4)|loop_state(1)|car_state(1)|misc_type(2)
|
||||
* [2..4] 频率 3B LE
|
||||
* [5..7] 变化量 3B LE (有符号补码)
|
||||
* [8..11] 杂项 4B LE
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef _SNAPSHOT_H__
|
||||
#define _SNAPSHOT_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include "offlog.h" /* OFFLOG_CHIP_xxx + g_offlog_part (事件区大小) */
|
||||
#include "loop_uart_proto.h" /* LUP_SensorReport / LUP_CoilSensor */
|
||||
|
||||
/*===========================================================================
|
||||
* 分区与容量
|
||||
*===========================================================================*/
|
||||
/* 固定分区 (与 offlog 一致): 参数区 64KB + OTA 512KB */
|
||||
#define SNAP_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */
|
||||
#define SNAP_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */
|
||||
#define SNAP_FIXED_SIZE (SNAP_PARAM_SIZE + SNAP_OTA_SIZE) /* 0x090000 */
|
||||
|
||||
#define SNAP_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
|
||||
#define SNAP_REC_PER_SECTOR 64 /* 4096 / 64 */
|
||||
|
||||
/* 运行时分区表 (snap_init 由 JEDEC ID + g_offlog_part 填充; 默认 W25Q32) */
|
||||
typedef struct {
|
||||
uint8_t chip; /* OFFLOG_CHIP_xxx */
|
||||
uint32_t area_base; /* 快照区起始 (事件区后) */
|
||||
uint32_t area_size; /* 快照区大小 (剩余容量) */
|
||||
uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */
|
||||
uint32_t max_records; /* 最大记录数 = data_sectors * 64 */
|
||||
} SnapPart;
|
||||
extern SnapPart g_snap_part;
|
||||
|
||||
/* 兼容宏: 展开为运行时值 */
|
||||
#define SNAP_DATA_BASE (g_snap_part.area_base + 4096UL)
|
||||
#define SNAP_AREA_SIZE (g_snap_part.area_size)
|
||||
#define SNAP_DATA_SECTOR_CNT (g_snap_part.data_sectors)
|
||||
#define SNAP_DATA_SIZE ((g_snap_part.data_sectors) * 4096UL)
|
||||
#define SNAP_MAX_RECORDS (g_snap_part.max_records)
|
||||
|
||||
/* 头扇区记录 (32B, 扇区切换时更新) */
|
||||
typedef struct {
|
||||
uint8_t magic[4]; /* "SNAP" */
|
||||
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
|
||||
uint32_t wr_sector; /* 上次写扇区号 (区内 1..n) */
|
||||
uint32_t wr_off; /* 上次写偏移 (绝对地址) */
|
||||
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
|
||||
uint32_t count; /* 有效记录数 (0..max_records) */
|
||||
uint32_t rsvd[2];
|
||||
} SnapHead; /* 28B, 按 32B 对齐使用 */
|
||||
|
||||
#define SNAP_HEAD_MAGIC0 'S'
|
||||
#define SNAP_HEAD_MAGIC1 'N'
|
||||
#define SNAP_HEAD_MAGIC2 'A'
|
||||
#define SNAP_HEAD_MAGIC3 'P'
|
||||
|
||||
/* 快照记录 (64B 定长, 字段重排保证自然对齐无 padding:
|
||||
4×uint8 → uint32×2 → uint16×2 → coils[48] = 16+48 = 64) */
|
||||
typedef struct {
|
||||
uint8_t magic; /* 0xA6 (区别于事件 0xA5) */
|
||||
uint8_t len; /* 线圈数据有效长度 = coil_count*12 (0..48) */
|
||||
uint8_t flags; /* bit0: 分包帧 (预留) */
|
||||
uint8_t rsvd;
|
||||
uint32_t seq; /* 全局序号 (跨 boot 递增, flush 时分配) */
|
||||
uint32_t ts_ms; /* boot 内相对时间 mstick() (采集时刻) */
|
||||
uint16_t boot_seq; /* 启动序号 */
|
||||
uint16_t rsvd2;
|
||||
uint8_t coils[48]; /* 4 × 12B 线圈数据 (0xC0 线上格式原样) */
|
||||
} SnapRec; /* 64B, 无 padding */
|
||||
|
||||
#define SNAP_MAGIC 0xA6
|
||||
#define SNAP_REC_SIZE sizeof(SnapRec)
|
||||
#define SNAP_COIL_BYTES 12
|
||||
#define SNAP_COILS_MAX 4
|
||||
|
||||
/* RAM 暂存深度 (中断 enqueue → 主循环 flush 之间的缓冲)
|
||||
Loop 帧最快 150ms, publish_sensor 每轮 flush, 8 深 ≈ 1.2s 余量;
|
||||
CH32V208 SRAM 紧张: 64B × 8 = 512B, 远小于 SPI_FLASH_BUF 的 4KB */
|
||||
#define SNAP_RAM_DEPTH 8
|
||||
#define SNAP_MAX_QUERY_RECORDS 2 /* QUERY 分页上限: BLE 原始 64B×2 (132B 缓冲); TCP/MQTT hex 上报 64B->128 字符×2 亦安全 (2026-08-18) */
|
||||
|
||||
|
||||
/*===========================================================================
|
||||
* API
|
||||
*===========================================================================*/
|
||||
void snap_init(void); /* 上电初始化 (扫描恢复) */
|
||||
void snap_delayed_init(void); /* 主循环延后初始化 (开机 3s 后, 2026-08-17) */
|
||||
uint8_t snap_enabled(void); /* Flash 初始化成功 */
|
||||
|
||||
void snap_enqueue(const LUP_SensorReport *sr); /* 主循环入队: 打包→RAM 暂存 (满丢新) */
|
||||
void snap_flush(void); /* 主循环: RAM 暂存→SPI 落盘 */
|
||||
|
||||
uint32_t snap_count(void); /* 有效记录数 */
|
||||
uint32_t snap_seq_last(void); /* 最新一条记录全局序号 (0=空) */
|
||||
uint32_t snap_boot_seq(void); /* 当前 boot 序号 */
|
||||
int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */
|
||||
void snap_clear(void); /* 清空快照区 (审计写入事件流) */
|
||||
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len); /* 记录 -> JSON (旧方案, 保留) */
|
||||
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len); /* 64B -> 128 hex (TCP/MQTT log_query, V1.03/V1.07) */
|
||||
|
||||
#endif /* _SNAPSHOT_H__ */
|
||||
@@ -32,7 +32,10 @@ void load_cfg_from_flash(void);
|
||||
|
||||
void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size);
|
||||
void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size);
|
||||
void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size);
|
||||
void SPI_Flash_Erase_Sector(uint32_t Dst_Addr);
|
||||
|
||||
void storage_init(void);
|
||||
uint8_t SPI_Flash_ReadJEDEC_ID(void); /* 0x9F JEDEC device id (W25Q32=0x16...) */
|
||||
|
||||
#endif
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
* Protocol Constants
|
||||
*===========================================================================*/
|
||||
#define TCP_JSON_PORT 5960 // TCP JSON protocol default port
|
||||
#define TCP_JSON_MAX_FRAME 4096 // Max JSON frame length
|
||||
#define TCP_JSON_MAX_FRAME 800 //4096 // Max JSON frame length
|
||||
#define TCP_JSON_DATA_BUF_LEN 1024 // 本地 JSON 组装缓冲长度 (data_json)
|
||||
#define TCP_JSON_AUTH_TIMEOUT_MS 60000 // 60s auth timeout (ms)
|
||||
#define TCP_JSON_RECV_BUF_LEN (TCP_JSON_MAX_FRAME * 2)
|
||||
#define TCP_JSON_MAX_PWD_RETRY 3 // Max password retries before lockout
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include "simple_json.h"
|
||||
#include "tcp_json_srv.h"
|
||||
#include "storage.h"
|
||||
#include "offlog.h"
|
||||
#include "snapshot.h"
|
||||
#include "fault_diag.h"
|
||||
#include "ch32v20x_iwdg.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@@ -36,8 +40,9 @@ extern ReportConfig g_report_cfg; // from tcp_json_srv.c
|
||||
extern uint8_t SocketId_TCP; // from net_srv.c, MQTT socket created by WCHNET_CreateTcpMqttSocket
|
||||
uint8_t g_iot_socket = 0xFF; // MQTT TCP socket ID
|
||||
IotMqttState g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
static IotMqttState _prev_iot_state = IOT_STATE_DISCONNECTED; // 事件日志: 状态沿检测
|
||||
uint8_t g_iot_msg_id = 0; // MQTT packet identifier
|
||||
static uint32_t _iot_last_heartbeat = 0; // 上次心跳时刻 (ms)
|
||||
static uint32_t _iot_last_heartbeat = 0; // 上次 PINGREQ 时刻 (ms), MQTT 保活
|
||||
static uint32_t _iot_reconnect_deadline = 0;
|
||||
static uint32_t _iot_reconnect_backoff = 0;
|
||||
static uint32_t _iot_connect_start = 0; // TCP connect 开始时刻
|
||||
@@ -107,6 +112,8 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
|
||||
IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH];
|
||||
e->type = type; e->ch = ch; e->value = value;
|
||||
_evt_count++;
|
||||
/* 事件日志: 线圈事件 (主循环上下文) */
|
||||
offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */
|
||||
}
|
||||
_evt_gaveup = 0; // 新事件到达 → 解除挂起, 触发合并上报
|
||||
PRINT("EVT: enqueue type=%d ch=%d val=%lu (cnt=%d)\n", type, ch, value, _evt_count);
|
||||
@@ -115,12 +122,14 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
|
||||
/* 沿检测: 每收到一帧有效 0xC0 调用一次 (两条消费路径都要喂) */
|
||||
static void iot_evt_feed(const LUP_SensorReport *sr) {
|
||||
uint8_t i;
|
||||
FAULT_MARKER(MK_EVT_FEED_IN);
|
||||
if (!_evt_prev_valid) { // 首帧: 只建快照 (上电已有车不算进入沿)
|
||||
for (i = 0; i < LUP_COIL_COUNT; i++) {
|
||||
_evt_prev_car[i] = (i < sr->coil_count) ? sr->coils[i].car_state : 0;
|
||||
_evt_prev_loop[i] = (i < sr->coil_count) ? sr->coils[i].loop_state : 0;
|
||||
}
|
||||
_evt_prev_valid = 1;
|
||||
FAULT_MARKER(MK_EVT_FEED_OUT);
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < sr->coil_count && i < LUP_COIL_COUNT; i++) {
|
||||
@@ -148,6 +157,7 @@ static void iot_evt_feed(const LUP_SensorReport *sr) {
|
||||
_evt_prev_car[i] = cs->car_state;
|
||||
_evt_prev_loop[i] = cs->loop_state;
|
||||
}
|
||||
FAULT_MARKER(MK_EVT_FEED_OUT);
|
||||
}
|
||||
|
||||
/* 统一帧摄取: 事件沿检测 + 刷新 loop_data 缓存 (单一数据源)
|
||||
@@ -156,18 +166,23 @@ static void iot_evt_feed(const LUP_SensorReport *sr) {
|
||||
帧竞争的概率实测 <2% → loop_data 发布 34s 前的陈旧快照(车走后 iscar
|
||||
仍 true), 且陈旧 diff 锁死 fast_mode 300ms 刷屏。事件与快照必须同源! */
|
||||
static void iot_sensor_ingest(const LUP_SensorReport *sr) {
|
||||
FAULT_MARKER(MK_INGEST_IN);
|
||||
iot_evt_feed(sr); // 事件沿检测
|
||||
memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照
|
||||
_cached_sr_valid = 1;
|
||||
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */
|
||||
FAULT_MARKER(MK_INGEST_OUT);
|
||||
}
|
||||
|
||||
/* lup 传感回调: uart_srv 消费路径的帧从这里喂入 (lup_process_frame 已过校验) */
|
||||
static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) {
|
||||
LUP_SensorReport sr;
|
||||
FAULT_MARKER(MK_EVT_CB_IN);
|
||||
memset(&sr, 0, sizeof(sr));
|
||||
if (lup_parse_sensor_report(pkg, len, &sr) == 0) {
|
||||
iot_sensor_ingest(&sr);
|
||||
}
|
||||
FAULT_MARKER(MK_EVT_CB_OUT);
|
||||
}
|
||||
|
||||
/* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */
|
||||
@@ -241,11 +256,13 @@ static void iot_evt_process(void) {
|
||||
if (_evt_retry >= IOT_EVT_MAX_RETRY) {
|
||||
PRINT("EVT: retry exhausted msg_id=%lu, hold %d events\n",
|
||||
_evt_pend_id, _evt_count);
|
||||
offlog_evt_giveup(_evt_pend_id); // 事件日志: 重试耗尽
|
||||
_evt_pend_id = 0;
|
||||
_evt_gaveup = 1; // 挂起: 新事件或重连时再触发
|
||||
return;
|
||||
}
|
||||
_evt_retry++;
|
||||
offlog_evt_retry(_evt_pend_id, _evt_retry); // 事件日志: ACK 超时重发
|
||||
iot_evt_send(_evt_pend_id, _evt_pend_ts, _evt_pend_n); // 同 id/ts 重发
|
||||
_evt_sent_ms = now;
|
||||
return;
|
||||
@@ -290,13 +307,36 @@ static int iot_make_topic(char *out, uint16_t out_len, const char *direction,
|
||||
* MQTT Packet Helpers
|
||||
*===========================================================================*/
|
||||
|
||||
/* 发送 MQTT 报文到 broker */
|
||||
/* 发送 MQTT 报文到 broker — 循环重试防 WCHNET 部分发送
|
||||
(2026-07-23: SocketSend 只发 520/607B → 残留拼到下行 → broker 见垃圾 → _raw+RST)
|
||||
连续失败追踪: ret=0x11 表示 TCP 发送队列满/连接断开, 3次连续失败 → 主动断连重连
|
||||
(TCP超时要等~2分钟, 太慢了, 导致长时间丢数据) */
|
||||
static int _iot_send_fail_cnt = 0; // 连续发送失败计数
|
||||
|
||||
static int iot_mqtt_send(const uint8_t *buf, uint16_t len) {
|
||||
uint16_t slen = len;
|
||||
PRINT("IOT: SocketSend sock=%d len=%d\n", g_iot_socket, len);
|
||||
uint8_t ret = WCHNET_SocketSend(g_iot_socket, (uint8_t *)buf, &slen);
|
||||
PRINT("IOT: SocketSend ret=0x%02X sent=%d\n", ret, slen);
|
||||
return (ret == WCHNET_ERR_SUCCESS) ? 0 : -1;
|
||||
uint16_t total_sent = 0;
|
||||
int retries = 0;
|
||||
while (total_sent < len) {
|
||||
uint32_t slen = len - total_sent;
|
||||
uint8_t ret = WCHNET_SocketSend(g_iot_socket, (uint8_t *)(buf + total_sent), &slen);
|
||||
total_sent += (uint16_t)slen;
|
||||
if (slen == 0 || ret != WCHNET_ERR_SUCCESS || ++retries > 10) {
|
||||
_iot_send_fail_cnt++;
|
||||
PRINT("IOT: SocketSend FAIL#%d ret=0x%02X sent=%u/%u\n",
|
||||
_iot_send_fail_cnt, ret, total_sent, len);
|
||||
if (_iot_send_fail_cnt >= 3) {
|
||||
PRINT("IOT: SocketSend 3 consecutive failures, forcing reconnect\n");
|
||||
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
|
||||
g_iot_socket = 0xFF;
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
_iot_send_fail_cnt = 0;
|
||||
_iot_reconnect_deadline = mstick() + 3000; // 3s 后重连, 让 WCHNET 清旧 socket
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
_iot_send_fail_cnt = 0; // 成功一次就清零
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 发送 MQTT CONNECT — 参考 net_srv.c mqtt_connect 实现 */
|
||||
@@ -429,6 +469,48 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
|
||||
g_sub_code_enable.iot_enable ? "true" : "false");
|
||||
iot_mqtt_publish(resp_topic, data_json, strlen(data_json), 1);
|
||||
|
||||
} else if (strcmp(cmd_str, "report_config") == 0) {
|
||||
/* 平台下发配置 → 更新 g_report_cfg, 回 ACK */
|
||||
char data[512] = {0};
|
||||
simple_parse_json(json, "\"data\"", data);
|
||||
if (strlen(data) > 0) {
|
||||
char tmp2[64];
|
||||
memset(tmp2, 0, sizeof(tmp2));
|
||||
simple_parse_json(data, "\"enable\"", tmp2);
|
||||
if (strlen(tmp2) > 0) g_report_cfg.enable = (strcmp(tmp2, "true") == 0);
|
||||
memset(tmp2, 0, sizeof(tmp2));
|
||||
simple_parse_json(data, "\"interval\"", tmp2);
|
||||
if (strlen(tmp2) > 0) g_report_cfg.interval = (uint16_t)strtoul(tmp2, NULL, 10);
|
||||
memset(tmp2, 0, sizeof(tmp2));
|
||||
simple_parse_json(data, "\"once\"", tmp2);
|
||||
if (strlen(tmp2) > 0) g_report_cfg.once = (strcmp(tmp2, "true") == 0);
|
||||
memset(tmp2, 0, sizeof(tmp2));
|
||||
simple_parse_json(data, "\"env_eval\"", tmp2);
|
||||
if (strlen(tmp2) > 0) g_report_cfg.env_eval = (strcmp(tmp2, "true") == 0);
|
||||
memset(tmp2, 0, sizeof(tmp2));
|
||||
simple_parse_json(data, "\"sensor_type\"", tmp2);
|
||||
if (strlen(tmp2) > 0) g_report_cfg.sensor_type = (uint8_t)strtoul(tmp2, NULL, 10);
|
||||
}
|
||||
char resp[256];
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"report_config\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\"}",
|
||||
msg_id, dev_time_now());
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
|
||||
} else if (strcmp(cmd_str, "event_report") == 0) {
|
||||
/* 平台对 event_report 的 ACK: 检查 code 字段 */
|
||||
char tmp3[64] = {0};
|
||||
int code = -1;
|
||||
simple_parse_json(json, "\"code\"", tmp3);
|
||||
if (strlen(tmp3) > 0) code = (int)strtol(tmp3, NULL, 10);
|
||||
if (code == 0) {
|
||||
iot_evt_handle_ack(msg_id, 0);
|
||||
} else {
|
||||
iot_evt_handle_ack(msg_id, code);
|
||||
}
|
||||
// ACK 不回复 (避免乒乓)
|
||||
|
||||
} else if (strcmp(cmd_str, "pwd_verify") == 0) {
|
||||
// 验证密码
|
||||
char password[16] = {0};
|
||||
@@ -466,6 +548,185 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd_str, "log_stat") == 0) {
|
||||
/* 日志统计/分页定位: event/snapshot 流 (协议 V1.07) */
|
||||
char stream_buf[16];
|
||||
memset(stream_buf, 0, sizeof(stream_buf));
|
||||
simple_parse_json(json, "\"stream\"", stream_buf);
|
||||
char resp[300];
|
||||
if (strcmp(stream_buf, "snapshot") == 0) {
|
||||
uint32_t total = snap_count();
|
||||
uint32_t seq_last = snap_seq_last();
|
||||
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"log_stat\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"stream\":\"snapshot\",\"enabled\":%s,\"boot_seq\":%lu,"
|
||||
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}}",
|
||||
msg_id, dev_time_now(),
|
||||
snap_enabled() ? "true" : "false",
|
||||
(unsigned long)snap_boot_seq(),
|
||||
(unsigned long)total, (unsigned long)SNAP_MAX_RECORDS,
|
||||
(unsigned long)seq_first, (unsigned long)seq_last);
|
||||
PRINT("IOT: log_stat(snapshot) count=%lu seq_first=%lu seq_last=%lu\n",
|
||||
(unsigned long)total, (unsigned long)seq_first, (unsigned long)seq_last);
|
||||
} else {
|
||||
uint32_t total = offlog_count();
|
||||
uint32_t seq_last = offlog_seq_last();
|
||||
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"log_stat\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu,"
|
||||
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}}",
|
||||
msg_id, dev_time_now(),
|
||||
offlog_enabled() ? "true" : "false",
|
||||
(unsigned long)offlog_boot_seq(),
|
||||
(unsigned long)total, (unsigned long)OFFLOG_MAX_RECORDS,
|
||||
(unsigned long)seq_first, (unsigned long)seq_last);
|
||||
PRINT("IOT: log_stat count=%lu seq_first=%lu seq_last=%lu\n",
|
||||
(unsigned long)total, (unsigned long)seq_first, (unsigned long)seq_last);
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
|
||||
} else if (strcmp(cmd_str, "log_query") == 0) {
|
||||
/* 分页拉取脱机日志: 按全局序号, event/snapshot 流, hex 原始字节上报 (协议 V1.07) */
|
||||
static char resp[IOT_MQTT_SEND_BUF_LEN]; /* static: 避免大栈开销 */
|
||||
char stream_buf[16];
|
||||
uint32_t start_seq = 0, req_count = 0;
|
||||
int is_snap = 0;
|
||||
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"start_seq\"", tmp);
|
||||
if (strlen(tmp) > 0) start_seq = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"count\"", tmp);
|
||||
if (strlen(tmp) > 0) req_count = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
memset(stream_buf, 0, sizeof(stream_buf));
|
||||
simple_parse_json(json, "\"stream\"", stream_buf);
|
||||
if (strcmp(stream_buf, "snapshot") == 0) is_snap = 1;
|
||||
|
||||
int pos = snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"log_query\",\"ts\":%lu,\"code\":0,"
|
||||
"\"msg\":\"success\",\"data\":{\"start_seq\":%lu,\"records\":[",
|
||||
msg_id, dev_time_now(), (unsigned long)start_seq);
|
||||
uint32_t fetched = 0;
|
||||
|
||||
if (is_snap) {
|
||||
uint32_t total = snap_count();
|
||||
uint32_t seq_last = snap_seq_last();
|
||||
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
|
||||
if (req_count > SNAP_MAX_QUERY_RECORDS) req_count = SNAP_MAX_QUERY_RECORDS;
|
||||
if (req_count == 0) req_count = SNAP_MAX_QUERY_RECORDS;
|
||||
if (snap_enabled() && total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
|
||||
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
|
||||
while (fetched < req_count && (idx + fetched) < total) {
|
||||
SnapRec rec;
|
||||
char hex[132]; /* 64B -> 128 hex + NUL */
|
||||
if (snap_read_idx(idx + fetched, &rec) != 0) break;
|
||||
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ",");
|
||||
snap_rec_to_hex(&rec, hex, sizeof(hex));
|
||||
pos += snprintf(resp + pos, sizeof(resp) - pos,
|
||||
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)rec.seq, hex);
|
||||
fetched++;
|
||||
}
|
||||
}
|
||||
PRINT("IOT: log_query(snapshot) start_seq=%lu req=%lu fetched=%lu\n",
|
||||
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
|
||||
} else {
|
||||
uint32_t total = offlog_count();
|
||||
uint32_t seq_last = offlog_seq_last();
|
||||
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
|
||||
if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS;
|
||||
if (req_count == 0) req_count = OFFLOG_MAX_QUERY_RECORDS;
|
||||
if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
|
||||
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
|
||||
while (fetched < req_count && (idx + fetched) < total) {
|
||||
OfflogEvt evt;
|
||||
char hex[68]; /* 32B -> 64 hex + NUL */
|
||||
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
|
||||
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ",");
|
||||
offlog_evt_to_hex(&evt, hex, sizeof(hex));
|
||||
pos += snprintf(resp + pos, sizeof(resp) - pos,
|
||||
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)evt.seq, hex);
|
||||
fetched++;
|
||||
}
|
||||
}
|
||||
PRINT("IOT: log_query start_seq=%lu req=%lu fetched=%lu\n",
|
||||
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
|
||||
}
|
||||
snprintf(resp + pos, sizeof(resp) - pos, "]}}");
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
|
||||
} else if (strcmp(cmd_str, "log_clear") == 0) {
|
||||
/* 清除日志 (审计留痕). event 阻塞 ~2.8s (63 扇区), snapshot 阻塞 ~45ms (逻辑清除+当前扇区) */
|
||||
char stream_buf[16];
|
||||
memset(stream_buf, 0, sizeof(stream_buf));
|
||||
simple_parse_json(json, "\"stream\"", stream_buf);
|
||||
char resp[256];
|
||||
if (strcmp(stream_buf, "snapshot") == 0) {
|
||||
snap_clear();
|
||||
PRINT("IOT: log_clear(snapshot) done\n");
|
||||
} else {
|
||||
offlog_clear();
|
||||
PRINT("IOT: log_clear done\n");
|
||||
}
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"log_clear\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\"}",
|
||||
msg_id, dev_time_now());
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
|
||||
} else if (strcmp(cmd_str, "ssc_net_query") == 0) {
|
||||
/* SSC 网络配置查询 (只读全局, 协议 V1.02) */
|
||||
char resp[400];
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ssc_net_query\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"dev_ip\":\"%d.%d.%d.%d\",\"subnet_mask\":\"%d.%d.%d.%d\","
|
||||
"\"route_ip\":\"%d.%d.%d.%d\",\"lssc_ip\":\"%d.%d.%d.%d\","
|
||||
"\"dns\":\"%d.%d.%d.%d\",\"port\":%d}}",
|
||||
msg_id, dev_time_now(),
|
||||
local_net_cfg.lip[0], local_net_cfg.lip[1], local_net_cfg.lip[2], local_net_cfg.lip[3],
|
||||
local_net_cfg.sub[0], local_net_cfg.sub[1], local_net_cfg.sub[2], local_net_cfg.sub[3],
|
||||
local_net_cfg.gw[0], local_net_cfg.gw[1], local_net_cfg.gw[2], local_net_cfg.gw[3],
|
||||
net_center_info.lssc_ip[0], net_center_info.lssc_ip[1],
|
||||
net_center_info.lssc_ip[2], net_center_info.lssc_ip[3],
|
||||
local_net_cfg.dns[0], local_net_cfg.dns[1], local_net_cfg.dns[2], local_net_cfg.dns[3],
|
||||
net_center_info.tcp_port);
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: ssc_net_query dev_ip=%d.%d.%d.%d\n",
|
||||
local_net_cfg.lip[0], local_net_cfg.lip[1], local_net_cfg.lip[2], local_net_cfg.lip[3]);
|
||||
|
||||
} else if (strcmp(cmd_str, "iot_net_query") == 0) {
|
||||
/* IoT 网络配置查询 (只读全局) */
|
||||
char resp[400];
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"iot_net_query\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"host\":\"%s\",\"port\":%d,\"client_id\":\"%s\","
|
||||
"\"username\":\"%s\",\"password\":\"%s\"}}",
|
||||
msg_id, dev_time_now(),
|
||||
iot_net_info.remote_addr, iot_net_info.mqtt_port,
|
||||
iot_net_info.client_id, iot_net_info.username, iot_net_info.password);
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: iot_net_query host=%s port=%d\n",
|
||||
iot_net_info.remote_addr, iot_net_info.mqtt_port);
|
||||
|
||||
} else if (strcmp(cmd_str, "iot_topic_query") == 0) {
|
||||
/* IoT Topic 配置查询 (只读全局) */
|
||||
char resp[300];
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"iot_topic_query\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"client_id_enable\":%s,\"topic_pub\":\"%s\",\"topic_sub\":\"%s\"}}",
|
||||
msg_id, dev_time_now(),
|
||||
g_iot_topic.clientid_enable ? "true" : "false",
|
||||
g_iot_topic.topic_pub, g_iot_topic.topic_sub);
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
PRINT("IOT: iot_topic_query pub=%s sub=%s\n",
|
||||
g_iot_topic.topic_pub, g_iot_topic.topic_sub);
|
||||
|
||||
} else {
|
||||
// 暂不支持的命令, 返回错误
|
||||
char resp[256];
|
||||
@@ -477,14 +738,29 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
|
||||
}
|
||||
}
|
||||
|
||||
/* 发送 initialize 上线消息 (IoT 路径) */
|
||||
static void iot_send_initialize(void) {
|
||||
char payload[512];
|
||||
char topic[IOT_MQTT_TOPIC_MAX_LEN];
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"initialize\",\"ts\":%lu,"
|
||||
"\"data\":{\"dev_serial\":\"%s\",\"model\":\"%s\","
|
||||
"\"hard_ver\":\"%s\",\"soft_ver\":\"%s\","
|
||||
"\"extra_info\":{\"code\":\"0\",\"csq\":\"0\",\"location\":\"\"}}}",
|
||||
(unsigned long)(g_iot_msg_id + 1), dev_time_now(),
|
||||
g_iot_dev_serial, PRODUCT_MODEL, HARDWARE_VER, FIRMWARE_VER);
|
||||
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
|
||||
iot_mqtt_publish(topic, payload, strlen(payload), 0);
|
||||
}
|
||||
|
||||
/* 处理 MQTT SUBACK */
|
||||
static void iot_handle_suback(void) {
|
||||
PRINT("IOT: ← SUBACK, topics subscribed\n");
|
||||
g_iot_state = IOT_STATE_READY;
|
||||
_iot_reconnect_backoff = 0; // 连接成功, 重置退避
|
||||
|
||||
// V1.03: 订阅成功后发布 initialize 告知服务器上线
|
||||
dev_initialize_pub();
|
||||
// V1.03: 订阅成功后发布 initialize 告知服务器上线 (IoT 路径)
|
||||
iot_send_initialize();
|
||||
}
|
||||
|
||||
/* 处理收到的 MQTT 数据 */
|
||||
@@ -532,6 +808,7 @@ static void iot_process_recv(void) {
|
||||
iot_mqtt_send_subscribe();
|
||||
} else {
|
||||
PRINT("IOT: CONNACK rejected, rc=%d\n", connack_rc);
|
||||
offlog_iot_disconnect(3); // 事件日志: broker 拒绝
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
}
|
||||
}
|
||||
@@ -685,20 +962,29 @@ void iot_mqtt_publish_sensor(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/*--- Step 4: 构建 JSON → 发布 (字段与 V1.02 协议一致) ---*/
|
||||
static char data_json[1024];
|
||||
static char payload[1400];
|
||||
char *p = data_json;
|
||||
int remaining = sizeof(data_json);
|
||||
/*--- Step 4: 构建 JSON → 发布 (字段与 V1.02 协议一致) ---
|
||||
直接构建到 payload, 省掉 data_json[1024]:
|
||||
- 用 iot_mqtt_publish() 代替 mqtt_publish(), 发送缓冲隔离
|
||||
(event_report→mqttBuf vs loop_data→iot_mqtt_publish::buf[1024])
|
||||
- coil_count 边界硬限, 防 0xC0 坏帧致溢出 */
|
||||
static char payload[800]; /* 2026-08-17: 1400→800, loop_data 最大~604B, 省 600B .bss */
|
||||
char *p = payload;
|
||||
int remaining = sizeof(payload);
|
||||
int written;
|
||||
const char *freq_level_names[] = {"high", "mid_high", "mid_low", "low"};
|
||||
uint8_t i;
|
||||
uint8_t coil_n = _cached_sr.coil_count;
|
||||
uint32_t my_msg_id = g_iot_msg_id + 1; /* iot_mqtt_publish 内部 ++g_iot_msg_id, 这里只预读 */
|
||||
if (coil_n > 4) coil_n = 4; /* 硬限: 防坏帧致 snprintf 循环溢出 */
|
||||
|
||||
written = snprintf(p, remaining, "{\"channels\":[");
|
||||
/* 先写外层包装头 (msg_id/cmd/ts/data) */
|
||||
written = snprintf(p, remaining,
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_data\",\"ts\":%lu,\"data\":{\"channels\":[",
|
||||
(unsigned long)my_msg_id, dev_time_now());
|
||||
if (written < 0 || written >= remaining) return;
|
||||
p += written; remaining -= written;
|
||||
|
||||
for (i = 0; i < _cached_sr.coil_count; i++) {
|
||||
for (i = 0; i < coil_n; i++) {
|
||||
const LUP_CoilSensor *cs = &_cached_sr.coils[i];
|
||||
const char *misc_type_str = "time";
|
||||
uint32_t misc_val = 0;
|
||||
@@ -725,56 +1011,25 @@ void iot_mqtt_publish_sensor(void) {
|
||||
p += written; remaining -= written;
|
||||
}
|
||||
|
||||
snprintf(p, remaining, "]}");
|
||||
|
||||
/* Wrap and publish — V1.01 双主题模型: dld960/{sn}/dev */
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{\"msg_id\":%d,\"cmd\":\"loop_data\",\"ts\":%lu,\"data\":%s}",
|
||||
++g_iot_msg_id, dev_time_now(), data_json);
|
||||
snprintf(p, remaining, "]}}");
|
||||
|
||||
/* 用 iot_mqtt_publish: 发送缓冲 buf[1024] 与 mqttBuf[1024] 物理隔离,
|
||||
避免 event_report 的 MQTT 二进制污染 loop_data 载荷 (2026-07-23 _raw 事故 v3) */
|
||||
{
|
||||
char topic[IOT_MQTT_TOPIC_MAX_LEN];
|
||||
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
|
||||
mqtt_publish(topic, payload, 0);
|
||||
iot_mqtt_publish(topic, payload, strlen(payload), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 心跳上报
|
||||
*===========================================================================*/
|
||||
static void iot_send_heartbeat(void) {
|
||||
if (g_iot_state != IOT_STATE_READY) return;
|
||||
|
||||
char payload[512];
|
||||
uint8_t loop_ok[4] = {1, 1, 1, 1};
|
||||
uint8_t i;
|
||||
for (i = 0; i < 4; i++) {
|
||||
/* 简化: 读取线圈状态 */
|
||||
loop_ok[i] = 1; // TODO: 从 Loop MCU 获取实际状态
|
||||
}
|
||||
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{\"msg_id\":%d,\"cmd\":\"heartbeat\","
|
||||
"\"ts\":%lu,\"data\":{"
|
||||
"\"uptime\":%lu,\"loop_status\":[%s,%s,%s,%s],"
|
||||
"\"net_status\":true,\"iot_status\":true}}",
|
||||
++g_iot_msg_id, dev_time_now(),
|
||||
mstick() / 1000,
|
||||
loop_ok[0] ? "true" : "false", loop_ok[1] ? "true" : "false",
|
||||
loop_ok[2] ? "true" : "false", loop_ok[3] ? "true" : "false");
|
||||
|
||||
char topic[IOT_MQTT_TOPIC_MAX_LEN];
|
||||
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
|
||||
iot_mqtt_publish(topic, payload, strlen(payload), 0);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 连接管理
|
||||
*===========================================================================*/
|
||||
|
||||
/* 连接到 MQTT Broker — 复用 SocketId_TCP(已由 WCHNET_CreateTcpMqttSocket 创建并连接) */
|
||||
/* 连接到 MQTT Broker — 重新创建 TCP socket 并发起连接 */
|
||||
static void iot_connect_broker(void) {
|
||||
g_iot_socket = SocketId_TCP; // 复用已创建的 MQTT socket
|
||||
WCHNET_CreateTcpMqttSocket(); // 重新 create + connect (不是复用旧的!)
|
||||
g_iot_socket = SocketId_TCP;
|
||||
g_iot_state = IOT_STATE_TCP_CONNECTING;
|
||||
_iot_connect_start = mstick();
|
||||
PRINT("IOT: TCP connecting to broker (sock=%d)...\n", g_iot_socket);
|
||||
@@ -786,13 +1041,15 @@ static void iot_connect_broker(void) {
|
||||
void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat) {
|
||||
if (socketid != g_iot_socket) return;
|
||||
|
||||
/* CONNECT 成功 — 仅标记状态,由 poll 延迟发送 CONNECT */
|
||||
/* CONNECT 成功 — 在中断上下文直接发 MQTT CONNECT
|
||||
(对齐旧 mqtt_connect 行为: WCHNET SocketSend 在中断外会 hard fault) */
|
||||
if (intstat & SINT_STAT_CONNECT) {
|
||||
PRINT("IOT: TCP connected (sock=%d)\n", socketid);
|
||||
WCHNET_ModifyRecvBuf(socketid, (uint32_t)_iot_wchnet_buf, RECE_BUF_LEN);
|
||||
g_iot_state = IOT_STATE_TCP_CONNECTED;
|
||||
_iot_recv_len = 0;
|
||||
// 不在此发送 CONNECT,等下一轮 poll 处理
|
||||
_iot_send_fail_cnt = 0; // 新连接, 重置失败计数
|
||||
iot_mqtt_send_connect(); // 在中断上下文发, 不延后到 poll
|
||||
g_iot_state = IOT_STATE_MQTT_CONNECTING;
|
||||
}
|
||||
|
||||
/* 收到数据 */
|
||||
@@ -802,7 +1059,7 @@ void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat) {
|
||||
uint16_t space = IOT_MQTT_RECV_BUF_LEN - _iot_recv_len;
|
||||
if (recv_len > space) recv_len = space;
|
||||
uint32_t rd_len = recv_len;
|
||||
uint8_t tmp[RECE_BUF_LEN];
|
||||
static uint8_t tmp[RECE_BUF_LEN]; /* static: 1152B 栈分配在中断上下文中会撑爆 2KB 栈 (2026-07-23) */
|
||||
WCHNET_SocketRecv(socketid, tmp, &rd_len);
|
||||
memcpy(_iot_recv_buf + _iot_recv_len, tmp, (uint16_t)rd_len);
|
||||
_iot_recv_len += (uint16_t)rd_len;
|
||||
@@ -823,10 +1080,46 @@ void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat) {
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 硬件看门狗 IWDG — 主循环卡死保护
|
||||
* LSI≈40kHz, Prescaler=256, Reload=625 → ~4s 超时
|
||||
* 保活链路: MQTT PINGREQ/PINGRESP → TCP KeepAlive → IWDG
|
||||
* (heartbeat 是单向设备上报, 平台不回复, 不能用作存活检测)
|
||||
*===========================================================================*/
|
||||
|
||||
/* IWDG 初始化 */
|
||||
void iot_watchdog_init(void) {
|
||||
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
|
||||
IWDG_SetPrescaler(IWDG_Prescaler_256);
|
||||
IWDG_SetReload(625); // 625 × (256/40000) ≈ 4.0s
|
||||
IWDG_ReloadCounter();
|
||||
IWDG_Enable();
|
||||
PRINT("IOT: IWDG init ok, timeout≈4s\n");
|
||||
}
|
||||
|
||||
/* 喂狗 — 主循环每轮调用 */
|
||||
void iot_watchdog_kick(void) {
|
||||
IWDG_ReloadCounter();
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 主循环轮询
|
||||
*===========================================================================*/
|
||||
void iot_mqtt_poll(void) {
|
||||
iot_watchdog_kick(); // 喂硬件 IWDG
|
||||
|
||||
/* 事件日志: MQTT 状态沿检测 (主循环上下文, 不在 socket 中断里写 SPI)
|
||||
真复位 → 会有 BOOT 事件 + boot_seq 递增; 仅断连重连 → 只有以下网络事件 */
|
||||
if (g_iot_state != _prev_iot_state) {
|
||||
switch (g_iot_state) {
|
||||
case IOT_STATE_MQTT_CONNECTED: offlog_iot_connect(); break; // TCP+CONNACK 成功
|
||||
case IOT_STATE_READY: offlog_iot_ready(); break; // SUBACK → 将发 initialize
|
||||
case IOT_STATE_DISCONNECTED: offlog_iot_disconnect(1); break; // 断连(细分原因在各分支单独记)
|
||||
default: break;
|
||||
}
|
||||
_prev_iot_state = g_iot_state;
|
||||
}
|
||||
|
||||
/* 断线重连 */
|
||||
if (g_iot_state == IOT_STATE_DISCONNECTED && g_iot_socket == 0xFF) {
|
||||
if (_iot_reconnect_deadline == 0 || mstick() > _iot_reconnect_deadline) {
|
||||
@@ -841,6 +1134,7 @@ void iot_mqtt_poll(void) {
|
||||
_iot_reconnect_backoff = IOT_MQTT_RECONNECT_MAX_MS;
|
||||
}
|
||||
_iot_reconnect_deadline = mstick() + _iot_reconnect_backoff;
|
||||
offlog_iot_reconn(_iot_reconnect_backoff); // 事件日志: 重连退避
|
||||
PRINT("IOT: reconnect in %lu ms\n", _iot_reconnect_backoff);
|
||||
}
|
||||
}
|
||||
@@ -851,6 +1145,7 @@ void iot_mqtt_poll(void) {
|
||||
if (g_iot_state == IOT_STATE_TCP_CONNECTING) {
|
||||
if (mstick() - _iot_connect_start > 10000) {
|
||||
PRINT("IOT: TCP connect timeout\n");
|
||||
offlog_iot_disconnect(4); // 事件日志: 连接超时
|
||||
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
|
||||
g_iot_socket = 0xFF;
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
@@ -864,7 +1159,8 @@ void iot_mqtt_poll(void) {
|
||||
g_iot_state = IOT_STATE_MQTT_CONNECTING;
|
||||
}
|
||||
|
||||
/* MQTT Keepalive — PINGREQ */
|
||||
/* MQTT Keepalive — PINGREQ (60s, broker 不回 → TCP 超时 → 重连)
|
||||
loop_data 按平台下发 g_report_cfg.interval 上报, 无需额外 heartbeat JSON */
|
||||
if (g_iot_state == IOT_STATE_READY) {
|
||||
uint32_t now = mstick();
|
||||
if (now - _iot_last_heartbeat > IOT_MQTT_HEARTBEAT_MS) {
|
||||
@@ -872,8 +1168,6 @@ void iot_mqtt_poll(void) {
|
||||
int ping_len = MQTTSerialize_pingreq(ping_buf, sizeof(ping_buf));
|
||||
iot_mqtt_send(ping_buf, (uint16_t)ping_len);
|
||||
_iot_last_heartbeat = now;
|
||||
// 心跳上报
|
||||
iot_send_heartbeat();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -899,4 +1193,7 @@ void iot_mqtt_init(void) {
|
||||
_iot_reconnect_backoff = 0;
|
||||
_iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
|
||||
/* 初始化硬件看门狗 IWDG (LSI≈40kHz, 4s 超时, 主循环喂狗) */
|
||||
iot_watchdog_init();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "CONFIG.h"
|
||||
#include "loop_uart_proto.h"
|
||||
#include "cmcng.h"
|
||||
#include "fault_diag.h"
|
||||
#include <string.h>
|
||||
|
||||
/*===========================================================================
|
||||
@@ -654,6 +655,110 @@ int lup_feed_byte(uint8_t byte)
|
||||
// 不应该到达这里,调用方应在 COMPLETE 后 reset
|
||||
lup_frame_reset();
|
||||
break;
|
||||
default:
|
||||
/* 未知状态 (含 0x9F OTA 专用状态值): 防御性复位, 防状态机跑飞 */
|
||||
lup_frame_reset();
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 0x9F OTA 帧解析器 (OTA 模式专用, 2026-08-19)
|
||||
*
|
||||
* 帧格式 (DLD960LoopBootloader 源码确认):
|
||||
* [9F] [SubL] [SubH] [LEN] [CMD] [DATA: LEN-1] [CHECK: SUM]
|
||||
* - SubL/SubH: 分包序号, 2 字节小端 (低字节在前)
|
||||
* - LEN: 含 CMD 的字节数, 故 DATA = LEN - 1
|
||||
* - CHECK: 仅 SUM 校验 (无 XOR), 从 SubL 累加到 DATA 最后字节
|
||||
* 总帧长 = LEN + 5 (与 0x7F 相同)
|
||||
*
|
||||
* 状态机复用 g_lup_parser (缓冲/索引), 状态值独立 (LUP_FRAME_STATE_OTA_*)
|
||||
* 调用方 (uart2_dma_poll) 在 OTA 模式切换时负责 lup_frame_reset()
|
||||
*===========================================================================*/
|
||||
int lup_feed_byte_ota(uint8_t byte)
|
||||
{
|
||||
LUP_FrameParser *p = &g_lup_parser;
|
||||
|
||||
switch (p->state) {
|
||||
case LUP_FRAME_STATE_IDLE:
|
||||
if (byte == LUP_MAGIC_OTA) {
|
||||
p->buf[0] = byte;
|
||||
p->idx = 1;
|
||||
p->state = LUP_FRAME_STATE_HEADER; /* 复用 HEADER: 收 SubL/SubH */
|
||||
}
|
||||
break;
|
||||
|
||||
case LUP_FRAME_STATE_HEADER:
|
||||
p->buf[p->idx++] = byte;
|
||||
if (p->idx == 3) { /* SubL + SubH 收完 */
|
||||
p->state = LUP_FRAME_STATE_OTA_LEN;
|
||||
}
|
||||
break;
|
||||
|
||||
case LUP_FRAME_STATE_OTA_LEN:
|
||||
p->buf[p->idx++] = byte;
|
||||
{
|
||||
uint8_t len_field = p->buf[3];
|
||||
uint16_t data_bytes;
|
||||
if (len_field < 1) {
|
||||
data_bytes = 0; /* LEN<1 非法, 按无数据直接到 CHECK */
|
||||
} else {
|
||||
data_bytes = len_field - 1; /* LEN 含 CMD */
|
||||
}
|
||||
if (data_bytes > LUP_MAX_VALUE_LEN) {
|
||||
lup_frame_reset(); /* 非法长度, 丢弃 */
|
||||
break;
|
||||
}
|
||||
p->value_len = data_bytes;
|
||||
p->value_idx = 0;
|
||||
}
|
||||
p->state = LUP_FRAME_STATE_OTA_CMD;
|
||||
break;
|
||||
|
||||
case LUP_FRAME_STATE_OTA_CMD:
|
||||
p->buf[p->idx++] = byte;
|
||||
if (p->value_len > 0) {
|
||||
p->state = LUP_FRAME_STATE_VALUE; /* 复用 VALUE: 收 DATA */
|
||||
} else {
|
||||
p->state = LUP_FRAME_STATE_OTA_CHECK;
|
||||
}
|
||||
break;
|
||||
|
||||
case LUP_FRAME_STATE_VALUE:
|
||||
p->buf[p->idx++] = byte;
|
||||
p->value_idx++;
|
||||
if (p->value_idx >= p->value_len) {
|
||||
p->state = LUP_FRAME_STATE_OTA_CHECK;
|
||||
}
|
||||
break;
|
||||
|
||||
case LUP_FRAME_STATE_OTA_CHECK:
|
||||
p->buf[p->idx++] = byte;
|
||||
{
|
||||
/* SUM = SubL+SubH+LEN+CMD+DATA (不含 magic 0x9F, 不含本 CHECK) */
|
||||
uint8_t sum = 0;
|
||||
uint16_t i;
|
||||
for (i = 1; i < (uint16_t)(p->idx - 1); i++) {
|
||||
sum += p->buf[i];
|
||||
}
|
||||
if (sum == p->buf[p->idx - 1]) {
|
||||
p->state = LUP_FRAME_STATE_COMPLETE;
|
||||
return 1; /* 帧完成 */
|
||||
}
|
||||
lup_frame_reset(); /* 校验失败, 丢弃 */
|
||||
}
|
||||
break;
|
||||
|
||||
case LUP_FRAME_STATE_COMPLETE:
|
||||
// 不应该到达这里,调用方应在 COMPLETE 后 reset
|
||||
lup_frame_reset();
|
||||
break;
|
||||
|
||||
default:
|
||||
lup_frame_reset();
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -681,14 +786,24 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
|
||||
int csr;
|
||||
uint8_t i;
|
||||
|
||||
if (len < 6) return;
|
||||
FAULT_MARKER(MK_LUP_FRAME_IN);
|
||||
|
||||
// Debug: print raw
|
||||
PRINT("LUP Rx:");
|
||||
for (i = 0; i < len; i++) {
|
||||
PRINT(" %02X", pkg[i]);
|
||||
if (len < 6) { FAULT_MARKER(MK_LUP_FRAME_OUT); return; }
|
||||
|
||||
// Debug: print raw (2026-08-13: 缓冲一次性输出 — 逐字节 PRINT 关中断 ~350us/字节
|
||||
// 会屏蔽 UART2 ISR 丢帧, 且高频打印放大 printf 重入窗口。
|
||||
// 再关闭: 190B 打印 @256000 = 7.4ms 关中断 → UART2(192000) 溢出丢字节 → 粘帧!
|
||||
// 需要调试时打开 #if 1)
|
||||
#if 0
|
||||
{
|
||||
char _hex[224];
|
||||
int _p = snprintf(_hex, sizeof(_hex), "LUP Rx:");
|
||||
for (i = 0; i < len && _p >= 0 && _p < (int)sizeof(_hex) - 5; i++) {
|
||||
_p += snprintf(_hex + _p, sizeof(_hex) - (size_t)_p, " %02X", pkg[i]);
|
||||
}
|
||||
PRINT("%s\n", _hex);
|
||||
}
|
||||
PRINT("\n");
|
||||
#endif
|
||||
|
||||
// --- Checksum ---
|
||||
csr = lup_verify_checksum(pkg, len);
|
||||
@@ -712,4 +827,5 @@ void lup_process_frame(const uint8_t *pkg, uint16_t len)
|
||||
|
||||
// Try matching with pending command
|
||||
lup_cmd_on_response(pkg, len);
|
||||
FAULT_MARKER(MK_LUP_FRAME_OUT);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "simple_json.h"
|
||||
#include "tcp_json_srv.h"
|
||||
#include "iot_mqtt_srv.h"
|
||||
#include "offlog.h"
|
||||
|
||||
|
||||
uint32_t slen;
|
||||
@@ -64,7 +65,7 @@ uint8_t MyBuf[RECE_BUF_LEN];
|
||||
|
||||
|
||||
#define MAX_TMP_BUF_LEN 64
|
||||
#define MAX_MQTTBUF_LEN 1024 //512: loop_data(4通道)≈604B > 512 → 序列化失败发全零垃圾包被broker RST (2026-07-15)
|
||||
#define MAX_MQTTBUF_LEN 800 // 2026-08-17: 1024→800 (loop_data~604B+头<800; 512仍不够勿改回) 省224B .bss
|
||||
char mqtt_username[64] = {0};
|
||||
char mqtt_password[32] = {0};
|
||||
char mqtt_clientid[64] = {0};
|
||||
@@ -97,6 +98,7 @@ void dev_time_sync(uint32_t unix_ts)
|
||||
if (unix_ts < 1600000000UL) return; // 合法性门槛(>2020-09), 挡掉上电秒数/0/异常
|
||||
_dev_time_base_unix = unix_ts;
|
||||
_dev_time_base_tick = mstick();
|
||||
offlog_time_anchor(unix_ts); // 事件日志: 时钟同步锚点 (boot_seq↔unix 回算)
|
||||
PRINT("TIME: synced unix=%lu\n", (unsigned long)unix_ts);
|
||||
}
|
||||
|
||||
@@ -578,28 +580,20 @@ uint8_t i;
|
||||
return;
|
||||
}
|
||||
|
||||
// IoT MQTT mode — inline MQTT handling (ref: DBN101GA)
|
||||
// IoT MQTT mode — delegate all socket events to iot_mqtt_srv
|
||||
// (2026-07-23 修复: 旧 mqtt_connect/mqtt_data_manage 与 IoT 栈抢 socket, 致双 CONNECT → broker 踢线 → 频繁 initialize)
|
||||
if (g_sub_code_enable.iot_enable) {
|
||||
if (intstat & SINT_STAT_RECV) {
|
||||
mqtt_data_manage(socketid);
|
||||
}
|
||||
if (intstat & SINT_STAT_CONNECT) {
|
||||
#if KEEPLIVE_ENABLE
|
||||
WCHNET_SocketSetKeepLive(socketid, ENABLE);
|
||||
#endif
|
||||
WCHNET_ModifyRecvBuf(socketid, (uint32_t)SocketRecvBuf[socketid], RECE_BUF_LEN);
|
||||
PRINT("TCP Connect Success (MQTT)\n");
|
||||
mqtt_connect();
|
||||
PRINT("TCP Connect Success (MQTT IoT)\n");
|
||||
}
|
||||
if (intstat & SINT_STAT_DISCONNECT) {
|
||||
PRINT("TCP Disconnect (MQTT)\n");
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
g_net_state.flag = 1;
|
||||
}
|
||||
if (intstat & SINT_STAT_TIM_OUT) {
|
||||
PRINT("TCP Timeout (MQTT)\n");
|
||||
g_net_state.flag = 1;
|
||||
if (intstat & SINT_STAT_DISCONNECT || intstat & SINT_STAT_TIM_OUT) {
|
||||
PRINT("TCP Disconnect/Timeout (MQTT IoT)\n");
|
||||
}
|
||||
iot_mqtt_handle_sock_int(socketid, intstat);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file offlog.c
|
||||
* @author wfq
|
||||
* @version V1.0
|
||||
* @date 2026-08-04
|
||||
* @brief DBN 脱机事件日志 — W25Q32 环形实现
|
||||
*
|
||||
* 依赖: storage.c (SPI_Flash_*), cmcng.h (mstick), net_srv.c (dev_time_now)
|
||||
* 所有写操作在主循环上下文调用 (不在中断), SPI 擦除 ~45ms 阻塞可接受。
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "offlog.h"
|
||||
#include "storage.h"
|
||||
#include "cmcng.h"
|
||||
#include "net_srv.h"
|
||||
#include <string.h>
|
||||
|
||||
/* 编译期断言: 记录必须是 32B (定长环形索引依赖, 踩过 36B padding 的坑) */
|
||||
typedef char offlog_evt_size_must_be_32[(sizeof(OfflogEvt) == 32) ? 1 : -1];
|
||||
typedef char offlog_head_size_must_be_32[(sizeof(OfflogHead) == 32) ? 1 : -1];
|
||||
|
||||
/*===========================================================================
|
||||
* 内部状态
|
||||
*===========================================================================*/
|
||||
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
|
||||
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..63) */
|
||||
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
|
||||
static uint16_t _boot_seq; /* 当前 boot 序号 */
|
||||
static uint32_t _count; /* 有效记录数 (0..max_records) */
|
||||
static uint8_t _ready; /* init 完成标志 */
|
||||
|
||||
/*===========================================================================
|
||||
* 运行时分区表 (ROADMAP 2026-08-12: 上电读 JEDEC ID 决定事件区容量)
|
||||
*===========================================================================*/
|
||||
OfflogPart g_offlog_part = {
|
||||
OFFLOG_CHIP_W25Q32, /* 默认 */
|
||||
0x80000UL, /* 512KB */
|
||||
127, /* (512KB/4096) - 1 头扇区 */
|
||||
16256, /* 127 * 128 */
|
||||
};
|
||||
|
||||
/* 读 JEDEC ID → 填分区表。未知芯片按 W25Q32 兜底 (与 storage_init 打印对照) */
|
||||
static void offlog_part_detect(void)
|
||||
{
|
||||
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
|
||||
uint32_t size = 0x80000UL;
|
||||
uint8_t chip = OFFLOG_CHIP_W25Q32;
|
||||
|
||||
switch (dev) {
|
||||
case OFFLOG_CHIP_W25Q32: size = 0x80000UL; break; /* 512KB */
|
||||
case OFFLOG_CHIP_W25Q64: size = 0x100000UL; break; /* 1MB */
|
||||
case OFFLOG_CHIP_W25Q128: size = 0x200000UL; break; /* 2MB */
|
||||
case OFFLOG_CHIP_W25Q256: size = 0x400000UL; break; /* 4MB */
|
||||
default: chip = OFFLOG_CHIP_W25Q32; break; /* 未知→默认 */
|
||||
}
|
||||
g_offlog_part.chip = chip;
|
||||
g_offlog_part.area_size = size;
|
||||
g_offlog_part.data_sectors = (size / 4096UL) - 1;
|
||||
g_offlog_part.max_records = g_offlog_part.data_sectors * OFFLOG_EVT_PER_SECTOR;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 基础读写
|
||||
*===========================================================================*/
|
||||
static void offlog_head_write(const OfflogHead *h)
|
||||
{
|
||||
uint8_t buf[32];
|
||||
memset(buf, 0xFF, sizeof(buf));
|
||||
memcpy(buf, h, sizeof(OfflogHead));
|
||||
SPI_Flash_Write(buf, OFFLOG_AREA_BASE, sizeof(buf));
|
||||
}
|
||||
|
||||
static void offlog_head_read(OfflogHead *h)
|
||||
{
|
||||
uint8_t buf[32];
|
||||
SPI_Flash_Read(buf, OFFLOG_AREA_BASE, sizeof(buf));
|
||||
memcpy(h, buf, sizeof(OfflogHead));
|
||||
}
|
||||
|
||||
static int offlog_evt_read_at(uint32_t abs_off, OfflogEvt *out)
|
||||
{
|
||||
SPI_Flash_Read((uint8_t *)out, abs_off, OFFLOG_EVT_SIZE);
|
||||
return (out->magic == OFFLOG_EVT_MAGIC) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 上电扫描恢复
|
||||
* 从头扇区记录的写位置向后扫, 校验 magic + seq 连续, 找真实写位置。
|
||||
* 正常情况 (无掉电) 第一条就无效, 立即恢复; 掉电则向前推进若干条。
|
||||
*===========================================================================*/
|
||||
static void offlog_scan_recover(const OfflogHead *h)
|
||||
{
|
||||
uint32_t pos = h->wr_off;
|
||||
uint32_t scanned = 0;
|
||||
uint32_t expect_seq = h->wr_seq;
|
||||
OfflogEvt e;
|
||||
|
||||
/* 防御: wr_off 必须在数据区范围内且 32B 对齐 */
|
||||
if (pos < OFFLOG_DATA_BASE || pos >= OFFLOG_AREA_BASE + OFFLOG_AREA_SIZE) {
|
||||
pos = OFFLOG_DATA_BASE;
|
||||
}
|
||||
if ((pos - OFFLOG_DATA_BASE) % OFFLOG_EVT_SIZE != 0) {
|
||||
pos = OFFLOG_DATA_BASE; /* 未对齐 → 从头开始 */
|
||||
}
|
||||
|
||||
while (scanned < OFFLOG_MAX_RECORDS) {
|
||||
if (offlog_evt_read_at(pos, &e) != 0) {
|
||||
break; /* 找到真实写位置 */
|
||||
}
|
||||
if (expect_seq != 0 && e.seq != expect_seq) {
|
||||
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
|
||||
}
|
||||
pos += OFFLOG_EVT_SIZE;
|
||||
if (pos >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
|
||||
pos = OFFLOG_DATA_BASE; /* 回绕数据区 */
|
||||
}
|
||||
scanned++;
|
||||
expect_seq++;
|
||||
}
|
||||
|
||||
_wr_off = pos;
|
||||
_wr_sector = (uint16_t)((pos - OFFLOG_DATA_BASE) / 4096) + 1; /* 区内 1..63 */
|
||||
_wr_seq = expect_seq; /* 下一条 = 最后有效 seq + 1 */
|
||||
_count = h->count + scanned;
|
||||
if (_count > OFFLOG_MAX_RECORDS) _count = OFFLOG_MAX_RECORDS;
|
||||
_boot_seq = (uint16_t)(h->boot_seq + 1);
|
||||
if (_boot_seq == 0) _boot_seq = 1;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 初始化
|
||||
*===========================================================================*/
|
||||
void offlog_init(void)
|
||||
{
|
||||
OfflogHead h;
|
||||
|
||||
offlog_part_detect(); /* JEDEC ID → 事件区容量 (W25Q32/Q64/Q128/Q256) */
|
||||
memset(&h, 0xFF, sizeof(h));
|
||||
offlog_head_read(&h);
|
||||
|
||||
if (!(h.magic[0] == OFFLOG_HEAD_MAGIC0 && h.magic[1] == OFFLOG_HEAD_MAGIC1 &&
|
||||
h.magic[2] == OFFLOG_HEAD_MAGIC2 && h.magic[3] == OFFLOG_HEAD_MAGIC3)) {
|
||||
/* 全新区: 懒擦 — 只擦当前写扇区 ~45ms (原擦全部 127 扇区 ~5.7s,
|
||||
主循环阻塞超 IWDG 4s 风险), 其余由环形写切扇区时自动擦 */
|
||||
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
|
||||
memset(&h, 0xFF, sizeof(h));
|
||||
h.magic[0] = OFFLOG_HEAD_MAGIC0;
|
||||
h.magic[1] = OFFLOG_HEAD_MAGIC1;
|
||||
h.magic[2] = OFFLOG_HEAD_MAGIC2;
|
||||
h.magic[3] = OFFLOG_HEAD_MAGIC3;
|
||||
h.boot_seq = 0;
|
||||
h.wr_sector = 1;
|
||||
h.wr_off = OFFLOG_DATA_BASE;
|
||||
h.wr_seq = 0;
|
||||
h.count = 0;
|
||||
offlog_head_write(&h);
|
||||
|
||||
_wr_off = OFFLOG_DATA_BASE;
|
||||
_wr_sector = 1;
|
||||
_wr_seq = 1;
|
||||
_count = 0;
|
||||
_boot_seq = 1;
|
||||
} else {
|
||||
offlog_scan_recover(&h);
|
||||
}
|
||||
|
||||
/* 回写头: boot_seq 递增 */
|
||||
h.magic[0] = OFFLOG_HEAD_MAGIC0;
|
||||
h.magic[1] = OFFLOG_HEAD_MAGIC1;
|
||||
h.magic[2] = OFFLOG_HEAD_MAGIC2;
|
||||
h.magic[3] = OFFLOG_HEAD_MAGIC3;
|
||||
h.boot_seq = _boot_seq;
|
||||
h.wr_sector = _wr_sector;
|
||||
h.wr_off = _wr_off;
|
||||
h.wr_seq = _wr_seq;
|
||||
h.count = _count;
|
||||
offlog_head_write(&h);
|
||||
|
||||
_ready = 1;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 写一条记录 (主循环上下文)
|
||||
*===========================================================================*/
|
||||
static void offlog_flush_head(void)
|
||||
{
|
||||
OfflogHead h;
|
||||
h.magic[0] = OFFLOG_HEAD_MAGIC0;
|
||||
h.magic[1] = OFFLOG_HEAD_MAGIC1;
|
||||
h.magic[2] = OFFLOG_HEAD_MAGIC2;
|
||||
h.magic[3] = OFFLOG_HEAD_MAGIC3;
|
||||
h.boot_seq = _boot_seq;
|
||||
h.wr_sector = _wr_sector;
|
||||
h.wr_off = _wr_off;
|
||||
h.wr_seq = _wr_seq;
|
||||
h.count = _count;
|
||||
offlog_head_write(&h);
|
||||
}
|
||||
|
||||
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic。
|
||||
扇区整擦后顺序写, 首条必先写, 故首条 0xA5 即可判定有数据 */
|
||||
static int offlog_sector_has_data(uint32_t abs_sec_off)
|
||||
{
|
||||
OfflogEvt e;
|
||||
SPI_Flash_Read((uint8_t *)&e, abs_sec_off, OFFLOG_EVT_SIZE);
|
||||
return (e.magic == OFFLOG_EVT_MAGIC) ? 1 : 0;
|
||||
}
|
||||
|
||||
static void offlog_write_raw(const OfflogEvt *e)
|
||||
{
|
||||
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
|
||||
if (_wr_off >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
|
||||
_wr_off = OFFLOG_DATA_BASE;
|
||||
}
|
||||
|
||||
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区。
|
||||
覆盖两种情形: ① 正好落在扇区边界 (sec_pos==0, 上一轮写满)
|
||||
② 当前扇区放不下下一条 (sec_pos + 32 > 4096) */
|
||||
{
|
||||
uint32_t sec_index = (_wr_off - OFFLOG_DATA_BASE) / 4096; /* 0..62 */
|
||||
if ((uint32_t)(_wr_sector - 1) != sec_index) {
|
||||
uint32_t target_abs = (OFFLOG_DATA_BASE / 4096 + sec_index) * 4096;
|
||||
/* 仅环形覆盖 (目标扇区有旧数据) 才扣减 count;
|
||||
顺序推进到空扇区擦除无记录损失 */
|
||||
if (offlog_sector_has_data(target_abs)) {
|
||||
uint32_t erased = (_count > OFFLOG_EVT_PER_SECTOR)
|
||||
? OFFLOG_EVT_PER_SECTOR : _count;
|
||||
_count -= erased;
|
||||
}
|
||||
SPI_Flash_Erase_Sector(target_abs / 4096);
|
||||
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..63 */
|
||||
offlog_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
|
||||
}
|
||||
}
|
||||
|
||||
SPI_Flash_Write_NoCheck((uint8_t *)e, _wr_off, OFFLOG_EVT_SIZE);
|
||||
_wr_off += OFFLOG_EVT_SIZE;
|
||||
_wr_seq++;
|
||||
if (_count < OFFLOG_MAX_RECORDS) _count++;
|
||||
}
|
||||
|
||||
/* 内部: 支持外部指定 unix_ts (TIME_ANCHOR 用平台下发值, 严格一致) */
|
||||
static void offlog_evt_ts(uint8_t type, const uint8_t *payload, uint8_t len, uint32_t unix_ts)
|
||||
{
|
||||
OfflogEvt e;
|
||||
uint32_t now = mstick();
|
||||
|
||||
if (!_ready) return;
|
||||
|
||||
memset(&e, 0, sizeof(e));
|
||||
e.magic = OFFLOG_EVT_MAGIC;
|
||||
e.type = type;
|
||||
e.len = (len > 12) ? 12 : len;
|
||||
e.seq = _wr_seq;
|
||||
e.boot_seq= _boot_seq;
|
||||
e.ts_ms = now;
|
||||
e.unix_ts = unix_ts;
|
||||
if (unix_ts >= 1600000000UL) { /* 与 dev_time_sync 合法性门槛一致 */
|
||||
e.flags |= OFFLOG_UNIX_VALID_FLAG;
|
||||
}
|
||||
if (payload != NULL && e.len > 0) {
|
||||
memcpy(e.payload, payload, e.len);
|
||||
}
|
||||
offlog_write_raw(&e);
|
||||
}
|
||||
|
||||
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len)
|
||||
{
|
||||
offlog_evt_ts(type, payload, len, dev_time_now());
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 便捷事件包装
|
||||
*===========================================================================*/
|
||||
void offlog_boot(uint32_t reset_reason)
|
||||
{
|
||||
uint8_t p[4];
|
||||
p[0] = (uint8_t)(reset_reason >> 24);
|
||||
p[1] = (uint8_t)(reset_reason >> 16);
|
||||
p[2] = (uint8_t)(reset_reason >> 8);
|
||||
p[3] = (uint8_t)reset_reason;
|
||||
offlog_evt(OFFLOG_EVT_BOOT, p, 4);
|
||||
}
|
||||
|
||||
void offlog_iot_connect(void)
|
||||
{
|
||||
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
|
||||
}
|
||||
|
||||
void offlog_iot_ready(void)
|
||||
{
|
||||
offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0);
|
||||
}
|
||||
|
||||
void offlog_iot_disconnect(uint8_t reason)
|
||||
{
|
||||
offlog_evt(OFFLOG_EVT_IOT_DISCONN, &reason, 1);
|
||||
}
|
||||
|
||||
void offlog_iot_reconn(uint32_t backoff_ms)
|
||||
{
|
||||
uint8_t p[4];
|
||||
p[0] = (uint8_t)(backoff_ms >> 24);
|
||||
p[1] = (uint8_t)(backoff_ms >> 16);
|
||||
p[2] = (uint8_t)(backoff_ms >> 8);
|
||||
p[3] = (uint8_t)backoff_ms;
|
||||
offlog_evt(OFFLOG_EVT_IOT_RECONN, p, 4);
|
||||
}
|
||||
|
||||
void offlog_evt_retry(uint32_t msg_id, uint8_t retry)
|
||||
{
|
||||
uint8_t p[5];
|
||||
p[0] = (uint8_t)(msg_id >> 24);
|
||||
p[1] = (uint8_t)(msg_id >> 16);
|
||||
p[2] = (uint8_t)(msg_id >> 8);
|
||||
p[3] = (uint8_t)msg_id;
|
||||
p[4] = retry;
|
||||
offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5);
|
||||
}
|
||||
|
||||
void offlog_evt_giveup(uint32_t msg_id)
|
||||
{
|
||||
uint8_t p[4];
|
||||
p[0] = (uint8_t)(msg_id >> 24);
|
||||
p[1] = (uint8_t)(msg_id >> 16);
|
||||
p[2] = (uint8_t)(msg_id >> 8);
|
||||
p[3] = (uint8_t)msg_id;
|
||||
offlog_evt(OFFLOG_EVT_EVT_GIVEUP, p, 4);
|
||||
}
|
||||
|
||||
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value)
|
||||
{
|
||||
uint8_t p[6];
|
||||
p[0] = sub;
|
||||
p[1] = ch;
|
||||
p[2] = (uint8_t)(value >> 24);
|
||||
p[3] = (uint8_t)(value >> 16);
|
||||
p[4] = (uint8_t)(value >> 8);
|
||||
p[5] = (uint8_t)value;
|
||||
offlog_evt(OFFLOG_EVT_COIL, p, 6);
|
||||
}
|
||||
|
||||
void offlog_time_anchor(uint32_t unix_ts)
|
||||
{
|
||||
offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 查询
|
||||
*===========================================================================*/
|
||||
uint32_t offlog_boot_seq(void)
|
||||
{
|
||||
return _boot_seq;
|
||||
}
|
||||
|
||||
uint16_t offlog_count(void)
|
||||
{
|
||||
return (uint16_t)_count;
|
||||
}
|
||||
|
||||
int offlog_read_idx(uint16_t idx, OfflogEvt *out)
|
||||
{
|
||||
uint32_t cnt = _count;
|
||||
if (!_ready || idx >= cnt) return -1;
|
||||
|
||||
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
|
||||
uint32_t phys = (_wr_off - OFFLOG_DATA_BASE); /* 0..DATA_SIZE */
|
||||
uint32_t start = (phys + OFFLOG_DATA_SIZE - (uint32_t)cnt * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
|
||||
uint32_t read_off = OFFLOG_DATA_BASE + (start + (uint32_t)idx * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
|
||||
|
||||
return offlog_evt_read_at(read_off, out);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 协议导出 (MQTT V1.06 / TCP JSON V1.02 共用)
|
||||
*===========================================================================*/
|
||||
uint8_t offlog_enabled(void)
|
||||
{
|
||||
return _ready;
|
||||
}
|
||||
|
||||
uint32_t offlog_seq_last(void)
|
||||
{
|
||||
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
|
||||
}
|
||||
|
||||
const char *offlog_type_str(uint8_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case OFFLOG_EVT_BOOT: return "boot";
|
||||
case OFFLOG_EVT_IOT_CONNECT: return "iot_connect";
|
||||
case OFFLOG_EVT_IOT_READY: return "iot_ready";
|
||||
case OFFLOG_EVT_IOT_DISCONN: return "iot_disconnect";
|
||||
case OFFLOG_EVT_IOT_RECONN: return "iot_reconn";
|
||||
case OFFLOG_EVT_EVT_RETRY: return "evt_retry";
|
||||
case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup";
|
||||
case OFFLOG_EVT_COIL: return "coil";
|
||||
case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor";
|
||||
case OFFLOG_EVT_LOG_CLEAR: return "log_clear";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/* 线圈事件子类型: 1=进 2=出 3=断开 4=恢复
|
||||
(与 iot_mqtt_srv.c offlog_coil 调用一致) */
|
||||
static const char *offlog_coil_sub_str(uint8_t sub)
|
||||
{
|
||||
switch (sub) {
|
||||
case 1: return "car_enter";
|
||||
case 2: return "car_leave";
|
||||
case 3: return "loop_cut";
|
||||
case 4: return "loop_restore";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/* payload 大端 uint32 (与 offlog_boot/offlog_coil 写入一致) */
|
||||
static uint32_t offlog_payload_u32(const uint8_t *p)
|
||||
{
|
||||
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
/* 记录 → JSON 对象 (data 按事件类型组装; 无参数事件 data=null)
|
||||
回返 snprintf 写入长度; 调用方须保证 buf_len 充足 (一条最大约130B) */
|
||||
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len)
|
||||
{
|
||||
const char *type = offlog_type_str(e->type);
|
||||
char data[96];
|
||||
|
||||
switch (e->type) {
|
||||
case OFFLOG_EVT_BOOT:
|
||||
snprintf(data, sizeof(data), "{\"rst\":%lu}",
|
||||
(unsigned long)offlog_payload_u32(e->payload));
|
||||
break;
|
||||
case OFFLOG_EVT_IOT_DISCONN:
|
||||
snprintf(data, sizeof(data), "{\"reason\":%u}", e->payload[0]);
|
||||
break;
|
||||
case OFFLOG_EVT_IOT_RECONN:
|
||||
snprintf(data, sizeof(data), "{\"backoff_ms\":%lu}",
|
||||
(unsigned long)offlog_payload_u32(e->payload));
|
||||
break;
|
||||
case OFFLOG_EVT_EVT_RETRY:
|
||||
snprintf(data, sizeof(data), "{\"msg_id\":%lu,\"retry\":%u}",
|
||||
(unsigned long)offlog_payload_u32(e->payload), e->payload[4]);
|
||||
break;
|
||||
case OFFLOG_EVT_EVT_GIVEUP:
|
||||
snprintf(data, sizeof(data), "{\"msg_id\":%lu}",
|
||||
(unsigned long)offlog_payload_u32(e->payload));
|
||||
break;
|
||||
case OFFLOG_EVT_COIL:
|
||||
snprintf(data, sizeof(data), "{\"sub\":\"%s\",\"ch\":%u,\"value\":%lu}",
|
||||
offlog_coil_sub_str(e->payload[0]), e->payload[1],
|
||||
(unsigned long)offlog_payload_u32(e->payload + 2));
|
||||
break;
|
||||
default:
|
||||
data[0] = '\0'; /* iot_connect/iot_ready/time_anchor/log_clear → null */
|
||||
break;
|
||||
}
|
||||
|
||||
if (data[0] == '\0') {
|
||||
return snprintf(buf, buf_len,
|
||||
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":null}",
|
||||
(unsigned long)e->seq, (unsigned)e->boot_seq,
|
||||
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type);
|
||||
}
|
||||
return snprintf(buf, buf_len,
|
||||
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":%s}",
|
||||
(unsigned long)e->seq, (unsigned)e->boot_seq,
|
||||
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type, data);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 清空 (审计: 清空后写一条 LOG_CLEAR)
|
||||
*===========================================================================*/
|
||||
void offlog_clear(void)
|
||||
{
|
||||
if (!_ready) return;
|
||||
|
||||
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms, 原 2.8s 接近 IWDG 4s 风险):
|
||||
count=0 使旧数据不可读, 其余扇区由环形写覆盖时自动擦 */
|
||||
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
|
||||
_wr_off = OFFLOG_DATA_BASE;
|
||||
_wr_sector = 1;
|
||||
_count = 0;
|
||||
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证日志全局唯一 */
|
||||
|
||||
offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0);
|
||||
offlog_flush_head();
|
||||
}
|
||||
|
||||
/* OfflogEvt -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §7 字段表解析)
|
||||
Used by TCP JSON / MQTT log_query stream=event (protocol V1.03/V1.07).
|
||||
Returns snprintf written length; 32B -> 64 hex chars. */
|
||||
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len)
|
||||
{
|
||||
int i, pos = 0;
|
||||
const uint8_t *p = (const uint8_t *)e;
|
||||
for (i = 0; i < (int)sizeof(OfflogEvt); i++) {
|
||||
if (pos >= buf_len - 3) break;
|
||||
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
@@ -147,6 +147,11 @@ static uint8_t attDeviceName[GAP_DEVICE_NAME_LEN] = "DLD960GA"; // "Simple Perip
|
||||
static peripheralConnItem_t peripheralConnList;
|
||||
|
||||
static uint16_t peripheralMTU = ATT_MTU_SIZE;
|
||||
/* getter for dbn_ble_srv.c: dynamic notify chunk by negotiated MTU */
|
||||
uint16_t peripheral_get_mtu(void)
|
||||
{
|
||||
return peripheralMTU;
|
||||
}
|
||||
/*********************************************************************
|
||||
* LOCAL FUNCTIONS
|
||||
*/
|
||||
@@ -679,13 +684,30 @@ static void peripheralStateNotificationCB(gapRole_States_t newState, gapRoleEven
|
||||
*/
|
||||
static void performPeriodicTask(void)
|
||||
{
|
||||
// uint8_t notiData[SIMPLEPROFILE_CHAR4_LEN] = {0x88};
|
||||
// peripheralChar4Notify(notiData, SIMPLEPROFILE_CHAR4_LEN);
|
||||
/* 2026-08-17 cleanup: removed _dbg_cnt 500ms debug heartbeat print */
|
||||
if(g_flag_notify_temp){
|
||||
g_flag_notify_temp = 0;
|
||||
PRINT("BLE send: len=%d resp=%d amt=%d seq=%d off=%d\n",
|
||||
g_notify_buftemp.len, g_buf_ble_response.flag,
|
||||
g_buf_ble_response.pkg_amount, g_buf_ble_response.pkg_seq,
|
||||
g_buf_ble_response.dat_offset);
|
||||
peripheralChar4Notify(g_notify_buftemp.buf, g_notify_buftemp.len);
|
||||
clear_ble_notify_buf(&g_notify_buftemp);
|
||||
}
|
||||
/* chunk continuation: when notify buf idle and response queue still
|
||||
has more fragments, actively pull next packet here (50ms period),
|
||||
do NOT rely on poll_dbn_ble rx events */
|
||||
if(g_notify_buftemp.flag == 0)
|
||||
{
|
||||
uint8_t _pull = set_response_to_notify(&g_buf_ble_response, &g_notify_buftemp);
|
||||
g_flag_notify_temp = _pull;
|
||||
if(_pull == 0 && g_buf_ble_response.flag)
|
||||
{
|
||||
PRINT("BLE pull FAIL: resp flag=%d amt=%d seq=%d off=%d\n",
|
||||
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
|
||||
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
@@ -701,9 +723,12 @@ static void performPeriodicTask(void)
|
||||
static void peripheralChar4Notify(uint8_t *pValue, uint16_t len)
|
||||
{
|
||||
attHandleValueNoti_t noti;
|
||||
PRINT("N4 enter: len=%d resp=%d amt=%d seq=%d off=%d\n", len,
|
||||
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
|
||||
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
|
||||
if(len > (peripheralMTU - 3))
|
||||
{
|
||||
PRINT("Too large noti\n");
|
||||
PRINT("Too large noti, len:%d, peripheralMTU:%d\n", len, peripheralMTU);
|
||||
return;
|
||||
}
|
||||
noti.len = len;
|
||||
@@ -713,8 +738,22 @@ static void peripheralChar4Notify(uint8_t *pValue, uint16_t len)
|
||||
tmos_memcpy(noti.pValue, pValue, noti.len);
|
||||
if(simpleProfile_Notify(peripheralConnList.connHandle, ¬i) != SUCCESS)
|
||||
{
|
||||
PRINT("BLE notify FAIL, len:%d, MTU:%d\n", len, peripheralMTU);
|
||||
GATT_bm_free((gattMsg_t *)¬i, ATT_HANDLE_VALUE_NOTI);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint8_t _ti = 0;
|
||||
PRINT("N4 after simpleProfile_Notify: resp=%d amt=%d seq=%d off=%d\n",
|
||||
g_buf_ble_response.flag, g_buf_ble_response.pkg_amount,
|
||||
g_buf_ble_response.pkg_seq, g_buf_ble_response.dat_offset);
|
||||
PRINT("BLE notify OK, len:%d, MTU:%d\n", len, peripheralMTU);
|
||||
for(_ti = 0; _ti < len; _ti++)
|
||||
{
|
||||
PRINT(" %02X", pValue[_ti]);
|
||||
}
|
||||
PRINT("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,12 @@
|
||||
#include "eth_driver.h"
|
||||
#include "net_srv.h"
|
||||
#include "storage.h"
|
||||
#include "offlog.h"
|
||||
#include "snapshot.h"
|
||||
#include "tcp_json_srv.h"
|
||||
#include "loop_uart_proto.h"
|
||||
#include "iot_mqtt_srv.h"
|
||||
#include "fault_diag.h"
|
||||
|
||||
/*********************************************************************
|
||||
* GLOBAL TYPEDEFS
|
||||
@@ -229,6 +232,9 @@ void Main_Circulation(void)
|
||||
uint32_t _counter = 0;
|
||||
while(1)
|
||||
{
|
||||
FAULT_MARKER(MK_LOOP_TOP);
|
||||
iot_watchdog_kick(); /* 无条件喂狗 (2026-08-13): 原只在 iot_mqtt_poll, tcp_json 分支不喂 */
|
||||
snap_delayed_init(); /* 快照区延后初始化: 开机 3s 后首次进入执行 (2026-08-17) */
|
||||
TMOS_SystemProcess();
|
||||
|
||||
if(g_net_state.flag < 2)
|
||||
@@ -265,13 +271,16 @@ void Main_Circulation(void)
|
||||
WCHNET_HandleGlobalInt();
|
||||
}
|
||||
|
||||
FAULT_MARKER(MK_UART_SRV_IN);
|
||||
uart_srv();
|
||||
|
||||
FAULT_MARKER(MK_UART_SRV_OUT);
|
||||
snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */
|
||||
FAULT_MARKER(MK_POLL_BLE_IN);
|
||||
poll_dbn_ble();
|
||||
|
||||
if (g_sub_code_enable.iot_enable) {
|
||||
iot_mqtt_publish_sensor(); // Push 0xC0 sensor data to MQTT broker
|
||||
poll_mqtt(); // MQTT PINGREQ heartbeat (in net_srv.c)
|
||||
iot_mqtt_poll(); // IoT MQTT state machine + PINGREQ keepalive
|
||||
} else {
|
||||
tcp_json_push_sensor(); // Push 0xC0 sensor data to TCP JSON client
|
||||
tcp_json_poll();
|
||||
@@ -298,12 +307,47 @@ int main(void)
|
||||
// USART_Printf_Init( 115200 );
|
||||
USART_Printf_Init( 256000 );
|
||||
#endif
|
||||
/* 诊断: 复位原因寄存器 (bit31=IWDG bit30=WWDG bit29=LPWR bit26=NRST bit25=POR bit24=软复位)
|
||||
每次上电必打, 区分看门狗死锁 vs 掉电 vs 外部复位 */
|
||||
PRINT("RST_REASON: 0x%08lx\n", (unsigned long)OFFLOG_RCC_RSTSCKR);
|
||||
fault_diag_init(); /* 打印上次复位现场 (HardFault mcause/mepc + 执行轨迹 marker) */
|
||||
iot_watchdog_init(); /* 无条件 IWDG (2026-08-13): iot_enable=0 时 iot_mqtt_init 不调,
|
||||
卡死无兜底 → 永久冻结。IWDG 4s 超时 + 主循环喂狗 */
|
||||
PRINT("%s\n", VER_LIB);
|
||||
PRINT("SystemCoreClock:%d\n", SystemCoreClock);
|
||||
/* ===== 实验: 禁用全部 Flash/SPI 操作 (2026-08-12) =====
|
||||
结论: 禁用后设备稳定 → SPI 操作触发复位+乱码实锤
|
||||
软件缓解: storage.c SPI 时钟降档 + GPIO 缓边沿 (见 SPI_Flash_Init)
|
||||
硬件根治: NRST 加 100nF 电容 / SPI 走线远离 NRST / 串阻缓边沿
|
||||
#if 0 = 恢复全部 SPI; 保留本块便于回退实验 */
|
||||
#if 0
|
||||
PRINT("ISOLATE: flash/SPI disabled (flash isolation test)\n");
|
||||
#else
|
||||
GetMacAddr(gMacAddr);
|
||||
storage_init();
|
||||
PRINT("INIT: storage ok\n");
|
||||
offlog_init();
|
||||
PRINT("INIT: offlog ok\n");
|
||||
/* 快照区: 延后初始化 (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) =====
|
||||
根因: .bss≈46KB → 栈仅~1.9KB; load_cfg/output_cfg 的 printf 栈峰值触顶
|
||||
→ 覆盖 .noinit+返回地址 → PC 跑飞循环。
|
||||
修复: ①实验D定位触发点 ②RAM 瘦身 (ETH 768/payload 800/MQTT buf 800,
|
||||
栈 1.9KB→~4.75KB) ③load_cfg 字符串 0 终止保险 ④打印恢复。 */
|
||||
load_cfg_from_flash();
|
||||
output_cfg_from_flash();
|
||||
output_cfg_from_flash(); /* 2026-08-17: 栈瘦身后恢复 (栈~4.75KB, printf 峰值安全) */
|
||||
#endif
|
||||
|
||||
PRINT("MAC: %02X %02X %02X %02X %02X %02X\r\n", gMacAddr[0],gMacAddr[1], gMacAddr[2], gMacAddr[3],gMacAddr[4],gMacAddr[5]);
|
||||
PRINT("net version:%x\n", WCHNET_GetVer());
|
||||
@@ -311,6 +355,24 @@ int main(void)
|
||||
HAL_Init();
|
||||
|
||||
uart_init();
|
||||
|
||||
/* ===== 实验: 禁用 UART2 (Loop 口) 隔离测试 (2026-08-12) =====
|
||||
结论: UART2 禁用后仍复位 → 与 Loop 数据路径无关 (后定位为 SPI 串扰)
|
||||
#if 0 = 恢复 UART2 正常 */
|
||||
#if 0
|
||||
{
|
||||
GPIO_InitTypeDef gpio;
|
||||
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
|
||||
USART_Cmd(USART2, DISABLE);
|
||||
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
|
||||
gpio.GPIO_Pin = GPIO_Pin_3;
|
||||
gpio.GPIO_Mode = GPIO_Mode_IPU; /* PA3 脱离 UART2, 上拉输入 */
|
||||
gpio.GPIO_Speed = GPIO_Speed_50MHz;
|
||||
GPIO_Init(GPIOA, &gpio);
|
||||
PRINT("ISOLATE: UART2 disabled (Loop isolation test)\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
TIM3_Init();
|
||||
TIM2_Init();
|
||||
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file snapshot.c
|
||||
* @author wfq
|
||||
* @version V1.0
|
||||
* @date 2026-08-12
|
||||
* @brief DBN 传感快照日志 — 环形实现 (与 offlog 同模式, 分区独立)
|
||||
*
|
||||
* 依赖: storage.c (SPI_Flash_*), cmcng.h (mstick), offlog.c (g_offlog_part + 审计)
|
||||
* 线程模型: enqueue 在中断 (USART2 ISR), flush/write_raw/clear 在主循环。
|
||||
* 所有 SPI 操作只在主循环 (扇区擦除 ~45ms 阻塞可接受, 与 offlog 同底线)。
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "snapshot.h"
|
||||
#include "storage.h"
|
||||
#include "cmcng.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* 编译期断言: 记录必须是 64B (定长环形索引依赖) */
|
||||
typedef char snap_rec_size_must_be_64[(sizeof(SnapRec) == 64) ? 1 : -1];
|
||||
typedef char snap_head_size_must_be_32[(sizeof(SnapHead) == 32) ? 1 : -1];
|
||||
|
||||
/*===========================================================================
|
||||
* 内部状态 (仅主循环访问: init/flush/clear/查询)
|
||||
*===========================================================================*/
|
||||
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
|
||||
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..n) */
|
||||
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
|
||||
static uint16_t _boot_seq; /* 当前 boot 序号 */
|
||||
static uint32_t _count; /* 有效记录数 (0..max_records) */
|
||||
static uint8_t _ready; /* init 完成标志 */
|
||||
|
||||
/* 中断安全 RAM 暂存 (单生产者=中断 enqueue / 单消费者=主循环 flush, 无锁)
|
||||
生产者只写 g_snap_wr / g_snap_count, 消费者只读 g_snap_rd; count 满则丢新 */
|
||||
static SnapRec g_snap_pending[SNAP_RAM_DEPTH];
|
||||
static volatile uint8_t g_snap_rd; /* 消费者 (主循环) 读指针 */
|
||||
static volatile uint8_t g_snap_wr; /* 生产者 (中断) 写指针 */
|
||||
static volatile uint8_t g_snap_count; /* 暂存条数 (容量 = SNAP_RAM_DEPTH) */
|
||||
static volatile uint32_t g_snap_drop; /* 满时丢弃计数 (调试用) */
|
||||
|
||||
/*===========================================================================
|
||||
* 运行时分区表 (ROADMAP 2026-08-12: JEDEC ID + 事件区大小 → 快照区 = 剩余)
|
||||
*===========================================================================*/
|
||||
SnapPart g_snap_part = {
|
||||
OFFLOG_CHIP_W25Q32, /* 默认 */
|
||||
0x110000UL, /* W25Q32: 0x090000 + 512KB 事件区 */
|
||||
0x2F0000UL, /* 3008KB = 4MB - 576KB - 512KB */
|
||||
751, /* 3008KB/4096 - 1 头扇区 */
|
||||
48064, /* 751 * 64 */
|
||||
};
|
||||
|
||||
/* 芯片总容量 (W25Q32 4MB → W25Q256 32MB) */
|
||||
static uint32_t snap_chip_total(uint8_t chip)
|
||||
{
|
||||
switch (chip) {
|
||||
case OFFLOG_CHIP_W25Q32: return 0x00400000UL;
|
||||
case OFFLOG_CHIP_W25Q64: return 0x00800000UL;
|
||||
case OFFLOG_CHIP_W25Q128: return 0x01000000UL;
|
||||
case OFFLOG_CHIP_W25Q256: return 0x02000000UL;
|
||||
default: return 0x00400000UL; /* 未知→默认 4MB */
|
||||
}
|
||||
}
|
||||
|
||||
/* 读 JEDEC ID → 填分区表。事件区大小与 offlog 同一事实来源 (g_offlog_part),
|
||||
快照区 = 总容量 - 固定区 - 事件区。未知芯片按 W25Q32 兜底 */
|
||||
static void snap_part_detect(void)
|
||||
{
|
||||
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
|
||||
uint8_t chip = OFFLOG_CHIP_W25Q32;
|
||||
uint32_t evt_size;
|
||||
uint32_t total;
|
||||
|
||||
switch (dev) {
|
||||
case OFFLOG_CHIP_W25Q32: chip = dev; break;
|
||||
case OFFLOG_CHIP_W25Q64: chip = dev; break;
|
||||
case OFFLOG_CHIP_W25Q128: chip = dev; break;
|
||||
case OFFLOG_CHIP_W25Q256: chip = dev; break;
|
||||
default: chip = OFFLOG_CHIP_W25Q32; break;
|
||||
}
|
||||
evt_size = g_offlog_part.area_size; /* 与事件区严格一致 */
|
||||
total = snap_chip_total(chip);
|
||||
|
||||
g_snap_part.chip = chip;
|
||||
g_snap_part.area_base = SNAP_FIXED_SIZE + evt_size;
|
||||
g_snap_part.area_size = total - SNAP_FIXED_SIZE - evt_size;
|
||||
g_snap_part.data_sectors = (g_snap_part.area_size / 4096UL) - 1;
|
||||
g_snap_part.max_records = g_snap_part.data_sectors * SNAP_REC_PER_SECTOR;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 基础读写
|
||||
*===========================================================================*/
|
||||
static void snap_head_write(const SnapHead *h)
|
||||
{
|
||||
uint8_t buf[32];
|
||||
memset(buf, 0xFF, sizeof(buf));
|
||||
memcpy(buf, h, sizeof(SnapHead));
|
||||
SPI_Flash_Write(buf, g_snap_part.area_base, sizeof(buf));
|
||||
}
|
||||
|
||||
static void snap_head_read(SnapHead *h)
|
||||
{
|
||||
uint8_t buf[32];
|
||||
SPI_Flash_Read(buf, g_snap_part.area_base, sizeof(buf));
|
||||
memcpy(h, buf, sizeof(SnapHead));
|
||||
}
|
||||
|
||||
static int snap_rec_read_at(uint32_t abs_off, SnapRec *out)
|
||||
{
|
||||
SPI_Flash_Read((uint8_t *)out, abs_off, SNAP_REC_SIZE);
|
||||
return (out->magic == SNAP_MAGIC) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 上电扫描恢复 (与 offlog 同模式: 从头记录写位置向后扫, magic + seq 连续)
|
||||
*===========================================================================*/
|
||||
static void snap_scan_recover(const SnapHead *h)
|
||||
{
|
||||
uint32_t pos = h->wr_off;
|
||||
uint32_t scanned = 0;
|
||||
uint32_t expect_seq = h->wr_seq;
|
||||
SnapRec r;
|
||||
|
||||
/* 防御: wr_off 必须在数据区范围内且 64B 对齐 */
|
||||
if (pos < SNAP_DATA_BASE || pos >= g_snap_part.area_base + g_snap_part.area_size) {
|
||||
pos = SNAP_DATA_BASE;
|
||||
}
|
||||
if ((pos - SNAP_DATA_BASE) % SNAP_REC_SIZE != 0) {
|
||||
pos = SNAP_DATA_BASE;
|
||||
}
|
||||
|
||||
while (scanned < SNAP_MAX_RECORDS) {
|
||||
if (snap_rec_read_at(pos, &r) != 0) {
|
||||
break; /* 找到真实写位置 */
|
||||
}
|
||||
if (expect_seq != 0 && r.seq != expect_seq) {
|
||||
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
|
||||
}
|
||||
pos += SNAP_REC_SIZE;
|
||||
if (pos >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
|
||||
pos = SNAP_DATA_BASE; /* 回绕数据区 */
|
||||
}
|
||||
scanned++;
|
||||
expect_seq++;
|
||||
}
|
||||
|
||||
_wr_off = pos;
|
||||
_wr_sector = (uint16_t)((pos - SNAP_DATA_BASE) / 4096) + 1; /* 区内 1..n */
|
||||
_wr_seq = expect_seq;
|
||||
_count = h->count + scanned;
|
||||
if (_count > SNAP_MAX_RECORDS) _count = SNAP_MAX_RECORDS;
|
||||
_boot_seq = (uint16_t)(h->boot_seq + 1);
|
||||
if (_boot_seq == 0) _boot_seq = 1;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 初始化
|
||||
*===========================================================================*/
|
||||
void snap_init(void)
|
||||
{
|
||||
SnapHead h;
|
||||
|
||||
snap_part_detect(); /* JEDEC ID + 事件区大小 → 快照区容量 */
|
||||
memset(&h, 0xFF, sizeof(h));
|
||||
snap_head_read(&h);
|
||||
|
||||
if (!(h.magic[0] == SNAP_HEAD_MAGIC0 && h.magic[1] == SNAP_HEAD_MAGIC1 &&
|
||||
h.magic[2] == SNAP_HEAD_MAGIC2 && h.magic[3] == SNAP_HEAD_MAGIC3)) {
|
||||
/* 全新区: 懒擦 — 只擦当前写扇区 (数据扇区1) ~45ms, 其余扇区由环形写
|
||||
切扇区时自动擦。原擦全部 751 扇区 ~34s → 主循环阻塞超 IWDG 4s → 复位 */
|
||||
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
|
||||
memset(&h, 0xFF, sizeof(h));
|
||||
h.magic[0] = SNAP_HEAD_MAGIC0;
|
||||
h.magic[1] = SNAP_HEAD_MAGIC1;
|
||||
h.magic[2] = SNAP_HEAD_MAGIC2;
|
||||
h.magic[3] = SNAP_HEAD_MAGIC3;
|
||||
h.boot_seq = 0;
|
||||
h.wr_sector = 1;
|
||||
h.wr_off = SNAP_DATA_BASE;
|
||||
h.wr_seq = 0;
|
||||
h.count = 0;
|
||||
snap_head_write(&h);
|
||||
|
||||
_wr_off = SNAP_DATA_BASE;
|
||||
_wr_sector = 1;
|
||||
_wr_seq = 1;
|
||||
_count = 0;
|
||||
_boot_seq = 1;
|
||||
} else {
|
||||
snap_scan_recover(&h);
|
||||
}
|
||||
|
||||
/* 回写头: boot_seq 递增 */
|
||||
h.magic[0] = SNAP_HEAD_MAGIC0;
|
||||
h.magic[1] = SNAP_HEAD_MAGIC1;
|
||||
h.magic[2] = SNAP_HEAD_MAGIC2;
|
||||
h.magic[3] = SNAP_HEAD_MAGIC3;
|
||||
h.boot_seq = _boot_seq;
|
||||
h.wr_sector = _wr_sector;
|
||||
h.wr_off = _wr_off;
|
||||
h.wr_seq = _wr_seq;
|
||||
h.count = _count;
|
||||
snap_head_write(&h);
|
||||
|
||||
g_snap_rd = 0;
|
||||
g_snap_wr = 0;
|
||||
g_snap_count = 0;
|
||||
g_snap_drop = 0;
|
||||
_ready = 1;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 延后初始化 (2026-08-17): 开机 SNAP_START_DELAY_MS 后首次进入时执行一次。
|
||||
* 目的: 避开启动早期 SPI 重负载窗口 (擦扇区 + 整扇区写回 ~80ms), 该窗口
|
||||
* 疑似触发 VDD 跌落/跑飞 (08-17 诊断: BOOT_CNT 恒=1, RSTSCKR 无标志)。
|
||||
* 附带: 3s 内的传感帧由 snap_enqueue/snap_flush 的 !_ready 门控自然丢弃,
|
||||
* 不落盘 (开机初期串口数据视为无效快照)。
|
||||
* 主循环每轮调用, 非阻塞 (未到延时/已初始化直接 return)。
|
||||
*===========================================================================*/
|
||||
#define SNAP_START_DELAY_MS 3000
|
||||
void snap_delayed_init(void)
|
||||
{
|
||||
if (_ready) return; /* 已初始化 */
|
||||
if (mstick() < SNAP_START_DELAY_MS) return; /* 未到延时窗口 */
|
||||
snap_init();
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 落盘 (主循环上下文)
|
||||
*===========================================================================*/
|
||||
static void snap_flush_head(void)
|
||||
{
|
||||
SnapHead h;
|
||||
h.magic[0] = SNAP_HEAD_MAGIC0;
|
||||
h.magic[1] = SNAP_HEAD_MAGIC1;
|
||||
h.magic[2] = SNAP_HEAD_MAGIC2;
|
||||
h.magic[3] = SNAP_HEAD_MAGIC3;
|
||||
h.boot_seq = _boot_seq;
|
||||
h.wr_sector = _wr_sector;
|
||||
h.wr_off = _wr_off;
|
||||
h.wr_seq = _wr_seq;
|
||||
h.count = _count;
|
||||
snap_head_write(&h);
|
||||
}
|
||||
|
||||
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic */
|
||||
static int snap_sector_has_data(uint32_t abs_sec_off)
|
||||
{
|
||||
SnapRec r;
|
||||
SPI_Flash_Read((uint8_t *)&r, abs_sec_off, SNAP_REC_SIZE);
|
||||
return (r.magic == SNAP_MAGIC) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 写一条记录 (仅主循环; rec->seq 已由 snap_flush 分配) */
|
||||
static void snap_write_raw(const SnapRec *rec)
|
||||
{
|
||||
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
|
||||
if (_wr_off >= SNAP_DATA_BASE + SNAP_DATA_SIZE) {
|
||||
_wr_off = SNAP_DATA_BASE;
|
||||
}
|
||||
|
||||
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区 */
|
||||
{
|
||||
uint32_t sec_index = (_wr_off - SNAP_DATA_BASE) / 4096; /* 0..n-2 */
|
||||
if ((uint32_t)(_wr_sector - 1) != sec_index) {
|
||||
uint32_t target_abs = (SNAP_DATA_BASE / 4096 + sec_index) * 4096;
|
||||
if (snap_sector_has_data(target_abs)) {
|
||||
uint32_t erased = (_count > SNAP_REC_PER_SECTOR)
|
||||
? SNAP_REC_PER_SECTOR : _count;
|
||||
_count -= erased;
|
||||
}
|
||||
SPI_Flash_Erase_Sector(target_abs / 4096);
|
||||
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..n */
|
||||
snap_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
|
||||
}
|
||||
}
|
||||
|
||||
SPI_Flash_Write_NoCheck((uint8_t *)rec, _wr_off, SNAP_REC_SIZE);
|
||||
_wr_off += SNAP_REC_SIZE;
|
||||
_wr_seq++;
|
||||
if (_count < SNAP_MAX_RECORDS) _count++;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 打包 (与 0xC0 线上线圈单元 12B 格式一致)
|
||||
*===========================================================================*/
|
||||
static void snap_pack_coil(const LUP_CoilSensor *cs, uint8_t *o)
|
||||
{
|
||||
o[0] = (uint8_t)((cs->freq_level << 6) | (cs->direction << 5)
|
||||
| (cs->freq_type << 4) | (cs->sensitivity & 0x0F));
|
||||
o[1] = (uint8_t)((cs->condition << 4) | (cs->loop_state << 3)
|
||||
| (cs->car_state << 2) | (cs->misc_type & 0x03));
|
||||
o[2] = (uint8_t)(cs->freq & 0xFF);
|
||||
o[3] = (uint8_t)((cs->freq >> 8) & 0xFF);
|
||||
o[4] = (uint8_t)((cs->freq >> 16) & 0xFF);
|
||||
o[5] = (uint8_t)(cs->variation & 0xFF);
|
||||
o[6] = (uint8_t)((cs->variation >> 8) & 0xFF);
|
||||
o[7] = (uint8_t)((cs->variation >> 16) & 0xFF);
|
||||
o[8] = (uint8_t)(cs->misc.passtime_ms & 0xFF);
|
||||
o[9] = (uint8_t)((cs->misc.passtime_ms >> 8) & 0xFF);
|
||||
o[10] = (uint8_t)((cs->misc.passtime_ms >> 16) & 0xFF);
|
||||
o[11] = (uint8_t)((cs->misc.passtime_ms >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
/* 打包传感帧 → SnapRec (seq 留待 flush 时分配; ts_ms/boot_seq 用采集时刻) */
|
||||
static void snap_pack(const LUP_SensorReport *sr, SnapRec *rec)
|
||||
{
|
||||
uint8_t i, n;
|
||||
|
||||
memset(rec, 0, sizeof(*rec));
|
||||
rec->magic = SNAP_MAGIC;
|
||||
n = (sr->coil_count > SNAP_COILS_MAX) ? SNAP_COILS_MAX : sr->coil_count;
|
||||
rec->len = n * SNAP_COIL_BYTES;
|
||||
rec->ts_ms = mstick();
|
||||
rec->boot_seq = _boot_seq;
|
||||
for (i = 0; i < n; i++) {
|
||||
snap_pack_coil(&sr->coils[i], rec->coils + i * SNAP_COIL_BYTES);
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 入队 (主循环上下文: uart_srv → lup_process_frame → iot_sensor_ingest)
|
||||
* USART2 ISR 只逐字节喂 lup_feed_byte, 完整帧由主循环 uart_srv 处理 →
|
||||
* 本函数不在中断里, 但保留 RAM 暂存 + flush 两段式: 批量落盘省 SPI 写次数
|
||||
* - 生产者只写 g_snap_wr / g_snap_pending / g_snap_count
|
||||
* - 消费者只读 g_snap_rd; count 为 volatile 字节, 单核单指令读写天然原子
|
||||
* - 满判定: count >= DEPTH (容量 = SNAP_RAM_DEPTH)
|
||||
*===========================================================================*/
|
||||
void snap_enqueue(const LUP_SensorReport *sr)
|
||||
{
|
||||
SnapRec rec;
|
||||
|
||||
if (!_ready || sr == NULL) return;
|
||||
|
||||
snap_pack(sr, &rec);
|
||||
|
||||
if (g_snap_count >= SNAP_RAM_DEPTH) {
|
||||
g_snap_drop++; /* 暂存满 → 丢新帧 (快照环形, 可接受) */
|
||||
return;
|
||||
}
|
||||
g_snap_pending[g_snap_wr] = rec;
|
||||
g_snap_wr = (uint8_t)((g_snap_wr + 1) & (SNAP_RAM_DEPTH - 1));
|
||||
g_snap_count++;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 主循环落盘 (iot_mqtt_publish_sensor 每轮调用; 断网/未使能照常落)
|
||||
* - seq 在主循环分配 (中断不碰 _wr_seq, 避免共享状态)
|
||||
*===========================================================================*/
|
||||
void snap_flush(void)
|
||||
{
|
||||
if (!_ready) return;
|
||||
|
||||
while (g_snap_count > 0) {
|
||||
SnapRec *rec = &g_snap_pending[g_snap_rd];
|
||||
rec->seq = _wr_seq; /* 主循环独占分配 */
|
||||
snap_write_raw(rec);
|
||||
g_snap_rd = (uint8_t)((g_snap_rd + 1) & (SNAP_RAM_DEPTH - 1));
|
||||
g_snap_count--;
|
||||
}
|
||||
if (g_snap_drop != 0) {
|
||||
g_snap_drop = 0; /* 丢弃计数清零 (调试观察点) */
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 查询
|
||||
*===========================================================================*/
|
||||
uint8_t snap_enabled(void)
|
||||
{
|
||||
return _ready;
|
||||
}
|
||||
|
||||
uint32_t snap_count(void)
|
||||
{
|
||||
return _count;
|
||||
}
|
||||
|
||||
uint32_t snap_seq_last(void)
|
||||
{
|
||||
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
|
||||
}
|
||||
|
||||
uint32_t snap_boot_seq(void)
|
||||
{
|
||||
return _boot_seq;
|
||||
}
|
||||
|
||||
int snap_read_idx(uint32_t idx, SnapRec *out)
|
||||
{
|
||||
uint32_t cnt = _count;
|
||||
if (!_ready || idx >= cnt) return -1;
|
||||
|
||||
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
|
||||
uint32_t phys = (_wr_off - SNAP_DATA_BASE); /* 0..DATA_SIZE */
|
||||
uint32_t start = (phys + SNAP_DATA_SIZE - cnt * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
|
||||
uint32_t read_off = SNAP_DATA_BASE + (start + idx * SNAP_REC_SIZE) % SNAP_DATA_SIZE;
|
||||
|
||||
return snap_rec_read_at(read_off, out);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 清空 (审计: 事件流记一条 LOG_CLEAR, payload[0]=2=快照流)
|
||||
*===========================================================================*/
|
||||
void snap_clear(void)
|
||||
{
|
||||
if (!_ready) return;
|
||||
|
||||
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms): count=0 使旧数据不可读,
|
||||
其余扇区由环形写覆盖时自动擦。原擦全部 751 扇区 ~34s > IWDG 4s → 复位 */
|
||||
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
|
||||
_wr_off = SNAP_DATA_BASE;
|
||||
_wr_sector = 1;
|
||||
_count = 0;
|
||||
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证快照全局唯一 */
|
||||
|
||||
{
|
||||
uint8_t p[1] = {2}; /* 2 = 快照流 (审计区分) */
|
||||
offlog_evt(OFFLOG_EVT_LOG_CLEAR, p, 1);
|
||||
}
|
||||
snap_flush_head();
|
||||
}
|
||||
|
||||
/* SnapRec -> JSON object (channels aligned with 0xC0 wire format).
|
||||
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
|
||||
Returns snprintf written length; caller must ensure buf_len enough. */
|
||||
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len)
|
||||
{
|
||||
int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES))
|
||||
? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES);
|
||||
if (coil_count < 0) coil_count = 0;
|
||||
|
||||
int pos = snprintf(buf, buf_len,
|
||||
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[",
|
||||
(unsigned long)r->seq, (unsigned)r->boot_seq,
|
||||
(unsigned long)r->ts_ms, (unsigned)coil_count);
|
||||
|
||||
int c;
|
||||
for (c = 0; c < coil_count; c++) {
|
||||
const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES];
|
||||
uint8_t cfg = p[0];
|
||||
uint8_t cond = p[1];
|
||||
uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16);
|
||||
int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16));
|
||||
uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8)
|
||||
| ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24);
|
||||
uint8_t freq_level = (cfg >> 6) & 0x03;
|
||||
uint8_t direction = (cfg >> 5) & 0x01;
|
||||
uint8_t freq_type = (cfg >> 4) & 0x01;
|
||||
uint8_t sensitivity = cfg & 0x0F;
|
||||
uint8_t condition = (cond >> 4) & 0x0F;
|
||||
uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */
|
||||
uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */
|
||||
uint8_t misc_type = cond & 0x03;
|
||||
const char *fl = "high";
|
||||
const char *mt = "time";
|
||||
|
||||
if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */
|
||||
|
||||
if (freq_level == 1) fl = "mid_high";
|
||||
else if (freq_level == 2) fl = "mid_low";
|
||||
else if (freq_level == 3) fl = "low";
|
||||
|
||||
if (misc_type == 1) mt = "cut_count";
|
||||
else if (misc_type == 2) mt = "flow_count";
|
||||
else if (misc_type == 3) mt = "relay_count";
|
||||
|
||||
if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ",");
|
||||
pos += snprintf(buf + pos, buf_len - pos,
|
||||
"{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u,"
|
||||
"\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s,"
|
||||
"\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}",
|
||||
(unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type,
|
||||
(unsigned)sensitivity, (unsigned)condition,
|
||||
loop_state ? "false" : "true", car_state ? "true" : "false",
|
||||
mt, (unsigned long)freq, (long)variation, (unsigned long)misc);
|
||||
}
|
||||
pos += snprintf(buf + pos, buf_len - pos, "]}");
|
||||
return pos;
|
||||
}
|
||||
|
||||
/* SnapRec -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §6.4 字段表解析)
|
||||
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
|
||||
Returns snprintf written length; 64B -> 128 hex chars. */
|
||||
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len)
|
||||
{
|
||||
int i, pos = 0;
|
||||
const uint8_t *p = (const uint8_t *)r;
|
||||
for (i = 0; i < (int)sizeof(SnapRec); i++) {
|
||||
if (pos >= buf_len - 3) break;
|
||||
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
|
||||
/* Winbond SPIFalsh ID */
|
||||
#define W25Q80 0XEF13
|
||||
#define W25Q16 0XEF14
|
||||
#define W25Q32 0XEF15
|
||||
#define W25Q64 0XEF16 //64Mbit, 8MByte
|
||||
#define W25Q128 0XEF17
|
||||
#define W25Q80 0X13 // device ID only, no vendor code
|
||||
#define W25Q16 0X14
|
||||
#define W25Q32 0X15
|
||||
#define W25Q64 0X16 //64Mbit, 8MByte
|
||||
#define W25Q128 0X17
|
||||
|
||||
/* Winbond SPIFalsh Instruction List */
|
||||
#define W25X_WriteEnable 0x06
|
||||
@@ -90,13 +90,13 @@ void SPI_Flash_Init(void)
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
|
||||
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
|
||||
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
|
||||
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
|
||||
GPIO_Init(GPIOA, &GPIO_InitStructure);
|
||||
GPIO_SetBits(GPIOA, GPIO_Pin_4);
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
|
||||
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
|
||||
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
|
||||
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
|
||||
GPIO_Init(GPIOA, &GPIO_InitStructure);
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
|
||||
@@ -105,7 +105,7 @@ void SPI_Flash_Init(void)
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
|
||||
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
|
||||
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
|
||||
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
|
||||
GPIO_Init(GPIOA, &GPIO_InitStructure);
|
||||
|
||||
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
|
||||
@@ -114,7 +114,7 @@ void SPI_Flash_Init(void)
|
||||
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
|
||||
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
|
||||
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
|
||||
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
|
||||
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
|
||||
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
|
||||
SPI_InitStructure.SPI_CRCPolynomial = 7;
|
||||
SPI_Init(SPI1, &SPI_InitStructure);
|
||||
@@ -225,6 +225,33 @@ u16 SPI_Flash_ReadID(void)
|
||||
}
|
||||
|
||||
|
||||
/*********************************************************************
|
||||
* @fn SPI_Flash_ReadJEDEC_ID
|
||||
*
|
||||
* @brief Read JEDEC ID (0x9F). Return 3rd byte (capacity code).
|
||||
* W25Q32=0x16 Q64=0x17 Q128=0x18 Q256=0x19.
|
||||
* 2026-08-19: no vendor code check (id[0]/id[1]) - compatible
|
||||
* with other vendors same-capacity parts.
|
||||
*
|
||||
* @return device id byte
|
||||
*/
|
||||
uint8_t SPI_Flash_ReadJEDEC_ID(void)
|
||||
{
|
||||
uint8_t id[3] = {0, 0, 0};
|
||||
|
||||
GPIO_WriteBit(GPIOA, GPIO_Pin_4, 0);
|
||||
SPI1_ReadWriteByte(W25X_JedecDeviceID);
|
||||
id[0] = SPI1_ReadWriteByte(0xFF);
|
||||
id[1] = SPI1_ReadWriteByte(0xFF);
|
||||
id[2] = SPI1_ReadWriteByte(0xFF);
|
||||
GPIO_WriteBit(GPIOA, GPIO_Pin_4, 1);
|
||||
|
||||
/* 2026-08-19: no vendor code check - JEDEC capacity code (id[2])
|
||||
is standardized across vendors (e.g. 0x16=32Mbit for ISSI/GD too) */
|
||||
return id[2];
|
||||
}
|
||||
|
||||
|
||||
/*********************************************************************
|
||||
* @fn SPI_Flash_Erase_Sector
|
||||
*
|
||||
@@ -481,7 +508,7 @@ void storage_init(void)
|
||||
SPI_Flash_Init();
|
||||
|
||||
uint16_t Flash_Model = SPI_Flash_ReadID();
|
||||
switch(Flash_Model)
|
||||
switch(Flash_Model & 0xFF) /* device ID low byte only, no vendor check */
|
||||
{
|
||||
case W25Q80: PRINT("W25Q80 OK!\r\n"); break;
|
||||
case W25Q16: PRINT("W25Q16 OK!\r\n"); break;
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
#include "loop_uart_proto.h"
|
||||
#include "simple_json.h"
|
||||
#include "storage.h"
|
||||
#include "offlog.h"
|
||||
#include "snapshot.h"
|
||||
#include "fault_diag.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@@ -636,7 +639,129 @@ static void handle_report_config(uint8_t socket, uint32_t msg_id, const char *js
|
||||
g_report_cfg.enable, g_report_cfg.interval, g_report_cfg.sensor_type);
|
||||
}
|
||||
|
||||
/* 4.16 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */
|
||||
/* 4.16 log_stat — 查询脱机日志统计 (event/snapshot 流, 协议 V1.03) */
|
||||
static void handle_log_stat(uint8_t socket, uint32_t msg_id, const char *json) {
|
||||
char data_json[256];
|
||||
char stream_buf[16];
|
||||
uint32_t count, seq_last, seq_first, capacity;
|
||||
const char *stream = "event";
|
||||
|
||||
memset(stream_buf, 0, sizeof(stream_buf));
|
||||
simple_parse_json(json, "\"stream\"", stream_buf);
|
||||
if (strcmp(stream_buf, "snapshot") == 0) stream = "snapshot";
|
||||
|
||||
if (strcmp(stream, "snapshot") == 0) {
|
||||
count = snap_count();
|
||||
seq_last = snap_seq_last();
|
||||
seq_first = (count > 0) ? (seq_last - count + 1) : 0;
|
||||
capacity = SNAP_MAX_RECORDS;
|
||||
snprintf(data_json, sizeof(data_json),
|
||||
"{\"stream\":\"snapshot\",\"enabled\":%s,\"boot_seq\":%lu,"
|
||||
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}",
|
||||
snap_enabled() ? "true" : "false",
|
||||
(unsigned long)snap_boot_seq(),
|
||||
(unsigned long)count, (unsigned long)capacity,
|
||||
(unsigned long)seq_first, (unsigned long)seq_last);
|
||||
PRINT("JSON: log_stat(snapshot) count=%lu seq_first=%lu seq_last=%lu\n",
|
||||
(unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last);
|
||||
} else {
|
||||
count = offlog_count();
|
||||
seq_last = offlog_seq_last();
|
||||
seq_first = (count > 0) ? (seq_last - count + 1) : 0;
|
||||
capacity = OFFLOG_MAX_RECORDS;
|
||||
snprintf(data_json, sizeof(data_json),
|
||||
"{\"stream\":\"event\",\"enabled\":%s,\"boot_seq\":%lu,"
|
||||
"\"count\":%lu,\"capacity\":%lu,\"seq_first\":%lu,\"seq_last\":%lu}",
|
||||
offlog_enabled() ? "true" : "false",
|
||||
(unsigned long)offlog_boot_seq(),
|
||||
(unsigned long)count, (unsigned long)capacity,
|
||||
(unsigned long)seq_first, (unsigned long)seq_last);
|
||||
PRINT("JSON: log_stat count=%lu seq_first=%lu seq_last=%lu\n",
|
||||
(unsigned long)count, (unsigned long)seq_first, (unsigned long)seq_last);
|
||||
}
|
||||
json_send_ok(socket, msg_id, "log_stat", data_json);
|
||||
}
|
||||
|
||||
/* 4.17 log_query — 分页拉取脱机日志 (event/snapshot 流, hex 原始字节上报, 协议 V1.03) */
|
||||
static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json) {
|
||||
char data_json[TCP_JSON_DATA_BUF_LEN];
|
||||
char stream_buf[16];
|
||||
uint32_t start_seq = json_get_uint_field(json, "\"start_seq\"");
|
||||
uint32_t req_count = json_get_uint_field(json, "\"count\"");
|
||||
int is_snap = 0;
|
||||
|
||||
memset(stream_buf, 0, sizeof(stream_buf));
|
||||
simple_parse_json(json, "\"stream\"", stream_buf);
|
||||
if (strcmp(stream_buf, "snapshot") == 0) is_snap = 1;
|
||||
|
||||
int pos = snprintf(data_json, sizeof(data_json),
|
||||
"{\"start_seq\":%lu,\"records\":[",
|
||||
(unsigned long)start_seq);
|
||||
uint32_t fetched = 0;
|
||||
|
||||
if (is_snap) {
|
||||
uint32_t total = snap_count();
|
||||
uint32_t seq_last = snap_seq_last();
|
||||
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
|
||||
if (req_count > SNAP_MAX_QUERY_RECORDS) req_count = SNAP_MAX_QUERY_RECORDS;
|
||||
if (req_count == 0) req_count = SNAP_MAX_QUERY_RECORDS;
|
||||
if (snap_enabled() && total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
|
||||
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
|
||||
while (fetched < req_count && (idx + fetched) < total) {
|
||||
SnapRec rec;
|
||||
char hex[132]; /* 64B -> 128 hex + NUL */
|
||||
if (snap_read_idx(idx + fetched, &rec) != 0) break;
|
||||
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
|
||||
snap_rec_to_hex(&rec, hex, sizeof(hex));
|
||||
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
|
||||
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)rec.seq, hex);
|
||||
fetched++;
|
||||
}
|
||||
}
|
||||
PRINT("JSON: log_query(snapshot) start_seq=%lu req=%lu fetched=%lu\n",
|
||||
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
|
||||
} else {
|
||||
uint32_t total = offlog_count();
|
||||
uint32_t seq_last = offlog_seq_last();
|
||||
uint32_t seq_first = (total > 0) ? (seq_last - total + 1) : 0;
|
||||
if (req_count > OFFLOG_MAX_QUERY_RECORDS) req_count = OFFLOG_MAX_QUERY_RECORDS;
|
||||
if (req_count == 0) req_count = OFFLOG_MAX_QUERY_RECORDS;
|
||||
if (total > 0 && start_seq >= seq_first && start_seq <= seq_last) {
|
||||
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
|
||||
while (fetched < req_count && (idx + fetched) < total) {
|
||||
OfflogEvt evt;
|
||||
char hex[68]; /* 32B -> 64 hex + NUL */
|
||||
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
|
||||
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
|
||||
offlog_evt_to_hex(&evt, hex, sizeof(hex));
|
||||
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
|
||||
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)evt.seq, hex);
|
||||
fetched++;
|
||||
}
|
||||
}
|
||||
PRINT("JSON: log_query start_seq=%lu req=%lu fetched=%lu\n",
|
||||
(unsigned long)start_seq, (unsigned long)req_count, (unsigned long)fetched);
|
||||
}
|
||||
snprintf(data_json + pos, sizeof(data_json) - pos, "]}");
|
||||
json_send_ok(socket, msg_id, "log_query", data_json);
|
||||
}
|
||||
|
||||
/* 4.18 log_clear — 清除脱机日志 (event/snapshot 流, 审计留痕, 协议 V1.03) */
|
||||
static void handle_log_clear(uint8_t socket, uint32_t msg_id, const char *json) {
|
||||
char stream_buf[16];
|
||||
memset(stream_buf, 0, sizeof(stream_buf));
|
||||
simple_parse_json(json, "\"stream\"", stream_buf);
|
||||
if (strcmp(stream_buf, "snapshot") == 0) {
|
||||
snap_clear(); /* 阻塞 ~45ms (逻辑清除 + 当前写扇区) */
|
||||
PRINT("JSON: log_clear(snapshot) done\n");
|
||||
} else {
|
||||
offlog_clear(); /* 阻塞 ~2.8s (63 扇区擦除) */
|
||||
PRINT("JSON: log_clear done\n");
|
||||
}
|
||||
json_send_ok(socket, msg_id, "log_clear", NULL);
|
||||
}
|
||||
|
||||
/* 4.19 loop_version_query — 获取地感MCU版本号 (CMD 0x4A) */
|
||||
static void handle_loop_version_query(uint8_t socket, uint32_t msg_id, const char *json) {
|
||||
lup_send_get_version();
|
||||
g_json_pending.active = 1;
|
||||
@@ -754,6 +879,9 @@ static const JsonCmdEntry g_cmd_table[] = {
|
||||
{"loop_sens_read", handle_loop_sens_read, 1},
|
||||
{"loop_sens_write", handle_loop_sens_write, 1},
|
||||
{"report_config", handle_report_config, 1},
|
||||
{"log_stat", handle_log_stat, 1},
|
||||
{"log_query", handle_log_query, 1},
|
||||
{"log_clear", handle_log_clear, 1},
|
||||
};
|
||||
|
||||
#define JSON_CMD_COUNT (sizeof(g_cmd_table) / sizeof(g_cmd_table[0]))
|
||||
@@ -813,13 +941,23 @@ static int format_loop_param_json(char *buf, uint16_t buf_size, const LUP_ParamG
|
||||
|
||||
static void json_sensor_callback(const uint8_t *pkg, uint16_t len)
|
||||
{
|
||||
FAULT_MARKER(MK_EVT_CB_IN);
|
||||
/* 2026-08-13 实验: enter PRINT 是 LUP 帧后卡死嫌疑点 (printf %d 处理死循环),
|
||||
注释验证 — 若卡死消失则坐实 printf 问题, 再深挖 _write/锁 */
|
||||
#if 0
|
||||
PRINT("JSON: sensor_cb enter socket=%d auth=%d report=%d\n",
|
||||
g_json_socket_listen, g_json_auth_state, g_report_cfg.enable);
|
||||
#endif
|
||||
|
||||
// Check: socket active, authed, report enabled
|
||||
if (g_json_socket_listen == 0xFF) { PRINT("JSON: sensor_cb skip: no socket\n"); return; }
|
||||
/* 2026-08-13: not authed 每帧打印太吵 (Loop MCU 高频上报), 且占用主循环
|
||||
关中断窗口 → UART2 丢字节。调试时打开 */
|
||||
#if 0
|
||||
if (g_json_auth_state != JSON_STATE_AUTHED) { PRINT("JSON: sensor_cb skip: not authed\n"); return; }
|
||||
if (!g_report_cfg.enable) { PRINT("JSON: sensor_cb skip: report disabled\\n"); return; }
|
||||
#endif
|
||||
if (g_json_auth_state != JSON_STATE_AUTHED) { return; }
|
||||
if (!g_report_cfg.enable) { PRINT("JSON: sensor_cb skip: report disabled\n"); return; }
|
||||
|
||||
LUP_SensorReport sr;
|
||||
memset(&sr, 0, sizeof(sr));
|
||||
@@ -829,7 +967,7 @@ static void json_sensor_callback(const uint8_t *pkg, uint16_t len)
|
||||
return;
|
||||
}
|
||||
|
||||
char data_json[2048];
|
||||
char data_json[TCP_JSON_DATA_BUF_LEN];
|
||||
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
|
||||
char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
|
||||
if (out) {
|
||||
@@ -933,13 +1071,13 @@ void tcp_json_handle_sock_int(uint8_t socketid, uint8_t intstat) {
|
||||
uint16_t space = TCP_JSON_RECV_BUF_LEN - g_json_recv_len;
|
||||
if (recv_len > space) recv_len = space;
|
||||
uint32_t rd_len = recv_len;
|
||||
uint8_t tmp_buf[RECE_BUF_LEN];
|
||||
static uint8_t tmp_buf[RECE_BUF_LEN]; /* 2026-08-17: static — 中断上下文 1KB 栈数组 (防溢出) */
|
||||
WCHNET_SocketRecv(socketid, tmp_buf, &rd_len);
|
||||
g_json_last_comm_ts = mstick(); // 有数据交互
|
||||
memcpy(g_json_recv_buf + g_json_recv_len, tmp_buf, (uint16_t)rd_len);
|
||||
g_json_recv_len += (uint16_t)rd_len;
|
||||
|
||||
char frame[TCP_JSON_MAX_FRAME];
|
||||
static char frame[TCP_JSON_MAX_FRAME]; /* 2026-08-17: static — 中断上下文 800B 栈数组 (防溢出) */
|
||||
while (json_extract_frame(g_json_recv_buf, &g_json_recv_len, frame, sizeof(frame))) {
|
||||
PRINT("JSON recv: %s\n", frame);
|
||||
json_process_frame(socketid, frame);
|
||||
@@ -1102,7 +1240,7 @@ static void json_check_pending(void) {
|
||||
memset(&pg, 0, sizeof(pg));
|
||||
int ret = lup_parse_param_get(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &pg);
|
||||
if (ret == 0) {
|
||||
char data_json[2048];
|
||||
char data_json[TCP_JSON_DATA_BUF_LEN];
|
||||
if (format_loop_param_json(data_json, sizeof(data_json), &pg) == 0) {
|
||||
json_send_ok(g_json_pending.socket, g_json_pending.msg_id,
|
||||
g_json_pending.cmd, data_json);
|
||||
@@ -1319,7 +1457,7 @@ void tcp_json_push_sensor(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
char data_json[2048];
|
||||
char data_json[TCP_JSON_DATA_BUF_LEN];
|
||||
if (format_sensor_json(data_json, sizeof(data_json), &sr) == 0) {
|
||||
char *out = (char *)malloc(TCP_JSON_MAX_FRAME);
|
||||
if (out) {
|
||||
|
||||
@@ -16,6 +16,102 @@ void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
|
||||
void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
|
||||
|
||||
|
||||
/*==================== UART2 RX DMA (2026-08-17 方案A) ====================
|
||||
* 根因: PRINT 临界区(关中断 ~7.4ms)期间 USART2 RXNE 中断被屏蔽 → 丢字节 → checksum fail
|
||||
* 方案: DMA1_Ch6 循环模式硬件收字节 (不依赖 CPU 中断), 主循环轮询消费
|
||||
* - 512B 环形缓冲: 能装 ~7 帧 (70B), 覆盖主循环长阻塞窗口 (SPI 擦除 45ms)
|
||||
* - 无新增中断 (关 RXNE, DMA 硬件接管) → 无优先级冲突; WCHNET 用独立 ETH DMA,
|
||||
* BLE 栈不用 DMA1 → Ch6 独占无冲突
|
||||
* - lup_feed_byte 状态机移入主循环 (原中断上下文) → 无中断竞争更安全
|
||||
*======================================================================*/
|
||||
#define UART2_DMA_BUF_LEN 512
|
||||
static uint8_t uart2_dma_buf[UART2_DMA_BUF_LEN] __attribute__((aligned(4)));
|
||||
static uint16_t uart2_dma_last = 0; /* 主循环消费位置 (DMA 写指针由硬件维护) */
|
||||
static uint32_t uart2_dma_drop = 0; /* 溢出丢弃计数 (主循环消费不及时) */
|
||||
static uint8_t uart2_dma_ota_mode = 0xFF; /* 上次 OTA 模式 (0xFF=未初始化, 首轮强制 reset) */
|
||||
|
||||
void uart2_dma_init(void)
|
||||
{
|
||||
DMA_InitTypeDef DMA_InitStructure;
|
||||
|
||||
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
|
||||
|
||||
DMA_DeInit(DMA1_Channel6);
|
||||
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART2->DATAR);
|
||||
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart2_dma_buf;
|
||||
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
|
||||
DMA_InitStructure.DMA_BufferSize = UART2_DMA_BUF_LEN;
|
||||
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
|
||||
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
|
||||
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
|
||||
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
|
||||
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
|
||||
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
|
||||
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
|
||||
DMA_Init(DMA1_Channel6, &DMA_InitStructure);
|
||||
|
||||
/* DMA 接管 USART2 RX: 硬件收字节, 打印关中断不丢 */
|
||||
USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE);
|
||||
DMA_Cmd(DMA1_Channel6, ENABLE);
|
||||
|
||||
/* 关闭 RXNE 中断 (DMA 接管后逐字节中断不再需要) */
|
||||
USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
|
||||
|
||||
uart2_dma_last = 0;
|
||||
}
|
||||
|
||||
/* 主循环每轮调用: 消费 DMA 环形缓冲新字节 → 批量喂 lup_feed_byte 状态机 */
|
||||
void uart2_dma_poll(void)
|
||||
{
|
||||
uint16_t cur = (uint16_t)(UART2_DMA_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel6));
|
||||
uint16_t n = (uint16_t)((cur + UART2_DMA_BUF_LEN - uart2_dma_last) & (UART2_DMA_BUF_LEN - 1));
|
||||
|
||||
if (n == 0) return;
|
||||
|
||||
/* 溢出保护: 未消费 > 半缓冲 (256B) → 消费太慢被 DMA 覆盖, 重置解析器丢帧计数
|
||||
2026-08-19: OTA 模式下 0x9F ACK 帧仅 7B, 停等协议同一时刻 Loop 至多回 1 帧,
|
||||
放宽阈值到整缓冲 — 256B 阈值在 TX 阻塞窗口可能误杀 ACK */
|
||||
{
|
||||
uint16_t drop_thr = g_flag_counter_ota.flag ? UART2_DMA_BUF_LEN : (UART2_DMA_BUF_LEN / 2);
|
||||
if (n > drop_thr) {
|
||||
uart2_dma_drop++;
|
||||
lup_frame_reset();
|
||||
uart2_dma_last = cur;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* OTA 模式切换 (0x7F ↔ 0x9F) 时重置帧解析器, 防旧模式残留状态污染 */
|
||||
if (g_flag_counter_ota.flag != uart2_dma_ota_mode) {
|
||||
lup_frame_reset();
|
||||
uart2_dma_ota_mode = g_flag_counter_ota.flag;
|
||||
}
|
||||
|
||||
while (n--) {
|
||||
uint8_t b = uart2_dma_buf[uart2_dma_last];
|
||||
uart2_dma_last = (uint16_t)((uart2_dma_last + 1) & (UART2_DMA_BUF_LEN - 1));
|
||||
|
||||
/* OTA 模式走 0x9F 帧解析 (bootloader 的 pre_ok/addr_ok/data ACK),
|
||||
非 OTA 保持 0x7F 协议解析 */
|
||||
int done = g_flag_counter_ota.flag ? lup_feed_byte_ota(b) : lup_feed_byte(b);
|
||||
if (done) {
|
||||
/* 帧接收完成 → 复制到 g_pkg_uart_2 (与中断版同逻辑, 主循环无竞争) */
|
||||
const uint8_t *frame = lup_frame_data();
|
||||
uint16_t frame_len = lup_frame_len();
|
||||
if (frame_len <= BUFF_STACK_SIZE) {
|
||||
memcpy(g_pkg_uart_2.pkg, frame, frame_len);
|
||||
g_pkg_uart_2.offset = frame_len;
|
||||
g_pkg_uart_2.flag = 1;
|
||||
g_pkg_uart_2.tick = 0;
|
||||
}
|
||||
lup_frame_reset();
|
||||
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
|
||||
/* 收帧中 → tick 归零 (对齐中断版行为, 防 TIM3 半帧兜底误判) */
|
||||
g_pkg_uart_2.tick = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void uart_init(void){
|
||||
GPIO_InitTypeDef GPIO_InitStructure = {0};
|
||||
USART_InitTypeDef USART_InitStructure = {0};
|
||||
@@ -44,8 +140,7 @@ void uart_init(void){
|
||||
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
|
||||
|
||||
USART_Init(USART2, &USART_InitStructure);
|
||||
// USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
|
||||
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
|
||||
/* RXNE 中断由 uart2_dma_init 关闭 (DMA 接管, 2026-08-17) */
|
||||
|
||||
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
|
||||
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
|
||||
@@ -55,6 +150,8 @@ void uart_init(void){
|
||||
|
||||
USART_Cmd(USART2, ENABLE);
|
||||
|
||||
uart2_dma_init(); /* DMA 循环接收接管 (2026-08-17) */
|
||||
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
|
||||
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
|
||||
@@ -90,27 +187,9 @@ void USART1_IRQHandler(void)
|
||||
*/
|
||||
void USART2_IRQHandler(void)
|
||||
{
|
||||
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
|
||||
{
|
||||
uint8_t _dat = USART_ReceiveData(USART2);
|
||||
|
||||
// 喂给帧解析器
|
||||
if (lup_feed_byte(_dat)) {
|
||||
// 帧接收完成,复制到 g_pkg_uart_2
|
||||
const uint8_t *frame = lup_frame_data();
|
||||
uint16_t frame_len = lup_frame_len();
|
||||
if (frame_len <= BUFF_STACK_SIZE) {
|
||||
memcpy(g_pkg_uart_2.pkg, frame, frame_len);
|
||||
g_pkg_uart_2.offset = frame_len;
|
||||
g_pkg_uart_2.flag = 1;
|
||||
g_pkg_uart_2.tick = 0;
|
||||
}
|
||||
lup_frame_reset();
|
||||
} else if (g_lup_parser.state != LUP_FRAME_STATE_IDLE) {
|
||||
// 正在接收帧中,tick 归零
|
||||
g_pkg_uart_2.tick = 0;
|
||||
}
|
||||
}
|
||||
/* 2026-08-17: RXNE 中断已关 (DMA 接管), 本 handler 不再触发。
|
||||
保留空函数 + IDLE 中断注释: 若后续需要帧边界辅助, 在此加
|
||||
USART_IT_IDLE 处理 (读 SR + 读 DR 清标志, 置标志由主循环消费)。 */
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +230,8 @@ void uart_srv(void)
|
||||
uint8_t i;
|
||||
uint8_t _report_flag = 0;
|
||||
|
||||
uart2_dma_poll(); /* DMA 环形缓冲 → 帧解析 (2026-08-17) */
|
||||
|
||||
// 检查命令超时
|
||||
lup_cmd_check_timeout();
|
||||
|
||||
@@ -172,43 +253,61 @@ void uart_srv(void)
|
||||
if(g_dbn_ble_state_acs_enable.flag != 0){
|
||||
// BLE ACS 已连接 → 改 Magic 为 0x8F 发给 BLE
|
||||
g_pkg_uart_2.pkg[0] = 0x8F;
|
||||
_report_flag = 1; // 保留给 BLE
|
||||
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp); // 保留给 BLE
|
||||
}
|
||||
// else: 回调已推送 TCP,直接清理
|
||||
/* 2026-08-13 修复: 0xC0 帧消费后必须清 flag —
|
||||
原缺失, auth 未通过时 tcp_json_push_sensor 提前 return
|
||||
也不清 → 坏帧/正常帧反复处理 (checksum fail 刷屏) */
|
||||
InitPkgUart(&g_pkg_uart_2);
|
||||
}
|
||||
else {
|
||||
if(g_flag_bt_state){
|
||||
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
|
||||
InitPkgUart(&g_pkg_uart_2);
|
||||
}
|
||||
else {
|
||||
/* 2026-08-13 修复: 非 0xC0 + 无 BLE 连接也清 flag (原缺失) */
|
||||
InitPkgUart(&g_pkg_uart_2);
|
||||
}
|
||||
}
|
||||
|
||||
// 调试打印
|
||||
// 调试打印 (2026-08-13: LUP Rx 已在 lup_process_frame 缓冲打印,
|
||||
// 此处重复逐字节打印冗余且高频 — 暂时关闭)
|
||||
#if 0
|
||||
for(i = 0; i < g_pkg_uart_2.offset; i++){
|
||||
PRINT(" %02X", g_pkg_uart_2.pkg[i]);
|
||||
}
|
||||
PRINT("\n");
|
||||
#endif
|
||||
|
||||
if(_report_flag){
|
||||
// 传感器帧保留在 pkg 中供上层 (tcp_json_srv) 处理
|
||||
// 不 InitPkgUart — 由 tcp_json_push_sensor 消费后清理
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 非 0x7F 魔法字节
|
||||
PRINT("Rcv_len:%d,dat: %s\n", g_pkg_uart_2.offset, g_pkg_uart_2.pkg);
|
||||
// 非 0x7F 魔法字节 — hex 打印 (原 %s 会把二进制当字符串 → 乱码)
|
||||
PRINT("Rcv_len:%d,dat:", g_pkg_uart_2.offset);
|
||||
for(i = 0; i < g_pkg_uart_2.offset; i++){
|
||||
PRINT(" %02X", g_pkg_uart_2.pkg[i]);
|
||||
}
|
||||
PRINT("\n");
|
||||
}
|
||||
}
|
||||
else{
|
||||
// OTA 模式 — 忽略 Loop MCU 数据
|
||||
/* OTA 模式 (2026-08-19): Loop bootloader 回的 0x9F 帧
|
||||
(back_9F_pre_ok / addr_ok / data ACK) 透传回 BLE 工具,
|
||||
否则停等协议 (0xA7 WITH_BACK) 永远等不到 ACK */
|
||||
if(g_flag_bt_state){
|
||||
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
|
||||
}
|
||||
/* OTA 模式同样清 flag — 残留帧会在 OTA 结束后被误处理 */
|
||||
InitPkgUart(&g_pkg_uart_2);
|
||||
}
|
||||
|
||||
|
||||
if(g_flag_bt_state){
|
||||
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
|
||||
|
||||
}
|
||||
else{
|
||||
g_dbn_ble_state_acs_enable.flag = 0;
|
||||
}
|
||||
|
||||
// 只有非 _report_flag 时才立即清空
|
||||
// _report_flag 的帧由 tcp_json_push_sensor 消费后清理
|
||||
if (!_report_flag) {
|
||||
InitPkgUart(&g_pkg_uart_2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,15 +171,23 @@ SECTIONS
|
||||
{
|
||||
. = ALIGN(4);
|
||||
PROVIDE( _sbss = .);
|
||||
*(.sbss*)
|
||||
*(.sbss*)
|
||||
*(.gnu.linkonce.sb.*)
|
||||
*(.bss*)
|
||||
*(.gnu.linkonce.b.*)
|
||||
*(.gnu.linkonce.b.*)
|
||||
*(COMMON*)
|
||||
. = ALIGN(4);
|
||||
PROVIDE( _ebss = .);
|
||||
} >RAM AT>FLASH
|
||||
|
||||
/* 故障诊断现场: 复位不清零 (startup 只清 .bss, 不碰 .noinit) */
|
||||
.noinit (NOLOAD) :
|
||||
{
|
||||
. = ALIGN(4);
|
||||
*(.noinit*)
|
||||
. = ALIGN(4);
|
||||
} >RAM
|
||||
|
||||
PROVIDE( _end = _ebss);
|
||||
PROVIDE( end = . );
|
||||
|
||||
|
||||
@@ -46,7 +46,16 @@ void USART_Printf_Init(uint32_t baudrate);
|
||||
void SDI_Printf_Enable(void);
|
||||
|
||||
#if(DEBUG)
|
||||
#define PRINT(format, ...) printf(format, ##__VA_ARGS__)
|
||||
/* 2026-08-13 printf 重入修复: BLE 协议栈回调(peripheral.c, BB 中断上下文)
|
||||
与主循环都有 PRINT, 交叉调用 printf → 输出乱码 + 堆/状态破坏 → HardFault。
|
||||
临界区用保存/恢复 MSTATUS: 中断里调用也能正确恢复, 不会嵌套误开中断。
|
||||
注意: 高频逐字节 PRINT (如 LUP Rx hex) 会长时间关中断, 应改缓冲一次性输出 */
|
||||
#define PRINT(format, ...) do { \
|
||||
uint32_t _print_ms = __get_MSTATUS(); \
|
||||
__disable_irq(); \
|
||||
printf(format, ##__VA_ARGS__); \
|
||||
__set_MSTATUS(_print_ms); \
|
||||
} while(0)
|
||||
#else
|
||||
#define PRINT(X...)
|
||||
#endif
|
||||
|
||||
+1522
-510
File diff suppressed because it is too large
Load Diff
@@ -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)")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user