Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572077031c | ||
|
|
6e711e35ef | ||
|
|
181467b9e6 | ||
|
|
3be9e504da | ||
|
|
51298693da | ||
|
|
1badba3893 | ||
|
|
c305ca651f | ||
|
|
2484a03326 | ||
|
|
0b51d84a9b | ||
|
|
49736c341c | ||
|
|
6c1f276002 | ||
|
|
2ccf0804a8 | ||
|
|
9d6f5ea412 | ||
|
|
b20fc57954 | ||
|
|
d4ce0ba23a | ||
|
|
881a774c0c | ||
|
|
8e58d5fdd1 | ||
|
|
f1d9ac4e48 |
@@ -5,6 +5,66 @@
|
||||
|
||||
---
|
||||
|
||||
## vd960DBN V1.02.05 — 2026-08-21(DBN 通信板单侧更新)
|
||||
|
||||
> 本次为 **vd960DBN 单侧发布**(Loop 固件未变,可正常配套)。涵盖 2026-08-20 联网稳定性修复系列 + 2026-08-21 MQTT 协议 V1.10 实现。
|
||||
|
||||
**配套版本矩阵**
|
||||
|
||||
| 组件 | 版本 |
|
||||
|------|------|
|
||||
| vd960DBN 固件 (CH32V208, DLD960GA) | **1.02.05** |
|
||||
| vd960Loop 固件 (AT32F421) | 1.0(不变) |
|
||||
| DLD960Loop 串口协议(0x7F / 0x9F OTA) | V1.05(不变) |
|
||||
| DLD960 TCP JSON 协议(:5960) | V1.03(不变) |
|
||||
| DLD960 IoT MQTT 协议 | **V1.10** |
|
||||
| DLD960 BLE 协议 | V1.02(不变) |
|
||||
|
||||
### 🆕 MQTT 协议 V1.10:网络上报携带地感版本(配合远程 OTA 版本核对)
|
||||
- `dev_info_query` 响应 / `initialize` 上报 data 新增 **`loop_ver`**(地感固件 `"主.次.次"`)+ **`loop_hw_ver`**(地感硬件)——来自 Loop `0x4A` 查询缓存(上电自动查一次 + OTA done 后刷新),查询未完成/失败为空串
|
||||
- 新增 §4.25 **`loop_version_query`** 命令:平台实时查询 Loop 版本(0x4A 异步 → 回包 `loop_ver`/`loop_hw_ver`/`version_str`),超时回 `code=5`
|
||||
- 固件实现:`loop_uart_proto.c/h` 新增 `LoopVerCache` 版本缓存 + `lup_cache_from_info()`/`lup_refresh_version_cache()`;`iot_mqtt_srv.c` 新增 `iot_verq_poll()` 异步回包 + 后台刷新
|
||||
- **单测 44 断言全过**(版本解析/缓存/异步回包时序,`tests/test_iot_loop_ver.c`)
|
||||
|
||||
### 🔧 联网稳定性修复(2026-08-20 系列)
|
||||
- **SocketSend 0x11 死循环**:WCHNET `WCHNET_ERR_MEM(0x11)` 为瞬时发送缓冲不足(`WCHNET_NUM_TCP_SEG=2`)——改为 200ms 退避重试(≤10 次),不再 3 次强制断连;`SocketCreat` 失败置 `SocketId_TCP=0xFF` + 中止 Connect(不再 timeout 死循环)
|
||||
- **OTA 下载失败**:`ota_json_extract_hex()` 兼容 `json.dumps` 空格(`"data": "..."`),单片 CRC 全错根因闭环
|
||||
- **OTA 刷写状态语义**:刷写成功状态回 `idle`(镜像保留可重刷)+ 补 `ota_report done/failed` 主动上报(平台判定主依据)
|
||||
- **联网复位事故**:OTA 命令缓冲合并 union(RAM 90% 栈余量不足 → HardFault 复位闭环)
|
||||
|
||||
### 📋 已知约束
|
||||
- UART2 0x4A 版本查询与 `loop_data`/事件上报共用总线(低频命令,理论无冲突)——**待板上验证时序**
|
||||
- OTA done 后缓存刷新存在 Loop 复位时间窗(查询可能超时,可接受;平台可用 `loop_version_query` 主动核对)
|
||||
- BLE→Loop OTA 透传模式无自动退出(V1.02.03 遗留,升级后需断电重启)
|
||||
|
||||
### 验证
|
||||
- ✅ gcc 隔离单测:`test_iot_loop_ver.c` 44 例(V1.10)+ 既有 ota/offlog/snapshot/mqtt 单测全过
|
||||
- [ ] 待板级回归:MQTT 联网长稳、`loop_version_query` 实际回包、OTA 全流程
|
||||
|
||||
---
|
||||
|
||||
## vd960DBN V1.02.04 — 2026-08-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 固件未变,协议未变更,可正常配套)。
|
||||
|
||||
@@ -9,6 +9,7 @@ DLD960 IoT MQTT 协议定义
|
||||
import json
|
||||
import time
|
||||
import struct
|
||||
import zlib
|
||||
from typing import Optional, Any
|
||||
from dataclasses import dataclass, field, asdict
|
||||
|
||||
@@ -273,6 +274,94 @@ def data_log_clear(stream: str = STREAM_EVENT) -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# OTA 远程升级 (V1.08: Loop MCU 先存后刷, ROADMAP P1.4 ①)
|
||||
# 协议依据《DLD960_IoT_MQTT协议.md》§4.19~4.24 / §5.5
|
||||
# ============================================================
|
||||
|
||||
CMD_OTA_BEGIN = "ota_begin"
|
||||
CMD_OTA_DATA = "ota_data"
|
||||
CMD_OTA_END = "ota_end"
|
||||
CMD_OTA_ABORT = "ota_abort"
|
||||
CMD_OTA_FLASH = "ota_flash"
|
||||
CMD_OTA_STATUS = "ota_status"
|
||||
CMD_OTA_REPORT = "ota_report" # dev→srv 主动上报
|
||||
|
||||
OTA_TARGET_LOOP = "loop"
|
||||
OTA_CHUNK_SIZE = 256 # 单片 256B(协议常量, 不接受协商)
|
||||
OTA_MAX_SIZE = 96 * 1024 # 镜像上限 96KB (Slot 100KB - 4KB 边界余量)
|
||||
OTA_SLOT_A = "a"
|
||||
OTA_SLOT_B = "b"
|
||||
|
||||
OTA_STATE_DESC = {
|
||||
"idle": "空闲(镜像保留,已刷写完成则 idle+size>0)",
|
||||
"downloading": "下载中",
|
||||
"ready": "校验通过(可刷写)",
|
||||
"flashing": "刷写中",
|
||||
"flash_failed": "刷写失败",
|
||||
"aborted": "已中止",
|
||||
}
|
||||
|
||||
OTA_STAGE_DESC = {
|
||||
"begin": "会话开启",
|
||||
"downloading": "分片落盘",
|
||||
"ready": "校验通过",
|
||||
"flashing": "刷写中",
|
||||
"done": "刷写成功(Loop 已重启)",
|
||||
"failed": "刷写失败",
|
||||
}
|
||||
|
||||
OTA_ERROR_DESC = {
|
||||
0x1001: "启动帧无响应",
|
||||
0x1002: "地址帧错误",
|
||||
0x1003: "数据块 ACK 超限",
|
||||
0x1004: "全镜像校验失败",
|
||||
0x1005: "安全窗口拒绝后强制失败",
|
||||
}
|
||||
|
||||
|
||||
def ota_crc32(data: bytes) -> int:
|
||||
"""CRC-32/ISO-HDLC (协议 §4.19.1; zlib.crc32 即此算法, 设备查表法一致)"""
|
||||
return zlib.crc32(data) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def ota_split_bin(data: bytes, chunk_size: int = OTA_CHUNK_SIZE) -> list[tuple[int, int, str]]:
|
||||
"""bin → 分片列表 [(offset, crc32, hex_str), ...] (单片 256B, hex 512 字符)"""
|
||||
if len(data) > OTA_MAX_SIZE:
|
||||
raise ValueError(f"镜像 {len(data)}B 超上限 {OTA_MAX_SIZE}B (96KB)")
|
||||
return [(off, ota_crc32(data[off:off + chunk_size]), data[off:off + chunk_size].hex())
|
||||
for off in range(0, len(data), chunk_size)]
|
||||
|
||||
|
||||
def data_ota_begin(size: int, crc32: int, version: str = "",
|
||||
target: str = OTA_TARGET_LOOP, force: bool = False) -> dict:
|
||||
"""ota_begin 请求 data (开启会话 / 断点续传定位)"""
|
||||
return {"target": target, "size": size, "crc32": crc32,
|
||||
"version": version, "force": force}
|
||||
|
||||
|
||||
def data_ota_data(offset: int, crc32: int, data_hex: str,
|
||||
target: str = OTA_TARGET_LOOP) -> dict:
|
||||
"""ota_data 请求 data (分片下发, offset 256 对齐)"""
|
||||
return {"target": target, "offset": offset, "crc32": crc32, "data": data_hex}
|
||||
|
||||
|
||||
def data_ota_end(crc32: int, target: str = OTA_TARGET_LOOP) -> dict:
|
||||
"""ota_end 请求 data (结束下载, 全镜像 CRC32 复核)"""
|
||||
return {"target": target, "crc32": crc32}
|
||||
|
||||
|
||||
def data_ota_abort(target: str = OTA_TARGET_LOOP) -> dict:
|
||||
"""ota_abort 请求 data (中止会话, 释放暂存)"""
|
||||
return {"target": target}
|
||||
|
||||
|
||||
def data_ota_flash(slot: str = OTA_SLOT_A, force: bool = False,
|
||||
target: str = OTA_TARGET_LOOP) -> dict:
|
||||
"""ota_flash 请求 data (触发本地 ISP 刷写, 仅 ready 态)"""
|
||||
return {"target": target, "slot": slot, "force": force}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 解析设备上报
|
||||
# ============================================================
|
||||
|
||||
@@ -6,6 +6,31 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-20 — OTA 页签:Loop 远程 OTA 分片下载(协议 V1.08 工具先行)
|
||||
|
||||
### 背景
|
||||
|
||||
ROADMAP P1.4 ①:Loop MCU (AT32F421) 远程 OTA,先存后刷。协议设计稿 V1.01 已定(`docs/DLD960_MQTT_OTA协议.md`),并入 MQTT 协议 V1.08。按"工具先行,固件后到"惯例,先在 DBNMQTTool 实现 OTA 页签 + 协议单测,验证协议自洽后再动固件。
|
||||
|
||||
### 变更
|
||||
|
||||
- **protocol.py**:新增 OTA 模块——`CMD_OTA_*` 命令常量、`ota_crc32`(CRC-32/ISO-HDLC = zlib.crc32,标准向量 0xCBF43926)、`ota_split_bin`(256B/片 hex,96KB 上限)、`data_ota_begin/data/end/abort/flash` 构建器、状态/阶段/错误码中文描述
|
||||
- **main.py**:新增 **OTA 页签**——固件 bin 选择(≤96KB 校验 + 全镜像 CRC32 显示)、目标版本/force/slot 参数、①下载(begin→data×N→end,`OtaDownloadThread` QThread 后台 + 响应队列,断点续传/缺片重定位/CRC 错重发)②刷写(ota_flash 二次确认)③中止 ④查询状态;进度条 + 状态日志(ota_report/ota_status 实时显示)
|
||||
- **tests/test_ota_protocol.py**:22 断言——CRC32 标准向量、分片边界(1B/256B/257B/超限)、构建器字段、**OtaDeviceSim 设备侧状态机**(顺序/幂等/乱序拒绝/单片 CRC/全镜像复核/断点续传)+ 完整下载流(正常/缺片补发)
|
||||
|
||||
### 验证
|
||||
|
||||
- `venv/bin/python -m unittest tests.test_ota_protocol`:22 全过
|
||||
- offscreen MainWindow 冒烟:OTA tab 构建正常、控件齐全、512B bin → 2 片 CRC 正确
|
||||
- 协议规则自洽性验证:设备模拟器按文档规则(offset==received 顺序片 / offset<received 幂等 / offset>received 拒收 code=1)与工具分片流程对跑,断点续传与缺片重定位全通
|
||||
|
||||
### 已知约束
|
||||
|
||||
- 下载线程每片停等 ACK(~512 片 × RTT);量产平台可改窗口并发,协议层无需变更
|
||||
- 固件未实现 ota_* 前,工具下发会收 `code=4 unsupported`——能力探测见协议 §4.19
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-18 — log_query 响应改 hex 原始字节解析展示(协议 V1.07 修订)
|
||||
|
||||
### 背景
|
||||
|
||||
+327
-1
@@ -7,6 +7,8 @@ DBN MQTT Tool — DLD960 IoT MQTT 设备管理工具
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import queue
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -18,8 +20,9 @@ from PySide6.QtWidgets import (
|
||||
QTabWidget, QTextEdit, QGroupBox, QGridLayout, QCheckBox,
|
||||
QSplitter, QMessageBox, QHeaderView, QFrame,
|
||||
QComboBox, QSpinBox, QPlainTextEdit,
|
||||
QFileDialog, QProgressBar,
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer, Signal, Slot
|
||||
from PySide6.QtCore import Qt, QTimer, Signal, Slot, QThread
|
||||
from PySide6.QtGui import QFont, QColor, QTextCursor
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
@@ -39,8 +42,128 @@ from dbn_mqtt_tool.protocol import (
|
||||
data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
|
||||
build_request, next_msg_id,
|
||||
topic_down, topic_up,
|
||||
# V1.08 OTA
|
||||
CMD_OTA_BEGIN, CMD_OTA_DATA, CMD_OTA_END, CMD_OTA_ABORT,
|
||||
CMD_OTA_FLASH, CMD_OTA_STATUS, CMD_OTA_REPORT,
|
||||
OTA_TARGET_LOOP, OTA_CHUNK_SIZE, OTA_SLOT_A, OTA_SLOT_B,
|
||||
OTA_STATE_DESC, OTA_STAGE_DESC, OTA_ERROR_DESC,
|
||||
data_ota_begin, data_ota_data, data_ota_end, data_ota_abort, data_ota_flash,
|
||||
ota_crc32, ota_split_bin,
|
||||
)
|
||||
|
||||
# OTA 相关命令集合 (用于响应分发)
|
||||
OTA_CMD_SET = {CMD_OTA_BEGIN, CMD_OTA_DATA, CMD_OTA_END, CMD_OTA_ABORT,
|
||||
CMD_OTA_FLASH, CMD_OTA_STATUS}
|
||||
|
||||
|
||||
class OtaDownloadThread(QThread):
|
||||
"""OTA 分片下载线程 (协议 V1.08): ota_begin → ota_data×N → ota_end
|
||||
断点续传: ota_begin 响应 offset 为设备已接收字节数, 从此处续传
|
||||
重定位: ota_data code=1 时按 data.offset 调整 (缺片/乱序/CRC)
|
||||
"""
|
||||
progress = Signal(int, int) # sent, total
|
||||
log = Signal(str)
|
||||
finished_ok = Signal(str)
|
||||
finished_err = Signal(str)
|
||||
|
||||
def __init__(self, mqtt_client: MqttClient, dev_serial: str, bin_data: bytes,
|
||||
version: str, force: bool, resp_queue: "queue.Queue"):
|
||||
super().__init__()
|
||||
self._mqtt = mqtt_client
|
||||
self._sn = dev_serial
|
||||
self._bin = bin_data
|
||||
self._version = version
|
||||
self._force = force
|
||||
self._resp_queue = resp_queue
|
||||
self._stop = False
|
||||
|
||||
def stop(self):
|
||||
self._stop = True
|
||||
|
||||
def _send(self, cmd: str, data: dict) -> int:
|
||||
from dbn_mqtt_tool.protocol import get_topic_for_cmd
|
||||
msg = build_request(cmd, data)
|
||||
self._mqtt.publish(get_topic_for_cmd(cmd, self._sn), msg, qos=1)
|
||||
return msg["msg_id"]
|
||||
|
||||
def _wait_resp(self, mid: int, cmd: str, timeout: float = 5.0) -> dict:
|
||||
"""等待与 msg_id 匹配的响应 (resp_queue 由 _on_message 填充)"""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline and not self._stop:
|
||||
try:
|
||||
c, payload = self._resp_queue.get(timeout=0.2)
|
||||
except queue.Empty:
|
||||
continue
|
||||
if c == cmd and payload.get("msg_id") == mid:
|
||||
return payload
|
||||
raise TimeoutError(f"{cmd} 响应超时 (msg_id={mid})")
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
size = len(self._bin)
|
||||
total_crc = ota_crc32(self._bin)
|
||||
chunks = ota_split_bin(self._bin)
|
||||
self.log.emit(f"[OTA] 镜像 {size}B CRC32=0x{total_crc:08X} → {len(chunks)} 片 @{OTA_CHUNK_SIZE}B")
|
||||
|
||||
# 1. ota_begin (断点续传定位)
|
||||
mid = self._send(CMD_OTA_BEGIN, data_ota_begin(
|
||||
size, total_crc, version=self._version, force=self._force))
|
||||
resp = self._wait_resp(mid, CMD_OTA_BEGIN)
|
||||
if resp.get("code") != 0:
|
||||
raise RuntimeError(f"ota_begin 失败: code={resp.get('code')} {resp.get('msg')} "
|
||||
f"(err_code={resp.get('data', {}).get('err_code', '-')})")
|
||||
d = resp.get("data", {})
|
||||
offset = int(d.get("offset", 0))
|
||||
self.log.emit(f"[OTA] begin OK slot={d.get('slot', '?')} state={d.get('state', '?')} "
|
||||
f"offset={offset} (断点续传点)")
|
||||
|
||||
# 2. 逐片下发 (停等: 每片等响应)
|
||||
retry = 0
|
||||
while offset < size and not self._stop:
|
||||
idx = offset // OTA_CHUNK_SIZE
|
||||
if idx >= len(chunks):
|
||||
# 设备 received 溢出, 直接截断到 size
|
||||
break
|
||||
off, crc, hexs = chunks[idx]
|
||||
mid = self._send(CMD_OTA_DATA, data_ota_data(off, crc, hexs))
|
||||
resp = self._wait_resp(mid, CMD_OTA_DATA)
|
||||
code = resp.get("code")
|
||||
if code == 0:
|
||||
offset = int(resp.get("data", {}).get("received", offset + OTA_CHUNK_SIZE))
|
||||
retry = 0
|
||||
self.progress.emit(min(offset, size), size)
|
||||
elif code == 1:
|
||||
# 缺片/乱序/CRC 失败 → 按设备指示重定位 (data.offset=received)
|
||||
new_off = int(resp.get("data", {}).get("offset", offset))
|
||||
if new_off == offset:
|
||||
retry += 1
|
||||
if retry > 3:
|
||||
raise RuntimeError("分片连续失败 (>3 次), 建议 ota_abort 后重来")
|
||||
else:
|
||||
retry = 0
|
||||
offset = new_off
|
||||
else:
|
||||
raise RuntimeError(f"ota_data@{off} 失败: code={code} {resp.get('msg')} "
|
||||
f"(err_code={resp.get('data', {}).get('err_code', '-')})")
|
||||
self.log.emit(f"[OTA] data@{off} → received={offset}")
|
||||
|
||||
if self._stop:
|
||||
self.log.emit("[OTA] 已停止")
|
||||
return
|
||||
self.progress.emit(size, size)
|
||||
|
||||
# 3. ota_end (全镜像 CRC32 复核)
|
||||
mid = self._send(CMD_OTA_END, data_ota_end(total_crc))
|
||||
resp = self._wait_resp(mid, CMD_OTA_END, timeout=10)
|
||||
if resp.get("code") != 0:
|
||||
raise RuntimeError(f"ota_end 失败: code={resp.get('code')} {resp.get('msg')} "
|
||||
f"crc_ok={resp.get('data', {}).get('crc_ok', '-')}")
|
||||
self.log.emit("[OTA] end OK crc_ok=true → state=ready, 可触发 ota_flash")
|
||||
self.finished_ok.emit("下载+校验完成, 可触发刷写 (ota_flash)")
|
||||
except Exception as e:
|
||||
self.log.emit(f"[OTA] ✗ {e}")
|
||||
self.finished_err.emit(str(e))
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
_mqtt_status = Signal(bool, str)
|
||||
_mqtt_msg = Signal(str, str, object)
|
||||
@@ -91,6 +214,7 @@ class MainWindow(QMainWindow):
|
||||
self._notebook.addTab(self._build_config_tab(), "参数配置")
|
||||
self._notebook.addTab(self._build_data_tab(), "实时数据")
|
||||
self._notebook.addTab(self._build_log_tab(), "日志")
|
||||
self._notebook.addTab(self._build_ota_tab(), "OTA")
|
||||
self._notebook.addTab(self._build_simulate_tab(), "模拟上报")
|
||||
self._notebook.addTab(self._build_proto_topic_tab(), "协议Topic")
|
||||
self._notebook.addTab(self._build_custom_topic_tab(), "自定义Topic")
|
||||
@@ -429,6 +553,192 @@ class MainWindow(QMainWindow):
|
||||
layout.addLayout(layout2)
|
||||
return w
|
||||
|
||||
def _build_ota_tab(self) -> QWidget:
|
||||
"""OTA 升级页签 (协议 V1.08: Loop 远程 OTA, 先存后刷)"""
|
||||
w = QWidget()
|
||||
layout = QVBoxLayout(w)
|
||||
|
||||
# -- 固件选择 --
|
||||
row1 = QHBoxLayout()
|
||||
row1.addWidget(QLabel("固件 bin:"))
|
||||
self._ota_file = QLineEdit()
|
||||
self._ota_file.setReadOnly(True)
|
||||
self._ota_file.setPlaceholderText("选择 Loop 固件 bin (≤96KB, 0x08003400 起 APP 映像)")
|
||||
row1.addWidget(self._ota_file, 1)
|
||||
btn_file = QPushButton("选择…")
|
||||
btn_file.clicked.connect(self._ota_choose_file)
|
||||
row1.addWidget(btn_file)
|
||||
layout.addLayout(row1)
|
||||
|
||||
# -- 参数 --
|
||||
row2 = QHBoxLayout()
|
||||
row2.addWidget(QLabel("目标版本:"))
|
||||
self._ota_version = QLineEdit("1.1.0")
|
||||
self._ota_version.setMaximumWidth(110)
|
||||
row2.addWidget(self._ota_version)
|
||||
self._ota_force = QCheckBox("force")
|
||||
self._ota_force.setToolTip("强制: 覆盖现有镜像 / 跳过安全检查 (高风险)")
|
||||
row2.addWidget(self._ota_force)
|
||||
row2.addWidget(QLabel("slot:"))
|
||||
self._ota_slot = QComboBox()
|
||||
self._ota_slot.addItems([OTA_SLOT_A, OTA_SLOT_B])
|
||||
self._ota_slot.setMaximumWidth(60)
|
||||
row2.addWidget(self._ota_slot)
|
||||
self._ota_info = QLabel("未选择固件")
|
||||
row2.addWidget(self._ota_info, 1)
|
||||
layout.addLayout(row2)
|
||||
|
||||
# -- 操作按钮 --
|
||||
row3 = QHBoxLayout()
|
||||
b_begin = QPushButton("① 下载 (begin/data/end)")
|
||||
b_begin.setToolTip("开启会话→分片下发→全镜像校验; 已下载过则断点续传")
|
||||
b_begin.clicked.connect(self._ota_start_download)
|
||||
row3.addWidget(b_begin)
|
||||
b_flash = QPushButton("② 刷写 (ota_flash)")
|
||||
b_flash.setToolTip("仅 state=ready 可触发; 安全检查 (有车拒绝) 后异步执行")
|
||||
b_flash.clicked.connect(self._ota_send_flash)
|
||||
row3.addWidget(b_flash)
|
||||
b_abort = QPushButton("中止 (ota_abort)")
|
||||
b_abort.clicked.connect(self._ota_send_abort)
|
||||
row3.addWidget(b_abort)
|
||||
b_status = QPushButton("查询状态 (ota_status)")
|
||||
b_status.clicked.connect(self._ota_send_status)
|
||||
row3.addWidget(b_status)
|
||||
b_clear = QPushButton("清空日志")
|
||||
b_clear.clicked.connect(lambda: self._ota_log_text.clear())
|
||||
row3.addWidget(b_clear)
|
||||
layout.addLayout(row3)
|
||||
|
||||
# -- 进度 --
|
||||
self._ota_progress = QProgressBar()
|
||||
self._ota_progress.setRange(0, 100)
|
||||
self._ota_progress.setValue(0)
|
||||
layout.addWidget(self._ota_progress)
|
||||
|
||||
# -- 状态日志 --
|
||||
self._ota_log_text = QPlainTextEdit()
|
||||
self._ota_log_text.setReadOnly(True)
|
||||
self._ota_log_text.setFont(QFont("Consolas", 9))
|
||||
self._ota_log_text.setMaximumBlockCount(2000)
|
||||
layout.addWidget(self._ota_log_text, 1)
|
||||
|
||||
# -- 响应队列 (下载线程 ← _on_message) --
|
||||
self._ota_resp_queue: "queue.Queue" = queue.Queue()
|
||||
self._ota_thread: Optional[OtaDownloadThread] = None
|
||||
return w
|
||||
|
||||
# ================================================================
|
||||
# OTA 升级 (V1.08)
|
||||
# ================================================================
|
||||
|
||||
def _ota_choose_file(self):
|
||||
path, _ = QFileDialog.getOpenFileName(self, "选择固件 bin", "",
|
||||
"固件文件 (*.bin);;所有文件 (*)")
|
||||
if path:
|
||||
self._ota_file.setText(path)
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
if len(data) > 96 * 1024:
|
||||
QMessageBox.warning(self, "固件过大", f"{len(data)}B 超上限 96KB (Slot 100KB-4KB)")
|
||||
crc = ota_crc32(data)
|
||||
self._ota_info.setText(f"{len(data)}B · CRC32=0x{crc:08X}")
|
||||
except OSError as e:
|
||||
QMessageBox.critical(self, "读取失败", str(e))
|
||||
|
||||
def _ota_load_bin(self) -> Optional[bytes]:
|
||||
path = self._ota_file.text()
|
||||
if not path:
|
||||
QMessageBox.warning(self, "提示", "请先选择固件 bin")
|
||||
return None
|
||||
try:
|
||||
data = open(path, "rb").read()
|
||||
except OSError as e:
|
||||
QMessageBox.critical(self, "读取失败", str(e))
|
||||
return None
|
||||
if len(data) > 96 * 1024:
|
||||
QMessageBox.warning(self, "固件过大", f"{len(data)}B 超上限 96KB")
|
||||
return None
|
||||
return data
|
||||
|
||||
def _ota_log(self, text: str):
|
||||
self._ota_log_text.appendPlainText(f"[{datetime.now().strftime('%H:%M:%S')}] {text}")
|
||||
|
||||
def _ota_start_download(self):
|
||||
sn = self._require_dev()
|
||||
if not sn:
|
||||
return
|
||||
if self._ota_thread and self._ota_thread.isRunning():
|
||||
QMessageBox.warning(self, "提示", "OTA 下载已在运行, 先中止")
|
||||
return
|
||||
data = self._ota_load_bin()
|
||||
if data is None:
|
||||
return
|
||||
self._ota_log(f"开始 OTA 下载: sn={sn} size={len(data)}B")
|
||||
self._ota_progress.setValue(0)
|
||||
self._ota_thread = OtaDownloadThread(
|
||||
self._mqtt, sn, data,
|
||||
version=self._ota_version.text().strip() or "0.0.0",
|
||||
force=self._ota_force.isChecked(),
|
||||
resp_queue=self._ota_resp_queue,
|
||||
)
|
||||
self._ota_thread.progress.connect(self._ota_on_progress)
|
||||
self._ota_thread.log.connect(self._ota_log)
|
||||
self._ota_thread.finished_ok.connect(
|
||||
lambda m: (self._ota_log(f"✓ {m}"),
|
||||
QMessageBox.information(self, "OTA", m)))
|
||||
self._ota_thread.finished_err.connect(
|
||||
lambda m: QMessageBox.critical(self, "OTA 失败", m))
|
||||
self._ota_thread.start()
|
||||
|
||||
def _ota_on_progress(self, sent: int, total: int):
|
||||
pct = int(sent * 100 / total) if total else 0
|
||||
self._ota_progress.setValue(pct)
|
||||
self._ota_progress.setFormat(f"{sent}/{total} B ({pct}%)")
|
||||
|
||||
def _ota_send_flash(self):
|
||||
sn = self._require_dev()
|
||||
if not sn:
|
||||
return
|
||||
confirm = QMessageBox.question(
|
||||
self, "确认刷写",
|
||||
"触发 ota_flash 将把暂存镜像刷入 Loop MCU (约 6~8s 窗口, 期间检测中断)。\n"
|
||||
"请确认现场无车压线圈。继续?",
|
||||
QMessageBox.Yes | QMessageBox.No)
|
||||
if confirm != QMessageBox.Yes:
|
||||
return
|
||||
self._send_cmd(CMD_OTA_FLASH, data_ota_flash(
|
||||
slot=self._ota_slot.currentText(), force=self._ota_force.isChecked()))
|
||||
|
||||
def _ota_send_abort(self):
|
||||
if self._ota_thread and self._ota_thread.isRunning():
|
||||
self._ota_thread.stop()
|
||||
self._send_cmd(CMD_OTA_ABORT, data_ota_abort())
|
||||
|
||||
def _ota_send_status(self):
|
||||
self._send_cmd(CMD_OTA_STATUS)
|
||||
|
||||
def _ota_show_status(self, data: dict):
|
||||
"""显示 ota_status / ota_report 状态"""
|
||||
line = []
|
||||
state = data.get("state", "")
|
||||
stage = data.get("stage", "")
|
||||
prog = data.get("progress", {})
|
||||
if state:
|
||||
line.append(f"state={state}({OTA_STATE_DESC.get(state, '?')})")
|
||||
if stage:
|
||||
line.append(f"stage={stage}({OTA_STAGE_DESC.get(stage, '?')})")
|
||||
if prog:
|
||||
line.append(f"progress={prog.get('sent', 0)}/{prog.get('total', 0)}B")
|
||||
if data.get("received") is not None:
|
||||
line.append(f"received={data['received']}B")
|
||||
if data.get("last_result"):
|
||||
line.append(f"last_result={data['last_result']}")
|
||||
if data.get("last_error"):
|
||||
err = OTA_ERROR_DESC.get(data["last_error"], data["last_error"])
|
||||
line.append(f"last_error={err}")
|
||||
self._ota_log("状态: " + (" | ".join(line) if line else json.dumps(data, ensure_ascii=False)))
|
||||
|
||||
# ================================================================
|
||||
# 模拟设备上报
|
||||
# ================================================================
|
||||
@@ -571,6 +881,8 @@ class MainWindow(QMainWindow):
|
||||
"model": "DLD960",
|
||||
"hard_ver": "1.0",
|
||||
"soft_ver": "1.0",
|
||||
"loop_ver": "1.2.3",
|
||||
"loop_hw_ver": "1.0.0",
|
||||
"extra_info": {
|
||||
"code": "869756049404948",
|
||||
"csq": "21",
|
||||
@@ -946,7 +1258,15 @@ class MainWindow(QMainWindow):
|
||||
self._apply_log_query_data(data)
|
||||
elif cmd == CMD_LOG_CLEAR:
|
||||
self._log(f"脱机日志已清除 (审计留痕, 需重新统计确认)")
|
||||
elif cmd in OTA_CMD_SET:
|
||||
# OTA 响应 → 喂下载线程响应队列 + 状态显示
|
||||
self._ota_resp_queue.put((cmd, payload))
|
||||
self._ota_show_status(payload.get("data", {}))
|
||||
else:
|
||||
if cmd in OTA_CMD_SET:
|
||||
self._ota_resp_queue.put((cmd, payload))
|
||||
self._ota_log(f"{cmd} 失败 code={code} {pmsg} "
|
||||
f"(err_code={payload.get('data', {}).get('err_code', '-')})")
|
||||
self._show_json({"error": f"code={code} {pmsg} ({ERROR_MSGS.get(code, '?')})"})
|
||||
|
||||
elif cmd == CMD_LOOP_DATA:
|
||||
@@ -982,6 +1302,12 @@ class MainWindow(QMainWindow):
|
||||
self._log_recv(topic, payload, f"initialize sn={dev_sn} model={model}")
|
||||
self._show_json({"online": dev_sn, "model": model, "extra": extra})
|
||||
|
||||
elif cmd == CMD_OTA_REPORT:
|
||||
# V1.08: OTA 进度/结果主动上报 (无 code 字段)
|
||||
data = payload.get("data", {})
|
||||
self._ota_show_status(data)
|
||||
self._log_recv(topic, payload, f"ota_report stage={data.get('stage', '?')}")
|
||||
|
||||
# 旧协议 / 无 cmd 字段 的消息(如 Initialize)
|
||||
elif "Method" in payload:
|
||||
self._log_recv(topic, payload, f"Method={payload.get('Method', '?')}")
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
DLD960 IoT MQTT OTA 协议单测 (V1.08)
|
||||
|
||||
验证对象: dbn_mqtt_tool/protocol.py 的 OTA 模块
|
||||
- ota_crc32: CRC-32/ISO-HDLC 标准向量
|
||||
- ota_split_bin: 分片边界 (256B 协议常量, 96KB 上限)
|
||||
- data_ota_*: 命令构建器字段
|
||||
- OtaDeviceSim: 设备侧接收状态机模拟 (协议 §4.19~4.21 规则)
|
||||
- 完整下载流: 正常/断点续传/缺片重定位/CRC 错重发/重复片幂等
|
||||
|
||||
运行: venv/bin/python -m unittest tests.test_ota_protocol -v
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from dbn_mqtt_tool.protocol import (
|
||||
ota_crc32, ota_split_bin, OTA_CHUNK_SIZE, OTA_MAX_SIZE,
|
||||
data_ota_begin, data_ota_data, data_ota_end, data_ota_abort, data_ota_flash,
|
||||
OTA_TARGET_LOOP, OTA_SLOT_A,
|
||||
)
|
||||
|
||||
|
||||
class OtaDeviceSim:
|
||||
"""模拟固件 OTA 接收状态机 (协议 §4.19 ota_begin / §4.20 ota_data / §4.21 ota_end)
|
||||
|
||||
规则 (与协议文档一致):
|
||||
- 顺序片 (offset == received): 单片 CRC32 校验 → 写暂存 → received += 256
|
||||
- 重复片 (offset < received): 幂等回 code=0, 不重写
|
||||
- 缺片 (offset > received): code=1, data.offset=received (要求续传)
|
||||
- 单片 CRC 失败: code=1 (平台重发本片)
|
||||
- ota_end: received!=size → code=1; 全镜像 CRC 复核 → ready
|
||||
"""
|
||||
|
||||
def __init__(self, bin_data: bytes):
|
||||
self.bin = bin_data
|
||||
self.size = len(bin_data)
|
||||
self.state = "idle"
|
||||
self.received = 0
|
||||
self.storage = bytearray(self.size)
|
||||
|
||||
def _base(self, offset: int) -> dict:
|
||||
return {"target": OTA_TARGET_LOOP, "slot": OTA_SLOT_A,
|
||||
"offset": offset, "received": self.received,
|
||||
"size": self.size, "crc32": ota_crc32(self.bin),
|
||||
"state": self.state}
|
||||
|
||||
def ota_begin(self, req: dict) -> dict:
|
||||
# 镜像一致且已 ready → 直接返回可刷写
|
||||
if self.state == "ready" and req["size"] == self.size \
|
||||
and req["crc32"] == ota_crc32(self.bin):
|
||||
return {"code": 0, "data": self._base(self.size)}
|
||||
# downloading 中断 → 返回已接收偏移 (断点续传)
|
||||
if self.state == "downloading":
|
||||
return {"code": 0, "data": self._base(self.received)}
|
||||
self.state = "downloading"
|
||||
self.received = 0
|
||||
self.storage = bytearray(self.size)
|
||||
return {"code": 0, "data": self._base(0)}
|
||||
|
||||
def ota_data(self, req: dict) -> dict:
|
||||
offset, crc, hexs = req["offset"], req["crc32"], req["data"]
|
||||
data = bytes.fromhex(hexs)
|
||||
if offset == self.received:
|
||||
if ota_crc32(data) != crc:
|
||||
return {"code": 1, "err_code": 1, "msg": "crc mismatch",
|
||||
"data": self._base(self.received)}
|
||||
self.storage[offset:offset + len(data)] = data
|
||||
self.received = min(offset + len(data), self.size)
|
||||
return {"code": 0, "data": self._base(offset)}
|
||||
if offset < self.received:
|
||||
return {"code": 0, "data": self._base(offset)} # 幂等
|
||||
return {"code": 1, "err_code": 1, "msg": "gap/out-of-order",
|
||||
"data": self._base(self.received)}
|
||||
|
||||
def ota_end(self, req: dict) -> dict:
|
||||
if self.received != self.size:
|
||||
return {"code": 1, "data": self._base(self.received)}
|
||||
if ota_crc32(bytes(self.storage)) != req["crc32"]:
|
||||
return {"code": 5, "err_code": 5, "data": {"crc_ok": False}}
|
||||
self.state = "ready"
|
||||
return {"code": 0, "data": {"crc_ok": True}}
|
||||
|
||||
|
||||
class TestOtaCrc32(unittest.TestCase):
|
||||
def test_standard_vector(self):
|
||||
"""CRC-32/ISO-HDLC 标准校验向量: crc32('123456789') = 0xCBF43926"""
|
||||
self.assertEqual(ota_crc32(b"123456789"), 0xCBF43926)
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(ota_crc32(b""), 0x00000000)
|
||||
|
||||
def test_zlib_alias(self):
|
||||
import zlib
|
||||
for data in (b"", b"a", os.urandom(300)):
|
||||
self.assertEqual(ota_crc32(data), zlib.crc32(data) & 0xFFFFFFFF)
|
||||
|
||||
|
||||
class TestOtaSplitBin(unittest.TestCase):
|
||||
def test_small_single_chunk(self):
|
||||
data = b"\x00\x01\x02\x03"
|
||||
chunks = ota_split_bin(data)
|
||||
self.assertEqual(len(chunks), 1)
|
||||
off, crc, hexs = chunks[0]
|
||||
self.assertEqual(off, 0)
|
||||
self.assertEqual(hexs, data.hex())
|
||||
self.assertEqual(len(hexs), 8) # 4B → 8 hex
|
||||
self.assertEqual(crc, ota_crc32(data))
|
||||
|
||||
def test_exact_chunk(self):
|
||||
data = bytes(range(256))
|
||||
chunks = ota_split_bin(data)
|
||||
self.assertEqual(len(chunks), 1)
|
||||
self.assertEqual(len(chunks[0][2]), 512) # 256B → 512 hex
|
||||
|
||||
def test_two_chunks(self):
|
||||
data = bytes(range(256)) + b"\x00" # 257B → 2 片
|
||||
chunks = ota_split_bin(data)
|
||||
self.assertEqual(len(chunks), 2)
|
||||
self.assertEqual(chunks[0][0], 0)
|
||||
self.assertEqual(chunks[1][0], 256)
|
||||
self.assertEqual(len(chunks[1][2]), 2) # 末片 1B
|
||||
|
||||
def test_offsets_256_aligned(self):
|
||||
data = os.urandom(5000)
|
||||
for off, _, _ in ota_split_bin(data):
|
||||
self.assertEqual(off % OTA_CHUNK_SIZE, 0)
|
||||
|
||||
def test_roundtrip_reconstruct(self):
|
||||
data = os.urandom(4097)
|
||||
merged = b"".join(bytes.fromhex(hexs) for _, _, hexs in ota_split_bin(data))
|
||||
self.assertEqual(merged, data)
|
||||
|
||||
def test_over_max_size(self):
|
||||
with self.assertRaises(ValueError):
|
||||
ota_split_bin(os.urandom(OTA_MAX_SIZE + 1))
|
||||
|
||||
|
||||
class TestOtaBuilders(unittest.TestCase):
|
||||
def test_begin(self):
|
||||
d = data_ota_begin(46864, 0x12345678, version="1.1.0")
|
||||
self.assertEqual(d, {"target": "loop", "size": 46864, "crc32": 0x12345678,
|
||||
"version": "1.1.0", "force": False})
|
||||
|
||||
def test_begin_force(self):
|
||||
d = data_ota_begin(100, 1, force=True)
|
||||
self.assertTrue(d["force"])
|
||||
|
||||
def test_data(self):
|
||||
d = data_ota_data(0, 0x9E3779B9, "abcd")
|
||||
self.assertEqual(d, {"target": "loop", "offset": 0,
|
||||
"crc32": 0x9E3779B9, "data": "abcd"})
|
||||
|
||||
def test_end_abort_flash(self):
|
||||
self.assertEqual(data_ota_end(0x11223344),
|
||||
{"target": "loop", "crc32": 0x11223344})
|
||||
self.assertEqual(data_ota_abort(), {"target": "loop"})
|
||||
self.assertEqual(data_ota_flash(slot="b", force=True),
|
||||
{"target": "loop", "slot": "b", "force": True})
|
||||
|
||||
|
||||
class TestDeviceSim(unittest.TestCase):
|
||||
"""设备侧规则: 顺序/幂等/乱序/CRC/全镜像复核"""
|
||||
|
||||
def setUp(self):
|
||||
self.bin_data = bytes(range(256)) * 20 # 5120B = 20 片
|
||||
self.dev = OtaDeviceSim(self.bin_data)
|
||||
|
||||
def test_normal_flow(self):
|
||||
dev = self.dev
|
||||
chunks = ota_split_bin(self.bin_data)
|
||||
resp = dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
self.assertEqual(resp["code"], 0)
|
||||
self.assertEqual(resp["data"]["offset"], 0)
|
||||
for off, crc, hexs in chunks:
|
||||
resp = dev.ota_data(data_ota_data(off, crc, hexs))
|
||||
self.assertEqual(resp["code"], 0, f"片@{off} 应成功")
|
||||
self.assertEqual(dev.received, len(self.bin_data))
|
||||
resp = dev.ota_end(data_ota_end(ota_crc32(self.bin_data)))
|
||||
self.assertEqual(resp["code"], 0)
|
||||
self.assertTrue(resp["data"]["crc_ok"])
|
||||
self.assertEqual(dev.state, "ready")
|
||||
self.assertEqual(bytes(dev.storage), self.bin_data)
|
||||
|
||||
def test_resume_after_begin(self):
|
||||
"""断点续传: downloading 中断后 begin 返回已接收偏移"""
|
||||
dev = self.dev
|
||||
chunks = ota_split_bin(self.bin_data)
|
||||
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
for off, crc, hexs in chunks[:7]: # 只收 7 片
|
||||
dev.ota_data(data_ota_data(off, crc, hexs))
|
||||
self.assertEqual(dev.received, 7 * OTA_CHUNK_SIZE)
|
||||
# 重新 begin → 续传点
|
||||
resp = dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
self.assertEqual(resp["data"]["offset"], 7 * OTA_CHUNK_SIZE)
|
||||
# 从续传点继续
|
||||
for off, crc, hexs in chunks[7:]:
|
||||
dev.ota_data(data_ota_data(off, crc, hexs))
|
||||
self.assertEqual(dev.received, len(self.bin_data))
|
||||
resp = dev.ota_end(data_ota_end(ota_crc32(self.bin_data)))
|
||||
self.assertTrue(resp["data"]["crc_ok"])
|
||||
|
||||
def test_dup_chunk_idempotent(self):
|
||||
"""重复片幂等: 已收片重发 → code=0, received 不变"""
|
||||
dev = self.dev
|
||||
chunks = ota_split_bin(self.bin_data)
|
||||
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
off0, crc0, hexs0 = chunks[0]
|
||||
dev.ota_data(data_ota_data(off0, crc0, hexs0))
|
||||
r1 = dev.ota_data(data_ota_data(off0, crc0, hexs0)) # 重复
|
||||
self.assertEqual(r1["code"], 0)
|
||||
self.assertEqual(r1["data"]["received"], OTA_CHUNK_SIZE)
|
||||
|
||||
def test_gap_rejected(self):
|
||||
"""缺片: offset > received → code=1, data.offset=received"""
|
||||
dev = self.dev
|
||||
chunks = ota_split_bin(self.bin_data)
|
||||
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
off2, crc2, hexs2 = chunks[2] # 跳过 0,1
|
||||
resp = dev.ota_data(data_ota_data(off2, crc2, hexs2))
|
||||
self.assertEqual(resp["code"], 1)
|
||||
self.assertEqual(resp["data"]["offset"], 0) # 要求从 0 续传
|
||||
|
||||
def test_chunk_crc_error(self):
|
||||
"""单片 CRC 错 → code=1, 不落盘"""
|
||||
dev = self.dev
|
||||
chunks = ota_split_bin(self.bin_data)
|
||||
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
off0, _, hexs0 = chunks[0]
|
||||
resp = dev.ota_data(data_ota_data(off0, 0xDEADBEEF, hexs0)) # 错 crc
|
||||
self.assertEqual(resp["code"], 1)
|
||||
self.assertEqual(dev.received, 0) # 未落盘
|
||||
# 重发正确片 → 成功
|
||||
resp = dev.ota_data(data_ota_data(off0, ota_crc32(self.bin_data[:256]), hexs0))
|
||||
self.assertEqual(resp["code"], 0)
|
||||
|
||||
def test_end_incomplete(self):
|
||||
"""ota_end 时 received != size → code=1"""
|
||||
dev = self.dev
|
||||
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
resp = dev.ota_end(data_ota_end(ota_crc32(self.bin_data)))
|
||||
self.assertEqual(resp["code"], 1)
|
||||
self.assertEqual(resp["data"]["offset"], 0)
|
||||
|
||||
def test_end_crc_fail(self):
|
||||
"""全镜像 CRC 复核失败 → code=5 crc_ok=false"""
|
||||
dev = self.dev
|
||||
chunks = ota_split_bin(self.bin_data)
|
||||
dev.ota_begin(data_ota_begin(len(self.bin_data), ota_crc32(self.bin_data)))
|
||||
for off, crc, hexs in chunks:
|
||||
dev.ota_data(data_ota_data(off, crc, hexs))
|
||||
resp = dev.ota_end(data_ota_end(0x00000000)) # 错的全镜像 crc
|
||||
self.assertEqual(resp["code"], 5)
|
||||
self.assertFalse(resp["data"]["crc_ok"])
|
||||
|
||||
|
||||
class TestDownloadFlow(unittest.TestCase):
|
||||
"""工具侧下载流程 (等价 OtaDownloadThread 核心逻辑, 无 Qt 依赖)"""
|
||||
|
||||
def _run_flow(self, bin_data: bytes, simulate_gap: bool = False):
|
||||
dev = OtaDeviceSim(bin_data)
|
||||
chunks = ota_split_bin(bin_data)
|
||||
total_crc = ota_crc32(bin_data)
|
||||
|
||||
# ota_begin
|
||||
resp = dev.ota_begin(data_ota_begin(len(bin_data), total_crc))
|
||||
assert resp["code"] == 0
|
||||
offset = resp["data"]["offset"]
|
||||
|
||||
sent_offsets = []
|
||||
gap_done = False
|
||||
while offset < len(bin_data):
|
||||
idx = offset // OTA_CHUNK_SIZE
|
||||
off, crc, hexs = chunks[idx]
|
||||
if simulate_gap and not gap_done and off == 256:
|
||||
# 模拟一次丢包: 跳过该片直接发下一片 → 设备回 gap → 重定位
|
||||
gap_done = True
|
||||
resp = dev.ota_data(data_ota_data(chunks[idx + 1][0],
|
||||
chunks[idx + 1][1],
|
||||
chunks[idx + 1][2]))
|
||||
self.assertEqual(resp["code"], 1)
|
||||
offset = resp["data"]["offset"] # 重定位
|
||||
continue
|
||||
resp = dev.ota_data(data_ota_data(off, crc, hexs))
|
||||
if resp["code"] == 0:
|
||||
offset = resp["data"]["received"]
|
||||
elif resp["code"] == 1:
|
||||
offset = resp["data"]["offset"] # 重定位
|
||||
else:
|
||||
self.fail(f"意外 code={resp['code']}")
|
||||
sent_offsets.append(off)
|
||||
|
||||
# ota_end
|
||||
resp = dev.ota_end(data_ota_end(total_crc))
|
||||
self.assertEqual(resp["code"], 0)
|
||||
self.assertTrue(resp["data"]["crc_ok"])
|
||||
self.assertEqual(dev.state, "ready")
|
||||
self.assertEqual(bytes(dev.storage), bin_data)
|
||||
return sent_offsets
|
||||
|
||||
def test_flow_normal(self):
|
||||
bin_data = os.urandom(3000)
|
||||
self._run_flow(bin_data)
|
||||
|
||||
def test_flow_with_gap_retransmit(self):
|
||||
"""缺片重传: 丢 1 片后设备要求重定位, 工具补发, 最终全量一致"""
|
||||
bin_data = os.urandom(3000)
|
||||
sent = self._run_flow(bin_data, simulate_gap=True)
|
||||
# 所有片最终都被发送过 (含补发)
|
||||
self.assertIn(256, sent)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -1,6 +1,6 @@
|
||||
# vd_960 — DLD960 四通道车辆检测器(双 MCU 架构)
|
||||
|
||||
> 当前发布:**vd960DBN V1.02.03**(2026-08-19,DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md)
|
||||
> 当前发布:**vd960DBN V1.02.05**(2026-08-21,DBN 单侧更新)· 完整版本见 [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## 产品概述
|
||||
|
||||
@@ -41,7 +41,7 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
|
||||
| 子项目 | 目录 | MCU | 固件版本 | 功能 |
|
||||
|--------|------|-----|---------|------|
|
||||
| vd960Loop | `vd960Loop/` | AT32F421 | 1.0 | 线圈驱动、频率采样、车辆检测算法、0x7F 串口上报 |
|
||||
| vd960DBN | `vd960DBN/` | CH32V208 | 1.02.03 | 蓝牙管理、TCP/IP(WCHNET)、MQTT、串口桥接、4G 扩展(RFU) |
|
||||
| vd960DBN | `vd960DBN/` | CH32V208 | 1.02.05 | 蓝牙管理、TCP/IP(WCHNET)、MQTT、串口桥接、4G 扩展(RFU) |
|
||||
|
||||
> ⚠ 发布约束:自协议 V1.05 起(variation 2B无符号 → 3B有符号),**Loop 固件与 DBN 固件必须同版本配套发布**,禁止混刷。
|
||||
|
||||
@@ -49,8 +49,8 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
|
||||
|
||||
| 文档 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.02 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 |
|
||||
| [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.02 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 |
|
||||
| [DLD960_产品手册.md](docs/DLD960_产品手册.md) | V1.03 | 面向安装调试与使用:接口、指示灯、线圈施工、配置、故障排查 |
|
||||
| [DLD960_技术规格书.md](docs/DLD960_技术规格书.md) | V1.03 | 完整技术参数:检测规格、灵敏度、算法机制、接口、协议矩阵 |
|
||||
|
||||
## 协议文档(docs/)
|
||||
|
||||
@@ -59,7 +59,7 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支
|
||||
| [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_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.10 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步、脱机日志(事件/快照流)、Loop 远程 OTA(ota_* 命令,先存后刷) |
|
||||
| [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 分配、继电器、指示灯、拨码 |
|
||||
|
||||
|
||||
+368
-4
@@ -96,6 +96,19 @@ dld960/{dev_serial}/{direction}
|
||||
| 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 时间:
|
||||
@@ -140,9 +153,17 @@ dld960/{dev_serial}/{direction}
|
||||
| `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 |
|
||||
| `initialize` | 设备上电初始化登陆 | dev→srv | — |
|
||||
| `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 |
|
||||
| `event_report` | 事件上报(**平台须应答**,见 §5.3) | dev→srv | — |
|
||||
| `ota_report` | OTA 进度/结果主动上报 | dev→srv | — |
|
||||
| `heartbeat` | 设备心跳 | dev→srv | — |
|
||||
|
||||
---
|
||||
@@ -205,6 +226,8 @@ dld960/{dev_serial}/{direction}
|
||||
"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": {
|
||||
@@ -224,14 +247,18 @@ dld960/{dev_serial}/{direction}
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `dev_serial` | string | 12 位十六进制序列码 |
|
||||
| `hard_ver` | string | 硬件版本,格式 `"主.次"` |
|
||||
| `soft_ver` | string | 软件版本,格式 `"主.次"` |
|
||||
| `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`
|
||||
@@ -798,6 +825,298 @@ dld960/{dev_serial}/{direction}
|
||||
|
||||
---
|
||||
|
||||
## 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}` 告警(平台必答)
|
||||
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`
|
||||
@@ -814,6 +1133,8 @@ dld960/{dev_serial}/{direction}
|
||||
"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",
|
||||
"csq": "21",
|
||||
@@ -827,12 +1148,16 @@ dld960/{dev_serial}/{direction}
|
||||
|------|------|------|
|
||||
| `data.dev_serial` | string | 设备序列码 |
|
||||
| `data.model` | string | 产品型号 |
|
||||
| `data.hard_ver` | string | 硬件版本 |
|
||||
| `data.soft_ver` | 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.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`
|
||||
|
||||
@@ -967,6 +1292,7 @@ dld960/{dev_serial}/{direction}
|
||||
| `car_leave` | 车辆离开 | 通过时间 (×50ms) |
|
||||
| `loop_cut` | 线圈断开 | 0 |
|
||||
| `loop_restore` | 线圈恢复 | 断开持续时长 (×50ms) |
|
||||
| `ota_error` | OTA 刷写失败告警(重试×3 仍失败,V1.08) | 0x1001=启动帧无响应 / 0x1002=地址帧错误 / 0x1003=数据块 ACK 超限 / 0x1004=全镜像校验失败 / 0x1005=安全窗口拒绝后强制失败 |
|
||||
|
||||
**平台应答(srv topic,收到后必须立即回复):**
|
||||
|
||||
@@ -1040,6 +1366,41 @@ dld960/{dev_serial}/{direction}
|
||||
| `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 喂狗不受影响。
|
||||
|
||||
---
|
||||
|
||||
# 修订记录
|
||||
@@ -1054,4 +1415,7 @@ dld960/{dev_serial}/{direction}
|
||||
| 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.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 + 单片/全镜像 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 |
|
||||
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
# DLD960 MQTT 远程 OTA 协议(Loop MCU 先存后刷)
|
||||
|
||||
> 文档版本 V1.01(设计稿·按修改意见修订)· 2026-08-20 · 适用范围:并入《DLD960 IoT 接口协议(MQTT + JSON)》V1.08 后生效
|
||||
> 依据:`docs/ROADMAP.md` P1.4 ① —— Loop MCU (AT32F421) 远程 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` 为准)。
|
||||
|
||||
**刷写结果(V1.09 明确):**
|
||||
- 成功(末块 ACK)→ 元数据 `state=idle`(**镜像保留**:`size`/`crc32`/`version`/`slot` 不变,`last_result=0`,`flash_cnt+1`)→ 上报 `ota_report{stage:done}`(重发 3 次×5s)→ 恢复 event_report/offlog 落盘
|
||||
- 失败 → `state=flash_failed` + `ota_report{stage:failed}` + `event_report{type:ota_error}`
|
||||
- **状态语义**:刷写完成后回 `idle`("本轮刷写已结束"),与"下载完成待刷 `ready`"严格区分——平台**不得把 `state=ready` 判为"刷写未启动"**:本地刷写 <1s,轮询大概率错过 `flashing` 中间态;平台判定以 `ota_report done/failed` 为主依据,`ota_status` 兜底(`idle+size>0+last_result=0`=成功)
|
||||
|
||||
## 5.6 查询状态 `ota_status`
|
||||
|
||||
> Topic: `dld960/{sn}/srv`
|
||||
|
||||
**请求:**
|
||||
|
||||
```json
|
||||
{ "msg_id": 406, "cmd": "ota_status", "ts": 1719000006 }
|
||||
```
|
||||
|
||||
**响应 data:**
|
||||
|
||||
```json
|
||||
{
|
||||
"target": "loop",
|
||||
"state": "flashing",
|
||||
"slot": "a",
|
||||
"size": 46864,
|
||||
"received": 46864,
|
||||
"crc32": 305419896,
|
||||
"version": "1.1.0",
|
||||
"progress": { "sent": 42112, "total": 46864 },
|
||||
"last_result": 0,
|
||||
"last_error": 0
|
||||
}
|
||||
```
|
||||
|
||||
| data 字段 | 类型 | 说明 |
|
||||
|-----------|------|------|
|
||||
| `state` | string | `idle` / `downloading` / `ready` / `flashing` / `flash_failed` / `aborted` |
|
||||
| `progress.sent` | uint32 | 刷写阶段已送 Loop 的字节数 |
|
||||
| `last_result` | uint32 | 上次刷写结果:0=无/成功,非 0=错误码 |
|
||||
| `last_error` | uint32 | 上次失败细分错误码 |
|
||||
|
||||
**状态机(设备侧):**
|
||||
|
||||
```
|
||||
ota_begin(新会话) ota_data×N ota_end(CRC✓)
|
||||
IDLE ─────────────────▶ DOWNLOADING ────────────▶ READY
|
||||
▲ │ ▲ │
|
||||
│ ota_abort │ │ ota_end(CRC✗) │ ota_flash(安全检查✓)
|
||||
│ / flash_failed │ └──────────────┐ ▼
|
||||
└────────────────────────┴─────────────────┴───── FLASHING ──成功──▶ IDLE(镜像保留, last_result=0)
|
||||
│
|
||||
└──失败×3──▶ FLASH_FAILED ──ota_abort/ota_begin──▶ IDLE
|
||||
```
|
||||
|
||||
**平台判定(V1.09)**:刷写结果以 `ota_report`(`stage=done`/`failed`)为主依据;`ota_status` 兜底——`state=idle 且 size>0 且 last_result=0` = 成功(镜像保留可重刷);`state=ready` = 待刷(**不是**"刷写未启动");轮询可能错过 `flashing` 中间态,不得以此判失败。
|
||||
|
||||
---
|
||||
|
||||
# 6 主动上报 `ota_report`
|
||||
|
||||
> Topic: `dld960/{sn}/dev` · QoS 1
|
||||
> 用途:刷写进度与结果主动推送(进度可丢,结果可经 `ota_status` 兜底查询;设备侧元数据持久化 last_result)
|
||||
|
||||
**上报:**
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 501,
|
||||
"cmd": "ota_report",
|
||||
"ts": 1719000007,
|
||||
"data": {
|
||||
"target": "loop",
|
||||
"stage": "flashing",
|
||||
"progress": { "sent": 42112, "total": 46864 },
|
||||
"code": 0,
|
||||
"msg": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| data 字段 | 说明 |
|
||||
|-----------|------|
|
||||
| `stage` | `begin`(会话开启)/ `downloading`(片落盘)/ `ready`(校验通过)/ `flashing`(刷写中)/ `done`(刷写成功,Loop 已重启)/ `failed`(刷写失败) |
|
||||
| `progress` | `sent`/`total` 字节(刷写阶段) |
|
||||
| `code` | stage 相关结果码 |
|
||||
|
||||
**上报节奏**:`begin`/`ready`/`done`/`failed` 各 1 次;`flashing` 阶段按块进度节流(建议每 64 块或每 8KB 一次,避免刷写期间消息风暴)。`done`/`failed` 设备侧重发 3 次(间隔 5s,同 msg_id/ts),平台去重窗口建议 10 分钟(与 event_report 同策略)。
|
||||
|
||||
**平台判定(V1.09 明确)**:`ota_report` 是刷写结果**主依据**——`stage=done` 判成功、`stage=failed` 判失败,无需轮询 `ota_status`。`ota_flash` 响应 `code=0` 仅表示已启动(异步),**不得以"轮询未见 `flashing`"或"状态持续 `ready`"判"刷写未启动"**(本地刷写 <1s,轮询大概率错过中间态);兜底判定见 §5.6。
|
||||
|
||||
## 6.1 失败告警(扩展 event_report)
|
||||
|
||||
刷写整体失败(重试 ×3 仍失败)时,设备经 `event_report` 上报(**平台必答**,复用 §5.3 ACK + 重发机制):
|
||||
|
||||
```json
|
||||
{
|
||||
"msg_id": 502,
|
||||
"cmd": "event_report",
|
||||
"ts": 1719000008,
|
||||
"data": {
|
||||
"events": [
|
||||
{ "type": "ota_error", "ch": 0, "value": 1003 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| type | value | 说明 |
|
||||
|------|-------|------|
|
||||
| `ota_error` | 0x1000 起 | `0x1001`=启动帧无响应 / `0x1002`=地址帧错误 / `0x1003`=数据块 ACK 超限 / `0x1004`=全镜像校验失败 / `0x1005`=安全窗口拒绝后强制失败 |
|
||||
|
||||
---
|
||||
|
||||
# 7 安全设计(会车产品底线)
|
||||
|
||||
| # | 措施 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | **升级窗口检查** | `ota_flash` 前检查 4 通道无车;有车 → `code=3`,平台提示延迟。`force=true` 可跳过(高风险,需平台权限控制) |
|
||||
| 2 | **维持 Loop 现状** | 刷写期间 Loop 行为与现网 BLE OTA 升级一致(复位进 bootloader → ISP 刷写 → 复位跑新 APP),继电器 GPIO 状态按当前硬件实测状态接受,不额外干预(BLE 通道已验证可用) |
|
||||
| 3 | **双重 CRC** | 单片 CRC32(传输层抓错)+ 全镜像 CRC32(落盘完整性);刷写前读回复核 |
|
||||
| 4 | **失败可重入** | 下载失败 → 断点续传;刷写失败 → 重试 ×3 → 保留镜像可重刷;AT32 ISP bootloader 兜底,不会真砖 |
|
||||
| 5 | **版本回滚** | Slot A/B 双槽;平台可 `ota_begin` 指定槽刷旧版(`force=true` 覆盖) |
|
||||
| 6 | **审计留痕** | 升级开始/结果写 offlog 事件日志(含目标版本、slot、结果码);平台侧 version 字段归档 |
|
||||
| 7 | **能力探测** | 老固件 `ota_*` 回 `code=4`;平台按 `ota_status`/`dev_info_query.soft_ver` 判断,不盲目下发 |
|
||||
| 8 | **幂等与防呆** | 重复片幂等 ACK;offset 乱序拒绝并要求续传;同版本默认拒绝重刷(force 绕过) |
|
||||
| 9 | **会话期间静默** | OTA 会话期间(begin ~ 结束)暂停 MQTT `event_report` 发送(队列积压,结束后补发)与 offlog/快照落盘("升级开始"日志在暂停前写入、"升级结果"在恢复后补记);避免刷写窗口与上报/日志抢 SPI 总线与主循环 |
|
||||
|
||||
---
|
||||
|
||||
# 8 错误码约定
|
||||
|
||||
顶层 `code` 沿用通用语义(0 成功 / 1 参数 / 2 密码 / 3 忙 / 4 不支持 / 5 内部 / 6 超长),OTA 细分错误经 `data.err_code` 表达:
|
||||
|
||||
| err_code | 场景 | 顶层 code |
|
||||
|----------|------|-----------|
|
||||
| 1 | 单片 CRC 失败 / 乱序缺片(data.offset 指示续传点) | 1 |
|
||||
| 2 | 会话状态不允许(未 begin / 非 downloading 收 ota_data) | 3 |
|
||||
| 3 | 安全窗口拒绝(有车压线圈) | 3 |
|
||||
| 4 | 版本冲突(同版本且非 force) | 1 |
|
||||
| 5 | 全镜像 CRC 不匹配(data.crc_ok=false) | 5 |
|
||||
| 6 | 暂存区写失败(SPI 异常/满) | 5 |
|
||||
|
||||
---
|
||||
|
||||
# 9 平台侧实现要点(edc_server / DBNMQTTool)
|
||||
|
||||
1. **hex↔bin**:`data` hex 解码(512 hex = 256B);bin 由 Intel HEX 转换(固定 0x08003400 偏移),转换在平台完成
|
||||
2. **CRC32**:`zlib.crc32(bin)` 全镜像;单片 `zlib.crc32(chunk)` 与设备逐字节一致(ISO-HDLC 即 Python 内置)
|
||||
3. **分片循环**:`for offset in range(0, size, 256)` 顺序下发,收到 `code=1 + data.offset` 时从该处续传;单片失败重发 ≤3 次后 abort
|
||||
4. **断点续传**:会话中断后重新 `ota_begin`,读 `data.offset` 续传
|
||||
5. **结果确认**:刷写启动后轮询 `ota_status`(或订阅 `ota_report`);`stage=done` 后经 dev_info_query 核对 Loop 版本(需 Loop 侧支持版本上报,见 §10 待办)
|
||||
6. **告警闭环**:收到 `event_report{type:ota_error}` 先落库后应答
|
||||
7. DBNMQTTool 增加 OTA 页签(工具先行惯例:先模拟分片下发,固件后到)
|
||||
|
||||
---
|
||||
|
||||
# 10 待板上验证项(实现前必须闭环)
|
||||
|
||||
| # | 项 | 影响 |
|
||||
|---|-----|------|
|
||||
| 1 | DBN UART2 TX 缓冲容量(能否容纳 254B A7 帧;现 BLE 透传块 ≤94B) | 刷写块大小 |
|
||||
| 2 | W25Qxx 写 256B 页 + 4KB 扇区擦除在 MQTT 接收回调内的耗时(阻塞窗口 vs MQTT 保活) | 单片落盘是否需移主循环 |
|
||||
| 3 | DBN RAM 预算:镜像块缓冲(248B)+ CRC 查表(1KB)是否挤占现有栈余量(历史 .bss 事故) | 内存方案 |
|
||||
| 4 | Loop APP 当前固件实际 bin 大小(验证 100KB 槽余量) | 槽位容量 |
|
||||
| 5 | 刷写期间 MQTT 保活验证(设计已保证非阻塞 tick 驱动,PINGREQ/心跳/IWDG 喂狗不受阻塞;板级抓包确认 6~8s 窗口内无 PINGREQ 超时断连、IWDG 不复位) | 刷写中断安全 |
|
||||
|
||||
---
|
||||
|
||||
# 11 与现有协议的关系
|
||||
|
||||
| 协议文档 | 变更 |
|
||||
|----------|------|
|
||||
| `DLD960_IoT_MQTT协议.md` | 本设计并入后升 **V1.08**:§3 命令表 + §4.19~4.24(ota_* 详情)+ §5.5(ota_report)+ §5.3 事件类型表补 `ota_error` + §8 错误码补 err_code 约定 + 修订记录;**V1.10 补充版本核对**:`dev_info_query`/`initialize` 带 `loop_ver`/`loop_hw_ver`,新增 `loop_version_query`(§4.25)供平台升级前后核对地感版本 |
|
||||
| `DLD960_TCP_JSON协议.md` | 本版**不扩展**(ROADMAP 先走 MQTT);后续按"复用命令 + stream/字段"模式补(与 log_* 扩展同套路)。注:TCP 侧 `loop_version_query` 代码已有(`tcp_json_srv.c`),文档待同步 |
|
||||
| `DLD960_BLE协议.md` | 不变(BLE OTA 维持流式透传现状;本地刷写状态机与 BLE 透传共用 `lup_feed_byte_ota`) |
|
||||
| `README.md` / `DLD960_技术规格书.md` | 协议矩阵同步 V1.10 |
|
||||
|
||||
**固件代码落点(协议拍板后动):**
|
||||
- `iot_mqtt_srv.c`:命令分发链加 6 个 ota_* 分支(现有 if-else 链)
|
||||
- 新增 `ota_srv.c/h`:会话状态机 + W25Qxx 暂存读写 + CRC32 查表
|
||||
- `usart_biz.c` / `loop_uart_proto.c`:本地刷写状态机(复用 `lup_feed_byte_ota` 解析 + `g_flag_counter_ota` 门控,注意退出机制——现 BLE 透传 flag 无退出点,本地刷写必须自管)
|
||||
- `offlog.c`:补"固件升级开始/结果"事件类型
|
||||
- 单测:`tests/test_ota_srv.c`(gcc 提取+嵌入模式,mock SPI/MQTT/Loop)
|
||||
|
||||
---
|
||||
|
||||
# 修订记录
|
||||
|
||||
| 版本 | 时间 | 说明 |
|
||||
|------|------|------|
|
||||
| V1.00 | 2026-08-20 | 设计稿:Loop MCU MQTT 远程 OTA 先存后刷协议(依据 ROADMAP P1.4 ①) |
|
||||
| V1.01 | 2026-08-20 | 按修改意见修订:①Slot A/B 容量 60KB→**100KB**(镜像上限 96KB,为 DBN 镜像预留)②删除"继电器 GPIO 默认态"待验证项,维持 Loop 现状(与 BLE OTA 行为一致,不额外干预)③新增"OTA 会话期间静默"约束:暂停 MQTT `event_report` 发送(队列积压结束后补发)+ offlog/快照落盘(开始/结果日志除外)④刷写调度明确**非阻塞 tick 驱动**,刷写窗口内 MQTT 保活(PINGREQ/心跳/IWDG 喂狗)不受影响 |
|
||||
+8
-4
@@ -1,7 +1,7 @@
|
||||
# DLD960 四通道车辆检测器 产品手册
|
||||
|
||||
> Product Manual · 产品型号:**DLD960GA**
|
||||
> 文档版本 V1.02 · 2026-08-19 · 适用固件:整机 V1.02.03(Loop 1.0 + DBN 1.02.03)
|
||||
> 文档版本 V1.03 · 2026-08-21 · 适用固件:整机 V1.02.05(Loop 1.0 + DBN 1.02.05)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,6 +26,8 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
- 以太网双协议:TCP JSON(局域网对接)+ MQTT(云平台),关键事件确认送达
|
||||
- 通信可靠性加固:串口硬件 DMA 接收 + 固件内存优化,长时间运行稳定不丢帧、不异常重启
|
||||
- BLE OTA 升级链路加固:检测 MCU 升级响应帧(0x9F 协议)完整透传回小程序,停等 ACK 不再丢失,升级成功率提升
|
||||
- 网络上报携带地感版本:云平台可实时核对地感固件/硬件版本(MQTT `loop_version_query`,配合远程 OTA 升级前后版本核对)
|
||||
- 联网稳定性加固:MQTT 发送退避与断连重连修复,长时间联网不掉线、不死循环
|
||||
|
||||
## 2. 硬件说明
|
||||
|
||||
@@ -132,7 +134,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
| 通信 MCU | 蓝牙 OTA | 小程序选择固件直接升级 |
|
||||
| 检测 MCU | ISP 透传 | 小程序经蓝牙→通信 MCU 串口透传升级 |
|
||||
|
||||
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.03。
|
||||
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.05。
|
||||
>
|
||||
> ⚠ **检测 MCU 升级完成后,若小程序无响应,请将设备断电重启**(DBN OTA 透传模式需重启恢复,V1.02.03 已知约束)。
|
||||
|
||||
@@ -148,6 +150,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
| 继电器输出正常但平台无数据 | 网络配置错误;MQTT Topic/序列码不符 | 查 IP/Broker 配置;核对 dev_serial 与 Topic |
|
||||
| 设备频繁自动重启 / 偶发数据丢失 | 通信 MCU 旧固件资源不足(V1.02.01 已修复) | 升级整机固件至 V1.02.01 及以上版本 |
|
||||
| BLE OTA 升级失败/进度卡住 | 通信 MCU 旧固件 0x9F 透传缺陷(V1.02.03 已修复) | 升级整机固件至 V1.02.03 及以上版本 |
|
||||
| 配置无法保存/每次上电恢复出厂 | 旧固件 factory 写入被禁用;换新存储芯片后参数区无出厂配置(V1.02.04 已修复) | 升级整机固件至 V1.02.05 及以上版本 |
|
||||
| 事件重复上报 | 平台未按协议应答 event_report | 平台须回显 msg_id 应答(先落库后应答) |
|
||||
| 线圈断开告警 | 线圈开路/引线断裂 | 万用表量线圈回路电阻(正常几欧姆以内) |
|
||||
|
||||
@@ -167,7 +170,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
| 《DLD960 技术规格书》 | 完整技术参数 |
|
||||
| 《DLD960 串口通信协议》V1.01 | 串口 对接开发 |
|
||||
| 《DLD960 TCP JSON 协议》V1.03 | 局域网对接开发 |
|
||||
| 《DLD960 IoT MQTT 协议》V1.07 | 云平台对接开发 |
|
||||
| 《DLD960 IoT MQTT 协议》V1.10 | 云平台对接开发(含 Loop 远程 OTA 与地感版本查询) |
|
||||
| 《DLD960 BLE 协议》V1.02 | 蓝牙小程序/APP 对接开发(脱机日志与传感快照读写) |
|
||||
| 《环路车辆检测器验收标准》 | 采购/部署验收 |
|
||||
|
||||
@@ -177,6 +180,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
|
||||
| 版本 | 修订时间 | 修订说明 | 修订人 |
|
||||
|------|----------|----------|--------|
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03:BLE OTA 升级链路加固说明(检测 MCU 0x9F 响应帧透传回小程序,停等 ACK 完整);故障排查新增 OTA 失败条目;固件升级章节补断电重启提示 | wangfq |
|
||||
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:核心特性补网络上报携带地感版本 + 联网稳定性加固;相关文档表 MQTT 协议 V1.07→V1.10(修正滞后);故障排查配置保存建议升至 V1.02.05 及以上 | wangfq |
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.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 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# DLD960 四通道车辆检测器 技术规格书
|
||||
|
||||
> Technical Specification · 产品型号:**DLD960GA**
|
||||
> 文档版本 V1.02 · 2026-08-19 · 配套整机发布 V1.02.03
|
||||
> 文档版本 V1.03 · 2026-08-21 · 配套整机发布 V1.02.05
|
||||
|
||||
---
|
||||
|
||||
@@ -100,13 +100,13 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
|
||||
| 外部存储 | SPI NOR Flash | 参数存储 + 日志存储 |
|
||||
| 按键 | K1 多功能按键 | 复位等 |
|
||||
|
||||
### 5.1 协议矩阵(V1.02.03 配套)
|
||||
### 5.1 协议矩阵(V1.02.05 配套)
|
||||
|
||||
| 协议 | 版本 | 通道 | 要点 |
|
||||
|------|------|------|------|
|
||||
| DLD960 串口通信协议 | V1.01 | TTL | 设备管理、参数配置、数据上报 |
|
||||
| DLD960 TCP JSON 协议 | V1.03 | ETH :5960 | 密码鉴权 + 18 条命令;event_report 客户端必答(5s×3 重发);脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
|
||||
| DLD960 IoT MQTT 协议 | V1.07 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`;initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分) |
|
||||
| DLD960 IoT MQTT 协议 | V1.10 | ETH → Broker | 双主题 `dld960/{sn}/srv`+`/dev`;initialize 上线、loop_data 三档调度、event_report 平台必答、设备时钟同步、脱机日志 log_stat/log_query/log_clear(事件/快照流,stream 区分);Loop 远程 OTA(ota_begin/ota_data/ota_end/ota_abort/ota_flash/ota_status + ota_report,256B/片 CRC32,先存后刷断点续传,Slot A/B 双槽回滚) |
|
||||
| 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 同语义 |
|
||||
|
||||
@@ -166,19 +166,20 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
|
||||
|
||||
> ⚠ **自内部协议 V1.05 起,vd960Loop 与 vd960DBN 固件必须同版本配套刷写**(variation 字段 2B→3B 帧格式变更,混刷导致数据静默错位)。
|
||||
|
||||
| 组件 | V1.02.03 配套版本 |
|
||||
| 组件 | V1.02.05 配套版本 |
|
||||
|------|-------------------|
|
||||
| vd960Loop 固件 | 1.0(不变) |
|
||||
| vd960DBN 固件 | 1.02.03 |
|
||||
| vd960DBN 固件 | 1.02.05 |
|
||||
|
||||
### 10.1 已知约束(V1.02.01)
|
||||
|
||||
- 固件版本三段式:`FIRMWARE_VER="1.02.03"`(MAIN=1 / SUB=2 / SUBSUB=3);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。
|
||||
- 固件版本三段式:`FIRMWARE_VER="1.02.05"`(MAIN=1 / SUB=2 / SUBSUB=5);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。
|
||||
- 设备无 RTC,时钟同步依赖平台下发(校准前 ts 为上电秒数)。
|
||||
- CH32V208 RAM 预算:BLE 协议栈固定占用低 16KB(不可裁剪),用户可用 48KB;固件经 RAM 瘦身优化,栈余量约 6KB,现场升级务必使用 V1.02.01 及以上版本。
|
||||
- 传感快照区延后 3s 初始化(开机 3s 内传感数据不落盘,设计接受)。
|
||||
- UART2 RX DMA 占用 DMA1_Ch6(全工程唯一,无冲突)。
|
||||
- **OTA 透传模式无自动退出机制**:进入 OTA 态(`g_flag_counter_ota.flag=1`)后需断电重启才恢复 0x7F 正常通信(V1.02.03 已知约束,升级完成后重启设备)。
|
||||
- **UART2 0x4A 版本查询与 `loop_data`/事件上报共用总线**:低频命令理论无冲突,时序待板上验证(V1.02.05)。
|
||||
|
||||
---
|
||||
|
||||
@@ -186,6 +187,7 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
|
||||
|
||||
| 版本 | 修订时间 | 修订说明 | 修订人 |
|
||||
|------|----------|----------|--------|
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03:UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出 | wangfq |
|
||||
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:协议矩阵标题/版本配套矩阵/固件三段式版本同步;新增已知约束 UART2 0x4A 版本查询总线时序;MQTT V1.10 地感版本上报与 loop_version_query 说明 | wangfq |
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04:UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出;存储适配(SPI 识别去厂商代码 + factory 写入恢复);2026-08-20 协议矩阵 MQTT 同步 V1.08(Loop 远程 OTA 协议设计先行,固件待实现,Slot A/B 100KB) | wangfq |
|
||||
| V1.01 | 2026-08-18 | 配套整机发布 V1.02.01:版本矩阵更新、UART2 RX DMA 通信可靠性说明、已知约束补充 | wangfq |
|
||||
| V1.00 | 2026-07-16 | 初始版本,配套整机发布 V1.0.0 | wangfq |
|
||||
|
||||
@@ -241,12 +241,15 @@ void load_cfg_from_flash(void)
|
||||
if(strncmp(mBuff, DEV_USER_FLASH_MAGIC, sizeof(DEV_USER_FLASH_MAGIC)) != 0)
|
||||
{
|
||||
free(mBuff);
|
||||
/* ===== 止血 (2026-08-13): factory SPI 写触发硬件复位死循环 =====
|
||||
现场: SPI_Flash_Write(参数区 0x00, 512B) → RST=0x00000000 硬件复位
|
||||
→ magic 写不进 → 无限复位循环。SPI 写触发硬件复位实锤(与快照区无关)。
|
||||
临时方案: 不写 Flash, 用内存默认配置继续 (恢复原逻辑删本块即可) */
|
||||
/* 2026-08-17 栈溢出修复后恢复 (栈~4.75KB) */
|
||||
PRINT("CFG: magic mismatch - MEMORY DEFAULTS (factory SPI write disabled)\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();
|
||||
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",
|
||||
@@ -290,8 +293,7 @@ void load_cfg_from_flash(void)
|
||||
g_sub_code_enable.dgdus_enable = 0;
|
||||
g_sub_code_enable.wbdus_enable = 0;
|
||||
g_sub_code_enable.radar_enable = 0;
|
||||
/* 2026-08-17 栈溢出修复后恢复 */
|
||||
PRINT("CFG: memory defaults OK (no factory write)\n");
|
||||
PRINT("CFG: memory defaults OK (factory written)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define PRODUCT_MODEL "DLD960GA"
|
||||
#define FIRMWARE_VER "1.02.03"
|
||||
#define FIRMWARE_VER "1.02.05"
|
||||
#define HARDWARE_VER "1.0"
|
||||
#define FIRMWARE_VER_MAIN 1
|
||||
#define FIRMWARE_VER_SUB 2
|
||||
#define FIRMWARE_VER_SUBSUB 3
|
||||
#define FIRMWARE_VER_SUBSUB 5
|
||||
#define HARDWARE_VER_MAIN 1
|
||||
#define HARDWARE_VER_SUB 1
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ void iot_mqtt_poll(void); // 主循环轮询 (状态机 + 心跳 +
|
||||
void iot_mqtt_handle_sock_int(uint8_t socketid, uint8_t intstat); // Socket 中断处理
|
||||
void iot_mqtt_publish_sensor(void); // 推送传感器数据到 MQTT
|
||||
void iot_evt_handle_ack(uint32_t msg_id, int code); // event_report 平台应答入口 (V1.04)
|
||||
void iot_evt_report_ota_error(uint32_t err); // OTA 刷写失败告警入队 (V1.08, 走 event_report 闭环)
|
||||
void iot_ota_report_result(uint8_t ok, uint32_t err); // OTA 刷写结果上报 ota_report (V1.09: ok=1 done / ok=0 failed)
|
||||
uint8_t iot_any_car(void); // 任一通道有车 (ota_flash 安全窗口检查)
|
||||
void iot_watchdog_init(void); // 初始化硬件 IWDG (无条件, 主循环卡死兜底)
|
||||
void iot_watchdog_kick(void); // 喂硬件 IWDG (主循环调用)
|
||||
|
||||
|
||||
@@ -137,6 +137,17 @@ typedef struct {
|
||||
char version_str[32]; // 格式化字符串
|
||||
} LUP_VersionInfo;
|
||||
|
||||
/* 版本缓存 (V1.10) — 0x4A 查询结果缓存, 供 MQTT dev_info_query/initialize 带缓存值 */
|
||||
typedef struct {
|
||||
uint8_t valid; /* 1=缓存有效 (查询成功过) */
|
||||
uint8_t hard_main;
|
||||
uint8_t hard_sub;
|
||||
uint8_t hard_ssub;
|
||||
uint8_t soft_main;
|
||||
uint8_t soft_sub;
|
||||
uint8_t soft_ssub;
|
||||
} LoopVerCache;
|
||||
|
||||
/* 命令状态机 */
|
||||
typedef enum {
|
||||
LUP_STATE_IDLE = 0, // 空闲
|
||||
@@ -160,6 +171,7 @@ typedef struct {
|
||||
* Global State
|
||||
*===========================================================================*/
|
||||
extern LUP_CmdTracker g_lup_cmd;
|
||||
extern LoopVerCache g_lup_ver_cache; /* V1.10: 0x4A 版本缓存 */
|
||||
|
||||
/*===========================================================================
|
||||
* Public API
|
||||
@@ -194,6 +206,10 @@ void lup_cmd_done(void);
|
||||
int lup_cmd_check_timeout(void);
|
||||
void lup_cmd_on_response(const uint8_t *pkg, uint16_t len);
|
||||
|
||||
/* --- 版本缓存 (V1.10) --- */
|
||||
void lup_cache_from_info(const LUP_VersionInfo *info); /* 解析结果写入缓存 */
|
||||
void lup_refresh_version_cache(void); /* 后台刷新: 命令通道空闲时发 0x4A */
|
||||
|
||||
/* --- High-level Commands (send + wait handled by state machine) --- */
|
||||
void lup_send_get_version(void);
|
||||
void lup_send_reset(void);
|
||||
|
||||
@@ -116,6 +116,8 @@ enum {
|
||||
OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */
|
||||
OFFLOG_EVT_COIL = 0x40, /* 线圈事件; payload[0]=sub(1=进 2=出 3=断开 4=恢复), payload[1]=ch, payload[2..5]=value(50ms) */
|
||||
OFFLOG_EVT_TIME_ANCHOR = 0x50, /* 时钟同步锚点; unix_ts=同步值 */
|
||||
OFFLOG_EVT_OTA_START = 0x60, /* 固件升级开始 (V1.08); payload[0]=slot, payload[1]=target, payload[2..5]=size BE */
|
||||
OFFLOG_EVT_OTA_RESULT = 0x61, /* 固件升级结果 (V1.08); payload[0..3]=result code BE (0=成功, 非0=OTA_ERR_*) */
|
||||
OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */
|
||||
};
|
||||
|
||||
@@ -150,6 +152,8 @@ void offlog_evt_retry(uint32_t msg_id, uint8_t retry);
|
||||
void offlog_evt_giveup(uint32_t msg_id);
|
||||
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value);
|
||||
void offlog_time_anchor(uint32_t unix_ts);
|
||||
void offlog_ota_start(uint8_t slot, uint32_t size); /* 固件升级开始 (V1.08, 会话静默前写入) */
|
||||
void offlog_ota_result(uint32_t code); /* 固件升级结果 (V1.08, 恢复后补记) */
|
||||
|
||||
uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */
|
||||
uint16_t offlog_count(void); /* 有效记录数 */
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file ota_srv.h
|
||||
* @author wangfq
|
||||
* @version V1.0
|
||||
* @date 2026-08-20
|
||||
* @brief DLD960 Loop MCU 远程 OTA 服务 (MQTT 协议 V1.08, ROADMAP P1.4 ①)
|
||||
*
|
||||
* 先存后刷: MQTT 分片下载 → W25Qxx 暂存区 (0x010000 起 512KB) →
|
||||
* 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU
|
||||
*
|
||||
* 协议依据: docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5
|
||||
* 设计稿: docs/DLD960_MQTT_OTA协议.md V1.01
|
||||
******************************************************************************
|
||||
*/
|
||||
#ifndef __OTA_SRV_H__
|
||||
#define __OTA_SRV_H__
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*===========================================================================
|
||||
* 分区常量 (与 offlog.h: OFFLOG_PARAM_SIZE=64KB + OFFLOG_OTA_SIZE=512KB)
|
||||
* OTA 区 = 0x010000 ~ 0x090000 (512KB)
|
||||
* 布局: 元数据扇区(4KB) + Slot A(100KB) + Slot B(100KB) + 预留(308KB)
|
||||
*===========================================================================*/
|
||||
#define OTA_AREA_BASE 0x10000UL /* 0x010000 (参数区 64KB 后) */
|
||||
#define OTA_AREA_SIZE 0x80000UL /* 512KB */
|
||||
|
||||
#define OTA_META_BASE (OTA_AREA_BASE + 0x0000UL) /* 元数据主备份 */
|
||||
#define OTA_META_BACKUP (OTA_AREA_BASE + 0x0200UL) /* 元数据备份 (同扇区) */
|
||||
#define OTA_META_SECTOR (OTA_AREA_BASE / 4096UL) /* 元数据所在扇区号 */
|
||||
|
||||
#define OTA_SLOT_A_BASE (OTA_AREA_BASE + 0x1000UL) /* 0x011000 */
|
||||
#define OTA_SLOT_B_BASE (OTA_AREA_BASE + 0x1A000UL) /* 0x02A000 */
|
||||
#define OTA_SLOT_SIZE (100UL * 1024UL) /* 100KB */
|
||||
#define OTA_MAX_IMAGE (96UL * 1024UL) /* 镜像上限 96KB (留 4KB 余量) */
|
||||
|
||||
/* 下载单片 (协议常量, 256B 页对齐) */
|
||||
#define OTA_CHUNK_SIZE 256
|
||||
|
||||
/* 刷写块 (0x9F LEN 为 uint8 → DATA ≤ 254B; 取 248B) */
|
||||
#define OTA_FLASH_BLOCK 248
|
||||
|
||||
/*===========================================================================
|
||||
* 会话状态
|
||||
*===========================================================================*/
|
||||
enum {
|
||||
OTA_STATE_IDLE = 0, /* 空闲 */
|
||||
OTA_STATE_DOWNLOADING, /* 下载中 */
|
||||
OTA_STATE_READY, /* 校验通过, 可刷写 */
|
||||
OTA_STATE_FLASHING, /* 刷写中 */
|
||||
OTA_STATE_FLASH_FAILED, /* 刷写失败 (保留镜像可重试) */
|
||||
OTA_STATE_ABORTED, /* 已中止 */
|
||||
};
|
||||
|
||||
/* 刷写子状态 (非阻塞 tick 驱动) */
|
||||
enum {
|
||||
OTA_FLASH_IDLE = 0,
|
||||
OTA_FLASH_START, /* 发 A5 启动帧 */
|
||||
OTA_FLASH_WAIT_READY, /* 等 pre_ok */
|
||||
OTA_FLASH_ADDR, /* 发 A6 地址帧 */
|
||||
OTA_FLASH_WAIT_ADDR, /* 等 addr_ok */
|
||||
OTA_FLASH_DATA, /* 发 A7 数据块 */
|
||||
OTA_FLASH_WAIT_ACK, /* 等 data ACK */
|
||||
};
|
||||
|
||||
/* OTA 错误码 (event_report type=ota_error 的 value, 协议 §5.3) */
|
||||
#define OTA_ERR_READY_TIMEOUT 0x1001 /* 启动帧无响应 (pre_ok 超时) */
|
||||
#define OTA_ERR_ADDR_FAIL 0x1002 /* 地址帧错误 (addr_err / 超时) */
|
||||
#define OTA_ERR_DATA_ACK_LIMIT 0x1003 /* 数据块 ACK 超限 (重试×3 耗尽) */
|
||||
#define OTA_ERR_CRC_FAIL 0x1004 /* 全镜像校验失败 */
|
||||
#define OTA_ERR_FORCE_FAIL 0x1005 /* 安全窗口拒绝后强制失败 */
|
||||
|
||||
/*===========================================================================
|
||||
* 元数据结构 (56B, 写 64B 槽)
|
||||
*===========================================================================*/
|
||||
typedef struct {
|
||||
uint32_t magic; /* OTA_MAGIC 'DLD9' */
|
||||
uint32_t state; /* OTA_STATE_* */
|
||||
uint8_t target; /* 0=loop (1=dbn 预留) */
|
||||
uint8_t slot; /* 0=A, 1=B */
|
||||
uint16_t rsv;
|
||||
uint32_t size; /* 镜像字节数 */
|
||||
uint32_t crc32; /* 全镜像 CRC32 */
|
||||
uint32_t received; /* 已下载字节数 (片对齐, 断点续传依据) */
|
||||
uint32_t last_result; /* 上次刷写结果: 0=无/成功, 非0=错误码 */
|
||||
uint32_t begin_ts; /* 会话开始 (同步后 Unix 秒, 0=未同步) */
|
||||
char version[16]; /* 目标固件版本 (审计) */
|
||||
uint32_t flash_cnt; /* 刷写尝试次数 */
|
||||
uint32_t rsv2;
|
||||
} OtaMeta; /* 56B */
|
||||
|
||||
#define OTA_MAGIC 0x39444C44UL /* 'DLD9' */
|
||||
|
||||
/*===========================================================================
|
||||
* API
|
||||
*===========================================================================*/
|
||||
|
||||
/* 初始化: 读元数据 (双备份校验), 刷写状态机复位 */
|
||||
void ota_init(void);
|
||||
|
||||
/* 主循环轮询: 刷写状态机 (非阻塞 tick 驱动, 绝不阻塞主循环) */
|
||||
void ota_poll(void);
|
||||
|
||||
/* OTA 会话期间静默标志: downloading/flashing 时置位
|
||||
→ event_report 暂停发送 (队列积压) + offlog/快照落盘暂停 */
|
||||
uint8_t ota_silent_active(void);
|
||||
|
||||
/* 当前会话元数据 (只读, MQTT 响应组装用) */
|
||||
const OtaMeta *ota_meta(void);
|
||||
|
||||
/* 状态字符串 (JSON state 字段) */
|
||||
const char *ota_state_str(uint8_t state);
|
||||
|
||||
/*===========================================================================
|
||||
* MQTT 命令入口 (iot_mqtt_srv.c 调用, 主循环上下文)
|
||||
* 返回 0=成功, 非0=错误码 (1=参数 3=忙 5=内部; data.err_code 用 OTA_ERR_*)
|
||||
*===========================================================================*/
|
||||
int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force);
|
||||
int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex);
|
||||
int ota_cmd_end(uint32_t crc32);
|
||||
int ota_cmd_abort(void);
|
||||
int ota_cmd_flash(uint8_t slot, uint8_t force);
|
||||
|
||||
/* 分片落盘用: hex → bin (n 字节), 成功 0 */
|
||||
int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n);
|
||||
/* 从 MQTT JSON 中提取 ota_data 的 data 字段 hex 值 (兼容 "data":"..." 与 "data": "...")
|
||||
返回 hex 字符串长度, 0=未找到/为空 */
|
||||
int ota_json_extract_hex(const char *json, char *out, int out_len);
|
||||
/* CRC-32/ISO-HDLC (逐位实现, 零查表 RAM) */
|
||||
uint32_t ota_crc32(const uint8_t *data, uint32_t len);
|
||||
|
||||
/*===========================================================================
|
||||
* 本地刷写与 UART2 联动 (usart_biz.c 使用)
|
||||
*===========================================================================*/
|
||||
/* 1 = 本地刷写进行中: uart_srv OTA 分支把 0x9F ACK 交给 ota_flash_feed_ack,
|
||||
而非透传回 BLE (extern 定义于 ota_srv.c) */
|
||||
extern uint8_t g_ota_flash_active;
|
||||
|
||||
/* usart_srv 收到 bootloader 0x9F ACK 帧时调用 (主循环上下文) */
|
||||
void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len);
|
||||
|
||||
/* 刷写进度 (已送字节 / 总量, ota_report 用) */
|
||||
uint32_t ota_flash_sent(void);
|
||||
uint32_t ota_flash_total(void);
|
||||
|
||||
#endif /* __OTA_SRV_H__ */
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "offlog.h"
|
||||
#include "snapshot.h"
|
||||
#include "fault_diag.h"
|
||||
#include "ota_srv.h"
|
||||
#include "ch32v20x_iwdg.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -58,6 +59,10 @@ static LUP_SensorReport _cached_sr; // 最新传感器数据缓存
|
||||
static uint8_t _cached_sr_valid; // 缓存有效标志
|
||||
static uint32_t _last_publish_ms; // 上次上报时刻 (ms)
|
||||
|
||||
/* 上行发布共享缓冲 (V1.09): event_report / ota_report 复用, 互斥调用
|
||||
(RAM 90% 环境: 独立数组挤栈, 文件级共享零新增) */
|
||||
static char _iot_pub_payload[512];
|
||||
|
||||
/*===========================================================================
|
||||
* 事件上报 event_report — 协议 V1.04: 平台必答 + 设备重发
|
||||
*
|
||||
@@ -73,7 +78,8 @@ static uint32_t _last_publish_ms; // 上次上报时刻 (ms)
|
||||
#define IOT_EVT_ACK_TIMEOUT_MS 5000 // 确认超时 (协议 V1.04)
|
||||
#define IOT_EVT_MAX_RETRY 3 // 最大重发次数 (含首发共4次)
|
||||
|
||||
enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE };
|
||||
enum { IOT_EVT_CAR_ENTER = 0, IOT_EVT_CAR_LEAVE, IOT_EVT_LOOP_CUT, IOT_EVT_LOOP_RESTORE,
|
||||
IOT_EVT_OTA_ERROR }; /* V1.08: OTA 刷写失败告警 (value=OTA_ERR_*) */
|
||||
|
||||
typedef struct {
|
||||
uint8_t type; // IOT_EVT_xxx
|
||||
@@ -112,9 +118,11 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
|
||||
IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH];
|
||||
e->type = type; e->ch = ch; e->value = value;
|
||||
_evt_count++;
|
||||
/* 事件日志: 线圈事件 (主循环上下文) */
|
||||
/* 事件日志: 线圈事件 (主循环上下文); OTA 会话期间暂停落盘 (V1.08) */
|
||||
if (!ota_silent_active()) {
|
||||
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);
|
||||
}
|
||||
@@ -170,7 +178,9 @@ static void iot_sensor_ingest(const LUP_SensorReport *sr) {
|
||||
iot_evt_feed(sr); // 事件沿检测
|
||||
memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照
|
||||
_cached_sr_valid = 1;
|
||||
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */
|
||||
if (!ota_silent_active()) {
|
||||
snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘; OTA 会话暂停 (V1.08) */
|
||||
}
|
||||
FAULT_MARKER(MK_INGEST_OUT);
|
||||
}
|
||||
|
||||
@@ -187,12 +197,18 @@ static void iot_evt_sensor_cb(const uint8_t *pkg, uint16_t len) {
|
||||
|
||||
/* 序列化并发布队列头 n 条事件 (首发与重发共用: 同 id/ts 同内容) */
|
||||
static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) {
|
||||
static const char *evt_names[] = {"car_enter", "car_leave", "loop_cut", "loop_restore"};
|
||||
static char payload[512]; // static: 避免 2KB 栈溢出
|
||||
int w, rem = sizeof(payload);
|
||||
static const char *evt_names[] = {"car_enter", "car_leave", "loop_cut", "loop_restore", "ota_error"};
|
||||
char *payload = _iot_pub_payload; // V1.09: 文件级共享缓冲
|
||||
int w, rem = sizeof(_iot_pub_payload);
|
||||
char *p = payload;
|
||||
uint8_t i;
|
||||
|
||||
/* OTA 会话期间静默 (V1.08): 暂停发送, 事件保留队列, 结束后补发 */
|
||||
if (ota_silent_active()) {
|
||||
PRINT("EVT: silent (OTA), skip publish\n");
|
||||
return;
|
||||
}
|
||||
|
||||
w = snprintf(p, rem, "{\"msg_id\":%lu,\"cmd\":\"event_report\",\"ts\":%lu,"
|
||||
"\"data\":{\"events\":[", id, ts);
|
||||
if (w < 0 || w >= rem) return;
|
||||
@@ -201,7 +217,7 @@ static void iot_evt_send(uint32_t id, uint32_t ts, uint8_t n) {
|
||||
for (i = 0; i < n; i++) {
|
||||
const IotEvent *e = &_evt_queue[(_evt_head + i) % IOT_EVT_QUEUE_DEPTH];
|
||||
w = snprintf(p, rem, "%s{\"type\":\"%s\",\"ch\":%d,\"value\":%lu}",
|
||||
(i > 0) ? "," : "", evt_names[e->type & 0x03], e->ch, e->value);
|
||||
(i > 0) ? "," : "", evt_names[(e->type < 5) ? e->type : 0], e->ch, e->value);
|
||||
if (w < 0 || w >= rem) return;
|
||||
p += w; rem -= w;
|
||||
}
|
||||
@@ -230,6 +246,118 @@ void iot_evt_handle_ack(uint32_t msg_id, int code) {
|
||||
_evt_retry = 0;
|
||||
}
|
||||
|
||||
/* OTA 刷写失败告警入队 (V1.08): 走 event_report ACK 闭环, 平台必答
|
||||
(ota_srv.c ota_flash_fail 调用; OTA_ERR_* 码见 ota_srv.h) */
|
||||
void iot_evt_report_ota_error(uint32_t err)
|
||||
{
|
||||
iot_evt_enqueue(IOT_EVT_OTA_ERROR, 0, err);
|
||||
PRINT("EVT: ota_error enqueue err=0x%lX\n", (unsigned long)err);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* OTA 结果上报 ota_report — 协议 V1.08 §5.5 / V1.09 平台判定主依据
|
||||
*
|
||||
* done/failed 主动推送, 无 ACK 闭环 → 定时重发 3 次×5s (同 msg_id/ts),
|
||||
* 平台按 (sn,msg_id) 去重 (窗口建议 10min)。共享 _iot_pub_payload 缓冲。
|
||||
* iot_ota_report_result(): 记录结果 + 立即首发; iot_ota_report_poll():
|
||||
* 主循环每轮调用, 到 5s 且未超 3 次 → 重发。
|
||||
*===========================================================================*/
|
||||
#define IOT_OTA_REP_MAX_RETRY 3 /* 重发次数 (含首发共 4 次) */
|
||||
#define IOT_OTA_REP_INTERVAL_MS 5000
|
||||
|
||||
static uint8_t _ota_rep_pend; /* 1=有未完成的 ota_report */
|
||||
static uint8_t _ota_rep_ok; /* 1=done 0=failed */
|
||||
static uint32_t _ota_rep_err; /* failed 的错误码 (OTA_ERR_*) */
|
||||
static uint32_t _ota_rep_id; /* 固定 msg_id (重发不变) */
|
||||
static uint32_t _ota_rep_ts; /* 固定 ts (重发不变) */
|
||||
static uint8_t _ota_rep_times; /* 已发次数 (首发=1) */
|
||||
static uint32_t _ota_rep_sent_ms; /* 上次发送时刻 */
|
||||
|
||||
/* V1.10: loop_version_query 异步查询 (0x4A) 状态 — 与 TCP 共用 g_lup_cmd 单命令通道 */
|
||||
static uint8_t _iot_verq_pending; /* 1=有查询在途 (等 0x4A 响应回包) */
|
||||
static uint32_t _iot_verq_msg_id; /* 命令 msg_id (回包回显) */
|
||||
static uint32_t _iot_verq_deadline; /* 回包超时时刻 (ms), 与 TCP json_check_pending 一致 300ms */
|
||||
static uint8_t _iot_ver_cache_dirty; /* 1=需后台刷新版本缓存 (上电 / OTA done 后置位) */
|
||||
|
||||
/* 版本缓存 → "x.y.z" 字符串 (缓存无效则空串) */
|
||||
static void iot_loop_ver_str(char *out, uint16_t out_len)
|
||||
{
|
||||
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
|
||||
snprintf(out, out_len, "%d.%d.%d",
|
||||
g_lup_ver_cache.soft_main, g_lup_ver_cache.soft_sub, g_lup_ver_cache.soft_ssub);
|
||||
}
|
||||
|
||||
static void iot_loop_hw_ver_str(char *out, uint16_t out_len)
|
||||
{
|
||||
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
|
||||
snprintf(out, out_len, "%d.%d.%d",
|
||||
g_lup_ver_cache.hard_main, g_lup_ver_cache.hard_sub, g_lup_ver_cache.hard_ssub);
|
||||
}
|
||||
|
||||
/* 序列化并发布 ota_report (首发与重发共用: 同 id/ts 同内容) */
|
||||
static void iot_ota_report_send(void)
|
||||
{
|
||||
char *p = _iot_pub_payload;
|
||||
int w, rem = sizeof(_iot_pub_payload);
|
||||
|
||||
w = snprintf(p, rem,
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_report\",\"ts\":%lu,"
|
||||
"\"data\":{\"target\":\"loop\",\"stage\":\"%s\","
|
||||
"\"progress\":{\"sent\":%lu,\"total\":%lu},\"code\":%lu,\"msg\":\"\"}}",
|
||||
_ota_rep_id, _ota_rep_ts,
|
||||
_ota_rep_ok ? "done" : "failed",
|
||||
(unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(),
|
||||
_ota_rep_ok ? 0UL : (unsigned long)_ota_rep_err);
|
||||
if (w < 0 || w >= rem) return;
|
||||
|
||||
mqtt_publish((char *)g_iot_topic.topic_pub, p, 1);
|
||||
_ota_rep_times++;
|
||||
_ota_rep_sent_ms = mstick();
|
||||
PRINT("IOT: ota_report %s msg_id=%lu sent=%lu/%lu times=%u\n",
|
||||
_ota_rep_ok ? "done" : "failed", _ota_rep_id,
|
||||
(unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(),
|
||||
_ota_rep_times);
|
||||
}
|
||||
|
||||
/* OTA 刷写结果上报入口 (ota_srv.c ota_flash_done/fail 调用):
|
||||
ok=1 → stage=done, ok=0 → stage=failed */
|
||||
void iot_ota_report_result(uint8_t ok, uint32_t err)
|
||||
{
|
||||
_ota_rep_pend = 1;
|
||||
_ota_rep_ok = ok ? 1 : 0;
|
||||
_ota_rep_err = err;
|
||||
_ota_rep_id = ++_evt_msg_id; /* 与 event_report 同源递增 (唯一性) */
|
||||
_ota_rep_ts = dev_time_now();
|
||||
_ota_rep_times = 0;
|
||||
if (ok) _iot_ver_cache_dirty = 1; /* V1.10: 刷写成功 → Loop 跑新固件, 后台刷新版本缓存 */
|
||||
iot_ota_report_send();
|
||||
}
|
||||
|
||||
/* 主循环轮询: 5s×3 定时重发 (无 ACK, 纯推送 + 平台去重) */
|
||||
static void iot_ota_report_poll(void)
|
||||
{
|
||||
if (!_ota_rep_pend) return;
|
||||
if (g_iot_state != IOT_STATE_READY) return; /* 断网不消耗次数 */
|
||||
if (_ota_rep_times > IOT_OTA_REP_MAX_RETRY) {
|
||||
_ota_rep_pend = 0; /* 重发耗尽 (共 4 次) */
|
||||
PRINT("IOT: ota_report retry exhausted\n");
|
||||
return;
|
||||
}
|
||||
if (mstick() - _ota_rep_sent_ms < IOT_OTA_REP_INTERVAL_MS) return;
|
||||
iot_ota_report_send();
|
||||
}
|
||||
|
||||
/* 任一通道有车 (ota_flash 安全窗口检查用, 基于最新 0xC0 缓存) */
|
||||
uint8_t iot_any_car(void)
|
||||
{
|
||||
uint8_t i;
|
||||
if (!_cached_sr_valid) return 0;
|
||||
for (i = 0; i < _cached_sr.coil_count && i < LUP_COIL_COUNT; i++) {
|
||||
if (_cached_sr.coils[i].car_state) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 事件发送状态机: 主循环每轮调用 (置于 READY 检查之前, 以感知重连沿) */
|
||||
static void iot_evt_process(void) {
|
||||
static uint8_t _was_ready = 0;
|
||||
@@ -320,7 +448,17 @@ static int iot_mqtt_send(const uint8_t *buf, uint16_t len) {
|
||||
uint32_t slen = len - total_sent;
|
||||
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) {
|
||||
if (slen == 0 || ret != WCHNET_ERR_SUCCESS) {
|
||||
/* 0x11 = WCHNET_ERR_MEM: 瞬时发送缓冲不足 (WCHNET_NUM_TCP_SEG=2 极小,
|
||||
重传队列占用即无缓冲)。200ms 退避重试等 WCHNET 释放 SEG —
|
||||
不要立即断连: 断连重连会让 WCHNET socket 表更乱
|
||||
(SocketCreat ISCONN 死循环, 2026-08-20 现场) */
|
||||
if (ret == WCHNET_ERR_MEM && retries < 10) {
|
||||
uint32_t w = mstick();
|
||||
while ((mstick() - w) < 200) { } /* 退避 (IWDG 4s 内安全) */
|
||||
}
|
||||
retries++;
|
||||
if (retries > 10) {
|
||||
_iot_send_fail_cnt++;
|
||||
PRINT("IOT: SocketSend FAIL#%d ret=0x%02X sent=%u/%u\n",
|
||||
_iot_send_fail_cnt, ret, total_sent, len);
|
||||
@@ -328,12 +466,15 @@ static int iot_mqtt_send(const uint8_t *buf, uint16_t len) {
|
||||
PRINT("IOT: SocketSend 3 consecutive failures, forcing reconnect\n");
|
||||
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
|
||||
g_iot_socket = 0xFF;
|
||||
SocketId_TCP = 0xFF; /* 防重连 SocketCreat 沿用旧 id (2026-08-20) */
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
_iot_send_fail_cnt = 0;
|
||||
_iot_reconnect_deadline = mstick() + 3000; // 3s 后重连, 让 WCHNET 清旧 socket
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
continue; /* 重试 (MEM 已退避 200ms, 其他错误立即重试) */
|
||||
}
|
||||
}
|
||||
_iot_send_fail_cnt = 0; // 成功一次就清零
|
||||
return 0;
|
||||
@@ -418,6 +559,12 @@ static int iot_mqtt_publish(const char *topic, const char *payload,
|
||||
/* 处理一条收到的 MQTT PUBLISH 消息 */
|
||||
static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_len) {
|
||||
static char json[IOT_MQTT_RECV_BUF_LEN]; // static: 避免 1KB 栈开销
|
||||
/* OTA 命令 IO 缓冲 (V1.08): 各分支响应组包 + ota_data hex 提取共用
|
||||
(RAM 90% 环境: 6 个独立 static 数组 ~2.5KB 挤栈, 合并 union 省 ~2KB) */
|
||||
static union {
|
||||
char resp[512]; /* ota_* 命令响应 JSON */
|
||||
char hexbuf[512 + 1]; /* ota_data hex 字符串 (512 hex + NUL) */
|
||||
} _ota_io;
|
||||
int copy_len = payload_len < (int)sizeof(json) - 1 ? payload_len : (int)sizeof(json) - 1;
|
||||
memcpy(json, payload, copy_len);
|
||||
json[copy_len] = '\0';
|
||||
@@ -454,17 +601,24 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
|
||||
/* 目前仅实现基本响应框架, 后续逐步对接 Loop MCU 命令 */
|
||||
|
||||
if (strcmp(cmd_str, "dev_info_query") == 0) {
|
||||
/* V1.10: data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 查询未完成/失败为空串) */
|
||||
char loop_ver_str[16], loop_hw_ver_str[16];
|
||||
char data_json[512];
|
||||
iot_loop_ver_str(loop_ver_str, sizeof(loop_ver_str));
|
||||
iot_loop_hw_ver_str(loop_hw_ver_str, sizeof(loop_hw_ver_str));
|
||||
snprintf(data_json, sizeof(data_json),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"dev_info_query\","
|
||||
"\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{"
|
||||
"\"dev_serial\":\"%s\",\"hard_ver\":\"%s\",\"soft_ver\":\"%s\","
|
||||
"\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\","
|
||||
"\"model\":\"%s\",\"product_code\":\"960001\","
|
||||
"\"sub_code\":{\"net\":%s,\"iot\":%s},"
|
||||
"\"bus\":{\"bus1\":0,\"bus2\":0,\"bus3\":0,\"bus4\":0}}}",
|
||||
msg_id, dev_time_now(),
|
||||
g_dev_number_str, HARDWARE_VER, FIRMWARE_VER, PRODUCT_MODEL,
|
||||
g_dev_number_str, HARDWARE_VER, FIRMWARE_VER,
|
||||
loop_ver_str, loop_hw_ver_str,
|
||||
PRODUCT_MODEL,
|
||||
g_sub_code_enable.net_enable ? "true" : "false",
|
||||
g_sub_code_enable.iot_enable ? "true" : "false");
|
||||
iot_mqtt_publish(resp_topic, data_json, strlen(data_json), 1);
|
||||
@@ -677,6 +831,186 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
|
||||
msg_id, dev_time_now());
|
||||
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_begin") == 0) {
|
||||
/* V1.08: 开启 OTA 会话 / 断点续传定位 (Loop MCU 远程 OTA, 先存后刷) */
|
||||
uint32_t size = 0, crc32 = 0;
|
||||
char version[16] = {0};
|
||||
uint8_t force = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"size\"", tmp);
|
||||
if (strlen(tmp) > 0) size = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"crc32\"", tmp);
|
||||
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"version\"", tmp);
|
||||
strncpy(version, tmp, sizeof(version) - 1);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"force\"", tmp);
|
||||
if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0);
|
||||
rc = ota_cmd_begin(size, crc32, version, force);
|
||||
if (rc == 0) {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"target\":\"loop\",\"slot\":\"%s\",\"offset\":%lu,\"received\":%lu,"
|
||||
"\"size\":%lu,\"crc32\":%lu,\"state\":\"%s\"}}",
|
||||
msg_id, dev_time_now(),
|
||||
(m->slot == 0) ? "a" : "b",
|
||||
(unsigned long)m->received, (unsigned long)m->received,
|
||||
(unsigned long)m->size, (unsigned long)m->crc32,
|
||||
ota_state_str((uint8_t)m->state));
|
||||
} else {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_begin\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\","
|
||||
"\"data\":{\"target\":\"loop\",\"err_code\":%d}}",
|
||||
msg_id, dev_time_now(), (rc == 3) ? 2 : 4);
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
|
||||
PRINT("IOT: ota_begin size=%lu rc=%d\n", (unsigned long)size, rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_data") == 0) {
|
||||
/* V1.08: 分片下发 (256B/片, 单片 CRC32; 重复片幂等, 乱序拒收指示续传) */
|
||||
uint32_t offset = 0, crc32 = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"offset\"", tmp);
|
||||
if (strlen(tmp) > 0) offset = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"crc32\"", tmp);
|
||||
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
/* hex 值提取: ota_json_extract_hex 兼容 "data":"..." 与 "data": "..." (json.dumps 带空格) */
|
||||
ota_json_extract_hex(json, _ota_io.hexbuf, (int)sizeof(_ota_io.hexbuf));
|
||||
rc = ota_cmd_data(offset, crc32, _ota_io.hexbuf);
|
||||
if (rc == 0) {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"offset\":%lu,\"received\":%lu}}",
|
||||
msg_id, dev_time_now(), (unsigned long)offset, (unsigned long)m->received);
|
||||
} else if (rc == 1) {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":1,\"msg\":\"crc/gap error\","
|
||||
"\"data\":{\"err_code\":1,\"offset\":%lu,\"received\":%lu}}",
|
||||
msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received);
|
||||
} else {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_data\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
|
||||
"\"data\":{\"err_code\":2}}",
|
||||
msg_id, dev_time_now());
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
|
||||
PRINT("IOT: ota_data offset=%lu rc=%d\n", (unsigned long)offset, rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_end") == 0) {
|
||||
/* V1.08: 结束下载, 全镜像 CRC32 复核 */
|
||||
uint32_t crc32 = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"crc32\"", tmp);
|
||||
if (strlen(tmp) > 0) crc32 = (uint32_t)strtoul(tmp, NULL, 10);
|
||||
rc = ota_cmd_end(crc32);
|
||||
if (rc == 0) {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"crc_ok\":true}}",
|
||||
msg_id, dev_time_now());
|
||||
} else if (rc == 5) {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":5,\"msg\":\"crc mismatch\","
|
||||
"\"data\":{\"err_code\":5,\"crc_ok\":false}}",
|
||||
msg_id, dev_time_now());
|
||||
} else if (rc == 3) {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
|
||||
"\"data\":{\"err_code\":2}}",
|
||||
msg_id, dev_time_now());
|
||||
} else {
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_end\",\"ts\":%lu,\"code\":1,\"msg\":\"incomplete\","
|
||||
"\"data\":{\"offset\":%lu,\"received\":%lu}}",
|
||||
msg_id, dev_time_now(), (unsigned long)m->received, (unsigned long)m->received);
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
|
||||
PRINT("IOT: ota_end rc=%d\n", rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_abort") == 0) {
|
||||
/* V1.08: 中止会话, 释放暂存 */
|
||||
int rc = ota_cmd_abort();
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_abort\",\"ts\":%lu,\"code\":%d,\"msg\":\"%s\"}",
|
||||
msg_id, dev_time_now(), (rc == 0) ? 0 : 1, (rc == 0) ? "success" : "internal error");
|
||||
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
|
||||
PRINT("IOT: ota_abort rc=%d\n", rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_flash") == 0) {
|
||||
/* V1.08: 触发本地 ISP 刷写 (同步安全检查 → 异步执行) */
|
||||
char slot_buf[8] = {0};
|
||||
uint8_t slot = 0, force = 0;
|
||||
int rc;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"slot\"", tmp);
|
||||
strncpy(slot_buf, tmp, sizeof(slot_buf) - 1);
|
||||
if (strcmp(slot_buf, "b") == 0) slot = 1;
|
||||
memset(tmp, 0, sizeof(tmp));
|
||||
simple_parse_json(json, "\"force\"", tmp);
|
||||
if (strlen(tmp) > 0) force = (strcmp(tmp, "true") == 0);
|
||||
rc = ota_cmd_flash(slot, force);
|
||||
if (rc == 0) {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":0,\"msg\":\"success, flashing async\"}",
|
||||
msg_id, dev_time_now());
|
||||
} else if (rc == 3) {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":3,\"msg\":\"busy\","
|
||||
"\"data\":{\"err_code\":3}}",
|
||||
msg_id, dev_time_now());
|
||||
} else {
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_flash\",\"ts\":%lu,\"code\":1,\"msg\":\"param error\"}",
|
||||
msg_id, dev_time_now());
|
||||
}
|
||||
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
|
||||
PRINT("IOT: ota_flash slot=%d force=%d rc=%d\n", slot, force, rc);
|
||||
|
||||
} else if (strcmp(cmd_str, "ota_status") == 0) {
|
||||
/* V1.08: 查询 OTA 状态 (含刷写进度) */
|
||||
const OtaMeta *m = ota_meta();
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"ota_status\",\"ts\":%lu,\"code\":0,\"msg\":\"success\","
|
||||
"\"data\":{\"target\":\"loop\",\"state\":\"%s\",\"slot\":\"%s\",\"size\":%lu,"
|
||||
"\"received\":%lu,\"crc32\":%lu,\"version\":\"%s\","
|
||||
"\"progress\":{\"sent\":%lu,\"total\":%lu},\"last_result\":%lu,\"last_error\":%lu}}",
|
||||
msg_id, dev_time_now(),
|
||||
ota_state_str((uint8_t)m->state), (m->slot == 0) ? "a" : "b",
|
||||
(unsigned long)m->size, (unsigned long)m->received, (unsigned long)m->crc32,
|
||||
m->version,
|
||||
(unsigned long)ota_flash_sent(), (unsigned long)ota_flash_total(),
|
||||
(unsigned long)m->last_result, (unsigned long)m->last_result);
|
||||
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
|
||||
PRINT("IOT: ota_status state=%s\n", ota_state_str((uint8_t)m->state));
|
||||
|
||||
} else if (strcmp(cmd_str, "loop_version_query") == 0) {
|
||||
/* V1.10 §4.25: 实时查询地感 Loop 版本 — 0x4A 异步, 响应在 iot_verq_poll() 回包
|
||||
(与 TCP json_check_pending 同模式: 暂存 msg_id, 等 g_lup_cmd 状态推进) */
|
||||
if (_iot_verq_pending) {
|
||||
/* 已有查询在途, 拒绝重复查询 */
|
||||
snprintf(_ota_io.resp, sizeof(_ota_io.resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,"
|
||||
"\"code\":3,\"msg\":\"busy\"}",
|
||||
msg_id, dev_time_now());
|
||||
iot_mqtt_publish(resp_topic, _ota_io.resp, strlen(_ota_io.resp), 1);
|
||||
} else {
|
||||
lup_send_get_version();
|
||||
_iot_verq_pending = 1;
|
||||
_iot_verq_msg_id = msg_id;
|
||||
_iot_verq_deadline = mstick() + 300; /* 与 TCP json_check_pending 一致 */
|
||||
PRINT("IOT: loop_version_query sent (async)\n");
|
||||
}
|
||||
|
||||
} else if (strcmp(cmd_str, "ssc_net_query") == 0) {
|
||||
/* SSC 网络配置查询 (只读全局, 协议 V1.02) */
|
||||
char resp[400];
|
||||
@@ -740,15 +1074,21 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
|
||||
|
||||
/* 发送 initialize 上线消息 (IoT 路径) */
|
||||
static void iot_send_initialize(void) {
|
||||
/* V1.10: data 补 loop_ver/loop_hw_ver (0x4A 缓存值, 尽力携带可为空) */
|
||||
char loop_ver_str[16], loop_hw_ver_str[16];
|
||||
char payload[512];
|
||||
char topic[IOT_MQTT_TOPIC_MAX_LEN];
|
||||
iot_loop_ver_str(loop_ver_str, sizeof(loop_ver_str));
|
||||
iot_loop_hw_ver_str(loop_hw_ver_str, sizeof(loop_hw_ver_str));
|
||||
snprintf(payload, sizeof(payload),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"initialize\",\"ts\":%lu,"
|
||||
"\"data\":{\"dev_serial\":\"%s\",\"model\":\"%s\","
|
||||
"\"hard_ver\":\"%s\",\"soft_ver\":\"%s\","
|
||||
"\"loop_ver\":\"%s\",\"loop_hw_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);
|
||||
g_iot_dev_serial, PRODUCT_MODEL, HARDWARE_VER, FIRMWARE_VER,
|
||||
loop_ver_str, loop_hw_ver_str);
|
||||
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
|
||||
iot_mqtt_publish(topic, payload, strlen(payload), 0);
|
||||
}
|
||||
@@ -899,6 +1239,9 @@ void iot_mqtt_publish_sensor(void) {
|
||||
/*--- 事件上报状态机: 发送/重发/重连补报 ---*/
|
||||
iot_evt_process();
|
||||
|
||||
/*--- OTA 结果上报重发 (V1.09): done/failed 5s×3, 同 msg_id/ts ---*/
|
||||
iot_ota_report_poll();
|
||||
|
||||
if (g_iot_state != IOT_STATE_READY) return;
|
||||
if (!g_report_cfg.enable) return;
|
||||
|
||||
@@ -1105,9 +1448,97 @@ void iot_watchdog_kick(void) {
|
||||
/*===========================================================================
|
||||
* 主循环轮询
|
||||
*===========================================================================*/
|
||||
/* V1.10: Loop 版本查询回包 (loop_version_query 异步) + 版本缓存后台刷新
|
||||
与 TCP 共用 g_lup_cmd 单命令通道 (后发覆盖, 先发者超时 — 低频命令可接受) */
|
||||
static void iot_verq_publish(const char *payload, uint16_t len)
|
||||
{
|
||||
char topic[IOT_MQTT_TOPIC_MAX_LEN];
|
||||
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
|
||||
iot_mqtt_publish(topic, payload, len, 1);
|
||||
}
|
||||
|
||||
static void iot_verq_poll(void)
|
||||
{
|
||||
if (_iot_verq_pending) {
|
||||
/* 命令查询在途: 等 0x4A 响应或超时 */
|
||||
if (g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
|
||||
LUP_VersionInfo info;
|
||||
char resp[320];
|
||||
char ver_str[16], hw_str[16], vs_str[64];
|
||||
int ok = 0;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
|
||||
lup_cache_from_info(&info); /* 同步刷新缓存 */
|
||||
snprintf(ver_str, sizeof(ver_str), "%d.%d.%d",
|
||||
info.soft_main, info.soft_sub, info.soft_ssub);
|
||||
snprintf(hw_str, sizeof(hw_str), "%d.%d.%d",
|
||||
info.hard_main, info.hard_sub, info.hard_ssub);
|
||||
snprintf(vs_str, sizeof(vs_str), "V%s (HW:%s)", ver_str, hw_str);
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":0,"
|
||||
"\"msg\":\"success\","
|
||||
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"%s\"}}",
|
||||
_iot_verq_msg_id, dev_time_now(), ver_str, hw_str, vs_str);
|
||||
ok = 1;
|
||||
} else {
|
||||
char lv[16], lhv[16];
|
||||
iot_loop_ver_str(lv, sizeof(lv));
|
||||
iot_loop_hw_ver_str(lhv, sizeof(lhv));
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
|
||||
"\"msg\":\"parse error\","
|
||||
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
|
||||
_iot_verq_msg_id, dev_time_now(), lv, lhv);
|
||||
}
|
||||
lup_cmd_done();
|
||||
_iot_verq_pending = 0;
|
||||
iot_verq_publish(resp, strlen(resp));
|
||||
PRINT("IOT: loop_version_query resp %s\n", ok ? "ok" : "parse error");
|
||||
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT || mstick() > _iot_verq_deadline) {
|
||||
char resp[320];
|
||||
char lv[16], lhv[16];
|
||||
iot_loop_ver_str(lv, sizeof(lv));
|
||||
iot_loop_hw_ver_str(lhv, sizeof(lhv));
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
|
||||
"\"msg\":\"no response\","
|
||||
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
|
||||
_iot_verq_msg_id, dev_time_now(), lv, lhv);
|
||||
lup_cmd_done();
|
||||
_iot_verq_pending = 0;
|
||||
iot_verq_publish(resp, strlen(resp));
|
||||
PRINT("IOT: loop_version_query timeout\n");
|
||||
}
|
||||
/* 其它 state (WAIT_RESPONSE) 继续等 */
|
||||
} else {
|
||||
/* 后台缓存刷新: 上电 / OTA done 后置 dirty, 命令通道空闲时发 0x4A */
|
||||
if (_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_IDLE) {
|
||||
lup_send_get_version();
|
||||
_iot_ver_cache_dirty = 0;
|
||||
PRINT("IOT: loop version cache refresh query\n");
|
||||
} else if (!_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
|
||||
/* 后台查询响应 (无命令 pending) → 更新缓存 + 释放通道 */
|
||||
LUP_VersionInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
|
||||
lup_cache_from_info(&info);
|
||||
PRINT("IOT: loop version cache refreshed %d.%d.%d (HW:%d.%d.%d)\n",
|
||||
info.soft_main, info.soft_sub, info.soft_ssub,
|
||||
info.hard_main, info.hard_sub, info.hard_ssub);
|
||||
}
|
||||
lup_cmd_done();
|
||||
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT) {
|
||||
lup_cmd_done(); /* 清残留超时态 (后台查询失败或被其它命令覆盖) */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void iot_mqtt_poll(void) {
|
||||
iot_watchdog_kick(); // 喂硬件 IWDG
|
||||
|
||||
/* V1.10: Loop 版本查询回包 + 缓存后台刷新 (放最前: 断连提前 return 前也要推进状态机) */
|
||||
iot_verq_poll();
|
||||
|
||||
/* 事件日志: MQTT 状态沿检测 (主循环上下文, 不在 socket 中断里写 SPI)
|
||||
真复位 → 会有 BOOT 事件 + boot_seq 递增; 仅断连重连 → 只有以下网络事件 */
|
||||
if (g_iot_state != _prev_iot_state) {
|
||||
@@ -1194,6 +1625,9 @@ void iot_mqtt_init(void) {
|
||||
_iot_reconnect_deadline = mstick() + 2000; // 启动后 2s 开始首次连接
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
|
||||
/* V1.10: 上电后台查询一次 Loop 版本 (0x4A), 缓存供 dev_info_query/initialize */
|
||||
_iot_ver_cache_dirty = 1;
|
||||
|
||||
/* 初始化硬件看门狗 IWDG (LSI≈40kHz, 4s 超时, 主循环喂狗) */
|
||||
iot_watchdog_init();
|
||||
}
|
||||
|
||||
@@ -509,6 +509,31 @@ void lup_send_get_version(void)
|
||||
lup_cmd_begin(LUP_CMD_GET_VERSION, 200);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 版本缓存 (V1.10) — 0x4A 查询结果缓存, 供 MQTT dev_info_query/initialize
|
||||
*===========================================================================*/
|
||||
LoopVerCache g_lup_ver_cache; /* BSS 清零: valid=0 */
|
||||
|
||||
void lup_cache_from_info(const LUP_VersionInfo *info)
|
||||
{
|
||||
if (!info) return;
|
||||
g_lup_ver_cache.valid = 1;
|
||||
g_lup_ver_cache.hard_main = info->hard_main;
|
||||
g_lup_ver_cache.hard_sub = info->hard_sub;
|
||||
g_lup_ver_cache.hard_ssub = info->hard_ssub;
|
||||
g_lup_ver_cache.soft_main = info->soft_main;
|
||||
g_lup_ver_cache.soft_sub = info->soft_sub;
|
||||
g_lup_ver_cache.soft_ssub = info->soft_ssub;
|
||||
}
|
||||
|
||||
/* 后台刷新: 仅命令通道空闲时发起 (g_lup_cmd 全局唯一, 与 TCP/MQTT 命令共用
|
||||
单通道; 有命令在途则跳过本轮, 下次空闲再发) */
|
||||
void lup_refresh_version_cache(void)
|
||||
{
|
||||
if (g_lup_cmd.state != LUP_STATE_IDLE) return;
|
||||
lup_send_get_version();
|
||||
}
|
||||
|
||||
void lup_send_reset(void)
|
||||
{
|
||||
uint8_t buf[LUP_MAX_PKG_LEN];
|
||||
|
||||
@@ -346,6 +346,13 @@ void WCHNET_CreateTcpMqttSocket(void)
|
||||
{
|
||||
PRINT("WCHNET_MQTT_SocketCreate %d,desIP:%d.%d.%d.%d, des_port:%d, srcport:%d\r\n", SocketId_TCP, RemoteIP[0],RemoteIP[1],RemoteIP[2],RemoteIP[3], iot_net_info.mqtt_port, TmpSocketInf.SourPort);
|
||||
}
|
||||
if (i != WCHNET_ERR_SUCCESS) {
|
||||
/* 2026-08-20: SocketCreat 失败 (0x1D ISCONN 等, 旧 socket 未清干净)
|
||||
置无效并返回, 由上层退避重试 — 继续 Connect 只会等超时死循环 */
|
||||
PRINT("MQTT: SocketCreat failed 0x%02X, abort connect\r\n", i);
|
||||
SocketId_TCP = 0xFF;
|
||||
return;
|
||||
}
|
||||
mStopIfError(i);
|
||||
i = WCHNET_SocketConnect(SocketId_TCP); //make a TCP connection
|
||||
if(i == WCHNET_ERR_SUCCESS)
|
||||
|
||||
@@ -342,6 +342,28 @@ void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value)
|
||||
offlog_evt(OFFLOG_EVT_COIL, p, 6);
|
||||
}
|
||||
|
||||
void offlog_ota_start(uint8_t slot, uint32_t size)
|
||||
{
|
||||
uint8_t p[6];
|
||||
p[0] = slot;
|
||||
p[1] = 0; /* target: 0=loop */
|
||||
p[2] = (uint8_t)(size >> 24);
|
||||
p[3] = (uint8_t)(size >> 16);
|
||||
p[4] = (uint8_t)(size >> 8);
|
||||
p[5] = (uint8_t)size;
|
||||
offlog_evt(OFFLOG_EVT_OTA_START, p, 6);
|
||||
}
|
||||
|
||||
void offlog_ota_result(uint32_t code)
|
||||
{
|
||||
uint8_t p[4];
|
||||
p[0] = (uint8_t)(code >> 24);
|
||||
p[1] = (uint8_t)(code >> 16);
|
||||
p[2] = (uint8_t)(code >> 8);
|
||||
p[3] = (uint8_t)code;
|
||||
offlog_evt(OFFLOG_EVT_OTA_RESULT, p, 4);
|
||||
}
|
||||
|
||||
void offlog_time_anchor(uint32_t unix_ts)
|
||||
{
|
||||
offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */
|
||||
@@ -398,6 +420,8 @@ const char *offlog_type_str(uint8_t type)
|
||||
case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup";
|
||||
case OFFLOG_EVT_COIL: return "coil";
|
||||
case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor";
|
||||
case OFFLOG_EVT_OTA_START: return "ota_start";
|
||||
case OFFLOG_EVT_OTA_RESULT: return "ota_result";
|
||||
case OFFLOG_EVT_LOG_CLEAR: return "log_clear";
|
||||
default: return "unknown";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,633 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file ota_srv.c
|
||||
* @author wangfq
|
||||
* @version V1.0
|
||||
* @date 2026-08-20
|
||||
* @brief DLD960 Loop MCU 远程 OTA 服务 (MQTT 协议 V1.08, ROADMAP P1.4 ①)
|
||||
*
|
||||
* 先存后刷:
|
||||
* 阶段1 下载: MQTT 分片 (256B/片, 单片+全镜像 CRC32) → W25Qxx 暂存区
|
||||
* 阶段2 校验: ota_end 全镜像 CRC32 复核 → state=ready
|
||||
* 阶段3 刷写: ota_flash → 本地 0x9F ISP 透传 (A5/A6/A7 停等协议),
|
||||
* 非阻塞 tick 驱动挂 ota_poll(), 刷写窗口内 MQTT 保活不受影响
|
||||
*
|
||||
* 会话静默: downloading/flashing 期间 event_report 暂停发送 (队列积压,
|
||||
* 结束后补发) + offlog/快照落盘暂停 ("升级开始"日志暂停前写,
|
||||
* "升级结果"恢复后补记)
|
||||
*
|
||||
* 协议依据: docs/DLD960_IoT_MQTT协议.md §4.19~4.24 / §5.5
|
||||
* 设计稿: docs/DLD960_MQTT_OTA协议.md V1.01
|
||||
******************************************************************************
|
||||
*/
|
||||
#include "CONFIG.h"
|
||||
#include "cmcng.h"
|
||||
#include "ota_srv.h"
|
||||
#include "offlog.h"
|
||||
#include "storage.h"
|
||||
#include "loop_uart_proto.h"
|
||||
#include <string.h>
|
||||
|
||||
/*===========================================================================
|
||||
* 本地状态
|
||||
*===========================================================================*/
|
||||
static OtaMeta _meta; /* 当前会话元数据 (RAM 副本) */
|
||||
static uint32_t _slot_base; /* 当前 Slot 数据区基址 */
|
||||
static uint32_t _meta_watermark; /* 上次落盘元数据时的 received (节流) */
|
||||
static uint8_t _init_done; /* ota_init 完成 */
|
||||
|
||||
/* 刷写状态机 (非阻塞 tick 驱动) */
|
||||
static uint8_t _fs; /* OTA_FLASH_* */
|
||||
static uint8_t _fs_retry; /* 当前步重试计数 */
|
||||
static uint32_t _fs_start_ms; /* 当前步起始时刻 (超时判断) */
|
||||
static uint16_t _fs_sub; /* 当前块序号 (首=总块数, 末=1) */
|
||||
static uint32_t _fs_sent; /* 已送 Loop 字节数 */
|
||||
|
||||
uint8_t g_ota_flash_active = 0; /* 本地刷写进行中 → uart_srv 把 ACK 交给 ota_srv */
|
||||
|
||||
/* 块缓冲 (static: 栈紧张教训; union: 下载片 256B 与 A7 帧 254B 互斥复用, 省 ~260B RAM) */
|
||||
static union {
|
||||
uint8_t chunk[OTA_CHUNK_SIZE]; /* 下载片缓冲 / 校验读缓冲 */
|
||||
uint8_t frame[5 + OTA_FLASH_BLOCK + 1]; /* A7 帧: 9F+SubL+SubH+LEN+CMD+DATA+CHECK */
|
||||
} _ota_buf;
|
||||
|
||||
/*===========================================================================
|
||||
* CRC-32/ISO-HDLC (协议 §4.19.1; 逐位实现, 零查表 RAM)
|
||||
* poly=0x04C11DB7 (reflected 0xEDB88320), init=0xFFFFFFFF, xorout=0xFFFFFFFF
|
||||
* 校验向量: crc32("123456789") = 0xCBF43926
|
||||
*===========================================================================*/
|
||||
/* 增量更新: 调用方初始 crc=0xFFFFFFFF, 分块续算, 最后 ^0xFFFFFFFF
|
||||
(全镜像 96KB 校验分块读, 不能一次入 RAM) */
|
||||
static uint32_t ota_crc32_update(uint32_t crc, const uint8_t *data, uint32_t len)
|
||||
{
|
||||
uint32_t i;
|
||||
uint8_t b;
|
||||
for (i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (b = 0; b < 8; b++) {
|
||||
crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
uint32_t ota_crc32(const uint8_t *data, uint32_t len)
|
||||
{
|
||||
return ota_crc32_update(0xFFFFFFFFu, data, len) ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* hex 工具
|
||||
*===========================================================================*/
|
||||
static uint8_t ota_hex_val(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
|
||||
if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10);
|
||||
if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10);
|
||||
return 0xFF;
|
||||
}
|
||||
|
||||
int ota_hex_decode(const char *hex, uint8_t *out, uint32_t n)
|
||||
{
|
||||
uint32_t i;
|
||||
for (i = 0; i < n; i++) {
|
||||
uint8_t hi = ota_hex_val(hex[i * 2]);
|
||||
uint8_t lo = ota_hex_val(hex[i * 2 + 1]);
|
||||
if (hi == 0xFF || lo == 0xFF) return -1;
|
||||
out[i] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 从 MQTT JSON 提取 ota_data 的 data 字段 hex 值。
|
||||
⚠ 2026-08-20 事故: json.dumps 默认带空格 ("data": "hex"),
|
||||
strstr("\"data\":\"") 无空格匹配 → 提取失败 → 单片 CRC 全错。
|
||||
本函数扫描 "data" 键后跳过任意空格/冒号, 兼容两种格式。 */
|
||||
int ota_json_extract_hex(const char *json, char *out, int out_len)
|
||||
{
|
||||
const char *p = json;
|
||||
int n = 0;
|
||||
if (!json || !out || out_len <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
while (*p) {
|
||||
if (strncmp(p, "\"data\"", 6) == 0) {
|
||||
const char *q = p + 6;
|
||||
while (*q == ' ' || *q == '\t') q++;
|
||||
if (*q == ':') {
|
||||
q++;
|
||||
while (*q == ' ' || *q == '\t') q++;
|
||||
if (*q == '"') {
|
||||
q++; /* 跳过起始引号 */
|
||||
while (*q && *q != '"' && n < out_len - 1) {
|
||||
out[n++] = *q;
|
||||
q++;
|
||||
}
|
||||
out[n] = '\0';
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
p++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 元数据读写 (双备份, 同扇区 0x010000~0x0103FF)
|
||||
* 写: 显式擦扇区 (45ms) + NoCheck 写主 + 写备份
|
||||
* 读: 主 magic 有效优先, 否则读备份; 双无效 → 默认 IDLE
|
||||
*===========================================================================*/
|
||||
static void ota_meta_write(const OtaMeta *m)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
memset(buf, 0xFF, sizeof(buf));
|
||||
memcpy(buf, m, sizeof(OtaMeta));
|
||||
|
||||
SPI_Flash_Erase_Sector((uint32_t)OTA_META_SECTOR);
|
||||
SPI_Flash_Write_NoCheck(buf, OTA_META_BASE, 64);
|
||||
SPI_Flash_Write_NoCheck(buf, OTA_META_BACKUP, 64);
|
||||
PRINT("OTA: meta write state=%lu received=%lu\n",
|
||||
(unsigned long)m->state, (unsigned long)m->received);
|
||||
}
|
||||
|
||||
static int ota_meta_read(OtaMeta *m)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
memset(buf, 0xFF, sizeof(buf));
|
||||
SPI_Flash_Read(buf, OTA_META_BASE, 64);
|
||||
memcpy(m, buf, sizeof(OtaMeta));
|
||||
if (m->magic == OTA_MAGIC) return 0;
|
||||
memset(buf, 0xFF, sizeof(buf));
|
||||
SPI_Flash_Read(buf, OTA_META_BACKUP, 64);
|
||||
memcpy(m, buf, sizeof(OtaMeta));
|
||||
if (m->magic == OTA_MAGIC) return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 节流落盘: 每累计 4KB 或关键节点写元数据 (擦 45ms 不能每片做) */
|
||||
static void ota_meta_flush(void)
|
||||
{
|
||||
if (_meta.received - _meta_watermark >= 4096u) {
|
||||
ota_meta_write(&_meta);
|
||||
_meta_watermark = _meta.received;
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 状态工具
|
||||
*===========================================================================*/
|
||||
const char *ota_state_str(uint8_t state)
|
||||
{
|
||||
static const char *names[] = {
|
||||
"idle", "downloading", "ready", "flashing", "flash_failed", "aborted"
|
||||
};
|
||||
return (state < 6) ? names[state] : "idle";
|
||||
}
|
||||
|
||||
uint8_t ota_silent_active(void)
|
||||
{
|
||||
return (_meta.state == OTA_STATE_DOWNLOADING || _meta.state == OTA_STATE_FLASHING);
|
||||
}
|
||||
|
||||
const OtaMeta *ota_meta(void)
|
||||
{
|
||||
return &_meta;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 初始化
|
||||
*===========================================================================*/
|
||||
void ota_init(void)
|
||||
{
|
||||
OtaMeta m;
|
||||
memset(&m, 0, sizeof(m));
|
||||
if (ota_meta_read(&m) == 0) {
|
||||
/* 恢复上次会话状态 (掉电/复位后) */
|
||||
_meta = m;
|
||||
if (_meta.state == OTA_STATE_DOWNLOADING) {
|
||||
/* 下载中断: 保持 downloading, 平台 ota_begin 时续传 */
|
||||
PRINT("OTA: resume downloading received=%lu\n", (unsigned long)_meta.received);
|
||||
} else if (_meta.state == OTA_STATE_FLASHING) {
|
||||
/* 刷写中断 (掉电): 置失败态, 保留镜像可重试 */
|
||||
_meta.state = OTA_STATE_FLASH_FAILED;
|
||||
_meta.last_result = OTA_ERR_READY_TIMEOUT;
|
||||
PRINT("OTA: flash interrupted on boot, mark failed\n");
|
||||
ota_meta_write(&_meta);
|
||||
}
|
||||
/* ready/aborted/flash_failed/idle: 原样保留 */
|
||||
} else {
|
||||
memset(&_meta, 0, sizeof(_meta));
|
||||
_meta.magic = OTA_MAGIC;
|
||||
_meta.state = OTA_STATE_IDLE;
|
||||
_meta.slot = 0;
|
||||
PRINT("OTA: fresh meta (idle)\n");
|
||||
}
|
||||
_slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE;
|
||||
_meta_watermark = _meta.received;
|
||||
_fs = OTA_FLASH_IDLE;
|
||||
_init_done = 1;
|
||||
PRINT("OTA: init done state=%s slot=%d received=%lu\n",
|
||||
ota_state_str(_meta.state), _meta.slot, (unsigned long)_meta.received);
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 会话命令 — ota_begin (开启会话 / 断点续传定位)
|
||||
* 返回: 0=成功 (data.offset = _meta.received 或 size), 非0=错误
|
||||
*===========================================================================*/
|
||||
int ota_cmd_begin(uint32_t size, uint32_t crc32, const char *version, uint8_t force)
|
||||
{
|
||||
(void)force; /* 版本冲突判断暂不校验 (bootloader 不校验版本), 预留 */
|
||||
if (size == 0 || size > OTA_MAX_IMAGE) {
|
||||
return 1; /* 参数错误 */
|
||||
}
|
||||
|
||||
/* 已有镜像且 size+crc32 一致 → 续传或直接 ready */
|
||||
if (_meta.magic == OTA_MAGIC && _meta.size == size && _meta.crc32 == crc32) {
|
||||
_slot_base = (_meta.slot == 0) ? OTA_SLOT_A_BASE : OTA_SLOT_B_BASE;
|
||||
if (_meta.state == OTA_STATE_READY) {
|
||||
/* 镜像已就绪, 平台可直接 ota_flash */
|
||||
return 0; /* data.offset = size */
|
||||
}
|
||||
if (_meta.state == OTA_STATE_IDLE) {
|
||||
/* V1.09: 刷写完成后的 idle 且镜像一致 → 直接回 ready, 免下载可重刷 */
|
||||
_meta.state = OTA_STATE_READY;
|
||||
ota_meta_write(&_meta);
|
||||
PRINT("OTA: begin idle+same image → ready (reflash)\n");
|
||||
return 0; /* data.offset = size */
|
||||
}
|
||||
if (_meta.state == OTA_STATE_DOWNLOADING) {
|
||||
_meta.state = OTA_STATE_DOWNLOADING;
|
||||
PRINT("OTA: begin resume received=%lu\n", (unsigned long)_meta.received);
|
||||
return 0; /* data.offset = received */
|
||||
}
|
||||
/* flash_failed / aborted: 重新走下载 */
|
||||
}
|
||||
|
||||
/* 新会话: 始终用 Slot A (B 保留上一版镜像做回滚) */
|
||||
_slot_base = OTA_SLOT_A_BASE;
|
||||
memset(&_meta, 0, sizeof(_meta));
|
||||
_meta.magic = OTA_MAGIC;
|
||||
_meta.state = OTA_STATE_DOWNLOADING;
|
||||
_meta.target = 0;
|
||||
_meta.slot = 0;
|
||||
_meta.size = size;
|
||||
_meta.crc32 = crc32;
|
||||
_meta.received = 0;
|
||||
_meta.last_result = 0;
|
||||
_meta.begin_ts = 0;
|
||||
memset(_meta.version, 0, sizeof(_meta.version));
|
||||
if (version) strncpy(_meta.version, version, sizeof(_meta.version) - 1);
|
||||
_meta.flash_cnt = 0;
|
||||
_meta_watermark = 0;
|
||||
/* 先擦目标 Slot 数据区 (100KB = 25 扇区 ~1.1s, 主循环上下文可接受 < IWDG 4s) */
|
||||
{
|
||||
uint32_t sec;
|
||||
for (sec = 0; sec < OTA_SLOT_SIZE / 4096UL; sec++) {
|
||||
SPI_Flash_Erase_Sector((uint32_t)(_slot_base / 4096UL) + sec);
|
||||
}
|
||||
}
|
||||
ota_meta_write(&_meta);
|
||||
PRINT("OTA: begin new session size=%lu crc32=0x%08lX slot=%d\n",
|
||||
(unsigned long)size, (unsigned long)crc32, _meta.slot);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 会话命令 — ota_data (分片下发, 256B/片)
|
||||
* 返回: 0=成功 (幂等), 1=参数/CRC/乱序 (err_code 由调用方区分), 3=会话状态错
|
||||
*===========================================================================*/
|
||||
int ota_cmd_data(uint32_t offset, uint32_t crc32, const char *hex)
|
||||
{
|
||||
uint32_t clen;
|
||||
|
||||
if (_meta.state != OTA_STATE_DOWNLOADING) {
|
||||
return 3; /* 会话状态不允许 (err_code=2 由调用方映射) */
|
||||
}
|
||||
if (offset % OTA_CHUNK_SIZE != 0 || offset >= _meta.size) {
|
||||
return 1; /* 参数错误 */
|
||||
}
|
||||
if (offset < _meta.received) {
|
||||
return 0; /* 重复片: 幂等 */
|
||||
}
|
||||
if (offset > _meta.received) {
|
||||
return 1; /* 乱序缺片: 调用方用 data.offset=received 指示续传 */
|
||||
}
|
||||
|
||||
clen = _meta.size - offset;
|
||||
if (clen > OTA_CHUNK_SIZE) clen = OTA_CHUNK_SIZE;
|
||||
|
||||
if (ota_hex_decode(hex, _ota_buf.chunk, clen) != 0) {
|
||||
return 1; /* hex 格式错误 */
|
||||
}
|
||||
if (ota_crc32(_ota_buf.chunk, clen) != crc32) {
|
||||
return 1; /* 单片 CRC 失败 */
|
||||
}
|
||||
|
||||
/* 落盘: SPI_Flash_Write 自带"目标区非 0xFF 则读-改-写擦扇区" (256B 页对齐) */
|
||||
SPI_Flash_Write(_ota_buf.chunk, _slot_base + offset, (uint16_t)clen);
|
||||
_meta.received = offset + clen;
|
||||
ota_meta_flush();
|
||||
PRINT("OTA: data@%lu crc_ok received=%lu\n",
|
||||
(unsigned long)offset, (unsigned long)_meta.received);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 会话命令 — ota_end (全镜像 CRC32 复核)
|
||||
*===========================================================================*/
|
||||
int ota_cmd_end(uint32_t crc32)
|
||||
{
|
||||
if (_meta.state != OTA_STATE_DOWNLOADING) {
|
||||
return 3;
|
||||
}
|
||||
if (_meta.received != _meta.size) {
|
||||
return 1; /* 不完整: data.offset=received 续传 */
|
||||
}
|
||||
if (crc32 != _meta.crc32) {
|
||||
return 1; /* 声明值不一致 */
|
||||
}
|
||||
|
||||
/* 读回暂存区计算全镜像 CRC32 (分块增量, 96KB ≈ 40ms @120MHz, 主循环上下文) */
|
||||
{
|
||||
uint32_t total = 0xFFFFFFFFu;
|
||||
uint32_t i = 0;
|
||||
while (i < _meta.size) {
|
||||
uint32_t n = _meta.size - i;
|
||||
if (n > OTA_CHUNK_SIZE) n = OTA_CHUNK_SIZE;
|
||||
SPI_Flash_Read(_ota_buf.chunk, _slot_base + i, (uint16_t)n);
|
||||
total = ota_crc32_update(total, _ota_buf.chunk, n);
|
||||
i += n;
|
||||
}
|
||||
total ^= 0xFFFFFFFFu;
|
||||
if (total != _meta.crc32) {
|
||||
_meta.state = OTA_STATE_DOWNLOADING; /* 保留已下载数据, 可重发错片 */
|
||||
PRINT("OTA: end CRC mismatch 0x%08lX != 0x%08lX\n",
|
||||
(unsigned long)total, (unsigned long)_meta.crc32);
|
||||
return 5; /* 内部错误 + data.crc_ok=false */
|
||||
}
|
||||
}
|
||||
|
||||
_meta.state = OTA_STATE_READY;
|
||||
_meta.last_result = 0;
|
||||
ota_meta_write(&_meta);
|
||||
PRINT("OTA: end OK crc_ok → ready (slot %d)\n", _meta.slot);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 会话命令 — ota_abort
|
||||
*===========================================================================*/
|
||||
int ota_cmd_abort(void)
|
||||
{
|
||||
if (_meta.state == OTA_STATE_IDLE) {
|
||||
return 0; /* 无会话: 幂等 */
|
||||
}
|
||||
_meta.state = OTA_STATE_ABORTED;
|
||||
ota_meta_write(&_meta);
|
||||
PRINT("OTA: abort → aborted\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 会话命令 — ota_flash (触发本地 ISP 刷写, 同步检查 + 异步执行)
|
||||
* 安全窗口: 4 通道任一有车 → 拒绝 (err_code=3); force 跳过
|
||||
*===========================================================================*/
|
||||
int ota_cmd_flash(uint8_t slot, uint8_t force)
|
||||
{
|
||||
uint8_t any_car;
|
||||
extern uint8_t iot_any_car(void);
|
||||
|
||||
if (_meta.state != OTA_STATE_READY) {
|
||||
return 3; /* 非 ready: 先 ota_end */
|
||||
}
|
||||
if (slot != 0 && slot != 1) {
|
||||
return 1;
|
||||
}
|
||||
if (slot != _meta.slot) {
|
||||
/* 指定了非当前 ready 槽: 读该槽元数据? 简化: 仅允许当前槽 */
|
||||
return 1;
|
||||
}
|
||||
/* 与 BLE OTA 透传互斥: g_flag_counter_ota.flag=1 且非本地刷写 → 拒绝 */
|
||||
if (g_flag_counter_ota.flag != 0 && !g_ota_flash_active) {
|
||||
return 3;
|
||||
}
|
||||
/* 安全窗口: 有车压线圈拒绝 (force 跳过) */
|
||||
any_car = iot_any_car();
|
||||
if (!force && any_car) {
|
||||
PRINT("OTA: flash rejected, car on loop\n");
|
||||
return 3;
|
||||
}
|
||||
|
||||
/* 启动刷写 (异步): offlog 开始事件先写 (会话静默前) */
|
||||
offlog_ota_start(_meta.slot, _meta.size);
|
||||
_meta.state = OTA_STATE_FLASHING;
|
||||
_meta.flash_cnt++;
|
||||
_meta.last_result = 0;
|
||||
ota_meta_write(&_meta);
|
||||
|
||||
_fs = OTA_FLASH_START;
|
||||
_fs_retry = 0;
|
||||
_fs_sent = 0;
|
||||
_fs_sub = 0;
|
||||
g_ota_flash_active = 1;
|
||||
g_flag_counter_ota.flag = 1; /* UART2 切 0x9F 解析 */
|
||||
g_flag_counter_ota.tick = 0;
|
||||
PRINT("OTA: flash start slot=%d size=%lu (force=%d)\n",
|
||||
_meta.slot, (unsigned long)_meta.size, force);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 刷写状态机 (非阻塞 tick 驱动, ota_poll 每主循环调用)
|
||||
*
|
||||
* A5 启动 → pre_ok → A6 地址 → addr_ok → A7 数据块×N → 末块(序号=1) → 完成
|
||||
* 每步超时 1s 重试 ×3; 失败 → state=flash_failed + event_report ota_error
|
||||
*===========================================================================*/
|
||||
|
||||
/* 0x9F 帧发送: 9F SubL SubH LEN CMD DATA... CHECK(SUM 不含 magic) */
|
||||
static void ota_send_9f(uint8_t sub_l, uint8_t sub_h, uint8_t cmd,
|
||||
const uint8_t *data, uint16_t datalen)
|
||||
{
|
||||
uint16_t i;
|
||||
uint8_t sum = 0;
|
||||
_ota_buf.frame[0] = 0x9F;
|
||||
_ota_buf.frame[1] = sub_l;
|
||||
_ota_buf.frame[2] = sub_h;
|
||||
_ota_buf.frame[3] = (uint8_t)(1 + datalen); /* LEN 含 CMD */
|
||||
_ota_buf.frame[4] = cmd;
|
||||
if (data && datalen) memcpy(&_ota_buf.frame[5], data, datalen);
|
||||
for (i = 1; i < 5 + datalen; i++) sum += _ota_buf.frame[i]; /* SubL..DATA */
|
||||
_ota_buf.frame[5 + datalen] = sum;
|
||||
UART2_SendString(_ota_buf.frame, (uint16_t)(5 + datalen + 1));
|
||||
}
|
||||
|
||||
/* 刷写失败收尾 (V1.09): 状态落盘 + ota_report failed + event_report 告警 + 恢复 0x7F 模式 */
|
||||
static void ota_flash_fail(uint32_t err)
|
||||
{
|
||||
PRINT("OTA: flash FAILED err=0x%lX\n", (unsigned long)err);
|
||||
_meta.state = OTA_STATE_FLASH_FAILED;
|
||||
_meta.last_result = err;
|
||||
ota_meta_write(&_meta);
|
||||
offlog_ota_result(err);
|
||||
/* 恢复 UART2 0x7F 模式 */
|
||||
g_ota_flash_active = 0;
|
||||
g_flag_counter_ota.flag = 0;
|
||||
lup_frame_reset();
|
||||
_fs = OTA_FLASH_IDLE;
|
||||
/* ota_report stage=failed (平台判定主依据, V1.09) */
|
||||
extern void iot_ota_report_result(uint8_t ok, uint32_t err);
|
||||
iot_ota_report_result(0, err);
|
||||
/* event_report ota_error 告警 (平台必答, 由 iot_mqtt_srv 发送) */
|
||||
extern void iot_evt_report_ota_error(uint32_t err);
|
||||
iot_evt_report_ota_error(err);
|
||||
}
|
||||
|
||||
/* 刷写成功收尾 (V1.09): 状态回 idle 但镜像保留 (可重刷, last_result=0),
|
||||
上报 ota_report stage=done (平台判定主依据), 恢复 0x7F 模式 */
|
||||
static void ota_flash_done(void)
|
||||
{
|
||||
PRINT("OTA: flash DONE sent=%lu\n", (unsigned long)_fs_sent);
|
||||
_meta.state = OTA_STATE_IDLE; /* V1.09: idle+size>0 = 已刷写完成 (区别于 ready=待刷) */
|
||||
_meta.last_result = 0;
|
||||
ota_meta_write(&_meta);
|
||||
offlog_ota_result(0);
|
||||
g_ota_flash_active = 0;
|
||||
g_flag_counter_ota.flag = 0;
|
||||
lup_frame_reset();
|
||||
_fs = OTA_FLASH_IDLE;
|
||||
/* ota_report stage=done (平台判定主依据, V1.09) */
|
||||
extern void iot_ota_report_result(uint8_t ok, uint32_t err);
|
||||
iot_ota_report_result(1, 0);
|
||||
}
|
||||
|
||||
/* UART2 收到 bootloader 0x9F ACK 帧 (uart_srv OTA 分支调用, 主循环上下文)
|
||||
* ACK 帧: 9F SubL SubH 02 CMD Status CHECK
|
||||
* pre_ok = 9F 01 00 02 A5 00 A8
|
||||
* addr_ok = 9F 01 00 02 A6 00 A9 | addr_err = 9F 01 00 02 A6 01 AA
|
||||
* data ack= 9F SubL SubH 02 A7 00 CK | data err = 9F SubL SubH 02 A7 01 CK */
|
||||
void ota_flash_feed_ack(const uint8_t *pkg, uint16_t len)
|
||||
{
|
||||
uint8_t cmd, status;
|
||||
if (len < 7 || pkg[0] != 0x9F) return;
|
||||
if (pkg[3] != 0x02) return; /* ACK 帧 LEN=2 */
|
||||
cmd = pkg[4];
|
||||
status = pkg[5];
|
||||
PRINT("OTA: ack cmd=0x%02X status=%d sub=%u\n", cmd, status, pkg[1]);
|
||||
|
||||
switch (_fs) {
|
||||
case OTA_FLASH_WAIT_READY:
|
||||
if (cmd == 0xA5 && status == 0x00) {
|
||||
_fs = OTA_FLASH_ADDR;
|
||||
_fs_retry = 0;
|
||||
}
|
||||
break;
|
||||
case OTA_FLASH_WAIT_ADDR:
|
||||
if (cmd == 0xA6 && status == 0x00) {
|
||||
/* 地址 OK → 进入数据阶段: 首块序号 = 总块数 (末块=1) */
|
||||
uint32_t blocks = (_meta.size + OTA_FLASH_BLOCK - 1) / OTA_FLASH_BLOCK;
|
||||
_fs_sub = (uint16_t)(blocks > 0xFFFF ? 0xFFFF : blocks);
|
||||
_fs_sent = 0;
|
||||
_fs = OTA_FLASH_DATA;
|
||||
_fs_retry = 0;
|
||||
} else if (cmd == 0xA6 && status != 0x00) {
|
||||
ota_flash_fail(OTA_ERR_ADDR_FAIL);
|
||||
}
|
||||
break;
|
||||
case OTA_FLASH_WAIT_ACK:
|
||||
if (cmd == 0xA7) {
|
||||
if (status == 0x00) {
|
||||
_fs_sent += OTA_FLASH_BLOCK;
|
||||
if (_fs_sent >= _meta.size) _fs_sent = _meta.size;
|
||||
_fs_retry = 0;
|
||||
if (_fs_sub <= 1) {
|
||||
/* 末块已 ACK → 完成 (bootloader 清 flag 复位跑新 APP) */
|
||||
ota_flash_done();
|
||||
} else {
|
||||
_fs_sub--; /* 序号递减: 首=总块数, 末=1 */
|
||||
_fs = OTA_FLASH_DATA;
|
||||
}
|
||||
} else {
|
||||
/* 数据块错误: 重发同块 (序号不变) */
|
||||
_fs_retry++;
|
||||
if (_fs_retry > 3) {
|
||||
ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT);
|
||||
} else {
|
||||
_fs = OTA_FLASH_DATA;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ota_flash_sent(void) { return _fs_sent; }
|
||||
uint32_t ota_flash_total(void) { return _meta.size; }
|
||||
|
||||
/* 主循环轮询: 推进刷写状态机 (每轮最多发 1~2 块, 绝不阻塞) */
|
||||
void ota_poll(void)
|
||||
{
|
||||
uint32_t now;
|
||||
|
||||
if (!_init_done || _fs == OTA_FLASH_IDLE) return;
|
||||
|
||||
now = mstick();
|
||||
if (_fs == OTA_FLASH_START) {
|
||||
/* 发 A5 启动帧: 9F 01 00 01 A5 A7 → Loop APP 写 flag 复位 → bootloader 回 pre_ok */
|
||||
static const uint8_t start_frame[6] = {0x9F, 0x01, 0x00, 0x01, 0xA5, 0xA7};
|
||||
UART2_SendString((uint8_t *)start_frame, 6);
|
||||
_fs = OTA_FLASH_WAIT_READY;
|
||||
_fs_start_ms = now;
|
||||
/* 重试计数保持 (首发由 ota_cmd_flash 置 0; 重发不清零, 否则超时重试永不失败) */
|
||||
PRINT("OTA: sent A5 start\n");
|
||||
return;
|
||||
}
|
||||
if (_fs == OTA_FLASH_WAIT_READY) {
|
||||
if (now - _fs_start_ms > 1000u) {
|
||||
_fs_retry++;
|
||||
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_READY_TIMEOUT); return; }
|
||||
_fs = OTA_FLASH_START; /* 重发启动帧 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_fs == OTA_FLASH_ADDR) {
|
||||
/* 发 A6 地址帧: 9F 01 00 05 A6 08 00 34 00 SUM (0x08003400 大端) */
|
||||
uint8_t addr[4] = {0x08, 0x00, 0x34, 0x00}; /* APP_START_ADDR */
|
||||
ota_send_9f(0x01, 0x00, 0xA6, addr, 4);
|
||||
_fs = OTA_FLASH_WAIT_ADDR;
|
||||
_fs_start_ms = now;
|
||||
/* 重试计数保持 (ACK 成功路径在 ota_flash_feed_ack 清 0) */
|
||||
return;
|
||||
}
|
||||
if (_fs == OTA_FLASH_WAIT_ADDR) {
|
||||
if (now - _fs_start_ms > 1000u) {
|
||||
_fs_retry++;
|
||||
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_ADDR_FAIL); return; }
|
||||
_fs = OTA_FLASH_ADDR; /* 重发地址帧 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_fs == OTA_FLASH_DATA) {
|
||||
/* 从 W25Qxx 读一块 → 组 A7 帧 → 发 (停等 ACK) */
|
||||
uint32_t read_off = _fs_sent;
|
||||
uint32_t remain = _meta.size - read_off;
|
||||
uint16_t datalen = (remain > OTA_FLASH_BLOCK) ? OTA_FLASH_BLOCK : (uint16_t)remain;
|
||||
SPI_Flash_Read(_ota_buf.frame + 5, _slot_base + read_off, datalen);
|
||||
ota_send_9f((uint8_t)(_fs_sub & 0xFF), (uint8_t)(_fs_sub >> 8), 0xA7,
|
||||
_ota_buf.frame + 5, datalen);
|
||||
_fs = OTA_FLASH_WAIT_ACK;
|
||||
_fs_start_ms = now;
|
||||
PRINT("OTA: sent A7 sub=%u off=%lu datalen=%u\n",
|
||||
_fs_sub, (unsigned long)read_off, datalen);
|
||||
return;
|
||||
}
|
||||
if (_fs == OTA_FLASH_WAIT_ACK) {
|
||||
if (now - _fs_start_ms > 1000u) {
|
||||
_fs_retry++;
|
||||
if (_fs_retry > 3) { ota_flash_fail(OTA_ERR_DATA_ACK_LIMIT); return; }
|
||||
_fs = OTA_FLASH_DATA; /* 重发同块 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "tcp_json_srv.h"
|
||||
#include "loop_uart_proto.h"
|
||||
#include "iot_mqtt_srv.h"
|
||||
#include "ota_srv.h"
|
||||
#include "fault_diag.h"
|
||||
|
||||
/*********************************************************************
|
||||
@@ -274,6 +275,7 @@ void Main_Circulation(void)
|
||||
FAULT_MARKER(MK_UART_SRV_IN);
|
||||
uart_srv();
|
||||
FAULT_MARKER(MK_UART_SRV_OUT);
|
||||
ota_poll(); /* OTA 刷写状态机 (V1.08): 非阻塞 tick 驱动, 先消费 UART2 ACK 再推进 */
|
||||
snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */
|
||||
FAULT_MARKER(MK_POLL_BLE_IN);
|
||||
poll_dbn_ble();
|
||||
@@ -328,6 +330,8 @@ int main(void)
|
||||
PRINT("INIT: storage ok\n");
|
||||
offlog_init();
|
||||
PRINT("INIT: offlog ok\n");
|
||||
ota_init(); /* OTA 元数据恢复 (V1.08): 下载中断续传 / 刷写中断标记失败 */
|
||||
PRINT("INIT: ota ok\n");
|
||||
/* 快照区: 延后初始化 (2026-08-17) — snap_init 移到主循环 3s 后
|
||||
(snap_delayed_init), 避开启动早期 SPI 重负载窗口 (擦+整扇区写回 ~80ms);
|
||||
3s 内的传感帧由 snap_enqueue/snap_flush 的 !_ready 门控自动丢弃不落盘 */
|
||||
|
||||
@@ -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
|
||||
@@ -228,8 +228,10 @@ u16 SPI_Flash_ReadID(void)
|
||||
/*********************************************************************
|
||||
* @fn SPI_Flash_ReadJEDEC_ID
|
||||
*
|
||||
* @brief Read JEDEC ID (0x9F). Return 3rd byte (device id).
|
||||
* W25Q32=0x16 Q64=0x17 Q128=0x18 Q256=0x19; 0 if not Winbond.
|
||||
* @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
|
||||
*/
|
||||
@@ -244,7 +246,8 @@ uint8_t SPI_Flash_ReadJEDEC_ID(void)
|
||||
id[2] = SPI1_ReadWriteByte(0xFF);
|
||||
GPIO_WriteBit(GPIOA, GPIO_Pin_4, 1);
|
||||
|
||||
if (id[0] != 0xEF || id[1] != 0x40) return 0;
|
||||
/* 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];
|
||||
}
|
||||
|
||||
@@ -505,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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "loop_uart_proto.h"
|
||||
#include <string.h>
|
||||
#include "dbn_ble_srv.h"
|
||||
#include "ota_srv.h"
|
||||
|
||||
void USART1_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
|
||||
void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
|
||||
@@ -291,10 +292,14 @@ void uart_srv(void)
|
||||
}
|
||||
}
|
||||
else{
|
||||
/* OTA 模式 (2026-08-19): Loop bootloader 回的 0x9F 帧
|
||||
(back_9F_pre_ok / addr_ok / data ACK) 透传回 BLE 工具,
|
||||
/* OTA 模式: Loop bootloader 回的 0x9F ACK 帧 (pre_ok/addr_ok/data ACK) 分发:
|
||||
- 本地远程 OTA (g_ota_flash_active, V1.08) → 交给 ota_srv 刷写状态机
|
||||
- BLE 透传 (现状, V1.02.03) → 透传回 BLE 工具
|
||||
否则停等协议 (0xA7 WITH_BACK) 永远等不到 ACK */
|
||||
if(g_flag_bt_state){
|
||||
if(g_ota_flash_active){
|
||||
ota_flash_feed_ack(g_pkg_uart_2.pkg, g_pkg_uart_2.offset);
|
||||
}
|
||||
else if(g_flag_bt_state){
|
||||
g_flag_notify_temp = set_response_tran_to_notify(g_pkg_uart_2.pkg, g_pkg_uart_2.offset, &g_notify_buftemp);
|
||||
}
|
||||
/* OTA 模式同样清 flag — 残留帧会在 OTA 结束后被误处理 */
|
||||
|
||||
@@ -4,6 +4,236 @@
|
||||
>
|
||||
> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接
|
||||
|
||||
## 2026-08-21 — MQTT 协议 V1.10 代码落地:网络上报携带地感版本(loop_ver/loop_hw_ver)
|
||||
|
||||
> **固件版本:归入 vd960DBN V1.02.05 发布**(2026-08-21 升版,cmcng.h 三段式一致:FIRMWARE_VER="1.02.05" + MAIN=1/SUB=2/SUBSUB=5)。V1.02.05 涵盖 8-20 联网稳定性修复系列(SocketSend 0x11 / OTA hex 空格 / union 合并 / 刷写状态语义)+ 本次 V1.10 协议实现。
|
||||
|
||||
### 背景
|
||||
|
||||
协议 V1.10(`DLD960_IoT_MQTT协议.md`,commit 3be9e50)三处新增,本次为固件实现:
|
||||
1. `dev_info_query` 响应 data 补 `loop_ver` / `loop_hw_ver`(地感 Loop 版本,0x4A 缓存值)
|
||||
2. `initialize` 上报同样携带(尽力,可为空)
|
||||
3. 新增 §4.25 `loop_version_query` 命令:实时查询 Loop 版本,配合远程 OTA 升级前后核对
|
||||
|
||||
### 改动
|
||||
|
||||
| 位置 | 内容 |
|
||||
|------|------|
|
||||
| `loop_uart_proto.h/c` | 新增 `LoopVerCache` 结构 + 全局 `g_lup_ver_cache`(0x4A 查询结果缓存);`lup_cache_from_info()` 解析结果写缓存、`lup_refresh_version_cache()` 后台刷新(仅命令通道空闲时发 0x4A) |
|
||||
| `iot_mqtt_srv.c` dev_info_query | data 补 `loop_ver` / `loop_hw_ver`(缓存格式化字符串,查询未完成/失败为空串) |
|
||||
| `iot_mqtt_srv.c` initialize | data 补 `loop_ver` / `loop_hw_ver`(尽力携带,可为空) |
|
||||
| `iot_mqtt_srv.c` 新命令 `loop_version_query` | 0x4A 异步发起 + 暂存 msg_id / deadline(300ms);`iot_verq_poll()` 在 `g_lup_cmd.state == RESPONSE_READY` 时解析回包(code=0 + loop_ver/loop_hw_ver/version_str)并同步刷新缓存;TIMEOUT/超时回 code=5(loop_ver 保持缓存值) |
|
||||
| `iot_mqtt_srv.c` 后台刷新 | 上电 `iot_mqtt_init()` 置 dirty + OTA done(`iot_ota_report_result ok=1`)置 dirty;poll 在命令通道空闲时自动发 0x4A 刷新缓存 |
|
||||
|
||||
### 关键设计
|
||||
|
||||
- **版本来源 = 0x4A 实时查询**(`lup_parse_version`),不是 OTA 元数据 `version`——那是"目标版本"(审计用),0x4A 才是 Loop 当前真实固件版本
|
||||
- **缓存值语义**:`dev_info_query`/`initialize` 带缓存值(上电自动查一次 + OTA done 后刷新);MQTT 命令处理是同步回包,不能干等 0x4A 异步响应,故查询未完成/失败回空串;平台要最新版本走 `loop_version_query` 实时查询
|
||||
- **异步回包**:与 TCP `json_check_pending` 同模式——暂存 msg_id(回包 topic 固定 `dld960/{sn}/dev`,无需存 topic),等 `g_lup_cmd.state` 状态机推进
|
||||
- **单命令通道互斥**:`g_lup_cmd` 全局唯一,TCP/MQTT/后台刷新共用;后发覆盖、先发者超时(低频命令,理论无冲突)
|
||||
|
||||
### 验证
|
||||
|
||||
- gcc 隔离单测 `tests/test_iot_loop_ver.c` **44 断言全过**:
|
||||
- 版本帧解析:正常帧(Status/Hard=1.0.0/Soft=1.2.3)+ 错误路径(len<10 / bad magic / bad cmd / LEN<7)
|
||||
- 缓存:`lup_cache_from_info` 写入、`iot_loop_ver_str`/`iot_loop_hw_ver_str` 格式化(含无效缓存空串、NULL 防护)
|
||||
- verq_poll 命令成功回包:code=0 + 三字段 + msg_id 回显 + topic 正确 + 缓存同步刷新 + 通道释放
|
||||
- 超时回包:code=5 + `no response` + 缓存值保留
|
||||
- 后台刷新:dirty=1+IDLE 发起查询(不设 pending)、通道忙不消费、响应消费更新缓存、残留超时清理
|
||||
- `check_c_balance.py` 三个改动文件语法平衡 OK
|
||||
- **待板上验证**:0x4A 在 MQTT 工作期间与 `loop_data`/事件上报共用 UART2 总线的时序(查询是低频命令,理论无冲突);OTA done 后缓存刷新(Loop 复位时间窗内查询可能超时,可接受——平台可用 `loop_version_query` 主动核对)
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-20 — 联网 SocketSend 0x11 死循环修复:发送退避 + SocketCreat 失败中止
|
||||
|
||||
### 现象(板级)
|
||||
|
||||
收 report_config 后 loop_data(606B)发送失败:`SocketSend FAIL ret=0x11 sent=0/606` → 立即重试 10 次无效 → 3 次强制断连 → 重连 SocketCreat 返回 **0x1D(ISCONN)** + Connect 0x17(CLSD) → TCP connect timeout 死循环,永久失联。
|
||||
|
||||
### 根因
|
||||
|
||||
1. **WCHNET_NUM_TCP_SEG = WCHNET_NUM_TCP×2 = 2**(RAM 瘦身改小):TCP 发送段缓冲仅 2 个,重传队列占用即无缓冲 → 606B 发送 `WCHNET_ERR_MEM(0x11)`
|
||||
2. **iot_mqtt_send 失败处理过激**:0x11 是瞬时(SEG 被占),却立即重试 10 次(无退避,WCHNET 没时间释放)+ 3 次就强制断连
|
||||
3. **重连缺陷**:`WCHNET_CreateTcpMqttSocket` 里 SocketCreat 失败(0x1D ISCONN,旧 socket 未清干净)后**仍继续 Connect**(mStopIfError 只打印不中止)→ 无效 socket 等超时 → 死循环;且强制重连时 `SocketId_TCP` 未置 0xFF,SocketCreat 沿用旧 id
|
||||
|
||||
### 修复
|
||||
|
||||
| 位置 | 改动 |
|
||||
|------|------|
|
||||
| `iot_mqtt_srv.c` iot_mqtt_send | 0x11 时 **200ms 退避重试**(等 WCHNET 释放 SEG,最多 10 次×200ms≈2s),不立即断连;非 MEM 错误立即重试 |
|
||||
| `iot_mqtt_srv.c` 强制重连 | 补 `SocketId_TCP = 0xFF`(防 SocketCreat 沿用旧 id) |
|
||||
| `net_srv.c` WCHNET_CreateTcpMqttSocket | SocketCreat 失败 → 打印 + `SocketId_TCP=0xFF` + **return 不 Connect**,由上层指数退避重试 |
|
||||
|
||||
### 验证
|
||||
|
||||
- 语法 0 错误
|
||||
- **待板级确认**:重发场景(瞬时 0x11)应自动恢复不断连;真断连重连失败应退避而非死循环
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-20 — OTA 下载失败修复:hex 提取不兼容 json.dumps 空格(单片 CRC 全错)
|
||||
|
||||
### 现象(板级)
|
||||
|
||||
DBNMQTTool 下载第一片即失败:`ota_data code=1 crc/gap error`,received=0,重试 4 次全败。
|
||||
|
||||
### 根因
|
||||
|
||||
工具 `json.dumps` 默认**带空格**(`"data": "hex..."`),设备端手工 `strstr(json, "\"data\":\"")` 找**无空格**格式 → 匹配不到 → hexbuf 空 → `ota_hex_decode` 失败 → code=1。其他命令走 `simple_parse_json`(容错空格)不受影响,只有 OTA 的 hex 提取踩坑。
|
||||
|
||||
### 修复
|
||||
|
||||
- `ota_srv.c/h` 新增 `ota_json_extract_hex()`:扫描 `"data"` 键后跳过任意空格/冒号/引号,兼容 `"data":"..."` 与 `"data": "..."` 两种格式
|
||||
- `iot_mqtt_srv.c` ota_data 分支改用该函数(删除 strstr 手工提取 + 残留 hp 变量)
|
||||
- 单测新增 `test_json_extract_hex`:无空格/带空格(事故场景)/512 hex 满片/找不到,4 场景全过
|
||||
|
||||
### 教训
|
||||
|
||||
嵌入式 JSON 协议字段提取**统一用 simple_parse_json 或兼容空格的扫描**,不要 strstr 精确匹配——上位机 JSON 序列化器(json.dumps/不同语言库)的空格行为不可控。
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-20 — 联网复位事故修复:OTA 命令缓冲合并 union(RAM 90% 栈余量不足)
|
||||
|
||||
### 现象(板级)
|
||||
|
||||
联网后 SUBACK → 平台下发 MQTT PUBLISH(len=197,即 `report_config` 时钟同步)→ **HardFault → NVIC_SystemReset**(`RST_REASON: 0x10000000` = SFT 软件复位位,非 IWDG 0x20000000;CH32V20x 默认 HardFault_Handler 即软件复位)→ 死循环复位。
|
||||
|
||||
### 根因
|
||||
|
||||
MRS 编译 RAM **90.02%**(44248/48KB)逼近历史 .bss 挤栈红线。本次 OTA 实现 6 个命令分支各声明独立 `static char resp[256~400]` + `static char hexbuf[513]`(合计 ~2.5KB BSS)→ 栈余量被挤 → `iot_handle_publish` 深调用链(MQTT 解析 → simple_json → snprintf,report_config 分支本身还有局部 `data[512]`)**栈溢出 → HardFault**。复现稳定(每次收 report_config 必崩)。
|
||||
|
||||
### 修复
|
||||
|
||||
| 项 | 改动 | 省 |
|
||||
|----|------|-----|
|
||||
| iot_mqtt_srv.c | 6 个 resp + hexbuf 合并为函数级 `static union _ota_io { resp[512]; hexbuf[513]; }`(互斥复用) | **~2KB BSS** |
|
||||
| ota_srv.c | `_chunk_buf[256]` + `_fs_frame[254]` 合并 union(下载/校验与刷写帧互斥) | ~260B |
|
||||
| ota_srv.c | 删无用 `_fs_block_crc` | 4B |
|
||||
|
||||
**教训**:RAM 90% 环境下"局部栈改 static"是**伪优化**——BSS 增加 = 栈余量减少,深调用链照样溢出。正确方向是**合并复用缓冲减少总占用**(union),不是搬家。
|
||||
|
||||
### 验证
|
||||
|
||||
- 语法 0 错误(基线 interrupt 假阳性除外);gcc 隔离单测 10/10 PASS
|
||||
- **待板级确认**:拉最新代码重编,RAM 应回 ~88%,联网收 report_config 不再复位
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-20 — Loop MCU 远程 OTA 固件实现(MQTT V1.08,ROADMAP P1.4 ①)
|
||||
|
||||
### 背景
|
||||
|
||||
协议设计稿 V1.01 拍板 + 并入 MQTT 协议 V1.08 + DBNMQTTool OTA 页签/协议单测先行。本次固件实现**先存后刷**:MQTT 分片下载 → W25Qxx OTA 暂存区(0x010000 起 512KB)→ 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU(AT32F421)。
|
||||
|
||||
### 变更
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `ota_srv.c/h`(新增) | CRC32(ISO-HDLC 逐位,零查表 RAM)、OtaMeta 元数据(双备份 0x010000/0x010200,节流 4KB flush)、会话状态机(begin/data/end/abort/flash)、本地刷写状态机(非阻塞 tick 驱动:A5→pre_ok→A6→addr_ok→A7×N 停等 ACK,1s 超时 ×3)、安全窗口(有车拒绝,force 跳过)、失败 event_report ota_error |
|
||||
| `iot_mqtt_srv.c` | 命令分发加 6 个 ota_* 分支;会话静默(downloading/flashing 暂停 event_report 发送 + offlog/快照落盘,队列积压结束后补发);IOT_EVT_OTA_ERROR 事件类型 + `iot_evt_report_ota_error` + `iot_any_car` |
|
||||
| `usart_biz.c` | uart_srv OTA 分支 ACK 分发:`g_ota_flash_active` 时交给 `ota_flash_feed_ack`(本地刷写),否则透传 BLE(现状) |
|
||||
| `peripheral_main.c` | `ota_init()` 初始化 + `ota_poll()` 挂主循环(uart_srv 后,先消费 ACK 再推进) |
|
||||
| `offlog.c/h` | 事件类型 OTA_START 0x60 / OTA_RESULT 0x61 + 包装函数(升级开始/结果审计留痕) |
|
||||
|
||||
### 关键参数
|
||||
|
||||
| 项 | 值 |
|
||||
|----|-----|
|
||||
| 下载单片 | 256B + 单片 CRC32(hex 512 字符 < RECV_BUF 1024) |
|
||||
| 镜像上限 | 96KB(Slot 100KB - 4KB 边界余量) |
|
||||
| 刷写块 | 248B(0x9F LEN 为 uint8 → DATA ≤254B;bootloader 2KB 扇区累积写) |
|
||||
| 分区 | 元数据 4KB + Slot A/B 各 100KB(0x011000/0x02A000)+ 预留 308KB |
|
||||
| 断点续传 | 元数据 received 节流 4KB flush,掉电丢 ≤4KB(平台重发幂等,协议已定义) |
|
||||
| 刷写窗口 | 64KB ≈ 265 块 × ~20ms ≈ 6~8s;非阻塞调度保 MQTT 保活/IWDG |
|
||||
|
||||
### 单测(gcc 隔离,`tests/test_ota_srv.c`,mock NOR/UART2/时钟)
|
||||
|
||||
9 断言组全过:CRC32 标准向量 0xCBF43926 / hex 解码 / 元数据双备份恢复 / 会话流 20 片→ready / 断点续传(16 片 flush 后精确续传)/ 幂等·乱序·单片 CRC / 刷写全流程(21 块 A7 逐块 ACK)/ 有车拒绝 + force 跳过 / pre_ok 超时重试失败告警。
|
||||
|
||||
**抓到 2 个真 bug(单测价值实证)**:
|
||||
1. **A7 块序号未递减**:`_fs_sub` 每块 ACK 后不减 → bootloader 永远等不到末包(sub==1)→ 刷写卡死。修复:ACK 成功路径 `_fs_sub--`。
|
||||
2. **重发时 retry 清零**:START/ADDR 分支 `_fs_retry=0` 每次重发清零 → 超时重试永不失败。修复:重发保持计数(首发由 ota_cmd_flash 置 0,ACK 成功由 feed_ack 清 0)。
|
||||
|
||||
### 待板上验证
|
||||
|
||||
- UART2 TX 阻塞轮询(248B ≈ 13ms)与主循环时序、擦除 45ms×N 分散阻塞对 MQTT 保活影响
|
||||
- Loop 实际 bin 大小(验证 96KB 余量)、bootloader 真实 ACK 时序
|
||||
- 会话静默后 event_report 补发行为(队列积压恢复)
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-19 — 固件版本 V1.02.04(SPI 存储适配两项修复)
|
||||
|
||||
### 内容
|
||||
|
||||
| 项 | 说明 |
|
||||
|------|------|
|
||||
| SPI Flash 识别去厂商代码 | `W25Qxx` 宏去 `0XEF` 前缀、`SPI_Flash_ReadJEDEC_ID()` 删厂商校验、`storage_init` 低字节匹配(f1d9ac4) |
|
||||
| 恢复 factory 配置写入 | 解除 8-13 止血,空片首启写 magic+默认参数,配置读写恢复正常(8e58d5f + 881a774) |
|
||||
| 版本号 | `cmcng.h` 三段式一致:FIRMWARE_VER="1.02.04" + MAIN=1/SUB=2/SUBSUB=4 |
|
||||
|
||||
**板级验证**:W25Q128 配置读写正常(用户 2026-08-19 确认)。
|
||||
|
||||
文档同步:README 当前发布/版本表、CHANGELOG V1.02.04 条目、产品手册/技术规格书配套版本(文档保持 V1.02,同日内补充不升版)。
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-19 — 恢复 factory 配置写入(换新空片后"老是进入出厂初始化配置")
|
||||
|
||||
### 背景
|
||||
|
||||
换 W25Q128 新片后每次上电日志 `CFG: magic mismatch - MEMORY DEFAULTS (factory SPI write disabled)`,用户保存的配置不生效。
|
||||
|
||||
### 根因
|
||||
|
||||
1. **8-13 止血禁用了 factory 写入**(当时 SPI 写触发硬件复位死循环,临时方案:不写 Flash 用内存默认)——但该根因已在 8-17 闭环(.bss 挤占 RAM → 栈溢出 + printf 重入,**非 SPI 硬件问题**),offlog/snapshot 自 8-18 起持续 SPI 写入稳定,止血早该解除
|
||||
2. **新空片(W25Q128)参数区全 0xFF**,magic(PRODUCT_MODEL 字符串)不匹配 → 每次上电走内存默认
|
||||
3. **`write_net_config`/`set_ble_safe_pass` 保存配置只改字段不写 magic** → 即使保存过,下次上电 magic 仍 0xFF → 配置"永久丢失"
|
||||
|
||||
### 变更(cfig_flash.c,UTF-8+CRLF)
|
||||
|
||||
`load_cfg_from_flash()` mismatch 分支:删除"不写 Flash"止血,恢复调用 `factory_dev_info()`(写 magic + 默认参数 + 网络配置到参数区 0x00),打印改 `factory defaults written`。首次上电自动初始化参数区,后续上电正常加载 + 保存配置生效。
|
||||
|
||||
### 验证
|
||||
|
||||
- `check_c_balance.py` OK;`gcc -fsyntax-only` 无新错误(仅原有 unused warning)
|
||||
- SPI 读写实现对 W25Q128 兼容确认:4KB 扇区擦除 + 256B 页编程分页(storage.c 标准实现),指令集与 W25Q32 相同
|
||||
|
||||
### 板级验证(用户 2026-08-19 确认)
|
||||
|
||||
- ✅ **读写正常**:新片(W25Q128)配置读写恢复正常,不再每次上电进出厂初始化
|
||||
- 新芯片规格书无需再核对(华邦 W25Q128,与 W25Q32 指令/结构兼容)
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-19 — SPI Flash 识别去掉厂商代码判断(兼容多厂家同容量型号)
|
||||
|
||||
### 背景
|
||||
|
||||
`SPI_Flash_ReadID()`(0x90 指令)返回 `0xEF15`(华邦:厂商 0xEF + 设备 ID 0x15),`SPI_Flash_ReadJEDEC_ID()`(0x9F 指令)返回 3 字节(0xEF 0x40 0x16)。原代码在 JEDEC 读取处硬编码校验 `id[0]==0xEF && id[1]==0x40`,且 `W25Qxx` 宏含 `0XEF` 厂商前缀——换其他厂家同容量型号(如 0x1A 开头)时识别不一致。
|
||||
|
||||
### 变更(storage.c,GBK+CRLF 保持原编码)
|
||||
|
||||
| 位置 | 改动 |
|
||||
|------|------|
|
||||
| `W25Q80~W25Q128` 宏 | `0XEF13~0XEF17` → `0X13~0X17`(去掉厂商前缀,只留设备 ID) |
|
||||
| `SPI_Flash_ReadJEDEC_ID()` | 删除 `if (id[0] != 0xEF \|\| id[1] != 0x40) return 0;`,直接 `return id[2];`(JEDEC 容量码跨厂商标准化:0x16=32Mbit 等) |
|
||||
| `storage_init()` | `switch(Flash_Model)` → `switch(Flash_Model & 0xFF)`(只匹配设备 ID 低字节) |
|
||||
|
||||
### 验证
|
||||
|
||||
- `check_c_balance.py` OK;`gcc -fsyntax-only` 无新错误(仅原有 unused warning)
|
||||
- 逻辑说明:真正决定分区容量的是 offlog/snapshot 用 `SPI_Flash_ReadJEDEC_ID()` 容量码(0x16/0x17/0x18/0x19)匹配 `OFFLOG_CHIP_*`——这部分本来就不含厂商判断,随本次一并生效
|
||||
|
||||
### 遗留
|
||||
|
||||
- `/* Winbond SPIFalsh ID */` 历史注释未改(宏已不限于 Winbond,后续可顺手更新)
|
||||
- 待板级:换非华邦同容量芯片实测识别
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-19 — UART2 RX DMA 下 BLE→Loop OTA 失效修复:0x9F 帧透传回 BLE
|
||||
|
||||
### 背景
|
||||
@@ -1416,6 +1646,10 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`,
|
||||
|
||||
| 版本 | 时间 | 说明 |
|
||||
|------|------|------|
|
||||
| V4.8 | 2026-08-21 | MQTT 协议 V1.10 固件实现: dev_info_query/initialize 补 loop_ver/loop_hw_ver + 新命令 loop_version_query 异步回包 + 版本缓存(上电/OTA done 刷新), 单测 44 例 |
|
||||
| V4.7 | 2026-08-19 | 固件版本 1.02.03→1.02.04 (SPI 存储适配: 去厂商识别 + 恢复 factory 写入, 板级验证通过) |
|
||||
| V4.6 | 2026-08-19 | 恢复 factory 配置写入: 解除 8-13 止血, 空片首启写 magic+默认参数 (换 W25Q128 后配置永久丢失修复) |
|
||||
| V4.5 | 2026-08-19 | SPI Flash 识别去厂商代码: W25Qxx 宏去 0XEF 前缀, ReadJEDEC_ID 删厂商校验, storage_init 低字节匹配 (兼容多厂家) |
|
||||
| V4.4 | 2026-08-19 | UART2 DMA 下 BLE→Loop OTA 修复: lup_feed_byte_ota 0x9F 帧解析+透传回 BLE (停等 ACK), DMA poll OTA 模式切换/溢出阈值/tick 归零, 单测 8 例 |
|
||||
| V4.3 | 2026-08-18 | log_query 改 hex 原始字节上报 (协议 V1.03/V1.07 修订): offlog_evt_to_hex/snap_rec_to_hex, 2 条快照 406B<800B, 快照 count 恢复 2 |
|
||||
| V4.2 | 2026-08-18 | MQTT 命令集补齐: ssc_net_query/iot_net_query/iot_topic_query 只读组包 (协议 V1.02 对齐), 工具禁用未实现按钮 |
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* mock CONFIG.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
|
||||
#ifndef __MOCK_CONFIG_H__
|
||||
#define __MOCK_CONFIG_H__
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
/* mock cmcng.h — ota_srv.c 隔离单测用 (gcc, 无 WCH 环境) */
|
||||
#ifndef __MOCK_CMCNG_H__
|
||||
#define __MOCK_CMCNG_H__
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
typedef struct {
|
||||
uint32_t flag;
|
||||
uint32_t timeout;
|
||||
uint32_t tick;
|
||||
} Flag_Counter;
|
||||
|
||||
extern Flag_Counter g_flag_counter_key;
|
||||
extern Flag_Counter g_flag_counter_ota;
|
||||
|
||||
uint32_t mstick(void);
|
||||
void UART2_SendString(uint8_t *buf, uint16_t len);
|
||||
|
||||
#define PRINT(fmt, ...) printf(fmt, ##__VA_ARGS__)
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,463 @@
|
||||
/*
|
||||
* test_iot_loop_ver.c — V1.10 验证: 0x4A 版本解析 + 版本缓存 + MQTT 异步回包时序
|
||||
*
|
||||
* 覆盖:
|
||||
* 1. lup_parse_version: 0x4A 响应帧解析 (字段/错误路径)
|
||||
* 2. lup_cache_from_info / iot_loop_ver_str / iot_loop_hw_ver_str: 缓存写入与格式化
|
||||
* 3. iot_verq_poll 异步回包时序:
|
||||
* - 命令 pending + RESPONSE_READY → code=0 回包 + 缓存刷新 + 通道释放
|
||||
* - 命令 pending + TIMEOUT → code=5 回包
|
||||
* - 后台刷新: dirty=1 + IDLE → 发起 0x4A (不设 pending)
|
||||
* - 后台响应消费: !dirty + RESPONSE_READY → 更新缓存 + 释放
|
||||
* - 残留超时清理: TIMEOUT → 归位 IDLE
|
||||
*
|
||||
* 编译: gcc -o test_iot_loop_ver test_iot_loop_ver.c && ./test_iot_loop_ver
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ---- mock: 最小依赖 ---- */
|
||||
#define LUP_MAGIC 0x7F
|
||||
#define LUP_CMD_GET_VERSION 0x4A
|
||||
#define LUP_MAX_PKG_LEN 70
|
||||
#define IOT_MQTT_TOPIC_MAX_LEN 64
|
||||
|
||||
typedef struct {
|
||||
uint8_t status;
|
||||
uint8_t hard_main, hard_sub, hard_ssub;
|
||||
uint8_t soft_main, soft_sub, soft_ssub;
|
||||
char version_str[32];
|
||||
} LUP_VersionInfo;
|
||||
|
||||
typedef struct {
|
||||
uint8_t valid;
|
||||
uint8_t hard_main, hard_sub, hard_ssub;
|
||||
uint8_t soft_main, soft_sub, soft_ssub;
|
||||
} LoopVerCache;
|
||||
|
||||
typedef enum {
|
||||
LUP_STATE_IDLE = 0,
|
||||
LUP_STATE_WAIT_RESPONSE,
|
||||
LUP_STATE_RESPONSE_READY,
|
||||
LUP_STATE_TIMEOUT
|
||||
} LUP_CmdState;
|
||||
|
||||
typedef struct {
|
||||
uint8_t pending_cmd;
|
||||
uint32_t send_tick;
|
||||
uint32_t timeout_ms;
|
||||
LUP_CmdState state;
|
||||
uint8_t resp_buf[LUP_MAX_PKG_LEN];
|
||||
uint16_t resp_len;
|
||||
} LUP_CmdTracker;
|
||||
|
||||
/* ---- mock 全局 ---- */
|
||||
static LUP_CmdTracker g_lup_cmd;
|
||||
static LoopVerCache g_lup_ver_cache;
|
||||
static char g_iot_dev_serial[16] = "A1B2C3D4E5F6";
|
||||
|
||||
/* ---- mock 时间 ---- */
|
||||
static uint32_t g_mock_ms = 100000;
|
||||
static uint32_t mstick(void) { return g_mock_ms; }
|
||||
static uint32_t dev_time_now(void) { return 1719000008UL; }
|
||||
|
||||
/* ---- mock 发布捕获 ---- */
|
||||
static char g_pub_topic[64];
|
||||
static char g_pub_payload[512];
|
||||
static uint16_t g_pub_len;
|
||||
static int g_pub_count;
|
||||
static int g_getver_calls;
|
||||
|
||||
static int iot_mqtt_publish(const char *topic, const char *payload,
|
||||
uint16_t payload_len, uint8_t qos)
|
||||
{
|
||||
(void)qos;
|
||||
g_pub_count++;
|
||||
snprintf(g_pub_topic, sizeof(g_pub_topic), "%s", topic ? topic : "");
|
||||
if (payload_len < sizeof(g_pub_payload)) {
|
||||
memcpy(g_pub_payload, payload, payload_len);
|
||||
g_pub_payload[payload_len] = '\0';
|
||||
} else {
|
||||
memcpy(g_pub_payload, payload, sizeof(g_pub_payload) - 1);
|
||||
g_pub_payload[sizeof(g_pub_payload) - 1] = '\0';
|
||||
}
|
||||
g_pub_len = payload_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* mock lup_send_get_version: 模拟 lup_cmd_begin 的状态推进 */
|
||||
static void lup_send_get_version(void)
|
||||
{
|
||||
g_getver_calls++;
|
||||
g_lup_cmd.pending_cmd = LUP_CMD_GET_VERSION;
|
||||
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE;
|
||||
g_lup_cmd.resp_len = 0;
|
||||
memset(g_lup_cmd.resp_buf, 0, sizeof(g_lup_cmd.resp_buf));
|
||||
}
|
||||
|
||||
/* mock lup_cmd_done: 释放命令通道 */
|
||||
static void lup_cmd_done(void)
|
||||
{
|
||||
g_lup_cmd.pending_cmd = 0;
|
||||
g_lup_cmd.state = LUP_STATE_IDLE;
|
||||
g_lup_cmd.resp_len = 0;
|
||||
}
|
||||
|
||||
#define PRINT(...) printf(__VA_ARGS__)
|
||||
|
||||
/* ---- 被测静态状态 (从 iot_mqtt_srv.c 提取的变量, 单测转为可访问全局) ---- */
|
||||
static uint8_t _iot_verq_pending;
|
||||
static uint32_t _iot_verq_msg_id;
|
||||
static uint32_t _iot_verq_deadline;
|
||||
static uint8_t _iot_ver_cache_dirty;
|
||||
|
||||
|
||||
/* ================= 嵌入被测函数 (从真实源码提取) ================= */
|
||||
|
||||
int lup_parse_version(const uint8_t *pkg, uint16_t len, LUP_VersionInfo *info)
|
||||
{
|
||||
if (len < 10) return -1; // 1+3+7+2 = 13 min
|
||||
|
||||
// pkg layout: [7F][Addr=0][LEN=8][CMD=0x4A][Status...7bytes][XOR][SUM]
|
||||
if (pkg[0] != LUP_MAGIC || pkg[3] != LUP_CMD_GET_VERSION) return -2;
|
||||
if (pkg[2] < 7) return -3; // LEN should be >= 7
|
||||
|
||||
const uint8_t *val = pkg + 4; // skip magic + header(3)
|
||||
info->status = val[0];
|
||||
info->hard_main = val[1];
|
||||
info->hard_sub = val[2];
|
||||
info->hard_ssub = val[3];
|
||||
info->soft_main = val[4];
|
||||
info->soft_sub = val[5];
|
||||
info->soft_ssub = val[6];
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void lup_cache_from_info(const LUP_VersionInfo *info)
|
||||
{
|
||||
if (!info) return;
|
||||
g_lup_ver_cache.valid = 1;
|
||||
g_lup_ver_cache.hard_main = info->hard_main;
|
||||
g_lup_ver_cache.hard_sub = info->hard_sub;
|
||||
g_lup_ver_cache.hard_ssub = info->hard_ssub;
|
||||
g_lup_ver_cache.soft_main = info->soft_main;
|
||||
g_lup_ver_cache.soft_sub = info->soft_sub;
|
||||
g_lup_ver_cache.soft_ssub = info->soft_ssub;
|
||||
}
|
||||
|
||||
static void iot_loop_ver_str(char *out, uint16_t out_len)
|
||||
{
|
||||
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
|
||||
snprintf(out, out_len, "%d.%d.%d",
|
||||
g_lup_ver_cache.soft_main, g_lup_ver_cache.soft_sub, g_lup_ver_cache.soft_ssub);
|
||||
}
|
||||
|
||||
static void iot_loop_hw_ver_str(char *out, uint16_t out_len)
|
||||
{
|
||||
if (!g_lup_ver_cache.valid || !out || out_len == 0) { if (out && out_len) out[0] = '\0'; return; }
|
||||
snprintf(out, out_len, "%d.%d.%d",
|
||||
g_lup_ver_cache.hard_main, g_lup_ver_cache.hard_sub, g_lup_ver_cache.hard_ssub);
|
||||
}
|
||||
|
||||
static void iot_verq_publish(const char *payload, uint16_t len)
|
||||
{
|
||||
char topic[IOT_MQTT_TOPIC_MAX_LEN];
|
||||
snprintf(topic, sizeof(topic), "dld960/%s/dev", g_iot_dev_serial);
|
||||
iot_mqtt_publish(topic, payload, len, 1);
|
||||
}
|
||||
|
||||
static void iot_verq_poll(void)
|
||||
{
|
||||
if (_iot_verq_pending) {
|
||||
/* 命令查询在途: 等 0x4A 响应或超时 */
|
||||
if (g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
|
||||
LUP_VersionInfo info;
|
||||
char resp[320];
|
||||
char ver_str[16], hw_str[16], vs_str[64];
|
||||
int ok = 0;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
|
||||
lup_cache_from_info(&info); /* 同步刷新缓存 */
|
||||
snprintf(ver_str, sizeof(ver_str), "%d.%d.%d",
|
||||
info.soft_main, info.soft_sub, info.soft_ssub);
|
||||
snprintf(hw_str, sizeof(hw_str), "%d.%d.%d",
|
||||
info.hard_main, info.hard_sub, info.hard_ssub);
|
||||
snprintf(vs_str, sizeof(vs_str), "V%s (HW:%s)", ver_str, hw_str);
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":0,"
|
||||
"\"msg\":\"success\","
|
||||
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"%s\"}}",
|
||||
_iot_verq_msg_id, dev_time_now(), ver_str, hw_str, vs_str);
|
||||
ok = 1;
|
||||
} else {
|
||||
char lv[16], lhv[16];
|
||||
iot_loop_ver_str(lv, sizeof(lv));
|
||||
iot_loop_hw_ver_str(lhv, sizeof(lhv));
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
|
||||
"\"msg\":\"parse error\","
|
||||
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
|
||||
_iot_verq_msg_id, dev_time_now(), lv, lhv);
|
||||
}
|
||||
lup_cmd_done();
|
||||
_iot_verq_pending = 0;
|
||||
iot_verq_publish(resp, strlen(resp));
|
||||
PRINT("IOT: loop_version_query resp %s\n", ok ? "ok" : "parse error");
|
||||
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT || mstick() > _iot_verq_deadline) {
|
||||
char resp[320];
|
||||
char lv[16], lhv[16];
|
||||
iot_loop_ver_str(lv, sizeof(lv));
|
||||
iot_loop_hw_ver_str(lhv, sizeof(lhv));
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"msg_id\":%lu,\"cmd\":\"loop_version_query\",\"ts\":%lu,\"code\":5,"
|
||||
"\"msg\":\"no response\","
|
||||
"\"data\":{\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\",\"version_str\":\"\"}}",
|
||||
_iot_verq_msg_id, dev_time_now(), lv, lhv);
|
||||
lup_cmd_done();
|
||||
_iot_verq_pending = 0;
|
||||
iot_verq_publish(resp, strlen(resp));
|
||||
PRINT("IOT: loop_version_query timeout\n");
|
||||
}
|
||||
/* 其它 state (WAIT_RESPONSE) 继续等 */
|
||||
} else {
|
||||
/* 后台缓存刷新: 上电 / OTA done 后置 dirty, 命令通道空闲时发 0x4A */
|
||||
if (_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_IDLE) {
|
||||
lup_send_get_version();
|
||||
_iot_ver_cache_dirty = 0;
|
||||
PRINT("IOT: loop version cache refresh query\n");
|
||||
} else if (!_iot_ver_cache_dirty && g_lup_cmd.state == LUP_STATE_RESPONSE_READY) {
|
||||
/* 后台查询响应 (无命令 pending) → 更新缓存 + 释放通道 */
|
||||
LUP_VersionInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (lup_parse_version(g_lup_cmd.resp_buf, g_lup_cmd.resp_len, &info) == 0) {
|
||||
lup_cache_from_info(&info);
|
||||
PRINT("IOT: loop version cache refreshed %d.%d.%d (HW:%d.%d.%d)\n",
|
||||
info.soft_main, info.soft_sub, info.soft_ssub,
|
||||
info.hard_main, info.hard_sub, info.hard_ssub);
|
||||
}
|
||||
lup_cmd_done();
|
||||
} else if (g_lup_cmd.state == LUP_STATE_TIMEOUT) {
|
||||
lup_cmd_done(); /* 清残留超时态 (后台查询失败或被其它命令覆盖) */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 测试工具 ================= */
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) { g_pass++; printf(" PASS: %s\n", msg); } \
|
||||
else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
static void reset_state(void)
|
||||
{
|
||||
memset(&g_lup_cmd, 0, sizeof(g_lup_cmd));
|
||||
memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache));
|
||||
memset(&g_pub_payload, 0, sizeof(g_pub_payload));
|
||||
g_pub_count = 0;
|
||||
g_pub_len = 0;
|
||||
g_getver_calls = 0;
|
||||
_iot_verq_pending = 0;
|
||||
_iot_verq_msg_id = 0;
|
||||
_iot_verq_deadline = 0;
|
||||
_iot_ver_cache_dirty = 0;
|
||||
g_mock_ms = 100000;
|
||||
}
|
||||
|
||||
/* 构造 0x4A 响应帧: 7F 00 LEN 4A [status][hard_m][hard_s][hard_ss][soft_m][soft_s][soft_ss] [xor][sum]
|
||||
(checksum 字节 lup_parse_version 不校验, 填 0) */
|
||||
static void build_version_frame(uint8_t *pkg, uint16_t *len,
|
||||
uint8_t status, uint8_t hm, uint8_t hs, uint8_t hss,
|
||||
uint8_t sm, uint8_t ss, uint8_t sss)
|
||||
{
|
||||
pkg[0] = LUP_MAGIC;
|
||||
pkg[1] = 0x00;
|
||||
pkg[2] = 0x08;
|
||||
pkg[3] = LUP_CMD_GET_VERSION;
|
||||
pkg[4] = status;
|
||||
pkg[5] = hm; pkg[6] = hs; pkg[7] = hss;
|
||||
pkg[8] = sm; pkg[9] = ss; pkg[10] = sss;
|
||||
pkg[11] = 0; pkg[12] = 0; /* XOR/SUM 不校验 */
|
||||
*len = 13;
|
||||
}
|
||||
|
||||
static void test_parse_version(void)
|
||||
{
|
||||
printf("[1] lup_parse_version\n");
|
||||
uint8_t pkg[16];
|
||||
uint16_t len;
|
||||
LUP_VersionInfo info;
|
||||
|
||||
build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3);
|
||||
CHECK(lup_parse_version(pkg, len, &info) == 0, "valid frame returns 0");
|
||||
CHECK(info.status == 0x00, "status=0");
|
||||
CHECK(info.hard_main == 1 && info.hard_sub == 0 && info.hard_ssub == 0, "hard=1.0.0");
|
||||
CHECK(info.soft_main == 1 && info.soft_sub == 2 && info.soft_ssub == 3, "soft=1.2.3");
|
||||
|
||||
/* 错误路径 */
|
||||
CHECK(lup_parse_version(pkg, 9, &info) == -1, "len<10 -> -1");
|
||||
pkg[0] = 0x00;
|
||||
CHECK(lup_parse_version(pkg, len, &info) == -2, "bad magic -> -2");
|
||||
pkg[0] = LUP_MAGIC;
|
||||
pkg[3] = 0x55;
|
||||
CHECK(lup_parse_version(pkg, len, &info) == -2, "bad cmd -> -2");
|
||||
pkg[3] = LUP_CMD_GET_VERSION;
|
||||
pkg[2] = 0x05;
|
||||
CHECK(lup_parse_version(pkg, len, &info) == -3, "LEN<7 -> -3");
|
||||
}
|
||||
|
||||
static void test_cache(void)
|
||||
{
|
||||
printf("[2] 版本缓存\n");
|
||||
LUP_VersionInfo info;
|
||||
char out[24];
|
||||
|
||||
memset(&info, 0, sizeof(info));
|
||||
info.hard_main = 1; info.hard_sub = 0; info.hard_ssub = 0;
|
||||
info.soft_main = 1; info.soft_sub = 2; info.soft_ssub = 3;
|
||||
lup_cache_from_info(&info);
|
||||
CHECK(g_lup_ver_cache.valid == 1, "cache valid after write");
|
||||
CHECK(g_lup_ver_cache.soft_main == 1 && g_lup_ver_cache.soft_ssub == 3, "cache soft fields");
|
||||
|
||||
iot_loop_ver_str(out, sizeof(out));
|
||||
CHECK(strcmp(out, "1.2.3") == 0, "loop_ver str = 1.2.3");
|
||||
iot_loop_hw_ver_str(out, sizeof(out));
|
||||
CHECK(strcmp(out, "1.0.0") == 0, "loop_hw_ver str = 1.0.0");
|
||||
|
||||
/* 无效缓存 → 空串 */
|
||||
memset(&g_lup_ver_cache, 0, sizeof(g_lup_ver_cache));
|
||||
iot_loop_ver_str(out, sizeof(out));
|
||||
CHECK(strcmp(out, "") == 0, "invalid cache -> empty str");
|
||||
iot_loop_hw_ver_str(out, sizeof(out));
|
||||
CHECK(strcmp(out, "") == 0, "invalid hw cache -> empty str");
|
||||
|
||||
/* NULL/0 长度防护 */
|
||||
iot_loop_ver_str(NULL, 0);
|
||||
CHECK(1, "NULL guard no crash");
|
||||
}
|
||||
|
||||
static void test_verq_success(void)
|
||||
{
|
||||
printf("[3] verq_poll: 命令成功回包\n");
|
||||
reset_state();
|
||||
uint8_t pkg[16];
|
||||
uint16_t len;
|
||||
build_version_frame(pkg, &len, 0x00, 1,0,0, 1,2,3);
|
||||
memcpy(g_lup_cmd.resp_buf, pkg, len);
|
||||
g_lup_cmd.resp_len = len;
|
||||
g_lup_cmd.state = LUP_STATE_RESPONSE_READY;
|
||||
|
||||
_iot_verq_pending = 1;
|
||||
_iot_verq_msg_id = 407;
|
||||
_iot_verq_deadline = g_mock_ms + 300;
|
||||
|
||||
iot_verq_poll();
|
||||
|
||||
CHECK(g_pub_count == 1, "publish once");
|
||||
CHECK(strstr(g_pub_payload, "\"msg_id\":407") != NULL, "msg_id echoed");
|
||||
CHECK(strstr(g_pub_payload, "\"code\":0") != NULL, "code=0");
|
||||
CHECK(strstr(g_pub_payload, "\"loop_ver\":\"1.2.3\"") != NULL, "loop_ver=1.2.3");
|
||||
CHECK(strstr(g_pub_payload, "\"loop_hw_ver\":\"1.0.0\"") != NULL, "loop_hw_ver=1.0.0");
|
||||
CHECK(strstr(g_pub_payload, "\"version_str\":\"V1.2.3 (HW:1.0.0)\"") != NULL, "version_str format");
|
||||
CHECK(strcmp(g_pub_topic, "dld960/A1B2C3D4E5F6/dev") == 0, "resp topic");
|
||||
CHECK(_iot_verq_pending == 0, "pending cleared");
|
||||
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd channel released");
|
||||
CHECK(g_lup_ver_cache.valid == 1, "cache refreshed by command resp");
|
||||
}
|
||||
|
||||
static void test_verq_timeout(void)
|
||||
{
|
||||
printf("[4] verq_poll: 命令超时\n");
|
||||
reset_state();
|
||||
/* 缓存先有效, 超时回包应携带缓存值 */
|
||||
g_lup_ver_cache.valid = 1;
|
||||
g_lup_ver_cache.soft_main = 9; g_lup_ver_cache.soft_sub = 9; g_lup_ver_cache.soft_ssub = 9;
|
||||
g_lup_ver_cache.hard_main = 8; g_lup_ver_cache.hard_sub = 8; g_lup_ver_cache.hard_ssub = 8;
|
||||
|
||||
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE; /* 还挂着 */
|
||||
_iot_verq_pending = 1;
|
||||
_iot_verq_msg_id = 408;
|
||||
_iot_verq_deadline = g_mock_ms + 300;
|
||||
g_mock_ms += 400; /* 超过 deadline */
|
||||
|
||||
iot_verq_poll();
|
||||
|
||||
CHECK(g_pub_count == 1, "timeout publish once");
|
||||
CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "code=5");
|
||||
CHECK(strstr(g_pub_payload, "\"msg\":\"no response\"") != NULL, "msg=no response");
|
||||
CHECK(strstr(g_pub_payload, "\"loop_ver\":\"9.9.9\"") != NULL, "cache value kept in resp");
|
||||
CHECK(_iot_verq_pending == 0, "pending cleared");
|
||||
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "cmd released after timeout");
|
||||
|
||||
/* 直接 TIMEOUT 态 */
|
||||
reset_state();
|
||||
g_lup_cmd.state = LUP_STATE_TIMEOUT;
|
||||
_iot_verq_pending = 1;
|
||||
_iot_verq_msg_id = 409;
|
||||
_iot_verq_deadline = g_mock_ms + 300;
|
||||
iot_verq_poll();
|
||||
CHECK(g_pub_count == 1, "TIMEOUT state publish once");
|
||||
CHECK(strstr(g_pub_payload, "\"code\":5") != NULL, "TIMEOUT state code=5");
|
||||
}
|
||||
|
||||
static void test_bg_refresh(void)
|
||||
{
|
||||
printf("[5] verq_poll: 后台缓存刷新\n");
|
||||
reset_state();
|
||||
|
||||
/* 5a. dirty=1 + IDLE → 发起查询, 不设 pending */
|
||||
_iot_ver_cache_dirty = 1;
|
||||
g_lup_cmd.state = LUP_STATE_IDLE;
|
||||
iot_verq_poll();
|
||||
CHECK(g_getver_calls == 1, "bg query issued");
|
||||
CHECK(_iot_ver_cache_dirty == 0, "dirty cleared");
|
||||
CHECK(_iot_verq_pending == 0, "no command pending set");
|
||||
CHECK(g_pub_count == 0, "no publish for bg query");
|
||||
|
||||
/* 5b. 通道忙 (WAIT_RESPONSE) 时 dirty 不消费 */
|
||||
{
|
||||
int base = g_getver_calls;
|
||||
_iot_ver_cache_dirty = 1;
|
||||
g_lup_cmd.state = LUP_STATE_WAIT_RESPONSE;
|
||||
iot_verq_poll();
|
||||
CHECK(g_getver_calls == base, "busy channel -> no query");
|
||||
CHECK(_iot_ver_cache_dirty == 1, "dirty kept while busy");
|
||||
}
|
||||
|
||||
/* 5c. 后台响应消费: !dirty + RESPONSE_READY → 更新缓存 + 释放, 不回包 */
|
||||
reset_state();
|
||||
uint8_t pkg[16];
|
||||
uint16_t len;
|
||||
build_version_frame(pkg, &len, 0x00, 2,0,0, 3,4,5);
|
||||
memcpy(g_lup_cmd.resp_buf, pkg, len);
|
||||
g_lup_cmd.resp_len = len;
|
||||
g_lup_cmd.state = LUP_STATE_RESPONSE_READY;
|
||||
_iot_ver_cache_dirty = 0;
|
||||
iot_verq_poll();
|
||||
CHECK(g_pub_count == 0, "bg resp no publish");
|
||||
CHECK(g_lup_ver_cache.valid == 1, "bg resp cache valid");
|
||||
CHECK(g_lup_ver_cache.soft_main == 3 && g_lup_ver_cache.soft_ssub == 5, "bg resp soft=3.4.5");
|
||||
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "bg resp channel released");
|
||||
|
||||
/* 5d. 残留超时清理 */
|
||||
reset_state();
|
||||
g_lup_cmd.state = LUP_STATE_TIMEOUT;
|
||||
iot_verq_poll();
|
||||
CHECK(g_lup_cmd.state == LUP_STATE_IDLE, "stale timeout cleaned");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== test_iot_loop_ver: V1.10 版本缓存 + 异步回包 ===\n");
|
||||
reset_state();
|
||||
test_parse_version();
|
||||
test_cache();
|
||||
test_verq_success();
|
||||
test_verq_timeout();
|
||||
test_bg_refresh();
|
||||
printf("\nRESULT: %d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* ota_srv.c 隔离单测 (gcc, 无 WCH 环境)
|
||||
*
|
||||
* 策略: mock CONFIG.h/cmcng.h + mock 外设 (NOR flash 模拟 / UART2 发送记录 /
|
||||
* 可推进时钟 / offlog 记录) → #include 真实 ../APP/ota_srv.c (同编译单元,
|
||||
* static 函数可见) → 断言会话状态机 + 刷写协议帧 + 超时重试。
|
||||
*
|
||||
* 编译:
|
||||
* gcc -I tests/ota_mock -I APP/include -I APP -o /tmp/test_ota_srv \
|
||||
* tests/test_ota_srv.c
|
||||
* 运行: /tmp/test_ota_srv
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "cmcng.h" /* mock: Flag_Counter / PRINT / mstick / UART2_SendString */
|
||||
|
||||
/* ============================================================================
|
||||
* mock 外设
|
||||
* ============================================================================*/
|
||||
Flag_Counter g_flag_counter_key = {0, 0, 0};
|
||||
Flag_Counter g_flag_counter_ota = {0, 0, 0};
|
||||
|
||||
static uint32_t _mock_ms = 0;
|
||||
uint32_t mstick(void) { return _mock_ms; }
|
||||
static void mock_advance(uint32_t ms) { _mock_ms += ms; }
|
||||
|
||||
/* UART2 发送记录: 保存最近一帧 */
|
||||
static uint8_t _tx_buf[512];
|
||||
static uint16_t _tx_len = 0;
|
||||
static uint16_t _tx_count = 0;
|
||||
void UART2_SendString(uint8_t *buf, uint16_t len)
|
||||
{
|
||||
_tx_len = (len <= sizeof(_tx_buf)) ? len : (uint16_t)sizeof(_tx_buf);
|
||||
memcpy(_tx_buf, buf, _tx_len);
|
||||
_tx_count++;
|
||||
}
|
||||
static void tx_reset(void) { _tx_len = 0; _tx_count = 0; }
|
||||
|
||||
/* ---- mock NOR flash (W25Q32 语义: 写=AND, 擦=0xFF) ---- */
|
||||
#define MOCK_FLASH_SIZE (4 * 1024 * 1024)
|
||||
static uint8_t flash[MOCK_FLASH_SIZE];
|
||||
static void flash_init(void) { memset(flash, 0xFF, sizeof(flash)); }
|
||||
|
||||
void SPI_Flash_Read(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t size)
|
||||
{
|
||||
assert(ReadAddr + size <= MOCK_FLASH_SIZE);
|
||||
memcpy(pBuffer, &flash[ReadAddr], size);
|
||||
}
|
||||
void SPI_Flash_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size)
|
||||
{
|
||||
uint16_t i;
|
||||
assert(WriteAddr + size <= MOCK_FLASH_SIZE);
|
||||
for (i = 0; i < size; i++) flash[WriteAddr + i] &= pBuffer[i];
|
||||
}
|
||||
void SPI_Flash_Erase_Sector(uint32_t Dst_Addr)
|
||||
{
|
||||
assert(Dst_Addr * 4096UL + 4096UL <= MOCK_FLASH_SIZE);
|
||||
memset(&flash[Dst_Addr * 4096UL], 0xFF, 4096);
|
||||
}
|
||||
/* 与 storage.c 语义一致: 目标区非 0xFF → 擦扇区 → 读-改-写保留同扇区其他数据 */
|
||||
void SPI_Flash_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t size)
|
||||
{
|
||||
uint32_t secpos, secoff, secremain;
|
||||
uint16_t i;
|
||||
while (size > 0) {
|
||||
secpos = WriteAddr / 4096;
|
||||
secoff = WriteAddr % 4096;
|
||||
secremain = 4096 - secoff;
|
||||
if (size <= secremain) secremain = size;
|
||||
/* 目标区是否全 0xFF */
|
||||
{
|
||||
uint8_t need_erase = 0;
|
||||
for (i = 0; i < secremain; i++) {
|
||||
if (flash[secpos * 4096 + secoff + i] != 0xFF) { need_erase = 1; break; }
|
||||
}
|
||||
if (need_erase) {
|
||||
uint8_t sector[4096];
|
||||
memcpy(sector, &flash[secpos * 4096], 4096);
|
||||
for (i = 0; i < secremain; i++) sector[secoff + i] = pBuffer[i];
|
||||
memset(&flash[secpos * 4096], 0xFF, 4096);
|
||||
for (i = 0; i < 4096; i++) flash[secpos * 4096 + i] &= sector[i];
|
||||
} else {
|
||||
for (i = 0; i < secremain; i++) flash[WriteAddr + i] &= pBuffer[i];
|
||||
}
|
||||
}
|
||||
pBuffer += secremain;
|
||||
WriteAddr += (uint32_t)secremain;
|
||||
size -= (uint16_t)secremain;
|
||||
}
|
||||
}
|
||||
uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* W25Q32 */
|
||||
|
||||
/* ---- mock offlog (记录最近 OTA 事件) ---- */
|
||||
static uint32_t _ota_start_cnt = 0, _ota_result_cnt = 0;
|
||||
static uint32_t _last_ota_result = 0;
|
||||
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) { (void)type; (void)payload; (void)len; }
|
||||
void offlog_ota_start(uint8_t slot, uint32_t size) { _ota_start_cnt++; (void)slot; (void)size; }
|
||||
void offlog_ota_result(uint32_t code) { _ota_result_cnt++; _last_ota_result = code; }
|
||||
void offlog_boot(uint32_t r) { (void)r; }
|
||||
|
||||
/* ---- mock loop_uart_proto ---- */
|
||||
void lup_frame_reset(void) { }
|
||||
|
||||
/* ---- mock iot_mqtt_srv ---- */
|
||||
static uint8_t _mock_any_car = 0;
|
||||
static uint32_t _ota_err_report = 0, _ota_err_cnt = 0;
|
||||
static uint8_t _ota_rep_ok = 0xFF;
|
||||
static uint32_t _ota_rep_err = 0, _ota_rep_cnt = 0;
|
||||
uint8_t iot_any_car(void) { return _mock_any_car; }
|
||||
void iot_evt_report_ota_error(uint32_t err) { _ota_err_report = err; _ota_err_cnt++; }
|
||||
/* V1.09: ota_report 结果上报 mock (ok=1 done, ok=0 failed) */
|
||||
void iot_ota_report_result(uint8_t ok, uint32_t err) { _ota_rep_ok = ok; _ota_rep_err = err; _ota_rep_cnt++; }
|
||||
|
||||
/* ============================================================================
|
||||
* 嵌入真实 ota_srv.c (同一编译单元, static 可见)
|
||||
* ============================================================================*/
|
||||
#include "../APP/ota_srv.c"
|
||||
|
||||
/* ============================================================================
|
||||
* 测试
|
||||
* ============================================================================*/
|
||||
#define CHK(cond) do { if (!(cond)) { \
|
||||
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); return 1; } } while (0)
|
||||
|
||||
/* 构造测试镜像 (5120B = 20 片) */
|
||||
static void make_image(uint8_t *buf, uint32_t size)
|
||||
{
|
||||
uint32_t i;
|
||||
for (i = 0; i < size; i++) buf[i] = (uint8_t)(i * 7 + 3);
|
||||
}
|
||||
|
||||
static int test_crc32(void)
|
||||
{
|
||||
/* CRC-32/ISO-HDLC 标准向量 */
|
||||
CHK(ota_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926UL);
|
||||
CHK(ota_crc32(NULL, 0) == 0x00000000UL);
|
||||
printf("PASS test_crc32\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_hex_decode(void)
|
||||
{
|
||||
uint8_t out[4];
|
||||
CHK(ota_hex_decode("0102aBff", out, 4) == 0);
|
||||
CHK(out[0] == 0x01 && out[1] == 0x02 && out[2] == 0xAB && out[3] == 0xFF);
|
||||
CHK(ota_hex_decode("01z2", out, 2) != 0); /* 非法字符 */
|
||||
CHK(ota_hex_decode("010", out, 2) != 0); /* 长度不足 */
|
||||
printf("PASS test_hex_decode\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_json_extract_hex(void)
|
||||
{
|
||||
char out[520];
|
||||
|
||||
/* 无空格 (紧凑格式) */
|
||||
CHK(ota_json_extract_hex(
|
||||
"{\"msg_id\":402,\"cmd\":\"ota_data\",\"ts\":1,\"data\":{\"target\":\"loop\",\"offset\":0,\"crc32\":1,\"data\":\"0102aBff\"}}",
|
||||
out, sizeof(out)) == 8);
|
||||
CHK(strcmp(out, "0102aBff") == 0);
|
||||
|
||||
/* 带空格 (json.dumps 默认) — 2026-08-20 事故场景 */
|
||||
CHK(ota_json_extract_hex(
|
||||
"{\"msg_id\": 402, \"cmd\": \"ota_data\", \"ts\": 1, \"data\": {\"target\": \"loop\", \"offset\": 0, \"crc32\": 1, \"data\": \"0102aBff\"}}",
|
||||
out, sizeof(out)) == 8);
|
||||
CHK(strcmp(out, "0102aBff") == 0);
|
||||
|
||||
/* 512 hex 满片 */
|
||||
{
|
||||
char json[700];
|
||||
char hex[513];
|
||||
int i, n;
|
||||
for (i = 0; i < 256; i++) sprintf(hex + i * 2, "%02x", i);
|
||||
snprintf(json, sizeof(json),
|
||||
"{\"msg_id\":1,\"cmd\":\"ota_data\",\"data\":{\"data\":\"%s\"}}", hex);
|
||||
n = ota_json_extract_hex(json, out, sizeof(out));
|
||||
CHK(n == 512);
|
||||
CHK(strcmp(out, hex) == 0);
|
||||
}
|
||||
|
||||
/* 找不到 data 字段 */
|
||||
CHK(ota_json_extract_hex("{\"cmd\":\"ota_status\"}", out, sizeof(out)) == 0);
|
||||
CHK(out[0] == '\0');
|
||||
printf("PASS test_json_extract_hex\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_meta_restore(void)
|
||||
{
|
||||
flash_init();
|
||||
tx_reset();
|
||||
ota_init();
|
||||
CHK(_meta.state == OTA_STATE_IDLE);
|
||||
/* 写一份 downloading 元数据 → 重新 init → 恢复续传状态 */
|
||||
_meta.magic = OTA_MAGIC;
|
||||
_meta.state = OTA_STATE_DOWNLOADING;
|
||||
_meta.size = 512;
|
||||
_meta.received = 256;
|
||||
ota_meta_write(&_meta);
|
||||
ota_init();
|
||||
CHK(_meta.state == OTA_STATE_DOWNLOADING);
|
||||
CHK(_meta.received == 256);
|
||||
printf("PASS test_meta_restore\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_session_flow(void)
|
||||
{
|
||||
uint8_t img[5120];
|
||||
uint32_t i;
|
||||
make_image(img, sizeof(img));
|
||||
flash_init();
|
||||
tx_reset();
|
||||
ota_init();
|
||||
|
||||
/* begin */
|
||||
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
|
||||
CHK(_meta.state == OTA_STATE_DOWNLOADING);
|
||||
CHK(_meta.received == 0);
|
||||
|
||||
/* 20 片下发 */
|
||||
for (i = 0; i < sizeof(img); i += 256) {
|
||||
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
|
||||
uint8_t chunk[256];
|
||||
char hex[513];
|
||||
uint32_t j;
|
||||
memcpy(chunk, &img[i], clen);
|
||||
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
|
||||
}
|
||||
CHK(_meta.received == sizeof(img));
|
||||
|
||||
/* end → ready */
|
||||
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
|
||||
CHK(_meta.state == OTA_STATE_READY);
|
||||
|
||||
/* 暂存区与镜像一致 (读回比对) */
|
||||
{
|
||||
uint8_t back[5120];
|
||||
SPI_Flash_Read(back, OTA_SLOT_A_BASE, sizeof(back));
|
||||
CHK(memcmp(back, img, sizeof(img)) == 0);
|
||||
}
|
||||
printf("PASS test_session_flow\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_begin_resume(void)
|
||||
{
|
||||
uint8_t img[5120];
|
||||
uint32_t i;
|
||||
make_image(img, sizeof(img));
|
||||
flash_init();
|
||||
ota_init();
|
||||
|
||||
/* 下载 16 片 (4096B, 触发元数据节流 flush) 后"断电重启" */
|
||||
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
|
||||
for (i = 0; i < 16 * 256; i += 256) {
|
||||
uint8_t chunk[256];
|
||||
char hex[513];
|
||||
uint32_t j;
|
||||
memcpy(chunk, &img[i], 256);
|
||||
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(i, ota_crc32(chunk, 256), hex) == 0);
|
||||
}
|
||||
CHK(_meta.received == 16 * 256); /* 4096B, 已 flush */
|
||||
|
||||
/* 重启 */
|
||||
ota_init();
|
||||
CHK(_meta.state == OTA_STATE_DOWNLOADING);
|
||||
CHK(_meta.received == 16 * 256); /* 节流点精确续传 */
|
||||
|
||||
/* 重新 begin → 续传点 */
|
||||
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
|
||||
CHK(_meta.received == 16 * 256);
|
||||
|
||||
/* 继续下载剩余 → ready */
|
||||
for (i = 16 * 256; i < sizeof(img); i += 256) {
|
||||
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
|
||||
uint8_t chunk[256];
|
||||
char hex[513];
|
||||
uint32_t j;
|
||||
memcpy(chunk, &img[i], clen);
|
||||
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
|
||||
}
|
||||
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
|
||||
CHK(_meta.state == OTA_STATE_READY);
|
||||
printf("PASS test_begin_resume\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_dup_and_gap(void)
|
||||
{
|
||||
uint8_t img[1024];
|
||||
uint8_t chunk[256];
|
||||
char hex[513];
|
||||
uint32_t j;
|
||||
make_image(img, sizeof(img));
|
||||
flash_init();
|
||||
ota_init();
|
||||
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
|
||||
|
||||
memcpy(chunk, &img[0], 256);
|
||||
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0);
|
||||
|
||||
/* 重复片幂等 */
|
||||
CHK(ota_cmd_data(0, ota_crc32(chunk, 256), hex) == 0);
|
||||
CHK(_meta.received == 256);
|
||||
|
||||
/* 乱序缺片 (跳 256 直接发 512) → 拒绝 */
|
||||
memcpy(chunk, &img[512], 256);
|
||||
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(512, ota_crc32(chunk, 256), hex) == 1);
|
||||
CHK(_meta.received == 256);
|
||||
|
||||
/* 单片 CRC 错 → 拒绝 */
|
||||
memcpy(chunk, &img[256], 256);
|
||||
for (j = 0; j < 256; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(256, 0xDEADBEEFUL, hex) == 1);
|
||||
CHK(_meta.received == 256);
|
||||
printf("PASS test_dup_and_gap\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 模拟 bootloader: 根据收到的 A7 块回 data ACK */
|
||||
static void flash_step_until_ack(uint32_t *blocks_done, uint32_t total_blocks)
|
||||
{
|
||||
/* ota_poll 推进一帧发送 + 喂 ACK, 直到收到末块 */
|
||||
uint32_t guard = 0;
|
||||
while (*blocks_done < total_blocks && guard++ < 10000) {
|
||||
ota_poll(); /* 发 A7 块 */
|
||||
CHK(_tx_len >= 7 && _tx_buf[0] == 0x9F);
|
||||
CHK(_tx_buf[4] == 0xA7);
|
||||
/* 构造 data ACK: 9F SubL SubH 02 A7 00 CHECK */
|
||||
{
|
||||
uint8_t ack[7];
|
||||
uint8_t sum = 0;
|
||||
ack[0] = 0x9F; ack[1] = _tx_buf[1]; ack[2] = _tx_buf[2];
|
||||
ack[3] = 0x02; ack[4] = 0xA7; ack[5] = 0x00;
|
||||
sum = (uint8_t)(ack[1] + ack[2] + ack[3] + ack[4] + ack[5]);
|
||||
ack[6] = sum;
|
||||
ota_flash_feed_ack(ack, 7);
|
||||
}
|
||||
(*blocks_done)++;
|
||||
}
|
||||
CHK(*blocks_done == total_blocks);
|
||||
}
|
||||
|
||||
static int test_flash_flow(void)
|
||||
{
|
||||
uint8_t img[5120];
|
||||
make_image(img, sizeof(img));
|
||||
flash_init();
|
||||
tx_reset();
|
||||
_ota_err_cnt = 0;
|
||||
_ota_rep_cnt = 0;
|
||||
_ota_rep_ok = 0xFF;
|
||||
ota_init();
|
||||
|
||||
/* 下载 → ready */
|
||||
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "", 0) == 0);
|
||||
{
|
||||
uint32_t i;
|
||||
for (i = 0; i < sizeof(img); i += 256) {
|
||||
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
|
||||
uint8_t chunk[256];
|
||||
char hex[513];
|
||||
uint32_t j;
|
||||
memcpy(chunk, &img[i], clen);
|
||||
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
|
||||
}
|
||||
}
|
||||
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
|
||||
CHK(_meta.state == OTA_STATE_READY);
|
||||
|
||||
/* flash: 无车 → 启动 */
|
||||
_mock_any_car = 0;
|
||||
tx_reset();
|
||||
CHK(ota_cmd_flash(0, 0) == 0);
|
||||
CHK(_meta.state == OTA_STATE_FLASHING);
|
||||
CHK(g_ota_flash_active == 1);
|
||||
CHK(g_flag_counter_ota.flag == 1);
|
||||
CHK(_ota_start_cnt >= 1); /* offlog 升级开始已写 */
|
||||
|
||||
/* START → 发 A5 启动帧 */
|
||||
ota_poll();
|
||||
CHK(_tx_len == 6 && _tx_buf[0] == 0x9F && _tx_buf[4] == 0xA5 && _tx_buf[5] == 0xA7);
|
||||
|
||||
/* pre_ok → ADDR */
|
||||
{
|
||||
uint8_t pre_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA5, 0x00, 0xA8};
|
||||
ota_flash_feed_ack(pre_ok, 7);
|
||||
}
|
||||
ota_poll();
|
||||
/* A6 地址帧: 9F 01 00 05 A6 08 00 34 00 CHECK */
|
||||
CHK(_tx_len == 10 && _tx_buf[4] == 0xA6);
|
||||
CHK(_tx_buf[5] == 0x08 && _tx_buf[6] == 0x00 && _tx_buf[7] == 0x34 && _tx_buf[8] == 0x00);
|
||||
/* 校验和: SubL+SubH+LEN+CMD+ADDR = 01+00+05+A6+08+00+34+00 */
|
||||
CHK(_tx_buf[9] == (uint8_t)(0x01 + 0x00 + 0x05 + 0xA6 + 0x08 + 0x00 + 0x34 + 0x00));
|
||||
|
||||
/* addr_ok → DATA 阶段 (总块数 = ceil(5120/248) = 21) */
|
||||
{
|
||||
uint8_t addr_ok[7] = {0x9F, 0x01, 0x00, 0x02, 0xA6, 0x00, 0xA9};
|
||||
ota_flash_feed_ack(addr_ok, 7);
|
||||
}
|
||||
CHK(_fs_sub == 21); /* 首块序号 = 总块数 */
|
||||
|
||||
/* 逐块 A7 + ACK 直到完成 */
|
||||
flash_step_until_ack(&(uint32_t){0}, 21);
|
||||
|
||||
/* 完成后 (V1.09): 状态回 idle (镜像保留可重刷), 恢复 0x7F 模式, 结果已记,
|
||||
ota_report stage=done 已上报 */
|
||||
CHK(_meta.state == OTA_STATE_IDLE);
|
||||
CHK(_meta.last_result == 0);
|
||||
CHK(g_ota_flash_active == 0);
|
||||
CHK(g_flag_counter_ota.flag == 0);
|
||||
CHK(_ota_result_cnt >= 1);
|
||||
CHK(_last_ota_result == 0);
|
||||
CHK(_ota_err_cnt == 0); /* 无失败告警 */
|
||||
CHK(_ota_rep_cnt == 1); /* ota_report done 上报一次 */
|
||||
CHK(_ota_rep_ok == 1);
|
||||
CHK(_ota_rep_err == 0);
|
||||
printf("PASS test_flash_flow (21 blocks)\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_flash_reject_car(void)
|
||||
{
|
||||
flash_init();
|
||||
ota_init();
|
||||
/* 直接构造 ready 态 */
|
||||
_meta.state = OTA_STATE_READY;
|
||||
_meta.size = 512;
|
||||
_mock_any_car = 1;
|
||||
CHK(ota_cmd_flash(0, 0) == 3); /* 有车 → 拒绝 */
|
||||
CHK(g_ota_flash_active == 0);
|
||||
_mock_any_car = 1;
|
||||
CHK(ota_cmd_flash(0, 1) == 0); /* force → 跳过 */
|
||||
CHK(g_ota_flash_active == 1);
|
||||
printf("PASS test_flash_reject_car\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_flash_ready_timeout(void)
|
||||
{
|
||||
flash_init();
|
||||
tx_reset();
|
||||
_ota_err_cnt = 0;
|
||||
_ota_rep_cnt = 0;
|
||||
_ota_rep_ok = 0xFF;
|
||||
ota_init();
|
||||
_meta.state = OTA_STATE_READY;
|
||||
_meta.size = 512;
|
||||
_mock_any_car = 0;
|
||||
|
||||
CHK(ota_cmd_flash(0, 0) == 0);
|
||||
ota_poll(); /* 发 A5 (首发) */
|
||||
/* 不喂 ACK: 首发后 3 次超时重发 + 第 4 个超时周期 → 失败
|
||||
轮次: 超时1(→START) A5(2) 超时2(→START) A5(3) 超时3(→START) A5(4) 超时4(→fail) */
|
||||
{
|
||||
int k;
|
||||
for (k = 0; k < 7; k++) { mock_advance(1001); ota_poll(); }
|
||||
}
|
||||
CHK(_meta.state == OTA_STATE_FLASH_FAILED);
|
||||
CHK(_meta.last_result == OTA_ERR_READY_TIMEOUT);
|
||||
CHK(_ota_err_cnt == 1); /* event_report ota_error 告警 */
|
||||
CHK(_ota_err_report == OTA_ERR_READY_TIMEOUT);
|
||||
CHK(g_ota_flash_active == 0);
|
||||
CHK(g_flag_counter_ota.flag == 0);
|
||||
CHK(_ota_result_cnt >= 1);
|
||||
CHK(_last_ota_result == OTA_ERR_READY_TIMEOUT);
|
||||
CHK(_ota_rep_cnt == 1); /* ota_report failed 上报一次 */
|
||||
CHK(_ota_rep_ok == 0);
|
||||
CHK(_ota_rep_err == OTA_ERR_READY_TIMEOUT);
|
||||
printf("PASS test_flash_ready_timeout\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* V1.09: 刷写完成后 idle+镜像一致 → ota_begin 直接回 ready (免下载重刷) */
|
||||
static int test_begin_reflash(void)
|
||||
{
|
||||
uint8_t img[5120];
|
||||
make_image(img, sizeof(img));
|
||||
flash_init();
|
||||
tx_reset();
|
||||
ota_init();
|
||||
|
||||
/* 下载 → ready */
|
||||
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
|
||||
{
|
||||
uint32_t i;
|
||||
for (i = 0; i < sizeof(img); i += 256) {
|
||||
uint32_t clen = (sizeof(img) - i > 256) ? 256 : (sizeof(img) - i);
|
||||
uint8_t chunk[256];
|
||||
char hex[513];
|
||||
uint32_t j;
|
||||
memcpy(chunk, &img[i], clen);
|
||||
for (j = 0; j < clen; j++) sprintf(hex + j * 2, "%02x", chunk[j]);
|
||||
CHK(ota_cmd_data(i, ota_crc32(chunk, clen), hex) == 0);
|
||||
}
|
||||
}
|
||||
CHK(ota_cmd_end(ota_crc32(img, sizeof(img))) == 0);
|
||||
CHK(_meta.state == OTA_STATE_READY);
|
||||
|
||||
/* 模拟刷写完成 → idle (镜像保留) */
|
||||
_meta.state = OTA_STATE_IDLE;
|
||||
CHK(_meta.size == sizeof(img));
|
||||
|
||||
/* 重刷: begin 同镜像 → 直接 ready, offset=size (免下载) */
|
||||
CHK(ota_cmd_begin(sizeof(img), ota_crc32(img, sizeof(img)), "1.1.0", 0) == 0);
|
||||
CHK(_meta.state == OTA_STATE_READY);
|
||||
CHK(_meta.received == sizeof(img));
|
||||
|
||||
/* 不同镜像 → 新会话重下 */
|
||||
CHK(ota_cmd_begin(sizeof(img) + 256, ota_crc32(img, sizeof(img)), "1.2.0", 0) == 0);
|
||||
CHK(_meta.state == OTA_STATE_DOWNLOADING);
|
||||
CHK(_meta.received == 0);
|
||||
printf("PASS test_begin_reflash\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int fails = 0;
|
||||
fails += test_crc32();
|
||||
fails += test_hex_decode();
|
||||
fails += test_json_extract_hex();
|
||||
fails += test_meta_restore();
|
||||
fails += test_session_flow();
|
||||
fails += test_begin_resume();
|
||||
fails += test_dup_and_gap();
|
||||
fails += test_flash_flow();
|
||||
fails += test_flash_reject_car();
|
||||
fails += test_flash_ready_timeout();
|
||||
fails += test_begin_reflash();
|
||||
if (fails == 0) {
|
||||
printf("\nALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
printf("\n%d TEST(S) FAILED\n", fails);
|
||||
return 1;
|
||||
}
|
||||
Reference in New Issue
Block a user