# DLD960 IoT 接口协议(MQTT + JSON) > 基于《DLD960 串口通信协议》V1.01,将设备管理、参数配置、数据上报映射到 MQTT 协议。 > 交互格式:JSON。 --- # 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` 设备订阅此主题,接收服务器下发的所有命令(配置设置、查询、控制等)。 服务器发布到此主题。 对应串口 CMD:0x09 ~ 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 | — | | `initialize` | 设备上电初始化登陆 | dev→srv | — | | `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 | | `event_report` | 事件上报(**平台须应答**,见 §5.3) | dev→srv | — | | `ota_report` | OTA 进度/结果主动上报 | dev→srv | — | | `heartbeat` | 设备心跳 | dev→srv | — | --- # 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", "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 | 软件版本,格式 `"主.次"` | | `model` | string | 产品型号,1~10 字符 | | `product_code` | string | 产品编码,6 位数字字符串 | | `sub_code.net` | bool | 网络功能是否启用 | | `sub_code.iot` | bool | IoT/MQTT 功能是否启用 | | `bus.bus1~4` | uint8 | 各总线探头数 | ## 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` | **响应 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**(逻辑清除 + 当前写扇区擦除,其余扇区由环形写覆盖时自动擦)。 --- ## 4.19 开启 OTA 会话 `ota_begin` > Topic: `dld960/{sn}/srv` > 依据《DLD960_MQTT_OTA协议.md》设计稿 V1.01(ROADMAP 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 = 96KB;Slot 数据区 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 true,xorout `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 原始字节小写 hex,512 字符 | **设备行为:** 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}` 告警(平台必答) **响应:** `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` | | `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 ``` --- # 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", "extra_info": { "code": "869756049404948", "csq": "21", "location": "113.9237976,022.6400375", } } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `data.dev_serial` | string | 设备序列码 | | `data.model` | string | 产品型号 | | `data.hard_ver` | string | 硬件版本 | | `data.soft_ver` | string | 固件版本 | | `data.extra_info.code` | string | 可选,设备代码如IMSI/ICCID | | `data.extra_info.csq` | string | 可选,当前信号强度 | | `data.extra_info.location` | string | 可选,经纬度(经度,纬度) | ## 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.08):OTA 会话期间(`ota_begin` ~ 结束)设备暂停 `event_report` 发送(入队积压,结束后补发)与 offlog/快照落盘("升级开始"日志暂停前写入、"升级结果"恢复后补记),保证刷写窗口内 MQTT 保活与 IWDG 喂狗不受影响。 --- # 修订记录 | 版本 | 修订时间 | 修订说明 | 修订人 | |------|----------|----------|--------| | 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≤1(4 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32(W25Q256 事件流 130944 超 16bit) | 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 + 单片/全镜像 CRC32(ISO-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 |