Files
vd_960/docs/DLD960_IoT_MQTT协议.md
T
wangfq 2044efe14f docs: 新增《DLD960_IoT_MQTT协议》§6.9 4G 配置同步(V1.14),0x7D 帧方案作废
- 协议 §6.9:BLE→DBN→Air780 配置同步(权威源 / 载体 0x8F / 命令复用 / 载荷拆两条 /
  拉取为主+推送为辅 / 明确不同步项 / 异常处理 / 实施状态)
- 版本 V1.13 → V1.14,修订记录加行(插 V1.13 前);§6.6 增补充说明
- 行尾:全文纯 LF,改动零行尾污染
- devlog:新增 2026-09-10 条目(结论 + 6 条代码事实级硬约束 + 文件改动 + 待办)
2026-09-10 21:46:00 +08:00

1703 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DLD960 IoT 接口协议(MQTT + JSON
> 基于《DLD960 串口通信协议》V1.01,将设备管理、参数配置、数据上报映射到 MQTT 协议。
> 交互格式:JSON。
> 版本:V1.142026-09-10,新增 §6.9 4G 配置同步(BLE → DBN → Air780);0x8F 定义为本机侧私有帧,帧长上限 260B)
---
# 1 通信说明
## 1.1 连接参数
| 项目 | 说明 |
|------|------|
| 协议 | MQTT 3.1.1 / 5.0 |
| 传输层 | TCP (TLS 可选) |
| QoS | 配置类 QoS 1,数据上报类 QoS 0/1 |
| 编码 | UTF-8 |
| 序列化 | JSON |
## 1.2 Topic 结构
协议仅使用两个主题,所有消息类型通过 JSON 内 `cmd` 字段区分。
```
dld960/{dev_serial}/{direction}
```
| 字段 | 说明 |
|------|------|
| `dev_serial` | 设备序列码(6字节十六进制字符串,如 `A1B2C3D4E5F6` |
| `direction` | `srv` = 服务器下发(设备订阅),`dev` = 设备上报(服务器订阅) |
### 1.2.1 服务器下发 Topic — `dld960/{sn}/srv`
设备订阅此主题,接收服务器下发的所有命令(配置设置、查询、控制等)。
服务器发布到此主题。
对应串口 CMD0x09 ~ 0x1F, 0x63, 0x64, 0xC5 等。
### 1.2.2 设备上报 Topic — `dld960/{sn}/dev`
设备发布到此主题,上报线圈数据、事件、心跳及命令响应。
服务器订阅此主题接收所有设备上行消息。
服务器端可订阅通配符 `dld960/+/dev` 监听所有设备。
---
# 2 JSON 消息格式
## 2.1 通用结构
```json
{
"msg_id": 12345,
"cmd": "dev_info_query",
"ts": 1719000000,
"data": { ... }
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `msg_id` | uint32 | 消息序列号,递增,用于请求-响应匹配(由发起方生成) |
| `cmd` | string | 命令标识符 |
| `ts` | uint32 | Unix 时间戳(秒),发起方填充 |
| `data` | object | 命令参数,结构依 cmd 而定 |
## 2.2 响应通用结构
```json
{
"msg_id": 12345,
"cmd": "dev_info_query",
"ts": 1719000001,
"code": 0,
"msg": "success",
"data": { ... }
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `code` | int | 0 = 成功,非 0 = 失败码 |
| `msg` | string | 错误描述(成功时为 `"success"` |
### 错误码定义
| code | 说明 |
|------|------|
| 0 | 成功 |
| 1 | 参数错误(格式/范围不正确) |
| 2 | 密码验证失败 |
| 3 | 设备忙(操作执行中) |
| 4 | 不支持的命令 |
| 5 | 内部错误 |
| 6 | 数据超长 |
### OTA 细分错误码(V1.08
顶层 `code` 保持通用语义,`ota_*` 命令的细分错误经响应 `data.err_code` 表达:
| err_code | 场景 | 顶层 code |
|----------|------|-----------|
| 1 | 单片 CRC 失败 / 乱序缺片(`data.offset` 指示续传点) | 1 |
| 2 | 会话状态不允许(未 begin / 非 downloading 收 ota_data | 3 |
| 3 | 安全窗口拒绝(有车压线圈) | 3 |
| 4 | 版本冲突(同版本且非 force) | 1 |
| 5 | 全镜像 CRC 不匹配(`data.crc_ok=false` | 5 |
| 6 | 暂存区写失败(SPI 异常/满) | 5 |
## 2.3 设备时钟同步(`ts` 语义)
设备无 RTC/SNTP 时间源,上电后本地时钟为**上电秒数**(从 0 递增)。为使上行数据带真实 Unix 时间:
1. 设备上电连接成功后发布 `initialize`(此时 `ts` = 上电秒数,值很小,平台据此识别"未校准");
2. **平台收到 `initialize` 后,下发 `report_config` 命令,其信封 `ts` 字段填当前 Unix 时间**
3. 设备收到后以该 `ts` 校准本地时钟基准,之后所有上行消息(`loop_data` / `event_report` / 心跳 / 命令响应)的 `ts` 均为**真实 Unix 时间**。
补充约定:
- `report_config` 为约定的**主同步点**;设备亦接受任意下行命令中**合法**(≥ 1600000000,即 2020-09 之后)的 `ts` 进行校准,非法/过小值被忽略。
- 校准前(含首个 `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 命令详表
| cmd | 说明 | 方向 | 对应串口 |
|-----|------|------|----------|
| `dev_serial_set` | 更改设备序列码 | srv→dev | 0x09 |
| `dev_info_query` | 查询设备信息 | srv→dev | 0x10 |
| `ssc_net_set` | 设置 SSC 网络配置 | srv→dev | 0x11 |
| `ssc_net_query` | 查询 SSC 网络配置 | srv→dev | 0x12 |
| `iot_net_set` | 设置 IoT 网络配置 | srv→dev | 0x13 |
| `iot_net_query` | 查询 IoT 网络配置 | srv→dev | 0x14 |
| `iot_topic_set` | 设置设备 Topic | srv→dev | 0x15 |
| `iot_topic_query` | 查询设备 Topic | srv→dev | 0x16 |
| `pwd_verify` | 验证设备密码 | srv→dev | 0x1C |
| `pwd_set` | 设置设备密码 | srv→dev | 0x1D |
| `factory_reset` | 设备出厂初始化 | srv→dev | 0x1E |
| `device_reset` | 设备复位 | srv→dev | 0x1F |
| `loop_param_set` | 设置车检器多路参数 | srv→dev | 0x63 |
| `loop_param_query` | 读取车检器多路参数 | srv→dev | 0x64 |
| `report_config` | 设置主动上报 | srv→dev | 0xC5 |
| `log_stat` | 查询脱机日志统计(事件/快照流) | srv→dev | — |
| `log_query` | 分页拉取脱机日志(事件/快照流) | srv→dev | — |
| `log_clear` | 清除脱机日志(事件/快照流,审计留痕) | srv→dev | — |
| `ota_begin` | 开启 OTA 会话 / 断点续传定位 | srv→dev | — |
| `ota_data` | OTA 分片下发(256B/片,单片 CRC32 | srv→dev | — |
| `ota_end` | 结束 OTA 下载,全镜像 CRC32 复核 | srv→dev | — |
| `ota_abort` | 中止 OTA 会话,释放暂存 | srv→dev | — |
| `ota_flash` | 触发本地 ISP 刷写(仅 ready 态) | srv→dev | — |
| `ota_status` | 查询 OTA 状态(含进度) | srv→dev | — |
| `loop_version_query` | 实时查询地感 Loop MCU 版本(V1.10 | srv→dev | 0x4A |
| `frame_cmd` | 4G 通道原始帧透传下发(hex 封装,可选兜底) | srv→dev | — |
| `initialize` | 设备上电初始化登陆 | dev→srv | — |
| `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 |
| `event_report` | 事件上报(**平台须应答**,见 §5.3 | dev→srv | — |
| `ota_report` | OTA 进度/结果主动上报 | dev→srv | — |
| `heartbeat` | 设备心跳 | dev→srv | — |
| `frame_report` | 4G 通道原始帧透传上报(hex 封装,可选兜底) | dev→srv | — |
> 4G 通道(方案 C,见 §6):上行由 Air780 解析 0x7F 帧并转换为**标准 JSON 命令**loop_data / event_report / initialize / heartbeat,与有线通道一致),平台无感。`frame_cmd` / `frame_report` 保留为**可选兜底**Air780 未实现转换的命令 / 未识别帧透传),有线通道不使用。
---
# 4 命令详情
## 4.1 更改设备序列码 `dev_serial_set`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 1,
"cmd": "dev_serial_set",
"ts": 1719000000,
"data": {
"dev_serial": "A1B2C3D4E5F6"
}
}
```
**响应:** Topic: `dld960/{sn}/dev`
```json
{
"msg_id": 1,
"cmd": "dev_serial_set",
"ts": 1719000001,
"code": 0,
"msg": "success"
}
```
## 4.2 查询设备信息 `dev_info_query`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 2,
"cmd": "dev_info_query",
"ts": 1719000000
}
```
**响应:**
```json
{
"msg_id": 2,
"cmd": "dev_info_query",
"ts": 1719000001,
"code": 0,
"msg": "success",
"data": {
"dev_serial": "A1B2C3D4E5F6",
"hard_ver": "1.1",
"soft_ver": "1.1",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"model": "DLD960",
"product_code": "960001",
"sub_code": {
"net": true,
"iot": true
},
"bus": {
"bus1": 0,
"bus2": 0,
"bus3": 0,
"bus4": 0
}
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `dev_serial` | string | 12 位十六进制序列码 |
| `hard_ver` | string | 硬件版本(整机),格式 `"主.次"` |
| `soft_ver` | string | 软件版本(整机 DBN MCU 固件),格式 `"主.次"` |
| `loop_ver` | string | **地感 Loop MCU 固件版本**V1.10),格式 `"主.次.次"`(如 `1.2.3`);来自 Loop 0x4A 查询缓存,**查询未完成/失败则为空字符串** |
| `loop_hw_ver` | string | **地感 Loop MCU 硬件版本**V1.10),格式 `"主.次.次"`;同上,可为空 |
| `model` | string | 产品型号,1~10 字符 |
| `product_code` | string | 产品编码,6 位数字字符串 |
| `sub_code.net` | bool | 网络功能是否启用 |
| `sub_code.iot` | bool | IoT/MQTT 功能是否启用 |
| `bus.bus1~4` | uint8 | 各总线探头数 |
> **loop_ver 语义(V1.10**`loop_ver`/`loop_hw_ver` 是**缓存值**(设备上电后自动经 Loop `0x4A` 查询一次,OTA 刷写成功后刷新);MQTT 命令处理为同步回包,不等 0x4A 异步响应,故查询未完成/失败时回空。平台需最新版本时用 `loop_version_query`(§4.25)实时查询。
## 4.3 设置 SSC 网络配置 `ssc_net_set`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 3,
"cmd": "ssc_net_set",
"ts": 1719000000,
"data": {
"dev_ip": "192.168.1.100",
"subnet_mask": "255.255.255.0",
"route_ip": "192.168.1.1",
"lssc_ip": "192.168.1.200",
"dns": "8.8.8.8",
"port": 502
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `dev_ip` | string | 设备 IP 地址 |
| `subnet_mask` | string | 子网掩码 |
| `route_ip` | string | 网关地址 |
| `lssc_ip` | string | LSSC 服务器 IP |
| `dns` | string | DNS 服务器 IP |
| `port` | uint16 | 端口号 |
**响应:**
```json
{
"msg_id": 3,
"cmd": "ssc_net_set",
"ts": 1719000001,
"code": 0,
"msg": "success"
}
```
## 4.4 查询 SSC 网络配置 `ssc_net_query`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 4,
"cmd": "ssc_net_query",
"ts": 1719000000
}
```
**响应:** 返回字段同 4.3 的 `data`
## 4.5 设置 IoT 网络配置 `iot_net_set`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 5,
"cmd": "iot_net_set",
"ts": 1719000000,
"data": {
"host": "mqtt.example.com",
"port": 1883,
"client_id": "dld960_A1B2C3D4E5F6",
"username": "admin",
"password": "secret"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `host` | string | MQTT Broker 域名或 IP |
| `port` | uint16 | MQTT 端口,默认 1883 |
| `client_id` | string | MQTT Client ID,空时用空格 |
| `username` | string | MQTT 用户名 |
| `password` | string | MQTT 密码 |
**响应:** 标准成功/失败。
## 4.6 查询 IoT 网络配置 `iot_net_query`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 6,
"cmd": "iot_net_query",
"ts": 1719000000
}
```
**响应:** 返回字段同 4.5 的 `data`
## 4.7 设置设备 Topic `iot_topic_set`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 7,
"cmd": "iot_topic_set",
"ts": 1719000000,
"data": {
"client_id_enable": true,
"topic_pub": "dld960/data/A1B2C3D4E5F6",
"topic_sub": "dld960/cmd/A1B2C3D4E5F6"
}
}
```
**响应:** 标准成功/失败。
## 4.8 查询设备 Topic `iot_topic_query`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 8,
"cmd": "iot_topic_query",
"ts": 1719000000
}
```
**响应:** 返回字段同 4.7 的 `data`
## 4.9 验证设备密码 `pwd_verify`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 9,
"cmd": "pwd_verify",
"ts": 1719000000,
"data": {
"password": "123456"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `password` | string | 6 位数字密码 |
**响应:**
```json
{
"msg_id": 9,
"cmd": "pwd_verify",
"ts": 1719000001,
"code": 0,
"msg": "success"
}
```
## 4.10 设置设备密码 `pwd_set`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 10,
"cmd": "pwd_set",
"ts": 1719000000,
"data": {
"old_password": "123456",
"new_password": "654321"
}
}
```
**响应:** 标准成功/失败。旧密码错误时 `code=2`
## 4.11 设备出厂初始化 `factory_reset`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 11,
"cmd": "factory_reset",
"ts": 1719000000
}
```
**响应:**
```json
{
"msg_id": 11,
"cmd": "factory_reset",
"ts": 1719000001,
"code": 0,
"msg": "success"
}
```
## 4.12 设备复位 `device_reset`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 12,
"cmd": "device_reset",
"ts": 1719000000
}
```
无响应(设备复位后断开连接)。
## 4.13 设置车检器多路参数 `loop_param_set`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 13,
"cmd": "loop_param_set",
"ts": 1719000000,
"data": {
"auto_mode": false,
"channels": [
{
"ch": 1,
"sens": 7,
"level": "high",
"delay": 0,
"output": "exist",
"exist": 0,
"dir": 0,
"safe": 0,
"fun_mode": 0
},
{
"ch": 2,
"sens": 7,
"level": "mid_high",
"delay": 5,
"output": "enter_pulse",
"exist": 10,
"dir": 0,
"safe": 0,
"fun_mode": 0
},
{
"ch": 3,
"sens": 5,
"level": "mid_low",
"delay": 0,
"output": "exist",
"exist": 0,
"dir": 1,
"safe": 5,
"fun_mode": 0
},
{
"ch": 4,
"sens": 8,
"level": "low",
"delay": 10,
"output": "leave_pulse",
"exist": 15,
"dir": 0,
"safe": 0,
"fun_mode": 0
}
]
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `auto_mode` | bool | 自动调频模式,默认 false |
| `channels` | array | 各路通道参数,1~4 路 |
| `channels[].ch` | uint8 | 通道号 1~4 |
| `channels[].sens` | uint8 | sensitivity, 灵敏度 0~9,默认 7 |
| `channels[].level` | string | freq_level, `"high"`(33nF) / `"mid_high"`(43nF) / `"mid_low"`(66nF) / `"low"`(76nF) |
| `channels[].delay` | uint8 | loop_delay, 延时时间,0~200(×0.1s),最大 20s |
| `channels[].output` | string | exist_mode, `"exist"` / `"enter_pulse"` / `"leave_pulse"` / `"direction"` |
| `channels[].exist` | uint8 | output_mode, 存在方式,0=永久,非0=分钟数 |
| `channels[].dir` | uint8 | direction_mode, 方向判别模式,0=触发,1~6=方向输出 |
| `channels[].safe` | uint8 | safe_mode, 安全模式,0=关闭,非0=分钟数 |
| `channels[].fun_mode` | uint8 | function_mode, 功能模式 |
**响应:**
```json
{
"msg_id": 13,
"cmd": "loop_param_set",
"ts": 1719000001,
"code": 0,
"msg": "success"
}
```
## 4.14 读取车检器多路参数 `loop_param_query`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 14,
"cmd": "loop_param_query",
"ts": 1719000000
}
```
**响应:**
```json
{
"msg_id": 14,
"cmd": "loop_param_query",
"ts": 1719000001,
"code": 0,
"msg": "success",
"data": {
"auto_mode": false,
"channels": [
{
"ch": 1,
"sens": 7,
"level": "high",
"delay": 0,
"output": "exist",
"exist": 0,
"dir": 0,
"safe": 0,
"fun_mode": 0,
"f_initial": 105300,
"f_current": 105280,
"diff": 20
}
]
}
}
```
| 额外字段 | 类型 | 单位 | 说明 |
|----------|------|------|------|
| `f_initial` | uint32 | Hz | freq_initial, 初始频率 |
| `f_current` | uint32 | Hz | freq_current, 当前实时频率 |
| `diff` | uint32 | Hz | 变化量(绝对值) |
## 4.15 设置主动上报 `report_config`
> Topic: `dld960/{sn}/srv`
> **⚠️ 兼任设备时钟同步点**:本命令信封 `ts` 须填当前 Unix 时间,设备据此校准本地时钟(见 §2.3)。平台应在每次收到设备 `initialize` 后下发一次。
**请求:**
```json
{
"msg_id": 15,
"cmd": "report_config",
"ts": 1719000000,
"data": {
"sensor_type": 12,
"enable": true,
"once": false,
"env_eval": false,
"interval": 5,
"ack_required": false,
"timeout": 0
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `sensor_type` | uint8 | 传感器类型,线圈=0x0C(12) |
| `enable` | bool | 是否使能主动上报 |
| `once` | bool | 仅上报一次(查询模式) |
| `env_eval` | bool | 环境评估使能 |
| `interval` | uint8 | 上报间隔(秒),0=实时 |
| `ack_required` | bool | 上报是否需要确认 |
| `timeout` | uint8 | 超时时间(分钟),0=无限制 |
**响应:** 标准成功/失败。
---
## 4.16 查询脱机日志统计 `log_stat`
> Topic: `dld960/{sn}/srv`
> 设备本地 W25Qxx 环形日志(事件区/快照区容量随存储芯片动态,掉电不丢)。用于日志拉取前的分页定位。
> 通过 `data.stream` 区分日志流:`event`(事件日志,缺省)/ `snapshot`(传感快照)。
**请求:**
```json
{
"msg_id": 16,
"cmd": "log_stat",
"ts": 1719000000,
"data": {
"stream": "event"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应 datastream=event):**
```json
{
"stream": "event",
"enabled": true,
"boot_seq": 2,
"count": 1234,
"capacity": 16256,
"seq_first": 100,
"seq_last": 1333
}
```
**响应 datastream=snapshot):**
```json
{
"stream": "snapshot",
"enabled": true,
"boot_seq": 2,
"count": 1234,
"capacity": 48064,
"seq_first": 100,
"seq_last": 1333
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event` / `snapshot` |
| `enabled` | bool | 日志功能是否启用(Flash 初始化成功) |
| `boot_seq` | uint16 | 当前启动序号(每次上电 +1,区分复位段) |
| `count` | uint32 | 有效记录条数(0~capacity,环形覆盖后 < capacity |
| `capacity` | uint32 | 容量上限(**随存储芯片与流动态**):事件流 W25Q32=16256 / Q64=32640 / Q128=65408 / Q256=130944;快照流 W25Q32=48064 / Q64=105408 / Q128=220096 / Q256=449472 |
| `seq_first` | uint32 | 逻辑首条记录全局序号(`seq_last - count + 1`count=0 时为 0 |
| `seq_last` | uint32 | 最新一条记录全局序号(跨 boot 单调递增) |
---
## 4.17 分页拉取脱机日志 `log_query`
> Topic: `dld960/{sn}/srv`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**(hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`。
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),平台按《DLD960 BLE 协议》字段表解析。
**请求:**
```json
{
"msg_id": 17,
"cmd": "log_query",
"ts": 1719000000,
"data": {
"stream": "event",
"start_seq": 1330,
"count": 4
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 data**
```json
{
"start_seq": 1330,
"records": [
{
"seq": 1330,
"hex": "a53200000200000001000000..."
}
]
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**(flash 存储字节原样,小端) |
**解析字段表(与 BLE 通道完全一致):**
| 流 | 结构 | 字段表 |
|----|------|--------|
| `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算(见 §2.3)。
---
## 4.18 清除脱机日志 `log_clear`
> Topic: `dld960/{sn}/srv`
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——谁在何时清了日志,留痕不可清除)。平台侧应做权限控制。
**请求:**
```json
{
"msg_id": 18,
"cmd": "log_clear",
"ts": 1719000000,
"data": {
"stream": "event"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `stream` | string | 日志流:`event`(缺省,可省略)或 `snapshot` |
**响应:** 标准成功/失败。
- `stream=event`:成功后 `log_stat``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。**阻塞 ~2.8s**(63 个数据扇区 SPI 擦除),请勿高频调用。
- `stream=snapshot`:成功后 `log_stat``count` 归 0`seq_last` 继续递增;清除动作写入事件流审计(`log_clear`,payload 标记快照流)。**阻塞 ~45ms**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。
---
## 4.19 开启 OTA 会话 `ota_begin`
> Topic: `dld960/{sn}/srv`
> 依据《DLD960_MQTT_OTA协议.md》设计稿 V1.01ROADMAP P1.4 ①:Loop MCU 远程 OTA,先存后刷)。
> 能力探测:老固件(V1.07 及以下)无 `ota_*` 命令,收到回 `code=4`;平台下发前用 `ota_status` 或 `dev_info_query.soft_ver` 判断。
**请求:**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000000,
"data": {
"target": "loop",
"size": 46864,
"crc32": 305419896,
"version": "1.1.0",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | `loop`(当前支持);`dbn` 预留 |
| `size` | uint32 | 镜像 bin 字节数(≤ 98304 = 96KBSlot 数据区 100KB 预留 4KB 边界余量) |
| `crc32` | uint32 | 全镜像 CRC32(十进制,算法见 §4.19.1 |
| `version` | string | 目标固件版本(写入元数据,审计用;bootloader 不校验版本) |
| `force` | bool | `true` = 覆盖现有暂存镜像 / 忽略版本冲突(默认 false) |
**设备行为:**
1. 读暂存元数据:若已有镜像且 `size+crc32` 与本次一致 →
- `state=ready` → 返回 `offset=size`(平台可直接 `ota_flash`
- `state=downloading` → 返回 `offset=received`(断点续传)
2. 不一致 → 分配 Slot(A 当前 / B 回滚),写元数据 `state=downloading, received=0`,返回 `offset=0`
3. `force=false` 且目标版本 == 当前运行版本 → 回 `code=1, err_code=4`(防重复刷写,可 force 绕过)
**响应 data**
```json
{
"msg_id": 401,
"cmd": "ota_begin",
"ts": 1719000001,
"code": 0,
"msg": "success",
"data": {
"target": "loop",
"slot": "a",
"offset": 0,
"received": 0,
"size": 46864,
"crc32": 305419896,
"state": "downloading"
}
}
```
### 4.19.1 CRC32 算法(必须双方一致)
标准 **CRC-32/ISO-HDLC**poly `0x04C11DB7`reflected `0xEDB88320`),init `0xFFFFFFFF`refin/refout truexorout `0xFFFFFFFF`。平台侧 Python `zlib.crc32()` / `binascii.crc32()` 即此算法;设备侧查表法。全镜像 CRC = offset 0 ~ size-1 连续;单片 CRC = 仅该 256B。
---
## 4.20 OTA 分片下发 `ota_data`
> Topic: `dld960/{sn}/srv`
> 单片大小 **256B** 是协议常量,不接受协商;hex 512 字符 + JSON 外壳 ≈ 640B < 设备接收缓冲 1024B。
**请求:**
```json
{
"msg_id": 402,
"cmd": "ota_data",
"ts": 1719000002,
"data": {
"target": "loop",
"offset": 0,
"crc32": 2524764894,
"data": "6a6173646f6e...512 hex 字符 = 256B"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `target` | string | 同 `ota_begin` |
| `offset` | uint32 | 本片在镜像中的绝对偏移(**256 对齐**,首片 0) |
| `crc32` | uint32 | 本片 256B 的 CRC32(十进制) |
| `data` | string | 256B 原始字节小写 hex512 字符 |
**设备行为:**
1. `offset == received`(顺序片)→ 单片 CRC32 校验 → 写 W25Qxx 暂存(256B 页对齐)→ `received += 256`(≥size 截断为 size)→ `code=0`
2. `offset < received`(重复片,平台重发)→ **幂等回 `code=0`**,不重写
3. `offset > received`(缺片/乱序)→ `code=1, err_code=1` + `data.offset=received`(指示平台从该处续传;协议不要求乱序重组)
4. 单片 CRC 失败 → `code=1, err_code=1`(平台重发本片;连续失败平台可 `ota_abort`
5. 会话未开始 / 状态非 downloading → `code=3, err_code=2`
6. `data` 非 512 hex / offset 非 256 对齐 / 超 size → `code=1` 参数错误
**响应:** 标准成功/失败 + data 回显 `{offset, received}`
---
## 4.21 结束下载 `ota_end`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{
"msg_id": 403,
"cmd": "ota_end",
"ts": 1719000003,
"data": {
"target": "loop",
"crc32": 305419896
}
}
```
**设备行为:**
1. `received != size``code=1` + `data.offset=received`(不完整,续传)
2. `received == size` → 读回暂存区全镜像计算 CRC32,与 ota_begin 声明值比对
- 一致 → 元数据 `state=ready, last_result=0``code=0, data={crc_ok:true}`
- 不一致 → 元数据 `state=downloading`(保留已下载数据,可重发错片)→ `code=5, err_code=5, data={crc_ok:false}`
---
## 4.22 中止会话 `ota_abort`
> Topic: `dld960/{sn}/srv`
```json
{
"msg_id": 404,
"cmd": "ota_abort",
"ts": 1719000004,
"data": { "target": "loop" }
}
```
设备行为:元数据 `state=aborted`,Slot 标记可覆盖;正在刷写时 abort → 停止发送后续 A7 块(Loop 端由 bootloader 超时复位回 APP 兜底)。响应标准成功。
---
## 4.23 触发刷写 `ota_flash`
> Topic: `dld960/{sn}/srv`
> ⚠ **会车安全关键命令**:平台应确认现场允许(无车压线圈、非高峰)再下发。
**请求:**
```json
{
"msg_id": 405,
"cmd": "ota_flash",
"ts": 1719000005,
"data": {
"target": "loop",
"slot": "a",
"force": false
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `slot` | string | `a` / `b`(缺省 = 当前 ready 的槽) |
| `force` | bool | `true` = 跳过安全窗口检查(高风险,平台授权) |
**设备行为(同步检查 → 异步刷写):**
1. **安全窗口检查**force=false 时):4 通道 Loop 有车(VD_FLAG 任一置位)→ `code=3, err_code=3`(有车,拒绝;平台提示"车辆离开后重试")。刷写期间会阻断检测与继电器控制 → 平台建议低峰执行
2. 元数据非 ready → `code=3`(先 `ota_end` 完成校验)
3. 通过 → 立即回 `code=0`(异步),进入刷写:
- 写 offlog 事件日志:`固件升级开始`target/slot/version/size
- **暂停事件上报与脱机日志(会话期间)**:MQTT `event_report` 暂停发送(入队积压,16 深溢出丢最旧,会话结束恢复后补发);offlog/快照落盘暂停("升级开始"日志在暂停前写入、"升级结果"在恢复后补记)
- 维持 Loop 现状:复位进 bootloader 后的 GPIO/继电器状态与现网 BLE OTA 升级一致,不额外干预
-`9F 01 00 01 A5 A7` 启动帧 → Loop APP 写 flag 复位 → bootloader 回 pre_ok
- 发 A6 地址帧(`0x08003400` 4 字节大端)→ addr_ok
- 从 W25Qxx 暂存读镜像,按 ≤254B/块发 A7(**非阻塞 tick 驱动**:每轮主循环发送 1~2 块并检查 ACK,绝不阻塞主循环,保证刷写窗口内 MQTT PINGREQ/心跳/IWDG 喂狗正常);停等 ACK,1s 超时重发 ×3
- 末块(sub_amount=1)→ bootloader 写剩余 → 清 flag → 复位跑新 APP
4. 进度经 `ota_report` 上行(§5.5);失败重试 ×3 仍失败 → 元数据 `state=flash_failed` + `event_report{type:ota_error}` 告警(平台必答)
5. **刷写结果(设备侧,V1.09 明确)**
- 成功(末块 ACK)→ 元数据 `state=idle`**镜像保留**`size`/`crc32`/`version`/`slot` 不变,`last_result=0``flash_cnt+1`)→ 立即上报 `ota_report{stage:done}`(重发 3 次×5s,同 `msg_id`/`ts`)→ 恢复 `event_report` 发送与 offlog/快照落盘
- 失败 → 元数据 `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}`done/failed 同重发策略)
- **状态语义**:刷写完成后 DBN 侧状态回 `idle`("本轮刷写已结束"),与"下载完成待刷(`ready`)"严格区分——**平台不得把 `state=ready` 判为"刷写未启动"**:本地刷写 70 块(17224B)实测 <1s,平台轮询间隔可能错过 `flashing` 中间态
**响应:** `code=0` 仅表示已启动,不代表刷写成功——结果以 `ota_report` / `ota_status` 为准。
---
## 4.24 查询状态 `ota_status`
> Topic: `dld960/{sn}/srv`
**请求:**
```json
{ "msg_id": 406, "cmd": "ota_status", "ts": 1719000006 }
```
**响应 data**
```json
{
"target": "loop",
"state": "flashing",
"slot": "a",
"size": 46864,
"received": 46864,
"crc32": 305419896,
"version": "1.1.0",
"progress": { "sent": 42112, "total": 46864 },
"last_result": 0,
"last_error": 0
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted`**`idle``size>0` = 已刷写完成(镜像保留可重刷)** |
| `progress.sent` | uint32 | 刷写阶段已送 Loop 的字节数 |
| `last_result` | uint32 | 上次刷写结果:0=无/成功,非 0=错误码 |
| `last_error` | uint32 | 上次失败细分错误码 |
**设备状态机:**
```
ota_begin(新会话) ota_data×N ota_end(CRC✓)
IDLE ─────────────────▶ DOWNLOADING ────────────▶ READY
▲ │ ▲ │
│ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓)
│ / flash_failed │ └──────────────┐ ▼
└────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ IDLE(镜像保留, last_result=0)
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
```
**平台判定指引(V1.09):**
- **主依据**`ota_report` 主动上报(`stage=done` 成功 / `stage=failed` 失败,§5.5)——`ota_flash` 后平台应优先等该信号
- **兜底**`ota_status` 查询——`state=idle 且 size>0 且 last_result=0` = 刷写成功(镜像保留,可重刷);`state=flash_failed` = 失败;`state=ready` = 下载完成待刷(**不是**"刷写未启动"
- 刷写窗口极短(<1s),轮询可能捕捉不到 `flashing` 中间态,不得以此判失败
---
## 4.25 查询地感版本 `loop_version_query`
> Topic: `dld960/{sn}/srv`
> 用途:实时查询地感 Loop MCU 固件/硬件版本(V1.10,配合远程 OTA 做升级前后版本核对)。
**请求:**
```json
{ "msg_id": 407, "cmd": "loop_version_query", "ts": 1719000007 }
```
**设备行为(异步):**
1. 经 UART2 向 Loop MCU 发 `0x4A` 查询帧(`7F 00 01 4A ...`,见 `DLD960Loop_串口通信协议.md` §3.01
2. 收到 Loop 响应后解析(Soft/Hard 各三段)→ 更新本地缓存 → 立即回包
3. Loop 无响应/超时 → 回 `code=5``loop_ver` 保持缓存值(可为空)
**响应:**
```json
{
"msg_id": 407,
"cmd": "loop_version_query",
"ts": 1719000008,
"code": 0,
"msg": "success",
"data": {
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"version_str": "V1.2.3 (HW:1.0.0)"
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `loop_ver` | string | 地感固件版本,格式 `"主.次.次"`Soft_Main.Sub.SSub |
| `loop_hw_ver` | string | 地感硬件版本,格式 `"主.次.次"`Hard_Main.Sub.SSub |
| `version_str` | string | 人类可读串,`"V{soft} (HW:{hard})"`Loop 无响应时可能为空 |
> **时序与缓存**:设备上电后自动查询一次并缓存;`ota_report stage=done`Loop 复位跑新固件)后自动刷新缓存。平台升级流程建议:`ota_flash` 前 `loop_version_query` 记录旧版本 → 刷写完成后再次查询核对新版本是否生效。
---
# 5 设备主动上报
## 5.1 设备上电登陆信息 `initialize`
> Topic: `dld960/{sn}/dev`
> QoS: 0/1
```json
{
"msg_id": 1,
"cmd": "initialize",
"ts": 1719000000,
"data": {
"dev_serial":"A1B2C3D4E5F6",
"model": "DLD960",
"hard_ver": "1.0",
"soft_ver": "1.0",
"loop_ver": "1.2.3",
"loop_hw_ver": "1.0.0",
"extra_info": {
"code": "869756049404948",
"imei": "860012345678901",
"iccid": "89860012345678901234",
"csq": "21",
"location": "113.9237976,022.6400375",
}
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `data.dev_serial` | string | 设备序列码 |
| `data.model` | string | 产品型号 |
| `data.hard_ver` | string | 硬件版本(整机) |
| `data.soft_ver` | string | 固件版本(整机 DBN MCU |
| `data.loop_ver` | string | **地感 Loop MCU 固件版本**V1.10),格式 `"主.次.次"`;来自 0x4A 缓存,查询未完成/失败则为空字符串 |
| `data.loop_hw_ver` | string | **地感 Loop MCU 硬件版本**V1.10),格式 `"主.次.次"`;同上可为空 |
| `data.extra_info.code` | string | 可选,设备代码如IMSI/ICCID |
| `data.extra_info.imei` | string | 可选,4G 模块 IMEIV1.13;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,见 §6) |
| `data.extra_info.iccid` | string | 可选,流量卡 ICCIDV1.13;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,见 §6) |
| `data.extra_info.csq` | string | 可选,当前信号强度 |
| `data.extra_info.location` | string | 可选,经纬度(经度,纬度) |
> **V1.10 说明**`loop_ver`/`loop_hw_ver` 为**尽力携带**(上电后异步 0x4A 查询,initialize 发出时可能未就绪 → 回空);平台核对版本请用 `loop_version_query`(§4.25)。
## 5.2 线圈传感数据 `loop_data`
> Topic: `dld960/{sn}/dev`
> QoS: 0/1
> **上报节奏(三档)**:① 任一通道 `iscar` 翻转(进/出车)→ **立即上报**,不受间隔限制;② 任一通道 `|diff|` 超阈值 → 300ms 快速档;③ 平稳 → 按 `report_config.interval`(默认 60s)。
```json
{
"msg_id": 100,
"cmd": "loop_data",
"ts": 1719000100,
"data": {
"channels": [
{
"ch": 1,
"level": "high",
"iscar": false,
"loop_ok": true,
"freq": 105280,
"diff": 20,
"sens": 7,
"cndtn": 0,
"misc": {
"type": "time",
"value": 0
}
},
{
"ch": 2,
"level": "mid_high",
"iscar": true,
"loop_ok": true,
"freq": 98700,
"diff": 1500,
"sens": 7,
"cndtn": 2,
"misc": {
"type": "time",
"value": 350
}
},
{
"ch": 3,
"level": "mid_low",
"iscar": false,
"loop_ok": false,
"freq": 0,
"diff": 0,
"sens": 5,
"cndtn": 0,
"misc": {
"type": "cut_count",
"value": 3
}
},
{
"ch": 4,
"level": "low",
"iscar": false,
"loop_ok": true,
"freq": 62100,
"diff": 5,
"sens": 8,
"cndtn": 0,
"misc": {
"type": "flow_count",
"value": 128
}
}
]
}
}
```
| 通道字段 | 类型 | 说明 |
|----------|------|------|
| `ch` | uint8 | 通道号 1~4 |
| `level` | string | freq_level 线圈高低频档位 |
| `iscar` | bool | 是否有车 |
| `loop_ok` | bool | 线圈是否正常(false=断开) |
| `freq` | uint32 | 当前频率 (Hz) |
| `diff` | uint32 | 变化量 |
| `sens` | uint8 | sensitivity, 当前灵敏度等级 |
| `cndtn` | uint8 | condition, 环境状态评估值(越大干扰越大) |
| `misc.type` | string | `"time"`(时间量) / `"cut_count"`(断开次数) / `"flow_count"`(车流量) |
| `misc.value` | uint32 | 杂项数值(时间量单位 50ms) |
## 5.3 事件上报 `event_report`
> 上报 Topic: `dld960/{sn}/dev`
> 应答 Topic: `dld960/{sn}/srv`
> QoS: 1(建议)
> **⚠️ 本指令要求平台必须应答**(区别于 `loop_data` / `heartbeat` 的单向上报)
设备检测到事件时主动上报,非周期性。事件(进车/出车/线圈断开等)是**不可再生的关键数据**,MQTT QoS 仅保证 broker 收到,不代表平台业务层已处理入库,因此引入**应用层应答 + 设备重发**机制闭环确认。
> **注**`event_report` **不受 `report_config.enable` 门控**——设备上电即检测并上报事件。`report_config` 的 `enable`/`interval` 仅控制 `loop_data` 周期上报,与事件上报解耦。
**设备上报(dev topic):**
```json
{
"msg_id": 101,
"cmd": "event_report",
"ts": 1719000200,
"data": {
"events": [
{
"type": "car_enter",
"ch": 2,
"value": 0
},
{
"type": "car_leave",
"ch": 2,
"value": 350
},
{
"type": "loop_cut",
"ch": 3,
"value": 0
}
]
}
}
```
| 事件类型 `type` | 说明 | `value` 含义 |
|----------------|------|-------------|
| `car_enter` | 车辆进入 | 0 |
| `car_leave` | 车辆离开 | 通过时间 (×50ms) |
| `loop_cut` | 线圈断开 | 0 |
| `loop_restore` | 线圈恢复 | 断开持续时长 (×50ms) |
| `ota_error` | OTA 刷写失败告警(重试×3 仍失败,V1.08) | 0x1001=启动帧无响应 / 0x1002=地址帧错误 / 0x1003=数据块 ACK 超限 / 0x1004=全镜像校验失败 / 0x1005=安全窗口拒绝后强制失败 |
**平台应答(srv topic,收到后必须立即回复):**
```json
{
"msg_id": 101,
"cmd": "event_report",
"ts": 1719000201,
"code": 0,
"msg": "success"
}
```
| 字段 | 说明 |
|------|------|
| `msg_id` | **必须回显**设备上报的 `msg_id`,设备以此匹配确认 |
| `cmd` | 固定 `"event_report"` |
| `code` | 0 = 已接收并处理;非 0 = 处理失败(设备视同未确认,进入重发) |
**设备端确认与重发机制:**
1. 发出 `event_report` 后启动确认定时器,**5s** 内未收到 `msg_id` 匹配且 `code=0` 的应答 → 重发;
2. 重发使用**相同的 `msg_id` 与原始 `ts`**(首次事件发生时间,不随重发刷新),平台以此去重;
3. 最多重发 **3 次**(含首发共 4 次)。仍未确认 → 事件保留在待发队列,待 **MQTT 重连成功或下一次事件触发**时合并上报;
4. 等待确认期间新产生的事件,合并进下一包的 `events` 数组(使用新 `msg_id`)。单包长度受发布缓冲限制(≤500B),超出时拆包分批;
5. 待发队列建议深度 **≥16 条事件**,溢出时丢弃最旧事件(先保新鲜数据)。
**平台端要求:**
1. 收到 `event_report` **先落库、后应答**,应答即承诺数据已持久化;
2.`(dev_serial, msg_id)` 在近期窗口(建议 10 分钟)内去重——重复包为设备未收到应答所致,**直接应答 `code=0`,不重复入库**(设备重启后 `msg_id` 从头递增,去重必须限定时间窗口);
3. 应答报文应控制在最小集(不带 `data`),减轻设备下行解析负担。
**时序(正常 / 应答丢失重发):**
```
设备 平台
│── event_report(id=101) ──▶│ 落库
│◀─ ack(id=101, code=0) ────│ 确认, 事件出队
│── event_report(id=102) ──▶│ 落库
│ ✗ 应答丢失 │
│ [5s 超时] │
│── event_report(id=102) ──▶│ (dev_serial,102) 命中去重 → 不入库
│◀─ ack(id=102, code=0) ────│ 确认, 事件出队
```
## 5.4 设备心跳 `heartbeat`
> Topic: `dld960/{sn}/dev`
> 周期:默认 60 秒
```json
{
"msg_id": 200,
"cmd": "heartbeat",
"ts": 1719000060,
"data": {
"uptime": 3600,
"loop_status": [true, true, false, true],
"net_status": true,
"iot_status": true
}
}
```
| data 字段 | 类型 | 说明 |
|-----------|------|------|
| `uptime` | uint32 | 设备运行时长(秒) |
| `loop_status` | bool[4] | 各路线圈是否正常 |
| `net_status` | bool | 以太网连接状态 |
| `iot_status` | bool | MQTT 连接状态 |
## 5.5 OTA 进度/结果上报 `ota_report`
> Topic: `dld960/{sn}/dev` · QoS 1
> 用途:OTA 会话与刷写进度/结果主动推送(V1.08)。进度可丢,结果可经 `ota_status` 兜底查询(设备侧元数据持久化 `last_result`)。
**上报:**
```json
{
"msg_id": 501,
"cmd": "ota_report",
"ts": 1719000007,
"data": {
"target": "loop",
"stage": "flashing",
"progress": { "sent": 42112, "total": 46864 },
"code": 0,
"msg": ""
}
}
```
| data 字段 | 说明 |
|-----------|------|
| `target` | 目标:`loop`(当前支持) |
| `stage` | `begin`(会话开启)/ `downloading`(片落盘)/ `ready`(校验通过)/ `flashing`(刷写中)/ `done`(刷写成功,Loop 已重启)/ `failed`(刷写失败) |
| `progress` | `sent`/`total` 字节(刷写阶段) |
| `code` | stage 相关结果码 |
**上报节奏**`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 `msg_id`/`ts`),平台去重窗口建议 10 分钟(与 `event_report` 同策略)。
**平台判定(V1.09 明确)**`ota_report` 是刷写结果的**主依据**——收到 `stage=done` 判成功、`stage=failed` 判失败,无需再轮询 `ota_status`。设备 `ota_flash` 响应 `code=0` 仅表示已启动(异步),**不得以"轮询 `ota_status` 未见 `flashing`"或"状态持续为 `ready`"判"刷写未启动"**——本地刷写 <1s 完成,轮询大概率错过中间态;兜底判定见 §4.24。
**会话期间静默**V1.08):OTA 会话期间(`ota_begin` ~ 结束)设备暂停 `event_report` 发送(入队积压,结束后补发)与 offlog/快照落盘("升级开始"日志暂停前写入、"升级结果"恢复后补记),保证刷写窗口内 MQTT 保活与 IWDG 喂狗不受影响。
---
# 6 4G 通道适配(方案 CAir780 协议转换,V1.12 修订)
> 适用场景:vd960DBN 有线网络失效时,经 **Air8781P 整板(Air780EPM 4G 模组,LuatOS vd960Air 工程)** 兜底上报。
> **方案 C**Air780 解析 0x7F 帧并转换为**标准 JSON 命令**(与有线通道一致),平台无感。
> 修订说明:V1.11 曾定方案 B(原始帧 hex 透传 frame_report/frame_cmd),V1.12 改为方案 Cframe_* 降级为可选兜底。
## 6.1 通道架构
```
上行: vd960Loop --0x7F帧(UART2)--> vd960DBN --原样转发(UART1)--> Air780 --解析转JSON--> MQTT(loop_data/event_report/响应)
下行: 平台 --标准JSON命令--> Air780 --转换0x7F帧--> vd960DBN --0x7F帧(UART2)--> vd960Loop
```
- **vd960DBN**UART2↔UART1 双向转发(魔数分流:`0x7F` 帧 → 转发 UART2vd960Loop);`0x8F` 帧 → DBN 本地处理)。列入 vd960DBN 开发计划,**固件未实现**。⚠ 转发必须**不丢帧**(沿检测依赖完整 0xC0 帧流)。
- **Air780**0x7F 帧解析(Lua 状态机)+ **协议转换**0x7F 帧 ↔ 标准 JSON,§6.3+ MQTT(标准 JSON 命令面)
- **平台**:标准 JSON 解析,与有线通道一致(**零新增依赖**)
## 6.2 命令面(4G 通道 = 标准 JSON)
| 通道 | 上行(dev→srv | 下行(srv→dev |
|------|----------------|----------------|
| 有线(ETH MQTT | initialize / loop_data / event_report / heartbeat / ota_report(标准 JSON | 标准 JSON 命令全表(dev_serial_set / ssc_net_* / iot_net_* / loop_param_* / report_config / log_* / ota_* / loop_version_query |
| 4GAir780 转换) | **标准 JSON**initialize / loop_data / event_report / heartbeatAir780 从 0x7F 帧生成)+ 可选 frame_report | **标准 JSON**Air780 转换为 0x7F 帧下发)+ 可选 frame_cmd |
- 4G 通道上行 JSON 结构与有线通道**完全一致**(平台按同一解析逻辑处理)
- 4G 通道 `initialize``extra_info` 填真实值:`imei` / `iccid` / `csq`(V1.13;有线通道无 4G 模块时省略或空串)
- `frame_report` / `frame_cmd` 保留为**可选兜底**(§6.8):Air780 未实现转换的命令 / 未识别帧,平台可直接发/收原始帧
## 6.3 Air780 协议转换职责(0x7F 帧 ↔ 标准 JSON)
**上行(0x7F → JSON):**
| 0x7F 帧 | 转换目标 | 说明 |
|---------|---------|------|
| 0xC0 传感上报 | `loop_data` | 4 路通道字段映射(§5.2),携带 link(§6.7 |
| 0xC0 car_state 沿 | `event_report` | 进出车事件(§6.4 |
| 0xC0 loop_state 沿 | `event_report` | 线圈断开/恢复(loop_cut / loop_restore,§6.4 |
| 0x09~0x1F 配置响应 | 对应命令响应 | code / msg / data(按 §4 各命令响应结构) |
| 0x63 / 0x64 车检器参数响应 | `loop_param_set` / `loop_param_query` 响应 | 多路参数结构(§4.13 / §4.14 |
| 0x4A 版本响应 | `loop_version_query` 响应 / `initialize.loop_ver` | 地感版本(§4.25 |
**下行(JSON → 0x7F):**
| 平台 JSON 命令 | 转换 0x7F 帧 | 说明 |
|---------------|-------------|------|
| loop_param_set / loop_param_query | 0x63 / 0x64 | 车检器多路参数 |
| loop_version_query | 0x4A | 地感版本查询 |
| (其余需转发的命令) | 对应 0x7F 命令 | 按《DLD960Loop_串口通信协议》 |
## 6.4 事件上报(仅线圈事件,V1.12 明确)
- **4G 通道事件面 = 仅线圈事件**car_enter / car_leave / loop_cut / loop_restore,源自 0xC0 帧 car_state / loop_state 沿)
- **不含 DBN 内部网络事件**iot_connect / iot_ready / iot_reconn 等——Air780 无法感知 DBN 内部状态,**平台勿依赖 4G 通道获取网络事件**)
- `event_report` 语义与有线通道**完全一致**(V1.04 机制):
- 平台必答(回显 msg_id + code=0)→ 出队
- 5s 超时重发,同 msg_id / 原始 ts,最多 3 次;耗尽挂起
- 16 深环形队列,溢出丢最旧;多事件合并一条 publish
- 跨重连保持同 msg_id(平台按 (sn, msg_id) 去重)
- Air780 以 Lua 复刻 DBN `iot_evt_*` 逻辑(沿检测 + ACK 状态机 + 重发定时器),可行性已评估(2026-08-31):逻辑块全部可映射 Luatable 队列 / sys.timer / mqtt 回调 / json),无硬障碍
## 6.5 命令响应链路
- 平台 JSON 命令 → Air780 转 0x7F → vd960DBN → vd960Loop → 响应帧 → vd960DBN → Air780 → JSON 回包
- Air780 维护**单命令状态机**(暂存 msg_id + 超时回 code=5,与 DBN `g_lup_cmd` 同模式)
- 链路 4 跳:响应超时建议与 DBN 命令超时一致(当前 300ms~1s 量级,待板级确认)
## 6.6 4G 通道不支持的命令(网络配置类)
| cmd | 说明 |
|-----|------|
| `ssc_net_set` / `ssc_net_query` | SSC 有线网络配置(4G 不适用) |
| `iot_net_set` / `iot_net_query` | IoT 有线网络配置(4G 不适用) |
| `iot_topic_set` / `iot_topic_query` | Topic 配置(4G 主题由 Air780 配置,同步链路见 §6.9 |
> 经 4G 通道下发以上命令:设备回 `code=4 unsupported`。
>
> 补充(§6.9):这些配置**仅能经 BLE 写入 DBN**。DBN 作为权威源,经 UART1 把配置同步给 4G 通道;
> Air780 **不接受**平台经 4G 下发配置(否则会改掉自己正在使用的连接参数,形成第二权威源)。
## 6.7 link 对象(4G 特有字段)
| 字段 | 来源 | 说明 |
|------|------|------|
| `imei` | mobile.imei() | 4G 模块 IMEI,设备唯一标识 |
| `iccid` | mobile.iccid() | **流量卡卡号**,物联网卡管理识别用(卡商未写入 → 空串) |
| `imsi` | mobile.imsi() | IMSI(部分卡返回空) |
| `msisdn` | mobile.msisdn() | 手机号(物联网卡通常拿不到 → 空串) |
| `csq` | mobile.csq() | 信号强度 0-3131 最强,99/255 无信号) |
| `net` | 固定 "4G" | 网络制式(预留扩展) |
## 6.8 平台侧要求
1. **标准 JSON 解析**:与有线通道一致(**零新增依赖**);依 `link.net` 或报文形态识别 4G 通道
2. **设备唯一标识**`dev_serial` 与有线通道同一序列号(Topic 族一致,平台认同一台设备);`link.imei` / `link.iccid` 辅助 4G 设备/流量卡管理
3. **时钟校准**Air780 上线发 `initialize`(JSON + link)后,平台照常下发 `report_config` 校准 `ts`(§2.3
4. **事件面约束**:4G 通道仅报线圈事件,不报 DBN 内部网络事件(§6.4)
5. **可选兜底**:若启用 `frame_report` / `frame_cmd`(Air780 未识别帧 / 未实现转换命令),平台需按《DLD960Loop_串口通信协议》解析/组帧
## 6.9 4G 配置同步(BLE → DBN → Air780V1.14 新增)
### 6.9.1 背景与权威源
4G 通道的 MQTT 连接参数(服务器地址、端口、ClientID、账号、密码、发布/订阅主题)由**蓝牙小程序经 BLE 写入 DBN**。§6.6 已规定这些配置**不允许经 4G 通道下发**(回 `code=4`),因此 **BLE 是网络配置的唯一入口**。若 DBN 不同步给 Air780,Air780 只能使用自身固件内的默认值,**平台侧改配置无效**。
配置权威源 = **DBN 侧 flash**`IOT_NET_INFO` / `IOT_Topic` 结构,`cfig_flash.c` 持久化)。
| 角色 | 职责 |
|------|------|
| 蓝牙小程序 | **唯一配置入口**,经 BLE 写 DBN |
| DBN | **唯一权威源**:持久化 + 应答拉取 + 主动推送 |
| Air780 | 配置**消费者**:fskv 缓存仅用于加速启动;**唯一合法写入者 = 收到的 DBN 配置帧**(平台下发、产线预置均不得写入,否则产生第二权威源) |
### 6.9.2 帧格式
配置同步复用 **0x8F 本机侧私有帧**(与 BLE 侧 `MAGIC_BYTE_DBN_DEFAULT` 同值),沿用《DLD960Loop 串口通信协议》帧布局:
| 偏移 | 字段 | 说明 |
|------|------|------|
| 0 | `0x8F` | 本机侧私有帧魔数 |
| 1 | `Addr` | 设备地址 |
| 2 | `LEN` | `1 + DATA 长度`(最小 1 |
| 3 | `CMD` | 见 §6.9.3 |
| 4.. | `DATA` | 载荷,见 §6.9.4 |
| -2 | `XOR` | 自 `Addr` 起算(**不含**魔数) |
| -1 | `SUM` | 自 `Addr` 起算(**不含**魔数) |
帧总长 = `LEN + 5``XOR` / `SUM` 覆盖 = `Addr``2 + LEN` 字节。
**UART1 魔数语义(链路两端一致)**
| 魔数 | 处理 |
|------|------|
| `0x7F` | 业务帧:转发 UART2Loop |
| `0x8F` | **本机侧私有帧**:链路两端各自本地消费,**均不转发** |
| 其他 | 丢弃,并回找最近魔数重新同步(resync) |
**⚠ 帧长上限(两侧必须同步扩容)**
| 帧类型 | 帧长上限 | 说明 |
|--------|----------|------|
| `0x7F` Loop 业务帧 | **70 B**`LUP_MAX_PKG_LEN`,不变) | 与 Loop 协议隔离,维持原纪律 |
| `0x8F` 本机侧同步帧 | **260 B**`LEN` ≤ 255) | 覆盖最坏载荷(见 §6.9.4.3),**DBN 帧缓冲需由 70 B 扩至 260 BAir780 parser 缓冲同步 ≥ 260 B** |
> 注:`LEN` 为 1 字节,理论上限 255,故同步帧最大 260 B255 + 5)。B 端(Air780)解析器与 A 端(DBN)装配器**必须使用同一上限**,否则超长帧会被判 `LEN` 非法丢弃。
### 6.9.3 命令
复用 BLE 侧既有命令码(`dbn_ble_srv.h`),保证**同一命令在 BLE 通道与 UART1 同步通道语义一致**:
| CMD | 名称 | 方向 | 说明 |
|-----|------|------|------|
| `0x14` | `GET_IOT_NET` | 双向 | →:请求 net 配置(DATA 空);←:应答 net 配置(DATA 见 §6.9.4.1 |
| `0x16` | `GET_IOT_TOPIC` | 双向 | →:请求 topic 配置(DATA 空);←:应答 topic 配置(DATA 见 §6.9.4.2 |
**应答方向统一使用 `GET_*` 命令码**:载荷由 DBN 侧既有构造器 `set_response_iot_net()` / `set_response_iot_topic()` 生成,与 **BLE 读响应逐字节同源**(零新增序列化代码,避免出现第二套口径)。DBN 主动推送时同样使用 `GET_*` + 对应载荷(语义 = “当前权威值”)。
### 6.9.4 载荷
`0x00` 分隔字符串序列;字符串按 `strlen` **紧凑发送**(不补足结构体定长)。
#### 6.9.4.1 netCMD `0x14`
| 序号 | 字段 | 类型 | 最大长度 | 说明 |
|------|------|------|----------|------|
| 1 | `remote_addr` | 字符串 | 63 | 服务器域名或 IP |
| 2 | `mqtt_port` | ASCII 十进制 | 5 | 如 `1883` |
| 3 | `client_id` | 字符串 | 63 | 空 = 使用本机序列号 |
| 4 | `username` | 字符串 | 63 | |
| 5 | `password` | 字符串 | 31 | 末字段**无**结尾 `0x00` |
#### 6.9.4.2 topicCMD `0x16`
| 序号 | 字段 | 类型 | 最大长度 | 说明 |
|------|------|------|----------|------|
| 1 | `clientid_enable` | ASCII `'0'` / `'1'` | 1 | `1` = 启用自定义 ClientID |
| 2 | `topic_pub` | 字符串 | 63 | 上报主题 |
| 3 | `topic_sub` | 字符串 | 63 | 下发主题 |
#### 6.9.4.3 长度核算
| 载荷 | 典型长度 | 最坏长度 | 结论 |
|------|----------|----------|------|
| net | ≈ 59 B | 63+1+5+1+63+1+63+1+31 = **229 B** | 单帧可传(≤ 260 B 上限) |
| topic | ≈ 51 B | 1+1+63+1+63 = **129 B** | 单帧可传 |
**必须拆成两条独立记录**`net` / `topic`):既与 BLE 两个命令的粒度对齐,也使 Air780 侧 fskv 单值(**≤ 255 B**`luat_fskv_set` 限制)安全:net 229 B < 255 B ✓,topic 129 B ✓。
Air780 侧建议**直接存原始载荷字节串(不转 JSON)**,加载时按 `0x00` 拆分 —— 与 DBN 侧构造器逐字节同源。
### 6.9.5 交互流程
**① 拉取(主:兜底一切不一致)**
```
Air780 上电 / 链路建立:
① fskv.init() → 失败则降级为纯拉取模式(功能不受影响)
② 读 fskv 缓存 → 有则立即用缓存发起 MQTT 连接(不必等 DBN 就绪)
③ 链路建立后 → 发 0x8F CMD=0x14 / 0x16DATA 空)
④ DBN 应答 → 与缓存比对:相同则不动;不同则写 fskv + 断开重连重订阅
```
**② 推送(辅:免重启立即生效)**
```
BLE 写 SET_IOT_NET(0x13) / SET_IOT_TOPIC(0x15) / UPDATE_DEV_SERIAL(0x09) 成功
→ DBN 持久化后,若 UART1 链路可用,立即发 0x8F CMD=0x14 / 0x16 + 权威载荷
→ Air780 比对,变化则写 fskv + 重连重订阅
```
DBN **不维护“待同步”状态位**:推送时链路不可用则不重试、不记账,等 Air780 下次拉取即可(无状态设计,避免状态机与真实链路状态不一致)。
**③ 约束**
- Air780 收到配置后 **先落 fskv 再重连**(避免掉电后反复以旧参数重连)
- fskv **仅在值实际变化时写入**littlefs 擦写磨损,4 KB 擦写块)
- 重连流程:断开 → 以新参数连接 → 重订阅 `topic_sub` → 恢复上报
### 6.9.6 其他触发同步的 BLE 命令
| BLE 命令 | 值 | 是否必须同步 | 说明 |
|----------|-----|--------------|------|
| `CMD_DBN_UPDATE_DEV_SERIAL``0x09` | 设备序列号 | **必须** | Air780 的 Topic 族与上报 JSON 的 `dev_serial` 均由序列号派生。序列号变更 → Topic 族变更,Air780 需重算并重连 |
| `CMD_DBN_SET_SUB_CODE``0x22` | `iot_enable` 位 | **必须** | IoT 通道总使能;DBN 有线 WCHNET MQTT 与 Air780 4G 通道均受此位控制。不同步 → DBN 已停发而 Air780 仍在连接(白耗流量),平台侧表现为“在线无数据” |
| `CMD_DBN_RW_UART_BAUD``0x31` | UART1 波特率 | **禁止在线修改** | 该命令可改任意串口波特率,含 UART1(4G 链路)。在线改动而两侧未同步 = **链路永久失联,只能拆机重刷**。建议固件侧对 `uart_num == 1` 直接拒绝 |
### 6.9.7 明确不同步的配置
| 配置 | 原因 |
|------|------|
| `local_net_cfg`(有线 IP/网关/掩码 + 各端口)、`net_center_info``lssc_ip` / `tcp_port`) | 有线以太网通道专用,与 4G 模组无关 |
| `CMD_DBN_SET_CJQ_*`(车检器参数)、`CMD_DBN_LOOP_*` | 经 UART2 下发给 Loop,方向与 4G 无关 |
| `CMD_DBN_SET_SUB_CODE``iot_enable` 外的位 | 局部外设使能(网口 / 雷达 / 激光 / LoRa 等) |
| `CMD_DBN_OFFLOG_*` / `CMD_DBN_SNAP_*` | 本地 flash 读写;4G 侧仅作数据通道(§4 `log_*``stream=snapshot` 已覆盖) |
| `CMD_DBN_CHECK_PASS` / `CMD_DBN_MODIFY_PASS` | 蓝牙访问密码,纯本地 |
| `CMD_DBN_SET_FACTORY` / `CMD_DBN_RESET_DEV` | 本地动作;若涉及重启,链路断开重连由 Air780 自行处理 |
| 所有 `GET_*`(读操作) | 无同步语义 |
**不随载荷同步的字段**`IOT_NET_INFO.mode`(IP / DNS 标志)。实测该字段**无 BLE 写入路径**、恒为 `0``IOT_Addr_IP_Mode`),当前仅 DBN 有线 MQTT 路径读取。Air780 侧按主机串自行判别地址类型(LuatOS `socket.connect(host, port)` 对 IP 与域名均兼容),无需同步。
### 6.9.8 异常处理
| 场景 | 行为 |
|------|------|
| 0x8F 帧校验失败 | 丢弃 + 回找最近魔数 resync;**不回错**(配置链路静默重试成本低) |
| `LEN` 非法 / 帧超上限 | 丢弃并重新同步(`LEN < 1``LEN + 5 > 260` |
| Air780 请求时 DBN 尚无有效配置 | DBN 回**出厂默认值**(`cfig_flash.c` 初始化值),保证 Air780 有明确行为而非空值 |
| fskv 挂载失败(首刷 / 分区异常) | `fskv.init()` 返回 false → **降级为纯拉取模式**,不阻塞业务;缓存丢失不影响正确性 |
| 推送时 UART1 链路不可用 | 不重试、不记状态;等 Air780 下次拉取 |
| Air780 重连失败(服务器不可达 / 鉴权失败) | 按既有重连策略退避重试;MQTT 失败**不影响** UART1 链路与配置同步 |
| 载荷含 `0x00` 的字段(如密码本身含 `0x00`) | 不支持;字段均为可见 ASCII 串 |
> **已知缺口(待办)**DBN 自身经**有线 WCHNET 直连 MQTT** 的路径(`peripheral_main.c`**仅实现 IP 模式,域名模式分支为空**。故配置为域名时,有线通道 MQTT 不工作(4G 通道不受影响)。是否补齐由产品决定。
### 6.9.9 实施状态
| 侧 | 项目 | 状态 |
|----|------|------|
| DBN | UART1 通道 + 0x8F 本机侧私有帧接入本机指令处理器(不转发) | **已实现**2026-09-10 |
| DBN | UART1 帧缓冲 70 B → 260 B 扩容 | 待实现 |
| DBN | 0x8F 配置请求应答 + BLE 写成功后主动推送 | 待实现 |
| DBN | `CMD_DBN_RW_UART_BAUD` 拒绝 `uart_num == 1` | 待实现 |
| Air780 | `frame_parser.lua` 支持 0x8F(本地消费,不转发)| 待实现 |
| Air780 | parser 缓冲 ≥ 260 B | 待实现 |
| Air780 | fskv 缓存 + 启动即连 + 拉取校准 + 变化重连重订阅 | 待实现 |
| Air780 | 配置读取由 require 期 `local` 改为重连时重读 | 待实现 |
---
# 修订记录
| 版本 | 修订时间 | 修订说明 | 修订人 |
|------|----------|----------|--------|
| V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq |
| V1.01 | 2026-07-07 | Topic 压缩为双主题(`{sn}/srv` + `{sn}/dev`),消息类型由 `cmd` 字段区分 | wangfq |
| V1.02 | 2026-07-09 | 缩写相关字段 | wangfq |
| V1.03 | 2026-07-09 | 增加设备上电初始化指令 | wangfq |
| V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq |
| V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq |
| V1.06 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);§2.3 补充日志双时间戳语义(`ts_ms` 相对 + `unix_ts` 已同步,0=未同步,锚点回算规则) | wangfq |
| V1.07 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
| V1.14 | 2026-09-10 | 新增 **§6.9 4G 配置同步(BLE → DBN → Air780**:明确配置权威源为 DBN flashAir780 侧 fskv 仅作启动缓存,**唯一写入者 = DBN 配置帧**,避免第二权威源);复用 **0x8F 本机侧私有帧**(与 BLE 同魔数 `MAGIC_BYTE_DBN_DEFAULT`,链路两端各自本地消费、均不转发),命令复用 `GET_IOT_NET(0x14)` / `GET_IOT_TOPIC(0x16)`,载荷与 BLE 读响应**逐字节同源**(复用 `set_response_iot_net()` / `set_response_iot_topic()`,零新增序列化)、拆 net/topic 两条(最坏 229B / 129B,均 ≤ fskv 单值 255B);**0x8F 帧长上限独立定为 260B**(Loop 业务帧 70B 上限不变,两侧帧缓冲需同步扩容);同步触发点补 `UPDATE_DEV_SERIAL(0x09)` / `SET_SUB_CODE``iot_enable` 位 / **禁改 UART1 波特率**V1.11 的 **0x7D 帧配置同步方案作废**`iot_net_info.mode` 实测无 BLE 写入路径、恒为 IP 模式,故不随载荷同步 | wangfq |
| V1.13 | 2026-08-31 | `initialize``extra_info` 增加可选字段 **`imei`** / **`iccid`**4G 模块 IMEI / 流量卡 ICCID;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,§6.2 说明) | wangfq |
| V1.12 | 2026-08-31 | **4G 通道适配修订:方案 B(hex 透传)改为方案 C(Air780 协议转换)**——Air780 解析 0x7F 帧并转换为**标准 JSON 命令**loop_data / event_report / initialize / heartbeat,与有线通道一致),平台零改动;V1.11 的 frame_report / frame_cmd 降级为**可选兜底**4G 事件面 = **仅线圈事件**car_enter/car_leave/loop_cut/loop_restore,不含 DBN 内部网络事件);命令响应链路 Air780 单命令状态机(超时回 code=5);link 对象保留;Air780 以 Lua 复刻 iot_event_report 逻辑(沿检测 + ACK + 5s×3 重发 + 16 深队列 + 跨重连同 msg_id),可行性已评估(2026-08-31 | wangfq |
| V1.11 | 2026-08-31 | **4G 通道适配(方案 B:原始帧透传 + hex 封装)**:新增 `frame_report`dev→srv,§6.3/ `frame_cmd`srv→dev,§6.4)两命令(4G 通道专用);4G 通道**不使用**有线标准 JSON 业务命令(loop_data/event_report),不适用网络配置类命令(ssc_net_*/iot_net_*/iot_topic_*4G 通道下发回 code=4);上行附加 `link` 对象(IMEI/ICCID/IMSI/MSISDN/CSQ,§6.6);链路层数据面 0x7F 帧字节流透传(魔数分流在 vd960DBN 侧),0x7D 帧仅 DBN↔Air780 配置同步/握手;平台双通道区分解析 + 新增《DLD960Loop_串口通信协议》解析依赖(§6.7);实施主体:Air8781PAir780EPMvd960Air 工程,vd960DBN UART1 通道列入开发计划(固件未实现) | wangfq |
| V1.10 | 2026-08-20 | **网络上报携带地感版本**(配合远程 OTA 升级前后版本核对):`dev_info_query` 响应 + `initialize` 上报新增 `loop_ver`/`loop_hw_ver`(地感 Loop MCU 固件/硬件版本,格式 `"主.次.次"`,来自 0x4A 查询缓存,尽力携带可为空);新增命令 `loop_version_query`(§4.25,srv→dev 实时查询,设备经 UART2 0x4A 异步查询后回包,含 `loop_ver`/`loop_hw_ver`/`version_str`);版本语义:`soft_ver`=整机 DBN 固件(`主.次`),`loop_ver`=地感 Loop 固件(`主.次.次`),二者区分 | wangfq |
| V1.09 | 2026-08-20 | **OTA 刷写结果判定修复**(现场:刷写物理成功但平台误判"刷写未启动"):① 刷写成功后台侧状态回 `idle`(镜像保留:`size`/`crc32`/`version` 不变,`last_result=0`,可重刷),与"下载完成待刷 `ready`"严格区分;② `ota_report` 升级为刷写结果**主依据**`stage=done`/`failed` 设备必报,重发 3 次×5s),`ota_status` 仅兜底;③ 明确平台判定指引——`idle+size>0+last_result=0`=成功,不得以轮询未见 `flashing` 或状态持续 `ready` 判"刷写未启动"(本地刷写 <1s,轮询大概率错过中间态);④ `ota_begin` 兼容 `idle+size/crc32 一致` → 免下载直接可刷(重刷) | wangfq |
| V1.08 | 2026-08-20 | 增加 **Loop MCU 远程 OTA**ROADMAP P1.4 ①,先存后刷):命令 `ota_begin` / `ota_data` / `ota_end` / `ota_abort` / `ota_flash` / `ota_status`srv→dev+ `ota_report`dev→srv);单片 256B + 单片/全镜像 CRC32ISO-HDLC,§4.19.1);断点续传(`ota_begin` 返回 offset);Slot A/B 双槽回滚(镜像 ≤96KB);`ota_flash` 安全窗口检查 + 非阻塞 tick 驱动刷写(刷写窗口内 MQTT 保活/IWDG 不受影响);会话期间暂停 `event_report` 发送与脱机日志落盘;`event_report` 扩展 `type=ota_error` 失败告警;§2.2 补 OTA 细分错误码(err_code);老固件兼容(`ota_*` 回 code=4 | wangfq |