Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12618578d8 | ||
|
|
2044efe14f | ||
|
|
c879ed04e2 | ||
|
|
d4d57f6487 | ||
|
|
b90842f89d | ||
|
|
77b667f1a0 | ||
|
|
4c4000eb7d | ||
|
|
3d9df52d1c | ||
|
|
3c9a6f900b | ||
|
|
d6873f4b45 | ||
|
|
2e73155388 | ||
|
|
31ca3d66c3 | ||
|
|
d1fb357989 | ||
|
|
40e6648445 | ||
|
|
e9a19b40d3 | ||
|
|
6483795079 | ||
|
|
af95660c68 | ||
|
|
bbf5492fe2 | ||
|
|
fad99a550b | ||
|
|
8151d5700e | ||
|
|
2a267967b4 | ||
|
|
e919ff477a | ||
|
|
1ec4a39d92 | ||
|
|
572077031c | ||
|
|
6e711e35ef | ||
|
|
181467b9e6 | ||
|
|
3be9e504da | ||
|
|
51298693da | ||
|
|
1badba3893 | ||
|
|
c305ca651f | ||
|
|
2484a03326 | ||
|
|
0b51d84a9b | ||
|
|
49736c341c | ||
|
|
6c1f276002 | ||
|
|
2ccf0804a8 | ||
|
|
9d6f5ea412 | ||
|
|
b20fc57954 |
@@ -5,6 +5,44 @@
|
||||
|
||||
---
|
||||
|
||||
## 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 固件未变,协议未变更,可正常配套)。
|
||||
|
||||
@@ -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.04**(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.04 | 蓝牙管理、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,8 +59,8 @@ 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_BLE协议.md](docs/DLD960_BLE协议.md) | V1.02 | 蓝牙 BLE | 帧格式 + 分包 + 脱机日志(OFFLOG_STAT/QUERY/CLEAR)+ 传感快照(SNAP_STAT/QUERY/CLEAR,0x28/0x29/0x2A),与 MQTT/TCP 同语义 |
|
||||
| [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.13 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize(extra_info 含 imei/iccid)、event_report 平台必答、设备时钟同步、脱机日志(事件/快照流)、Loop 远程 OTA(ota_* 命令,先存后刷)、**4G 通道适配(方案 C:Air780 协议转换,标准 JSON 命令面 + link,V1.12)** |
|
||||
| [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.03 | 蓝牙 BLE | 帧格式 + 分包 + 配置命令全表(0x09~0x24/0x31/0x8A/0x92/0xC5,未实现如实标注)+ 脱机日志(OFFLOG_STAT/QUERY/CLEAR)+ 传感快照(SNAP_STAT/QUERY/CLEAR)+ 透传(0x7F/0x9F) |
|
||||
| [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 |
|
||||
|
||||
## 开发文档
|
||||
|
||||
@@ -147,7 +147,7 @@ A2 00 51 00: 第0级灵敏度,0x00A2为进入 162,0x0051为离开 81
|
||||
|
||||
* AutoMode: 是否处于自动模式(自动调频),0表示不启用,默认0。
|
||||
|
||||
* Sensitivity: 低四位表示灵敏度,0~9级,值越大,灵敏度越高,默认7;高四位表示高低频,其中,\[4,5\]表示线圈的高低频。
|
||||
* Sensitivity: 低四位表示灵敏度序号,**4级制 0~3**(0 最低,3 最高),默认 2;9级制灵敏度后续版本实现。高四位表示高低频,其中,\[4,5\]表示线圈的高低频。
|
||||
|
||||
* Loop\_Delay: 地感延时时间,0~200, 0.1s为一级,延时时间范围:\[0, 20秒\];
|
||||
|
||||
@@ -176,10 +176,28 @@ A2 00 51 00: 第0级灵敏度,0x00A2为进入 162,0x0051为离开 81
|
||||
|
||||
返回:
|
||||
|
||||
| | Header | AutoMode+Amount + Data\[Sensitivity, Loop_Delay, Output_Mode, Exist_Mode, Direction_Mode, freq1, freq2, freq3\] |
|
||||
| | Header | AutoMode + Amount + Data\[Sensitivity, Loop_Delay, Output_Mode, Exist_Mode, Direction_Mode, freq1, freq2, freq3\] + **LOOP_MEASURE_BASE(3B LE)** |
|
||||
| --- | --- | --- |
|
||||
| **Value(Hex)** | 00 xx 64 | XX |
|
||||
| **Length** | 3 Byte | (2 + 8 \* Amount) Byte |
|
||||
| **Value(Hex)** | 00 xx 64 | XX |
|
||||
| **Length** | 3 Byte | (2 + 8 \* Amount + **3**) Byte |
|
||||
|
||||
**V1.09 起响应尾部追加 3B LE `LOOP_MEASURE_BASE`(variation 归一化基数,本产品 = 131072 = 2^17)**:
|
||||
|
||||
| 偏移(相对 Data 起点) | 字节 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| 0 | 1 | AutoMode(smart_mode) |
|
||||
| 1 | 1 | 线圈数量 Amount |
|
||||
| 2 .. 2+8×Amount−1 | 8×Amount | 每路线圈参数(Sensitivity, Loop_Delay, Output_Mode, Exist_Mode, Direction_Mode, freq1, freq2, freq3) |
|
||||
| 2+8×Amount .. +2 | 3 | **LOOP_MEASURE_BASE** 小端(低字节在前)。本产品固定 `00 00 02` = 0x00020000 = **131072** |
|
||||
|
||||
**用途(第三方评估换算)**:上报变化量 variation 是**周期域计数值**(`variation = Origin − CAPVD`,CAPVD ∝ √L 而非 L),归一化必须除本基数:
|
||||
|
||||
```
|
||||
Δf/f ≈ variation / LOOP_MEASURE_BASE
|
||||
ΔL/L ≈ -2 × variation / LOOP_MEASURE_BASE (系数 2 来自 CAPVD ∝ √L 展开,阈值区间残差 <0.2%)
|
||||
```
|
||||
|
||||
> ⚠ **各产品 MEASUREMENT_BASE 不同**(AT32@120MHz = 131072;CH59x@60MHz = 524288),接收端/第三方应用**禁止写死任一型号常量**,一律从 0x64 响应读取本基数,否则所有百分比放大/缩小 4 倍。
|
||||
|
||||
## 3.07 设备主动上报传感信息(0xC0)
|
||||
|
||||
@@ -212,10 +230,10 @@ SensData:传感数据
|
||||
|
||||
| 字段 | 字节数 | 内容 | 详情 |
|
||||
| --- | --- | --- | --- |
|
||||
| 配置 1 | 1 | freq\_level(2bit), Direction(1bit),freq\_type(1bit), sens(4bit) | 线圈 1 配置freq\_level:高低频,两个比特位,00 表示高频(33nF),01表示中高(43nF), 10表示中低(66nF), 11表示低频(76nF)。Direction: 0 表示触发,1 表示方向判别,freq\_type: 0 表示初始频率,1 表示当前实时频率。sens:低四位表示当前的灵敏度等级 |
|
||||
| 配置 1 | 1 | freq\_level(2bit), Direction(1bit),freq\_type(1bit), sens(4bit) | 线圈 1 配置freq\_level:高低频,两个比特位,00 表示高频(33nF),01表示中高(43nF), 10表示中低(66nF), 11表示低频(76nF)。Direction: 0 表示触发,1 表示方向判别,freq\_type: 0 表示初始频率,1 表示当前实时频率。sens:低四位表示当前的灵敏度等级(**4级制 0~3,值越大越灵敏**) |
|
||||
| 线圈评估条件 | 1 | condition(4bit), loop\_state(1bit),<br>car\_state(1bit),<br>report\_msic(2bit) | loop\_condition 正常(0)有效,环境状态条件(环境状态评估值,值越大,干扰越大),高四位有效。低四位中 ,其中第3位表示线圈状态,0 表示正常,1 表示线圈断开;第 2 位表示有无车,0 表示无车,1 表示有车;低两位表示**杂项类型**,0b00表示时间量,0b01表示线圈断开次数;0b10表示车流量数;0b11表示继电器输出次数 |
|
||||
| 频率 1 | 3 | frequent | 低字节在前,无符号 |
|
||||
| 变化量 1 | 3 | variation | **有符号** 3 字节,低字节在前(小端),补码表示。定义 `variation = loop_Origin − loop_CAPVD`(基线 − 当前值)。**正值**表示当前值低于基线(车辆/金属进入,检测裕量方向);**负值**表示当前值高于基线(反向漂移/异常抬升)。值域 ±8388607(±2²³−1),发送端已做饱和限幅,不会回绕。<br>**接收端解析**:读入 3 字节后须做符号扩展——`v = b0\|(b1<<8)\|(b2<<16); if (v & 0x800000) v \|= 0xFF000000;` |
|
||||
| 变化量 1 | 3 | variation | **有符号** 3 字节,低字节在前(小端),补码表示。定义 `variation = loop_Origin − loop_CAPVD`(基线 − 当前值)。**正值**表示当前值低于基线(车辆/金属进入,检测裕量方向);**负值**表示当前值高于基线(反向漂移/异常抬升)。值域 −8388608 ~ +8388607(−2²³ ~ +2²³−1,3B 补码标准值域),发送端已做饱和限幅,不会回绕。<br>**接收端解析**:读入 3 字节后须做符号扩展——`v = b0\|(b1<<8)\|(b2<<16); if (v & 0x800000) v \|= 0xFF000000;` |
|
||||
| 杂项 | 4 | in\_out\_passtime/cut\_amount/flow\_amount | **杂项类型**,可能是时间量,也可能是线圈断开次数、车流量数,说明:<br>1、时间量有两种类型,通过时间或车间距,**50ms 为单位**(固件 50ms tick,TMR15 5ms×10 分频)。通过时间是从有车开始到车辆离开的时间(上报时car\_state 为0);车间距是 从上一次车辆离开到有车时候的时间(上报时car\_state 为1)。非0有效。<br>2、线圈断开次数。<br>3、车流量数。<br>4、继电器输出次数。 |
|
||||
|
||||
eg:
|
||||
@@ -260,4 +278,7 @@ FE 38 校验字节(XOR=0xFE, SUM=0x38)
|
||||
| V1.03 | 2026-07-02 | 0x8A 响应格式 + LEN 计算修正 | wangfq |
|
||||
| V1.04 | 2026-07-02 | 主动上报增加继电器输出次数 类型 | wangfq |
|
||||
| V1.05 | 2026-07-14 | 多线圈上报(0x0C)变化量 variation 由 2 字节无符号扩展为 **3 字节有符号**(补码, 小端),定义 `variation = Origin − CAPVD`(正=车/裕量, 负=反向漂移);每单元 11→12 字节,Len 47→51。接收端须做符号扩展。**Loop 固件与 DBN 固件须同版本发布。** | wangfq |
|
||||
| V1.06 | 2026-07-15 | 修正杂项时间量单位说明:5ms → **50ms**(对齐固件 50ms tick 实现,TMR15 5ms×10 分频) | wangfq |
|
||||
| V1.06 | 2026-07-15 | 修正杂项时间量单位说明:5ms → **50ms**(对齐固件 50ms tick 实现,TMR15 5ms×10 分频) | wangfq |
|
||||
| V1.07 | 2026-08-25 | variation 值域表述勘误:`±8388607` → **−8388608 ~ +8388607**(对齐代码负向饱和边界 0x800000,3B 补码标准值域) | wangfq |
|
||||
| V1.08 | 2026-08-25 | 灵敏度收敛为 **4级制 0~3**(0 最低,3 最高,默认 2):修复 9级制映射 `&0x03` 折叠(7→3、8→0 错乱)导致"改灵敏度感应高度无区别";运行中配置灵敏度立即生效(不再依赖重启)。9级制后续版本实现 | wangfq |
|
||||
| V1.09 | 2026-08-27 | 0x64 读参响应尾部追加 **3B LE `LOOP_MEASURE_BASE`**(variation 归一化基数 131072 = 2^17,与固件 MEASUREMENT_BASE 同步);新增换算公式 `ΔL/L ≈ -2×variation/LOOP_MEASURE_BASE`、`Δf/f ≈ variation/LOOP_MEASURE_BASE`(跨产品基数对齐,防第三方评估 ×4/÷4 陷阱) | wangfq |
|
||||
+276
-33
@@ -1,15 +1,13 @@
|
||||
# DLD960 BLE 通信协议(脱机事件日志 + 传感快照)
|
||||
# DLD960 BLE 通信协议
|
||||
|
||||
> 版本: V1.02(2026-08-12,新增传感快照 3 命令;分包上限随协商 MTU 动态)
|
||||
> 版本: V1.03(2026-08-31,命令总表补齐既有配置命令 0x09~0xC5 + 透传通道;未实现命令如实标注)
|
||||
> 适用: vd960DBN(CH32V208,WCH BLE 协议栈)
|
||||
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Qxx 环形日志(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
|
||||
> 用途: 小程序/APP 经蓝牙对 vd960DBN 进行**配置管理、参数查询、脱机日志/传感快照读取**(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
|
||||
|
||||
---
|
||||
|
||||
## 1 帧格式
|
||||
|
||||
与既有 BLE 配置命令一致:
|
||||
|
||||
```
|
||||
Magic | Header | Data | CheckByte
|
||||
| Addr/Sub Len CMD | | Xor Sum
|
||||
@@ -18,11 +16,21 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
- `Len = len(CMD) + len(Data)`(即 `pkg[2] = 1 + data_len`)
|
||||
- 校验:`Xor = XOR(pkg[1..Len+2])`,`Sum = SUM(pkg[1..Len+2])`,覆盖 header + cmd + data(不含 magic)
|
||||
- 本命令族 Magic = `0x8F`(MAGIC_BYTE_DBN_DEFAULT)
|
||||
- 校验实现:`check_pkg()` / `compute_ckb()`(dbn_ble_srv.c)
|
||||
|
||||
### 分包(长响应)
|
||||
### 1.1 魔数(Magic)
|
||||
|
||||
响应单包数据上限**随协商 MTU 动态变化**(V1.01,修复"Too large noti 丢包"):`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B)。分包时 header 字节 = `(pkg_amount << 4) | pkg_seq`,`pkg_seq` 从 1 递增;收端按 `pkg_amount`/`pkg_seq` 重组,最后一片 `pkg_amount == pkg_seq`。
|
||||
| 魔数 | 名称 | 用途 |
|
||||
|------|------|------|
|
||||
| `0x8F` | MAGIC_BYTE_DBN_DEFAULT | 设备配置/查询/日志/快照命令族(本协议主体) |
|
||||
| `0x7F` | MAGIC_BYTE_DBN_TRANSPARENT | 透传:显示屏协议、地感协议、探头协议等 → 原样转发 UART2(Loop MCU 口) |
|
||||
| `0x9F` | MAGIC_BYTE_DBN_SUB_OTA | 透传子设备 OTA 升级(g_flag_counter_ota.flag 时进入透传模式) |
|
||||
|
||||
### 1.2 分包(请求与响应)
|
||||
|
||||
**请求分包**(unpack_packs,0x13/0x15/0x1C/0x1D/0x22 等长请求):header 字节高 4 位 = 总包数,低 4 位 = 序号(从 1 递增);首包初始化重组缓冲,中间包校验 `amount/cmd/seq 连续`,末包 `amount == seq` 触发处理。校验失败丢弃整包。
|
||||
|
||||
**响应分包**(set_response_to_notify):单包 data 上限**随协商 MTU 动态**(V1.01,修复"Too large noti 丢包"):`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B)。分包时 header 字节 = `(pkg_amount << 4) | pkg_seq`,`pkg_seq` 从 1 递增;收端按 `pkg_amount`/`pkg_seq` 重组,最后一片 `pkg_amount == pkg_seq`。
|
||||
|
||||
| 协商 MTU | 单包最大 dat | 整包最大长度 |
|
||||
|---------|-------------|-------------|
|
||||
@@ -36,22 +44,256 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
---
|
||||
|
||||
## 2 命令码
|
||||
## 2 命令总表
|
||||
|
||||
| 命令码 | 名称 | 方向 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) |
|
||||
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
|
||||
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
|
||||
| `0x28` | `SNAP_STAT` | APP→设备 | 查询传感快照统计(分页定位) |
|
||||
| `0x29` | `SNAP_QUERY` | APP→设备 | 按全局序号分页拉取快照记录 |
|
||||
| `0x2A` | `SNAP_CLEAR` | APP→设备 | 清除快照(审计写入事件流) |
|
||||
| 命令码 | 名称 | 方向 | 实现状态 |
|
||||
|--------|------|------|---------|
|
||||
| `0x09` | UPDATE_DEV_SERIAL | APP→设备 | ✅ §3.1 |
|
||||
| `0x10` | GET_DEV_INFO | APP→设备 | ✅ §3.2 |
|
||||
| `0x11` | SET_LSSC_NET | APP→设备 | ✅ §3.3 |
|
||||
| `0x12` | GET_LSSC_NET | APP→设备 | ✅ §3.4 |
|
||||
| `0x13` | SET_IOT_NET | APP→设备 | ✅ §3.5 |
|
||||
| `0x14` | GET_IOT_NET | APP→设备 | ✅ §3.6 |
|
||||
| `0x15` | SET_IOT_TOPIC | APP→设备 | ✅ §3.7 |
|
||||
| `0x16` | GET_IOT_TOPIC | APP→设备 | ✅ §3.8 |
|
||||
| `0x17` | SET_SUB_TRAFFIC_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
|
||||
| `0x18` | GET_SUB_TRAFFIC_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
|
||||
| `0x1C` | CHECK_PASS | APP→设备 | ✅ §4.1 |
|
||||
| `0x1D` | MODIFY_PASS | APP→设备 | ✅ §4.2 |
|
||||
| `0x1E` | SET_FACTORY | APP→设备 | ✅ §4.3 |
|
||||
| `0x1F` | RESET_DEV | APP→设备 | ✅ §4.4 |
|
||||
| `0x20` | SET_NOTIFY | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
|
||||
| `0x21` | SET_NOTIFY_LOOP | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
|
||||
| `0x22` | SET_SUB_CODE | APP→设备 | ✅ §4.5 |
|
||||
| `0x23` | SET_CJQ_PARAM | APP→设备 | ⚠ 空壳(仅回 [0],参数未处理)§4.6 |
|
||||
| `0x24` | GET_CJQ_PARAM | APP→设备 | ⚠ 空响应(无数据)§4.7 |
|
||||
| `0x25` | OFFLOG_STAT | APP→设备 | ✅ §6 |
|
||||
| `0x26` | OFFLOG_QUERY | APP→设备 | ✅ §7 |
|
||||
| `0x27` | OFFLOG_CLEAR | APP→设备 | ✅ §8 |
|
||||
| `0x28` | SNAP_STAT | APP→设备 | ✅ §9 |
|
||||
| `0x29` | SNAP_QUERY | APP→设备 | ✅ §9 |
|
||||
| `0x2A` | SNAP_CLEAR | APP→设备 | ✅ §9 |
|
||||
| `0x31` | RW_UART_BAUD | APP→设备 | ✅ §4.8 |
|
||||
| `0x87` | LOOP_SAMPLE_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
|
||||
| `0x88` | LOOP_BALANCE_PARAM | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
|
||||
| `0x89` | LOOP_RELEASE_PLANB | APP→设备 | ⚠ 未实现(头文件定义,无 case) |
|
||||
| `0x8A` | LOOP_SENS_LIST | APP→设备 | ⚠ 空响应(无数据)§4.9 |
|
||||
| `0x92` | SET_CJQ_FACTORY | APP→设备 | ✅ §4.10 |
|
||||
| `0xC0` | SUB_SENS_REPORT | 设备→APP | ⚠ 未实现(report_sens_acs 仅声明无定义;0xC5 使能后无上报动作) |
|
||||
| `0xC5` | SENS_ACS_ENABLE | APP→设备 | ✅(设置生效,无响应)§4.11 |
|
||||
|
||||
> 与 MQTT V1.06 / TCP JSON V1.02 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。
|
||||
> 与 MQTT / TCP JSON 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同(V1.02 起)。
|
||||
|
||||
---
|
||||
|
||||
## 3 查询日志统计 `OFFLOG_STAT` (0x25)
|
||||
## 3 设备配置命令
|
||||
|
||||
### 3.1 更改设备序列码 `UPDATE_DEV_SERIAL` (0x09)
|
||||
|
||||
**请求 data(6B):** 新序列码(6 字节,g_dev_number 更新源为 `alter_dev_serila()`)。
|
||||
|
||||
**响应 data(1B):** `status`:`0x01`=成功。
|
||||
|
||||
### 3.2 查询设备信息 `GET_DEV_INFO` (0x10)
|
||||
|
||||
**请求 data:** 无(`Len=1`)。
|
||||
|
||||
**响应 data(19 + N 字节):**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 6 | `mac` | 设备 MAC 地址(gMacAddr) |
|
||||
| 6 | 1 | `hw_ver_main` | 硬件版本主 |
|
||||
| 7 | 1 | `hw_ver_sub` | 硬件版本次 |
|
||||
| 8 | 1 | `fw_ver_main` | 固件版本主 |
|
||||
| 9 | 1 | `fw_ver_sub` | 固件版本次 |
|
||||
| 10 | 1 | `model_len` | 型号字符串长度 N |
|
||||
| 11 | N | `model` | 产品型号(PRODUCT_MODEL,ASCII) |
|
||||
| 11+N | 6 | `dev_number` | 设备编号(6B) |
|
||||
| 17+N | 1 | `sub_code_lo` | 子功能码低字节(当前恒 0) |
|
||||
| 18+N | 1 | `sub_code_hi` | 子功能码高字节(当前恒 0) |
|
||||
| 19+N | 1 | `bus1_amount` | 总线1 设备数(当前恒 0) |
|
||||
| 20+N | 1 | `bus2_amount` | 总线2 设备数(当前恒 0) |
|
||||
|
||||
> `sub_code` / `bus_amount` 字段代码中为占位(注释掉的旧逻辑),恒 0。
|
||||
|
||||
### 3.3 设置 SSC 网络配置 `SET_LSSC_NET` (0x11)
|
||||
|
||||
**请求 data(30B):**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 4 | `lip` | 本机 IP |
|
||||
| 4 | 4 | `subnet_mask` | 子网掩码 |
|
||||
| 8 | 4 | `route_ip` | 网关 |
|
||||
| 12 | 4 | `lssc_ip` | 中心 IP |
|
||||
| 16 | 4 | `dns` | DNS(代码未读入,保留位) |
|
||||
| 20 | 2 | `port_ssc_tcp` | SSC TCP 端口(小端) |
|
||||
| 22 | 2 | `port_ssc_udp` | SSC UDP 端口(小端) |
|
||||
| 24 | 2 | `port_ssc_udp_message` | SSC UDP 消息端口(小端) |
|
||||
| 26 | 2 | `port_dev_tcp` | 设备 TCP 端口(小端) |
|
||||
| 28 | 2 | `port_dev_udp` | 设备 UDP 端口(小端) |
|
||||
|
||||
**响应 data(1B):** `status`:`0x00`=成功(配置写入 Flash)。
|
||||
|
||||
### 3.4 查询 SSC 网络配置 `GET_LSSC_NET` (0x12)
|
||||
|
||||
**请求 data:** 无。
|
||||
|
||||
**响应 data(30B):** 与请求布局一致:`lip(4)+sub(4)+gw(4)+lssc_ip(4)+dns(4)+` 5×端口(2B 小端)。注意响应含 `dns`(读 local_net_cfg.dns)。
|
||||
|
||||
### 3.5 设置 IoT 网络配置 `SET_IOT_NET` (0x13)
|
||||
|
||||
**请求 data:** 字符串数组,`0x00` 分隔(支持分包,见 §1.2):
|
||||
|
||||
```
|
||||
<host>\0<port(ascii)>\0<client_id>\0<username>\0<password>\0
|
||||
```
|
||||
|
||||
- `host`:MQTT 服务器地址
|
||||
- `port`:MQTT 端口(ASCII 十进制,代码逐字符换算)
|
||||
- `client_id`:ClientID;**单字节 `0x20`(空格)表示使用本地序列号**
|
||||
- `username` / `password`:MQTT 鉴权
|
||||
|
||||
**响应 data(1B):** `status`:`0x00`=成功(写入 Flash)。
|
||||
|
||||
### 3.6 查询 IoT 网络配置 `GET_IOT_NET` (0x14)
|
||||
|
||||
**请求 data:** 无。
|
||||
|
||||
**响应 data:** 字符串数组,`0x00` 分隔(同 §3.5 布局):`<remote_addr>\0<port(ascii)>\0<client_id>\0<username>\0<password>\0`。
|
||||
|
||||
### 3.7 设置设备 Topic `SET_IOT_TOPIC` (0x15)
|
||||
|
||||
**请求 data:** 字符串数组,`0x00` 分隔(支持分包):
|
||||
|
||||
```
|
||||
<clientid_enable+0x30>\0<topic_pub>\0<topic_sub>\0
|
||||
```
|
||||
|
||||
- `clientid_enable`:ASCII 数值,解析时减 `0x30` 还原
|
||||
|
||||
**响应 data(1B):** `status`:`0x00`=成功(写入 Flash)。
|
||||
|
||||
### 3.8 查询设备 Topic `GET_IOT_TOPIC` (0x16)
|
||||
|
||||
**请求 data:** 无。
|
||||
|
||||
**响应 data:**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `clientid_enable` | `g_iot_topic.clientid_enable + 0x30` |
|
||||
| 1 | 1 | `rsvd` | 保留(0) |
|
||||
| 2 | N | `topic_pub` | 发布主题(`\0` 结尾) |
|
||||
| 2+N+1 | M | `topic_sub` | 订阅主题(`\0` 结尾) |
|
||||
|
||||
---
|
||||
|
||||
## 4 安全与控制命令
|
||||
|
||||
### 4.1 验证密码 `CHECK_PASS` (0x1C)
|
||||
|
||||
**请求 data(6B):** 密码明文(ASCII)。
|
||||
|
||||
**响应 data(1B):** `status`:`0x00`=通过;`0x01`=失败(或长度≠6)。
|
||||
|
||||
### 4.2 修改密码 `MODIFY_PASS` (0x1D)
|
||||
|
||||
**请求 data(12B):** 旧密码(6B)+ 新密码(6B)。
|
||||
|
||||
**响应 data(1B):** `status`:`0x00`=成功(旧密码校验通过并写入);`0x01`=失败(长度≠12 或旧密码错误)。
|
||||
|
||||
> 实现位置:`unpack_packs()`(switch 无 case,属分包收尾处理),V1.03 文档补录。
|
||||
|
||||
### 4.3 设备出厂初始化 `SET_FACTORY` (0x1E)
|
||||
|
||||
**请求 data:** 无。
|
||||
|
||||
**响应 data(1B):** `status`:`0x00`(先回包后执行 `factory_dev_info()` 写默认参数)。⚠ 未整机复位(代码 TODO:仅复位蓝牙以外部分)。
|
||||
|
||||
### 4.4 设备复位 `RESET_DEV` (0x1F)
|
||||
|
||||
**请求 data:** 无。**无响应**(直接 `NVIC_SystemReset()`)。
|
||||
|
||||
### 4.5 设置子功能码 `SET_SUB_CODE` (0x22)
|
||||
|
||||
**请求 data(2B):** `code_set`(小端,bit 定义如下)。**无响应**(生效并持久化 `update_sub_code_enable()`)。
|
||||
|
||||
| bit | 子功能 | bit | 子功能 |
|
||||
|-----|--------|-----|--------|
|
||||
| 0 | net_enable(SSC 网络) | 6 | radar_enable(雷达) |
|
||||
| 1 | iot_enable(IoT MQTT) | 9 | laser_enable(激光) |
|
||||
| 2 | custom_enable(自定义) | 15 | lora_enable(LoRa) |
|
||||
| 3 | loop_enable(地感) | | |
|
||||
| 4 | dgdus_enable(地感采集器) | | |
|
||||
| 5 | wbdus_enable(称重采集器) | | |
|
||||
|
||||
### 4.6 设置车检器参数 `SET_CJQ_PARAM` (0x23)
|
||||
|
||||
**⚠ 空壳:** 代码中参数解析被注释(`unpack_pkg_set_cjq_param` 未调用),仅返回 `[0x00]` 伪成功。**参数实际未写入。**
|
||||
|
||||
### 4.7 读取车检器参数 `GET_CJQ_PARAM` (0x24)
|
||||
|
||||
**⚠ 空响应:** 仅构造响应帧,data 长度 = 0。**无有效数据返回。**
|
||||
|
||||
### 4.8 读写 UART 波特率 `RW_UART_BAUD` (0x31)
|
||||
|
||||
**请求 data:** `rw(1B)` + 数据(`rw≠0` 时):`uart_num(1B) + baud(3B 小端)`。
|
||||
|
||||
- `rw=0`:读。
|
||||
|
||||
**读响应 data(10B):**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `rw` | 0(读) |
|
||||
| 1 | 1 | `status` | 0x01 |
|
||||
| 2 | 1 | `idle_timeout` | BLE 空闲超时(g_max_counter_bt_min) |
|
||||
| 3 | 3 | `baud_1` | UART1 波特率(3B 小端) |
|
||||
| 6 | 1 | `idx_2` | 0x02(第二组标识) |
|
||||
| 7 | 3 | `baud_2` | 第二组波特率(3B 小端) |
|
||||
|
||||
- `rw≠0`:写。`alter_dev_baud(&pkg[5])` + 更新 `g_storage_uart_num/baud`。
|
||||
|
||||
**写响应 data(9B 语义):** `rw(1B) + status(1B) + baud(3B 小端) + reserved(4B)`。
|
||||
> ⚠ **固件实际多发 1B**(baud 低字节重复写入,共 10B):接收端以前 3B(offset 2..4)为准。
|
||||
|
||||
### 4.9 读取 Loop 灵敏度列表 `LOOP_SENS_LIST` (0x8A)
|
||||
|
||||
**⚠ 空响应:** case 仅 `memset` 无组包。**无有效数据返回。**
|
||||
|
||||
### 4.10 车检器出厂初始化 `SET_CJQ_FACTORY` (0x92)
|
||||
|
||||
与 §4.3 `SET_FACTORY`(0x1E)合并处理:响应 `[0x00]` + 执行 `factory_dev_info()`。
|
||||
|
||||
### 4.11 传感主动上报使能 `SENS_ACS_ENABLE` (0xC5)
|
||||
|
||||
**请求 data(3B):**
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `enable` | 0=关闭;非0=开启 |
|
||||
| 1 | 1 | `interval` | 上报间隔系数(实际 = 值 × 0x7F,单位 tick 待确认) |
|
||||
| 2 | 1 | `timeout_min` | 超时分钟数(timeout_counter = 值 × 60 × 100 tick) |
|
||||
|
||||
**无响应。** ⚠ 即使使能,`report_sens_acs()` 无定义无调用,**当前无 0xC0 BLE 主动上报**(上报走 MQTT/TCP 通道)。
|
||||
|
||||
---
|
||||
|
||||
## 5 透传通道
|
||||
|
||||
### 5.1 通用透传 (0x7F)
|
||||
|
||||
Magic=`0x7F` 的整帧(含校验)→ `manage_dbn_ble_transparent()` → **原样转发 UART2**(Loop MCU 口)。用于显示屏协议、地感协议、探头协议等。响应分包时 header 的 Addr/Sub 字节 = pkg_seq(见 set_response_to_notify 单包分支)。
|
||||
|
||||
### 5.2 子设备 OTA 透传 (0x9F)
|
||||
|
||||
`g_flag_counter_ota.flag != 0` 时所有 BLE 接收直接走透传 → UART2。用于 Loop MCU OTA(0x9F A5/A6/A7 帧透传,协议见《DLD960Loop 串口通信协议》OTA 章节)。
|
||||
|
||||
---
|
||||
|
||||
## 6 查询日志统计 `OFFLOG_STAT` (0x25)
|
||||
|
||||
**请求 data:** 无(`Len=1`,仅 cmd 字节)。
|
||||
|
||||
@@ -61,10 +303,10 @@ Magic | Header | Data | CheckByte
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=日志未启用(Flash 初始化失败) |
|
||||
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
|
||||
| 3 | 4 | `count` | 有效记录条数(0~8064,环形覆盖后 < capacity) |
|
||||
| 7 | 4 | `capacity` | 容量上限(8064) |
|
||||
| 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity) |
|
||||
| 7 | 4 | `capacity` | 容量上限(**随存储芯片动态**:W25Q32=16256 / W25Q64=32640 / W25Q128=65408 / W25Q256=130944) |
|
||||
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
|
||||
| 15 | 4 | `seq_last` | 最新一条全局序号 |
|
||||
|
||||
示例(boot_seq=2, count=1234, capacity=16256, seq_first=100, seq_last=1333):
|
||||
|
||||
@@ -77,7 +319,7 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
---
|
||||
|
||||
## 4 分页拉取日志 `OFFLOG_QUERY` (0x26)
|
||||
## 7 分页拉取日志 `OFFLOG_QUERY` (0x26)
|
||||
|
||||
**请求 data(5B):**
|
||||
|
||||
@@ -92,7 +334,7 @@ Magic | Header | Data | CheckByte
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=日志未启用;`0x02`=请求帧过短 |
|
||||
| 1 | 1 | `count` | 本次实际返回记录条数(0~4) |
|
||||
| 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §6) |
|
||||
| 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §10) |
|
||||
|
||||
记录顺序 = 逻辑序号升序(与 `start_seq` 一致)。越界/空日志:`count=0`。
|
||||
|
||||
@@ -109,7 +351,7 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
---
|
||||
|
||||
## 5 清除日志 `OFFLOG_CLEAR` (0x27)
|
||||
## 8 清除日志 `OFFLOG_CLEAR` (0x27)
|
||||
|
||||
**请求 data:** 无。
|
||||
|
||||
@@ -123,17 +365,17 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
---
|
||||
|
||||
## 6 传感快照日志(0x28/0x29/0x2A,V1.02 新增)
|
||||
## 9 传感快照日志(0x28/0x29/0x2A,V1.02 新增)
|
||||
|
||||
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
|
||||
|
||||
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
|
||||
|
||||
### 6.1 查询快照统计 `SNAP_STAT` (0x28)
|
||||
### 9.1 查询快照统计 `SNAP_STAT` (0x28)
|
||||
|
||||
**请求 data:** 无(`Len=1`)。
|
||||
|
||||
**响应 data(19B,全小端):** 与 §3 `OFFLOG_STAT` 布局相同:
|
||||
**响应 data(19B,全小端):** 与 §6 `OFFLOG_STAT` 布局相同:
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
@@ -144,7 +386,7 @@ Magic | Header | Data | CheckByte
|
||||
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
|
||||
| 15 | 4 | `seq_last` | 最新一条全局序号 |
|
||||
|
||||
### 6.2 分页拉取快照 `SNAP_QUERY` (0x29)
|
||||
### 9.2 分页拉取快照 `SNAP_QUERY` (0x29)
|
||||
|
||||
**请求 data(5B):**
|
||||
|
||||
@@ -159,11 +401,11 @@ Magic | Header | Data | CheckByte
|
||||
|------|------|------|------|
|
||||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=快照未启用;`0x02`=请求帧过短 |
|
||||
| 1 | 1 | `count` | 本次实际返回记录条数(0~2) |
|
||||
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §6.4) |
|
||||
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §9.4) |
|
||||
|
||||
记录顺序 = 逻辑序号升序。QUERY 响应最大 130B:MTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
|
||||
|
||||
### 6.3 清除快照 `SNAP_CLEAR` (0x2A)
|
||||
### 9.3 清除快照 `SNAP_CLEAR` (0x2A)
|
||||
|
||||
**请求 data:** 无。**响应 data(1B):** `status`:`0x00`=OK。
|
||||
|
||||
@@ -172,7 +414,7 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
成功后 `SNAP_STAT` 的 `count` 归 0,`seq_last` 继续递增(序号不复位)。
|
||||
|
||||
### 6.4 快照记录格式(SnapRec,64B 定长,小端)
|
||||
### 9.4 快照记录格式(SnapRec,64B 定长,小端)
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
@@ -200,7 +442,7 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
---
|
||||
|
||||
## 7 记录格式(OfflogEvt,32B 定长,小端)
|
||||
## 10 记录格式(OfflogEvt,32B 定长,小端)
|
||||
|
||||
| 偏移 | 长度 | 字段 | 说明 |
|
||||
|------|------|------|------|
|
||||
@@ -234,10 +476,11 @@ Magic | Header | Data | CheckByte
|
||||
|
||||
---
|
||||
|
||||
## 8 版本历史
|
||||
## 11 版本历史
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |
|
||||
| V1.01 | 2026-08-12 | 分包上限改为随协商 MTU 动态:`chunk = min(MTU-9, 94)`;修复 Too large noti 丢包 |
|
||||
| V1.02 | 2026-08-12 | 新增传感快照 3 命令:SNAP_STAT / SNAP_QUERY / SNAP_CLEAR(0x28/0x29/0x2A) |
|
||||
| V1.03 | 2026-08-31 | 命令总表补齐既有配置命令(0x09~0x24、0x31、0x8A、0x92、0xC5)+ 透传通道(0x7F/0x9F);对照 dbn_ble_srv.c 如实标注未实现命令(0x17/0x18/0x20/0x21/0x87/0x88/0x89 无 case;0x23/0x24/0x8A 空壳;0xC0 上报未实现) |
|
||||
|
||||
+658
-4
@@ -2,6 +2,7 @@
|
||||
|
||||
> 基于《DLD960 串口通信协议》V1.01,将设备管理、参数配置、数据上报映射到 MQTT 协议。
|
||||
> 交互格式:JSON。
|
||||
> 版本:V1.14(2026-09-10,新增 §6.9 4G 配置同步(BLE → DBN → Air780);0x8F 定义为本机侧私有帧,帧长上限 260B)
|
||||
|
||||
---
|
||||
|
||||
@@ -96,6 +97,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,10 +154,22 @@ 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 |
|
||||
| `frame_cmd` | 4G 通道原始帧透传下发(hex 封装,可选兜底) | srv→dev | — |
|
||||
| `initialize` | 设备上电初始化登陆 | dev→srv | — |
|
||||
| `loop_data` | 线圈传感数据上报 | dev→srv | 0xC0 |
|
||||
| `event_report` | 事件上报(**平台须应答**,见 §5.3) | dev→srv | — |
|
||||
| `ota_report` | OTA 进度/结果主动上报 | dev→srv | — |
|
||||
| `heartbeat` | 设备心跳 | dev→srv | — |
|
||||
| `frame_report` | 4G 通道原始帧透传上报(hex 封装,可选兜底) | dev→srv | — |
|
||||
|
||||
> 4G 通道(方案 C,见 §6):上行由 Air780 解析 0x7F 帧并转换为**标准 JSON 命令**(loop_data / event_report / initialize / heartbeat,与有线通道一致),平台无感。`frame_cmd` / `frame_report` 保留为**可选兜底**(Air780 未实现转换的命令 / 未识别帧透传),有线通道不使用。
|
||||
|
||||
---
|
||||
|
||||
@@ -205,6 +231,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 +252,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 +830,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,8 +1138,12 @@ 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",
|
||||
"imei": "860012345678901",
|
||||
"iccid": "89860012345678901234",
|
||||
"csq": "21",
|
||||
"location": "113.9237976,022.6400375",
|
||||
}
|
||||
@@ -827,12 +1155,18 @@ 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.imei` | string | 可选,4G 模块 IMEI(V1.13;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,见 §6) |
|
||||
| `data.extra_info.iccid` | string | 可选,流量卡 ICCID(V1.13;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,见 §6) |
|
||||
| `data.extra_info.csq` | string | 可选,当前信号强度 |
|
||||
| `data.extra_info.location` | string | 可选,经纬度(经度,纬度) |
|
||||
|
||||
> **V1.10 说明**:`loop_ver`/`loop_hw_ver` 为**尽力携带**(上电后异步 0x4A 查询,initialize 发出时可能未就绪 → 回空);平台核对版本请用 `loop_version_query`(§4.25)。
|
||||
|
||||
|
||||
## 5.2 线圈传感数据 `loop_data`
|
||||
|
||||
@@ -967,6 +1301,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 +1375,317 @@ 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 喂狗不受影响。
|
||||
|
||||
---
|
||||
|
||||
# 6 4G 通道适配(方案 C:Air780 协议转换,V1.12 修订)
|
||||
|
||||
> 适用场景:vd960DBN 有线网络失效时,经 **Air8781P 整板(Air780EPM 4G 模组,LuatOS vd960Air 工程)** 兜底上报。
|
||||
> **方案 C**:Air780 解析 0x7F 帧并转换为**标准 JSON 命令**(与有线通道一致),平台无感。
|
||||
> 修订说明:V1.11 曾定方案 B(原始帧 hex 透传 frame_report/frame_cmd),V1.12 改为方案 C,frame_* 降级为可选兜底。
|
||||
|
||||
## 6.1 通道架构
|
||||
|
||||
```
|
||||
上行: vd960Loop --0x7F帧(UART2)--> vd960DBN --原样转发(UART1)--> Air780 --解析转JSON--> MQTT(loop_data/event_report/响应)
|
||||
下行: 平台 --标准JSON命令--> Air780 --转换0x7F帧--> vd960DBN --0x7F帧(UART2)--> vd960Loop
|
||||
```
|
||||
|
||||
- **vd960DBN**:UART2↔UART1 双向转发(魔数分流:`0x7F` 帧 → 转发 UART2(vd960Loop);`0x8F` 帧 → DBN 本地处理)。列入 vd960DBN 开发计划,**固件未实现**。⚠ 转发必须**不丢帧**(沿检测依赖完整 0xC0 帧流)。
|
||||
- **Air780**:0x7F 帧解析(Lua 状态机)+ **协议转换**(0x7F 帧 ↔ 标准 JSON,§6.3)+ MQTT(标准 JSON 命令面)
|
||||
- **平台**:标准 JSON 解析,与有线通道一致(**零新增依赖**)
|
||||
|
||||
## 6.2 命令面(4G 通道 = 标准 JSON)
|
||||
|
||||
| 通道 | 上行(dev→srv) | 下行(srv→dev) |
|
||||
|------|----------------|----------------|
|
||||
| 有线(ETH MQTT) | initialize / loop_data / event_report / heartbeat / ota_report(标准 JSON) | 标准 JSON 命令全表(dev_serial_set / ssc_net_* / iot_net_* / loop_param_* / report_config / log_* / ota_* / loop_version_query) |
|
||||
| 4G(Air780 转换) | **标准 JSON**(initialize / loop_data / event_report / heartbeat,Air780 从 0x7F 帧生成)+ 可选 frame_report | **标准 JSON**(Air780 转换为 0x7F 帧下发)+ 可选 frame_cmd |
|
||||
|
||||
- 4G 通道上行 JSON 结构与有线通道**完全一致**(平台按同一解析逻辑处理)
|
||||
- 4G 通道 `initialize` 的 `extra_info` 填真实值:`imei` / `iccid` / `csq`(V1.13;有线通道无 4G 模块时省略或空串)
|
||||
- `frame_report` / `frame_cmd` 保留为**可选兜底**(§6.8):Air780 未实现转换的命令 / 未识别帧,平台可直接发/收原始帧
|
||||
|
||||
## 6.3 Air780 协议转换职责(0x7F 帧 ↔ 标准 JSON)
|
||||
|
||||
**上行(0x7F → JSON):**
|
||||
|
||||
| 0x7F 帧 | 转换目标 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 0xC0 传感上报 | `loop_data` | 4 路通道字段映射(§5.2),携带 link(§6.7) |
|
||||
| 0xC0 car_state 沿 | `event_report` | 进出车事件(§6.4) |
|
||||
| 0xC0 loop_state 沿 | `event_report` | 线圈断开/恢复(loop_cut / loop_restore,§6.4) |
|
||||
| 0x09~0x1F 配置响应 | 对应命令响应 | code / msg / data(按 §4 各命令响应结构) |
|
||||
| 0x63 / 0x64 车检器参数响应 | `loop_param_set` / `loop_param_query` 响应 | 多路参数结构(§4.13 / §4.14) |
|
||||
| 0x4A 版本响应 | `loop_version_query` 响应 / `initialize.loop_ver` | 地感版本(§4.25) |
|
||||
|
||||
**下行(JSON → 0x7F):**
|
||||
|
||||
| 平台 JSON 命令 | 转换 0x7F 帧 | 说明 |
|
||||
|---------------|-------------|------|
|
||||
| loop_param_set / loop_param_query | 0x63 / 0x64 | 车检器多路参数 |
|
||||
| loop_version_query | 0x4A | 地感版本查询 |
|
||||
| (其余需转发的命令) | 对应 0x7F 命令 | 按《DLD960Loop_串口通信协议》 |
|
||||
|
||||
## 6.4 事件上报(仅线圈事件,V1.12 明确)
|
||||
|
||||
- **4G 通道事件面 = 仅线圈事件**(car_enter / car_leave / loop_cut / loop_restore,源自 0xC0 帧 car_state / loop_state 沿)
|
||||
- **不含 DBN 内部网络事件**(iot_connect / iot_ready / iot_reconn 等——Air780 无法感知 DBN 内部状态,**平台勿依赖 4G 通道获取网络事件**)
|
||||
- `event_report` 语义与有线通道**完全一致**(V1.04 机制):
|
||||
- 平台必答(回显 msg_id + code=0)→ 出队
|
||||
- 5s 超时重发,同 msg_id / 原始 ts,最多 3 次;耗尽挂起
|
||||
- 16 深环形队列,溢出丢最旧;多事件合并一条 publish
|
||||
- 跨重连保持同 msg_id(平台按 (sn, msg_id) 去重)
|
||||
- Air780 以 Lua 复刻 DBN `iot_evt_*` 逻辑(沿检测 + ACK 状态机 + 重发定时器),可行性已评估(2026-08-31):逻辑块全部可映射 Lua(table 队列 / sys.timer / mqtt 回调 / json),无硬障碍
|
||||
|
||||
## 6.5 命令响应链路
|
||||
|
||||
- 平台 JSON 命令 → Air780 转 0x7F → vd960DBN → vd960Loop → 响应帧 → vd960DBN → Air780 → JSON 回包
|
||||
- Air780 维护**单命令状态机**(暂存 msg_id + 超时回 code=5,与 DBN `g_lup_cmd` 同模式)
|
||||
- 链路 4 跳:响应超时建议与 DBN 命令超时一致(当前 300ms~1s 量级,待板级确认)
|
||||
|
||||
## 6.6 4G 通道不支持的命令(网络配置类)
|
||||
|
||||
| cmd | 说明 |
|
||||
|-----|------|
|
||||
| `ssc_net_set` / `ssc_net_query` | SSC 有线网络配置(4G 不适用) |
|
||||
| `iot_net_set` / `iot_net_query` | IoT 有线网络配置(4G 不适用) |
|
||||
| `iot_topic_set` / `iot_topic_query` | Topic 配置(4G 主题由 Air780 配置,同步链路见 §6.9) |
|
||||
|
||||
> 经 4G 通道下发以上命令:设备回 `code=4 unsupported`。
|
||||
>
|
||||
> 补充(§6.9):这些配置**仅能经 BLE 写入 DBN**。DBN 作为权威源,经 UART1 把配置同步给 4G 通道;
|
||||
> Air780 **不接受**平台经 4G 下发配置(否则会改掉自己正在使用的连接参数,形成第二权威源)。
|
||||
|
||||
## 6.7 link 对象(4G 特有字段)
|
||||
|
||||
| 字段 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| `imei` | mobile.imei() | 4G 模块 IMEI,设备唯一标识 |
|
||||
| `iccid` | mobile.iccid() | **流量卡卡号**,物联网卡管理识别用(卡商未写入 → 空串) |
|
||||
| `imsi` | mobile.imsi() | IMSI(部分卡返回空) |
|
||||
| `msisdn` | mobile.msisdn() | 手机号(物联网卡通常拿不到 → 空串) |
|
||||
| `csq` | mobile.csq() | 信号强度 0-31(31 最强,99/255 无信号) |
|
||||
| `net` | 固定 "4G" | 网络制式(预留扩展) |
|
||||
|
||||
## 6.8 平台侧要求
|
||||
|
||||
1. **标准 JSON 解析**:与有线通道一致(**零新增依赖**);依 `link.net` 或报文形态识别 4G 通道
|
||||
2. **设备唯一标识**:`dev_serial` 与有线通道同一序列号(Topic 族一致,平台认同一台设备);`link.imei` / `link.iccid` 辅助 4G 设备/流量卡管理
|
||||
3. **时钟校准**:Air780 上线发 `initialize`(JSON + link)后,平台照常下发 `report_config` 校准 `ts`(§2.3)
|
||||
4. **事件面约束**:4G 通道仅报线圈事件,不报 DBN 内部网络事件(§6.4)
|
||||
5. **可选兜底**:若启用 `frame_report` / `frame_cmd`(Air780 未识别帧 / 未实现转换命令),平台需按《DLD960Loop_串口通信协议》解析/组帧
|
||||
|
||||
## 6.9 4G 配置同步(BLE → DBN → Air780,V1.14 新增)
|
||||
|
||||
### 6.9.1 背景与权威源
|
||||
|
||||
4G 通道的 MQTT 连接参数(服务器地址、端口、ClientID、账号、密码、发布/订阅主题)由**蓝牙小程序经 BLE 写入 DBN**。§6.6 已规定这些配置**不允许经 4G 通道下发**(回 `code=4`),因此 **BLE 是网络配置的唯一入口**。若 DBN 不同步给 Air780,Air780 只能使用自身固件内的默认值,**平台侧改配置无效**。
|
||||
|
||||
配置权威源 = **DBN 侧 flash**(`IOT_NET_INFO` / `IOT_Topic` 结构,`cfig_flash.c` 持久化)。
|
||||
|
||||
| 角色 | 职责 |
|
||||
|------|------|
|
||||
| 蓝牙小程序 | **唯一配置入口**,经 BLE 写 DBN |
|
||||
| DBN | **唯一权威源**:持久化 + 应答拉取 + 主动推送 |
|
||||
| Air780 | 配置**消费者**:fskv 缓存仅用于加速启动;**唯一合法写入者 = 收到的 DBN 配置帧**(平台下发、产线预置均不得写入,否则产生第二权威源) |
|
||||
|
||||
### 6.9.2 帧格式
|
||||
|
||||
配置同步复用 **0x8F 本机侧私有帧**(与 BLE 侧 `MAGIC_BYTE_DBN_DEFAULT` 同值),沿用《DLD960Loop 串口通信协议》帧布局:
|
||||
|
||||
| 偏移 | 字段 | 说明 |
|
||||
|------|------|------|
|
||||
| 0 | `0x8F` | 本机侧私有帧魔数 |
|
||||
| 1 | `Addr` | 设备地址 |
|
||||
| 2 | `LEN` | `1 + DATA 长度`(最小 1) |
|
||||
| 3 | `CMD` | 见 §6.9.3 |
|
||||
| 4.. | `DATA` | 载荷,见 §6.9.4 |
|
||||
| -2 | `XOR` | 自 `Addr` 起算(**不含**魔数) |
|
||||
| -1 | `SUM` | 自 `Addr` 起算(**不含**魔数) |
|
||||
|
||||
帧总长 = `LEN + 5`;`XOR` / `SUM` 覆盖 = `Addr` 起 `2 + LEN` 字节。
|
||||
|
||||
**UART1 魔数语义(链路两端一致)**
|
||||
|
||||
| 魔数 | 处理 |
|
||||
|------|------|
|
||||
| `0x7F` | 业务帧:转发 UART2(Loop) |
|
||||
| `0x8F` | **本机侧私有帧**:链路两端各自本地消费,**均不转发** |
|
||||
| 其他 | 丢弃,并回找最近魔数重新同步(resync) |
|
||||
|
||||
**⚠ 帧长上限(两侧必须同步扩容)**
|
||||
|
||||
| 帧类型 | 帧长上限 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `0x7F` Loop 业务帧 | **70 B**(`LUP_MAX_PKG_LEN`,不变) | 与 Loop 协议隔离,维持原纪律 |
|
||||
| `0x8F` 本机侧同步帧 | **260 B**(`LEN` ≤ 255) | 覆盖最坏载荷(见 §6.9.4.3),**DBN 帧缓冲需由 70 B 扩至 260 B,Air780 parser 缓冲同步 ≥ 260 B** |
|
||||
|
||||
> 注:`LEN` 为 1 字节,理论上限 255,故同步帧最大 260 B(255 + 5)。B 端(Air780)解析器与 A 端(DBN)装配器**必须使用同一上限**,否则超长帧会被判 `LEN` 非法丢弃。
|
||||
|
||||
### 6.9.3 命令
|
||||
|
||||
复用 BLE 侧既有命令码(`dbn_ble_srv.h`),保证**同一命令在 BLE 通道与 UART1 同步通道语义一致**:
|
||||
|
||||
| CMD | 名称 | 方向 | 说明 |
|
||||
|-----|------|------|------|
|
||||
| `0x14` | `GET_IOT_NET` | 双向 | →:请求 net 配置(DATA 空);←:应答 net 配置(DATA 见 §6.9.4.1) |
|
||||
| `0x16` | `GET_IOT_TOPIC` | 双向 | →:请求 topic 配置(DATA 空);←:应答 topic 配置(DATA 见 §6.9.4.2) |
|
||||
|
||||
**应答方向统一使用 `GET_*` 命令码**:载荷由 DBN 侧既有构造器 `set_response_iot_net()` / `set_response_iot_topic()` 生成,与 **BLE 读响应逐字节同源**(零新增序列化代码,避免出现第二套口径)。DBN 主动推送时同样使用 `GET_*` + 对应载荷(语义 = “当前权威值”)。
|
||||
|
||||
### 6.9.4 载荷
|
||||
|
||||
`0x00` 分隔字符串序列;字符串按 `strlen` **紧凑发送**(不补足结构体定长)。
|
||||
|
||||
#### 6.9.4.1 net(CMD `0x14`)
|
||||
|
||||
| 序号 | 字段 | 类型 | 最大长度 | 说明 |
|
||||
|------|------|------|----------|------|
|
||||
| 1 | `remote_addr` | 字符串 | 63 | 服务器域名或 IP |
|
||||
| 2 | `mqtt_port` | ASCII 十进制 | 5 | 如 `1883` |
|
||||
| 3 | `client_id` | 字符串 | 63 | 空 = 使用本机序列号 |
|
||||
| 4 | `username` | 字符串 | 63 | |
|
||||
| 5 | `password` | 字符串 | 31 | 末字段**无**结尾 `0x00` |
|
||||
|
||||
#### 6.9.4.2 topic(CMD `0x16`)
|
||||
|
||||
| 序号 | 字段 | 类型 | 最大长度 | 说明 |
|
||||
|------|------|------|----------|------|
|
||||
| 1 | `clientid_enable` | ASCII `'0'` / `'1'` | 1 | `1` = 启用自定义 ClientID |
|
||||
| 2 | `topic_pub` | 字符串 | 63 | 上报主题 |
|
||||
| 3 | `topic_sub` | 字符串 | 63 | 下发主题 |
|
||||
|
||||
#### 6.9.4.3 长度核算
|
||||
|
||||
| 载荷 | 典型长度 | 最坏长度 | 结论 |
|
||||
|------|----------|----------|------|
|
||||
| net | ≈ 59 B | 63+1+5+1+63+1+63+1+31 = **229 B** | 单帧可传(需 260 B 帧上限 **+ 缓冲扩容**,见下注) |
|
||||
| topic | ≈ 51 B | 1+1+63+1+63 = **129 B** | 单帧可传 |
|
||||
|
||||
**必须拆成两条独立记录**(`net` / `topic`):既与 BLE 两个命令的粒度对齐,也使 Air780 侧 fskv 单值(**≤ 255 B**,`luat_fskv_set` 限制)安全:net 229 B < 255 B ✓,topic 129 B ✓。
|
||||
|
||||
Air780 侧建议**直接存原始载荷字节串(不转 JSON)**,加载时按 `0x00` 拆分 —— 与 DBN 侧构造器逐字节同源。
|
||||
|
||||
> ⚠️ **承载与缓冲前置条件(2026-09-10 补充)**:229 B 经 BLE **分包**确可送达 —— 分包头编码于 `pkg[1]` 高/低 4 位(总包数 / 序号),每包 dat ≤ 94 B(MTU ≥ 103),4 位限制下最多 15 包。
|
||||
> 但 DBN 侧 `g_buf_ble_response.dat` / `tmp_ble_buf` 仅 **132 B** 且 `set_response_iot_net()` / `set_response_iot_topic()` **无长度钳制** ⇒ 现版本载荷一旦 > 131 B 即**越界写**(可操作门槛:端口 4 位时 `host + clientid + username + password` 字符数 > 124)。
|
||||
> 故本节的 229 B 能力**以完成 §6.9.9 前置改造项 1–4 为前提**,否则属纸面能力。
|
||||
|
||||
### 6.9.5 交互流程
|
||||
|
||||
**① 拉取(主:兜底一切不一致)**
|
||||
|
||||
```
|
||||
Air780 上电 / 链路建立:
|
||||
① fskv.init() → 失败则降级为纯拉取模式(功能不受影响)
|
||||
② 读 fskv 缓存 → 有则立即用缓存发起 MQTT 连接(不必等 DBN 就绪)
|
||||
③ 链路建立后 → 发 0x8F CMD=0x14 / 0x16(DATA 空)
|
||||
④ DBN 应答 → 与缓存比对:相同则不动;不同则写 fskv + 断开重连重订阅
|
||||
```
|
||||
|
||||
**② 推送(辅:免重启立即生效)**
|
||||
|
||||
```
|
||||
BLE 写 SET_IOT_NET(0x13) / SET_IOT_TOPIC(0x15) / UPDATE_DEV_SERIAL(0x09) 成功
|
||||
→ DBN 持久化后,若 UART1 链路可用,立即发 0x8F CMD=0x14 / 0x16 + 权威载荷
|
||||
→ Air780 比对,变化则写 fskv + 重连重订阅
|
||||
```
|
||||
|
||||
DBN **不维护“待同步”状态位**:推送时链路不可用则不重试、不记账,等 Air780 下次拉取即可(无状态设计,避免状态机与真实链路状态不一致)。
|
||||
|
||||
**③ 约束**
|
||||
|
||||
- Air780 收到配置后 **先落 fskv 再重连**(避免掉电后反复以旧参数重连)
|
||||
- fskv **仅在值实际变化时写入**(littlefs 擦写磨损,4 KB 擦写块)
|
||||
- 重连流程:断开 → 以新参数连接 → 重订阅 `topic_sub` → 恢复上报
|
||||
|
||||
### 6.9.6 其他触发同步的 BLE 命令
|
||||
|
||||
| BLE 命令 | 值 | 是否必须同步 | 说明 |
|
||||
|----------|-----|--------------|------|
|
||||
| `CMD_DBN_UPDATE_DEV_SERIAL`(`0x09`) | 设备序列号 | **必须** | Air780 的 Topic 族与上报 JSON 的 `dev_serial` 均由序列号派生。序列号变更 → Topic 族变更,Air780 需重算并重连 |
|
||||
| `CMD_DBN_SET_SUB_CODE`(`0x22`) | `iot_enable` 位 | **必须** | IoT 通道总使能;DBN 有线 WCHNET MQTT 与 Air780 4G 通道均受此位控制。不同步 → DBN 已停发而 Air780 仍在连接(白耗流量),平台侧表现为“在线无数据” |
|
||||
| `CMD_DBN_RW_UART_BAUD`(`0x31`) | UART1 波特率 | **禁止在线修改** | 该命令可改任意串口波特率,含 UART1(4G 链路)。在线改动而两侧未同步 = **链路永久失联,只能拆机重刷**。建议固件侧对 `uart_num == 1` 直接拒绝 |
|
||||
|
||||
### 6.9.7 明确不同步的配置
|
||||
|
||||
| 配置 | 原因 |
|
||||
|------|------|
|
||||
| `local_net_cfg`(有线 IP/网关/掩码 + 各端口)、`net_center_info`(`lssc_ip` / `tcp_port`) | 有线以太网通道专用,与 4G 模组无关 |
|
||||
| `CMD_DBN_SET_CJQ_*`(车检器参数)、`CMD_DBN_LOOP_*` | 经 UART2 下发给 Loop,方向与 4G 无关 |
|
||||
| `CMD_DBN_SET_SUB_CODE` 除 `iot_enable` 外的位 | 局部外设使能(网口 / 雷达 / 激光 / LoRa 等) |
|
||||
| `CMD_DBN_OFFLOG_*` / `CMD_DBN_SNAP_*` | 本地 flash 读写;4G 侧仅作数据通道(§4 `log_*` 的 `stream=snapshot` 已覆盖) |
|
||||
| `CMD_DBN_CHECK_PASS` / `CMD_DBN_MODIFY_PASS` | 蓝牙访问密码,纯本地 |
|
||||
| `CMD_DBN_SET_FACTORY` / `CMD_DBN_RESET_DEV` | 本地动作;若涉及重启,链路断开重连由 Air780 自行处理 |
|
||||
| 所有 `GET_*`(读操作) | 无同步语义 |
|
||||
|
||||
**不随载荷同步的字段**:`IOT_NET_INFO.mode`(IP / DNS 标志)。实测该字段**无 BLE 写入路径**、恒为 `0`(`IOT_Addr_IP_Mode`),当前仅 DBN 有线 MQTT 路径读取。Air780 侧按主机串自行判别地址类型(LuatOS `socket.connect(host, port)` 对 IP 与域名均兼容),无需同步。
|
||||
|
||||
### 6.9.8 异常处理
|
||||
|
||||
| 场景 | 行为 |
|
||||
|------|------|
|
||||
| 0x8F 帧校验失败 | 丢弃 + 回找最近魔数 resync;**不回错**(配置链路静默重试成本低) |
|
||||
| `LEN` 非法 / 帧超上限 | 丢弃并重新同步(`LEN < 1` 或 `LEN + 5 > 260`) |
|
||||
| Air780 请求时 DBN 尚无有效配置 | DBN 回**出厂默认值**(`cfig_flash.c` 初始化值),保证 Air780 有明确行为而非空值 |
|
||||
| fskv 挂载失败(首刷 / 分区异常) | `fskv.init()` 返回 false → **降级为纯拉取模式**,不阻塞业务;缓存丢失不影响正确性 |
|
||||
| 推送时 UART1 链路不可用 | 不重试、不记状态;等 Air780 下次拉取 |
|
||||
| Air780 重连失败(服务器不可达 / 鉴权失败) | 按既有重连策略退避重试;MQTT 失败**不影响** UART1 链路与配置同步 |
|
||||
| 载荷含 `0x00` 的字段(如密码本身含 `0x00`) | 不支持;字段均为可见 ASCII 串 |
|
||||
|
||||
> **已知缺口(待办)**:DBN 自身经**有线 WCHNET 直连 MQTT** 的路径(`peripheral_main.c`)**仅实现 IP 模式,域名模式分支为空**。故配置为域名时,有线通道 MQTT 不工作(4G 通道不受影响)。是否补齐由产品决定。
|
||||
|
||||
### 6.9.9 实施状态
|
||||
|
||||
| 侧 | 项目 | 状态 |
|
||||
|----|------|------|
|
||||
| DBN | UART1 通道 + 0x8F 本机侧私有帧接入本机指令处理器(不转发) | **已实现**(2026-09-10) |
|
||||
| DBN | UART1 帧缓冲 70 B → 260 B 扩容 | 待实现 |
|
||||
| DBN | ⚠️ **前置**:`g_buf_ble_response.dat` / `tmp_ble_buf` 132 B → ≥ 260 B(与蓝牙小程序/MRS 工程**同步**扩容) | 待实现 |
|
||||
| DBN | ⚠️ **前置**:`set_response_iot_net/topic()` 补长度钳制(对齐 `set_response_buf()` @`dbn_ble_srv.c:288`) | 待实现 |
|
||||
| DBN | ⚠️ **前置**:`unpack_packs()` 接收侧累加加钳制(`dat_len + (_len-1) > MAX_BLE_DAT_BUF_LEN` 即丢包清缓冲,防 `uint8_t` 回绕) | 待实现 |
|
||||
| DBN | ⚠️ **前置**:`unpack_packs()` 用起 `len` 形参(现全程未引用,memcpy 长度全取自 `pkg[2]`) | 待实现 |
|
||||
| DBN | 0x8F 配置请求应答 + BLE 写成功后主动推送 | 待实现 |
|
||||
| DBN | `CMD_DBN_RW_UART_BAUD` 拒绝 `uart_num == 1` | 待实现 |
|
||||
| Air780 | `frame_parser.lua` 支持 0x8F(本地消费,不转发)| 待实现 |
|
||||
| Air780 | parser 缓冲 ≥ 260 B | 待实现 |
|
||||
| Air780 | fskv 缓存 + 启动即连 + 拉取校准 + 变化重连重订阅 | 待实现 |
|
||||
| Air780 | 配置读取由 require 期 `local` 改为重连时重读 | 待实现 |
|
||||
|
||||
---
|
||||
|
||||
# 修订记录
|
||||
@@ -1054,4 +1700,12 @@ 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.15 | 2026-09-10 | §6.9 补充 **BLE 分包承载与缓冲边界**:核实 BLE 链路**分包机制存在且收发双向对称**(分包头编码于 `pkg[1]` 高/低 4 位 = 总包数/序号;每包 dat ≤ 94 B @MTU ≥ 103;4 位 ⇒ 最多 15 包)⇒ 229 B 载荷**确可送达**,故「载荷 > 131 B 即越界写 `g_buf_ble_response.dat` / `tmp_ble_buf`(132 B,构造器无钳制)」属**可达缺陷**而非纸面;§6.9.9 新增 4 条**实现前置改造项**(缓冲扩 ≥260 B / 构造器补钳制 / 接收侧累加钳制防 `uint8_t` 回绕 / `unpack_packs` 用起 `len` 形参);§6.9.4.3 补承载前置条件说明 | wangfq |
|
||||
| V1.14 | 2026-09-10 | 新增 **§6.9 4G 配置同步(BLE → DBN → Air780)**:明确配置权威源为 DBN flash(Air780 侧 fskv 仅作启动缓存,**唯一写入者 = DBN 配置帧**,避免第二权威源);复用 **0x8F 本机侧私有帧**(与 BLE 同魔数 `MAGIC_BYTE_DBN_DEFAULT`,链路两端各自本地消费、均不转发),命令复用 `GET_IOT_NET(0x14)` / `GET_IOT_TOPIC(0x16)`,载荷与 BLE 读响应**逐字节同源**(复用 `set_response_iot_net()` / `set_response_iot_topic()`,零新增序列化)、拆 net/topic 两条(最坏 229B / 129B,均 ≤ fskv 单值 255B);**0x8F 帧长上限独立定为 260B**(Loop 业务帧 70B 上限不变,两侧帧缓冲需同步扩容);同步触发点补 `UPDATE_DEV_SERIAL(0x09)` / `SET_SUB_CODE` 的 `iot_enable` 位 / **禁改 UART1 波特率**;V1.11 的 **0x7D 帧配置同步方案作废**;`iot_net_info.mode` 实测无 BLE 写入路径、恒为 IP 模式,故不随载荷同步 | wangfq |
|
||||
| V1.13 | 2026-08-31 | `initialize` 的 `extra_info` 增加可选字段 **`imei`** / **`iccid`**(4G 模块 IMEI / 流量卡 ICCID;无 4G 模块时省略或空串;4G 通道由 Air780 填真实值,§6.2 说明) | wangfq |
|
||||
| V1.12 | 2026-08-31 | **4G 通道适配修订:方案 B(hex 透传)改为方案 C(Air780 协议转换)**——Air780 解析 0x7F 帧并转换为**标准 JSON 命令**(loop_data / event_report / initialize / heartbeat,与有线通道一致),平台零改动;V1.11 的 frame_report / frame_cmd 降级为**可选兜底**;4G 事件面 = **仅线圈事件**(car_enter/car_leave/loop_cut/loop_restore,不含 DBN 内部网络事件);命令响应链路 Air780 单命令状态机(超时回 code=5);link 对象保留;Air780 以 Lua 复刻 iot_event_report 逻辑(沿检测 + ACK + 5s×3 重发 + 16 深队列 + 跨重连同 msg_id),可行性已评估(2026-08-31) | wangfq |
|
||||
| V1.11 | 2026-08-31 | **4G 通道适配(方案 B:原始帧透传 + hex 封装)**:新增 `frame_report`(dev→srv,§6.3)/ `frame_cmd`(srv→dev,§6.4)两命令(4G 通道专用);4G 通道**不使用**有线标准 JSON 业务命令(loop_data/event_report),不适用网络配置类命令(ssc_net_*/iot_net_*/iot_topic_*,4G 通道下发回 code=4);上行附加 `link` 对象(IMEI/ICCID/IMSI/MSISDN/CSQ,§6.6);链路层数据面 0x7F 帧字节流透传(魔数分流在 vd960DBN 侧),0x7D 帧仅 DBN↔Air780 配置同步/握手;平台双通道区分解析 + 新增《DLD960Loop_串口通信协议》解析依赖(§6.7);实施主体:Air8781P(Air780EPM)vd960Air 工程,vd960DBN UART1 通道列入开发计划(固件未实现) | wangfq |
|
||||
| V1.10 | 2026-08-20 | **网络上报携带地感版本**(配合远程 OTA 升级前后版本核对):`dev_info_query` 响应 + `initialize` 上报新增 `loop_ver`/`loop_hw_ver`(地感 Loop MCU 固件/硬件版本,格式 `"主.次.次"`,来自 0x4A 查询缓存,尽力携带可为空);新增命令 `loop_version_query`(§4.25,srv→dev 实时查询,设备经 UART2 0x4A 异步查询后回包,含 `loop_ver`/`loop_hw_ver`/`version_str`);版本语义:`soft_ver`=整机 DBN 固件(`主.次`),`loop_ver`=地感 Loop 固件(`主.次.次`),二者区分 | wangfq |
|
||||
| V1.09 | 2026-08-20 | **OTA 刷写结果判定修复**(现场:刷写物理成功但平台误判"刷写未启动"):① 刷写成功后台侧状态回 `idle`(镜像保留:`size`/`crc32`/`version` 不变,`last_result=0`,可重刷),与"下载完成待刷 `ready`"严格区分;② `ota_report` 升级为刷写结果**主依据**(`stage=done`/`failed` 设备必报,重发 3 次×5s),`ota_status` 仅兜底;③ 明确平台判定指引——`idle+size>0+last_result=0`=成功,不得以轮询未见 `flashing` 或状态持续 `ready` 判"刷写未启动"(本地刷写 <1s,轮询大概率错过中间态);④ `ota_begin` 兼容 `idle+size/crc32 一致` → 免下载直接可刷(重刷) | wangfq |
|
||||
| V1.08 | 2026-08-20 | 增加 **Loop MCU 远程 OTA**(ROADMAP P1.4 ①,先存后刷):命令 `ota_begin` / `ota_data` / `ota_end` / `ota_abort` / `ota_flash` / `ota_status`(srv→dev)+ `ota_report`(dev→srv);单片 256B + 单片/全镜像 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 喂狗)不受影响 |
|
||||
+10
-6
@@ -1,7 +1,7 @@
|
||||
# DLD960 四通道车辆检测器 产品手册
|
||||
|
||||
> Product Manual · 产品型号:**DLD960GA**
|
||||
> 文档版本 V1.02 · 2026-08-19 · 适用固件:整机 V1.02.04(Loop 1.0 + DBN 1.02.04)
|
||||
> 文档版本 V1.04 · 2026-09-02 · 适用固件:整机 V1.02.05(Loop V1.03 + 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. 硬件说明
|
||||
|
||||
@@ -48,7 +50,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
| LED_LP1~LP4 | 绿色 | 对应线圈 1~4:有车亮 / 无车灭 |
|
||||
| ETH_LED_G / ETH_LED_Y | 绿/黄 | 网口连接/活动指示 |
|
||||
|
||||
> 上电后 LED_PWR 呼吸、四路车检灯全灭(无车),约 1.3 秒完成基线建立进入检测状态。若某路车检灯常亮且现场无车,参见 §7 故障排查。
|
||||
> 上电后 LED_PWR 呼吸;各通道车检灯自检慢闪,该路绿灯闪至**本路基准稳定**才停(约 2~5 s,最长 5 s 兜底),随后无车全灭进入检测状态。若某路车检灯常亮且现场无车,参见 §7 故障排查。
|
||||
|
||||
### 2.3 线圈施工要求(关键!)
|
||||
|
||||
@@ -67,7 +69,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
## 3. 快速上手
|
||||
|
||||
1. **接线**:接入线圈、继电器输出至道闸/控制器,需要联网则接网线。
|
||||
2. **上电**:LED_PWR 呼吸闪烁,约 1.3 秒后进入检测状态(上电 3 秒内不主动上报数据)。
|
||||
2. **上电**:LED_PWR 呼吸闪烁;各通道绿灯自检慢闪至基准稳定(约 2~5 s,最长 5 s 兜底)后进入检测状态(上电 3 秒内不主动上报数据)。
|
||||
3. **配置**:手机小程序经蓝牙连接设备 → 验证密码 → 设置各路灵敏度/频率/输出模式。
|
||||
4. **验证**:车辆压线,对应绿色车检灯亮、继电器吸合;离开后灯灭、继电器释放。
|
||||
5. **联网(可选)**:小程序或 TCP 命令配置 IP/MQTT 参数,接入平台。
|
||||
@@ -132,7 +134,7 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
| 通信 MCU | 蓝牙 OTA | 小程序选择固件直接升级 |
|
||||
| 检测 MCU | ISP 透传 | 小程序经蓝牙→通信 MCU 串口透传升级 |
|
||||
|
||||
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.04。
|
||||
> ⚠ **双 MCU 固件必须使用同一发布包配套升级**(V1.0.0 起为强制要求),只升级一侧会导致内部通信数据错位。当前配套:Loop 1.0 + DBN 1.02.05。
|
||||
>
|
||||
> ⚠ **检测 MCU 升级完成后,若小程序无响应,请将设备断电重启**(DBN OTA 透传模式需重启恢复,V1.02.03 已知约束)。
|
||||
|
||||
@@ -148,7 +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.04 及以上版本 |
|
||||
| 配置无法保存/每次上电恢复出厂 | 旧固件 factory 写入被禁用;换新存储芯片后参数区无出厂配置(V1.02.04 已修复) | 升级整机固件至 V1.02.05 及以上版本 |
|
||||
| 事件重复上报 | 平台未按协议应答 event_report | 平台须回显 msg_id 应答(先落库后应答) |
|
||||
| 线圈断开告警 | 线圈开路/引线断裂 | 万用表量线圈回路电阻(正常几欧姆以内) |
|
||||
|
||||
@@ -168,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 对接开发(脱机日志与传感快照读写) |
|
||||
| 《环路车辆检测器验收标准》 | 采购/部署验收 |
|
||||
|
||||
@@ -178,6 +180,8 @@ DLD960 是一款四通道环形线圈车辆检测器,一台设备即可覆盖
|
||||
|
||||
| 版本 | 修订时间 | 修订说明 | 修订人 |
|
||||
|------|----------|----------|--------|
|
||||
| V1.04 | 2026-09-02 | Loop V1.03 单侧更新:绿灯上电自检说明改为闪至基准稳定才停(约 2~5 s,最长 5 s 兜底),上电流程/故障排查同步 | wangfq |
|
||||
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:核心特性补网络上报携带地感版本 + 联网稳定性加固;相关文档表 MQTT 协议 V1.07→V1.10(修正滞后);故障排查配置保存建议升至 V1.02.05 及以上 | wangfq |
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.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 |
|
||||
|
||||
+11
-8
@@ -1,7 +1,7 @@
|
||||
# DLD960 四通道车辆检测器 技术规格书
|
||||
|
||||
> Technical Specification · 产品型号:**DLD960GA**
|
||||
> 文档版本 V1.02 · 2026-08-19 · 配套整机发布 V1.02.04
|
||||
> 文档版本 V1.04 · 2026-09-02 · 配套整机发布 V1.02.05(Loop V1.03 单侧更新)
|
||||
|
||||
---
|
||||
|
||||
@@ -62,7 +62,7 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
|
||||
| 基线跟踪 | 滑动平均窗口 500 样本(5s 更新周期,噪声抑制 22×) | 跟随温漂/环境慢变 |
|
||||
| 基线冻结保护 | 偏差超限即冻结基线更新 | 防止过车信号污染基线 |
|
||||
| 冻结超时自适应 | 10 s 超时 + ±2% 稳定性窗校验 | 温漂/换线圈后自动重建基线;慢速进车不误吸收 |
|
||||
| 上电稳定期 | 128 样本(≈1.3 s)快速收敛,上电 3 s 内抑制主动上报 | 快速可用且不上报无效数据 |
|
||||
| 上电稳定期 | 基准稳定判定(Loop V1.03 起):连续 2 窗(100 样本/窗≈1 s)Origin 均值漂移 ≤0.1% + 最少 128 样本(≈1.3 s),硬兜底 500 样本(≈5 s);上电 3 s 内抑制主动上报 | 基准真正稳定才进入检测,避免上电误判;绿灯自检闪至本路基准稳定才停 |
|
||||
| 线圈安全模式 | 可配置超时(单位 10 s,0=关闭) | 异常长时间占用强制复位重建基线 |
|
||||
|
||||
### 3.4 检测性能指标(设计目标,依据《环路车辆检测器验收标准》v1.0)
|
||||
@@ -100,13 +100,13 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
|
||||
| 外部存储 | SPI NOR Flash | 参数存储 + 日志存储 |
|
||||
| 按键 | K1 多功能按键 | 复位等 |
|
||||
|
||||
### 5.1 协议矩阵(V1.02.04 配套)
|
||||
### 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.04 配套版本 |
|
||||
| 组件 | V1.02.05 配套版本 |
|
||||
|------|-------------------|
|
||||
| vd960Loop 固件 | 1.0(不变) |
|
||||
| vd960DBN 固件 | 1.02.04 |
|
||||
| vd960DBN 固件 | 1.02.05 |
|
||||
|
||||
### 10.1 已知约束(V1.02.01)
|
||||
|
||||
- 固件版本三段式:`FIRMWARE_VER="1.02.04"`(MAIN=1 / SUB=2 / SUBSUB=4);BLE 上报仅 MAIN/SUB 两字节(1.02),SUBSUB 只体现在字符串上报(MQTT/TCP JSON)。
|
||||
- 固件版本三段式:`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,8 @@ DLD960 是一款基于环形线圈(LC 振荡)检测原理的四通道车辆
|
||||
|
||||
| 版本 | 修订时间 | 修订说明 | 修订人 |
|
||||
|------|----------|----------|--------|
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.04:UART2 RX 0x9F OTA 透传机制说明(§2/§5/§7)、协议矩阵与版本配套矩阵更新、已知约束补充 OTA 无自动退出;存储适配(SPI 识别去厂商代码 + factory 写入恢复) | wangfq |
|
||||
| V1.04 | 2026-09-02 | Loop V1.03 单侧更新:上电稳定期由固定 128 样本改为基准判稳(连续 2 窗 Origin 漂移 ≤0.1% + 5 s 硬兜底),绿灯自检闪至基准稳定才停止 | wangfq |
|
||||
| V1.03 | 2026-08-21 | 配套整机发布 V1.02.04→V1.02.05:协议矩阵标题/版本配套矩阵/固件三段式版本同步;新增已知约束 UART2 0x4A 版本查询总线时序;MQTT V1.10 地感版本上报与 loop_version_query 说明 | wangfq |
|
||||
| V1.02 | 2026-08-19 | 配套整机发布 V1.02.03→V1.02.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 |
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define PRODUCT_MODEL "DLD960GA"
|
||||
#define FIRMWARE_VER "1.02.04"
|
||||
#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 4
|
||||
#define FIRMWARE_VER_SUBSUB 5
|
||||
#define HARDWARE_VER_MAIN 1
|
||||
#define HARDWARE_VER_SUB 1
|
||||
|
||||
@@ -154,4 +154,16 @@ void uart_srv(void);
|
||||
void UART2_SendString(uint8_t *buf, uint16_t len);
|
||||
void UART1_SendString(uint8_t *buf, uint16_t len);
|
||||
|
||||
/* ---- 4G 通道: UART1 <-> Air780 (协议 §6.1 纯字节流透传) ---- */
|
||||
void uart1_dma_init(void); /* 初始化 (DEBUG!=0 时为空实现) */
|
||||
void uart1_dma_poll(void);
|
||||
|
||||
/* DBN(CH32V208GBU6) 私有指令分发器 (dbn_ble_srv.c) —— 0x8F 帧, 4G 下行本地指令复用同一入口 */
|
||||
void manage_dbn_ble_default(uint8_t *pkg, uint8_t len); /* 主循环轮询: 消费 DMA 缓冲 -> 分流 */
|
||||
extern uint32_t g_uart1_fwd_to_air; /* 上行转发帧数 (UART2->UART1) */
|
||||
extern uint32_t g_uart1_fwd_to_loop; /* 下行转发帧数 (UART1->UART2, 0x7F) */
|
||||
extern uint32_t g_uart1_local_cnt; /* 下行本地处理帧数 (0x8F) */
|
||||
extern uint32_t g_uart1_badchk; /* 下行校验失败帧数 */
|
||||
extern uint32_t g_uart1_drop; /* 下行 DMA 溢出丢弃次数 */
|
||||
|
||||
#endif /* INCLUDE_CMCNG_H_ */
|
||||
|
||||
@@ -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,8 +118,10 @@ static void iot_evt_enqueue(uint8_t type, uint8_t ch, uint32_t value) {
|
||||
IotEvent *e = &_evt_queue[(_evt_head + _evt_count) % IOT_EVT_QUEUE_DEPTH];
|
||||
e->type = type; e->ch = ch; e->value = value;
|
||||
_evt_count++;
|
||||
/* 事件日志: 线圈事件 (主循环上下文) */
|
||||
offlog_coil((uint8_t)type + 1, ch, value); /* sub: 1=进 2=出 3=断开 4=恢复 */
|
||||
/* 事件日志: 线圈事件 (主循环上下文); 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,19 +448,32 @@ 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) {
|
||||
_iot_send_fail_cnt++;
|
||||
PRINT("IOT: SocketSend FAIL#%d ret=0x%02X sent=%u/%u\n",
|
||||
_iot_send_fail_cnt, ret, total_sent, len);
|
||||
if (_iot_send_fail_cnt >= 3) {
|
||||
PRINT("IOT: SocketSend 3 consecutive failures, forcing reconnect\n");
|
||||
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
|
||||
g_iot_socket = 0xFF;
|
||||
g_iot_state = IOT_STATE_DISCONNECTED;
|
||||
_iot_send_fail_cnt = 0;
|
||||
_iot_reconnect_deadline = mstick() + 3000; // 3s 后重连, 让 WCHNET 清旧 socket
|
||||
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 内安全) */
|
||||
}
|
||||
return -1;
|
||||
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);
|
||||
if (_iot_send_fail_cnt >= 3) {
|
||||
PRINT("IOT: SocketSend 3 consecutive failures, forcing reconnect\n");
|
||||
WCHNET_SocketClose(g_iot_socket, TCP_CLOSE_NORMAL);
|
||||
g_iot_socket = 0xFF;
|
||||
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; // 成功一次就清零
|
||||
@@ -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\","
|
||||
"\"extra_info\":{\"code\":\"0\",\"csq\":\"0\",\"location\":\"\"}}}",
|
||||
"\"loop_ver\":\"%s\",\"loop_hw_ver\":\"%s\","
|
||||
"\"extra_info\":{\"code\":\"0\",\"imei\":\"\",\"iccid\":\"\",\"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];
|
||||
|
||||
@@ -122,7 +122,7 @@ uint32_t dev_time_now(void)
|
||||
void mStopIfError(u8 iError)
|
||||
{
|
||||
if (iError == WCHNET_ERR_SUCCESS) return;
|
||||
printf("Error: 0x%02X\r\n", (u16)iError);
|
||||
PRINT("Error: 0x%02X\r\n", (u16)iError); /* 原裸 printf: DEBUG=0 时会去写未被初始化的 USART1 */
|
||||
}
|
||||
|
||||
void mqtt_connect(void)
|
||||
@@ -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,8 @@ void Main_Circulation(void)
|
||||
FAULT_MARKER(MK_UART_SRV_IN);
|
||||
uart_srv();
|
||||
FAULT_MARKER(MK_UART_SRV_OUT);
|
||||
uart1_dma_poll(); /* 4G 通道下行收帧 (协议 §6.1): 置于 fault marker 之外, 不改变 MK_UART_SRV 语义 */
|
||||
ota_poll(); /* OTA 刷写状态机 (V1.08): 非阻塞 tick 驱动, 先消费 UART2 ACK 再推进 */
|
||||
snap_flush(); /* 快照落盘: 无条件挂主循环 (原在 iot_enable 分支, TCP 模式不落盘) */
|
||||
FAULT_MARKER(MK_POLL_BLE_IN);
|
||||
poll_dbn_ble();
|
||||
@@ -328,6 +331,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 门控自动丢弃不落盘 */
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "cmcng.h"
|
||||
#include "debug.h" /* DEBUG 宏: 4G 版本以 -DDEBUG=0 编译, 此时 printf 不占用 USART1 */
|
||||
#include "loop_uart_proto.h"
|
||||
#include "dbn_ble_srv.h" /* MAGIC_BYTE_DBN_DEFAULT(0x8F): DBN 私有指令魔数 */
|
||||
#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")));
|
||||
@@ -112,6 +115,245 @@ void uart2_dma_poll(void)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*==================== UART1 <-> Air780 4G 通道 (协议 §6.1) ====================
|
||||
* 定位: 4G 通道中 vd960DBN 只做"字节流透传", 零业务转换 ——
|
||||
* 上行: Loop 0x7F 帧(UART2) --校验通过后原样转发--> UART1 --> Air780 解析转 JSON
|
||||
* 下行: Air780 帧(UART1) --魔数分流--> 0x7F: 转发 UART2(给 vd960Loop)
|
||||
* 0x8F: DBN(CH32V208GBU6) 私有指令 ——
|
||||
* 只作用在自身读写配置上, 一般不转发
|
||||
*
|
||||
* 硬约束:
|
||||
* 1) 只转发 lup_verify_checksum() 通过的完整帧。Air780 侧靠 loop_state 变化沿判车,
|
||||
* 转发一个残缺帧 = 凭空多判一台车。
|
||||
* 2) 接收必须走 DMA (与 UART2 方案A 同理): PRINT 临界区 / BLE 栈回调 / SPI 擦除等
|
||||
* 长阻塞窗口会屏蔽 RXNE 中断 -> 丢字节 -> 校验失败。本通道"不丢帧"是硬要求。
|
||||
* 3) 帧装配器独立于 g_lup_parser: 后者专供 UART2(Loop) 通道, 两条流的字节绝不能
|
||||
* 混进同一个状态机 (混了就是互相残杀)。
|
||||
*
|
||||
* 帧格式 (与 Loop/BLE 协议完全一致):
|
||||
* [7F][Addr][LEN][CMD][Value: LEN-1B][XOR][SUM] 总长 = LEN + 5
|
||||
* 校验覆盖 Addr..Value (不含 magic), 复用 lup_verify_checksum()
|
||||
*
|
||||
* 编译开关:
|
||||
* 调试版 (DEBUG != 0): printf 占用 USART1/PB6, 本通道不参与编译 -> 走空实现
|
||||
* 4G 版 (DEBUG == 0): -DDEBUG=0 编译即自动启用 (PB6/PB7 由本文件接管)
|
||||
* 亦可 -DUART1_AIR780_EN=1/0 强制覆盖
|
||||
*===========================================================================*/
|
||||
#ifndef UART1_AIR780_EN
|
||||
#define UART1_AIR780_EN (DEBUG == 0)
|
||||
#endif
|
||||
|
||||
#define UART1_MAGIC_LOOP LUP_MAGIC /* 0x7F: 转发给 vd960Loop */
|
||||
#define UART1_MAGIC_LOCAL MAGIC_BYTE_DBN_DEFAULT /* 0x8F: DBN 私有指令(自身配置读写), 不转发 */
|
||||
#define UART1_DMA_BUF_LEN 512
|
||||
|
||||
#if (UART1_AIR780_EN)
|
||||
|
||||
static uint8_t uart1_dma_buf[UART1_DMA_BUF_LEN] __attribute__((aligned(4)));
|
||||
static uint16_t uart1_dma_last = 0; /* 主循环消费位置 (DMA 写指针由硬件维护) */
|
||||
|
||||
/* 下行帧装配器 (独立实例, 不与 g_lup_parser 共享) */
|
||||
static uint8_t uart1_rx_frame[LUP_MAX_PKG_LEN];
|
||||
static uint16_t uart1_rx_idx = 0; /* 已收字节数 */
|
||||
static uint16_t uart1_rx_need = 0; /* 本帧总长, 0 = 尚未定长 */
|
||||
|
||||
/* 通道计数: 现场核对"上行转发数 == Air780 实收数", 比翻日志可靠 */
|
||||
uint32_t g_uart1_fwd_to_air = 0; /* 上行: UART2 -> UART1 转发帧数 */
|
||||
uint32_t g_uart1_fwd_to_loop = 0; /* 下行: UART1 -> UART2 转发帧数 */
|
||||
uint32_t g_uart1_local_cnt = 0; /* 下行: DBN 私有指令帧数 (0x8F) */
|
||||
uint32_t g_uart1_badchk = 0; /* 下行: 校验失败帧数 */
|
||||
uint32_t g_uart1_drop = 0; /* 下行: DMA 溢出丢弃次数 */
|
||||
|
||||
/* ---- 下行分流 ---- */
|
||||
static void uart1_dispatch_frame(uint8_t *pkg, uint16_t len)
|
||||
{
|
||||
if (pkg[0] == UART1_MAGIC_LOOP) {
|
||||
/* 0x7F -> 原样转发给 vd960Loop */
|
||||
g_uart1_fwd_to_loop++;
|
||||
UART2_SendString(pkg, len);
|
||||
} else {
|
||||
/* 0x8F -> DBN(CH32V208GBU6) 私有指令: 只作用在自身读写配置上, 不转发给 Loop/Air780。
|
||||
* 直接复用 BLE 侧同一个分发器 manage_dbn_ble_default() —— 两边帧布局完全一致
|
||||
* (pkg[1]=Addr, pkg[2]=LEN, pkg[3]=CMD, pkg[4..]=Value), 零适配。
|
||||
* OTA 期间拒绝: 私有指令会写自身配置(碰 flash), 与 OTA 刷写并发是自找麻烦。 */
|
||||
g_uart1_local_cnt++;
|
||||
if (g_flag_counter_ota.flag == 0) {
|
||||
manage_dbn_ble_default(pkg, (uint8_t)len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 坏帧回找魔数: 把 [1..len) 里第一个魔数起的残余搬到头部继续装配 ----
|
||||
* 防"单帧损坏 -> 后续帧全部失步" */
|
||||
static void uart1_resync(const uint8_t *buf, uint16_t len)
|
||||
{
|
||||
uint16_t i;
|
||||
for (i = 1; i < len; i++) {
|
||||
if (buf[i] == UART1_MAGIC_LOOP || buf[i] == UART1_MAGIC_LOCAL) {
|
||||
uint16_t k, n = 0;
|
||||
for (k = i; k < len; k++) {
|
||||
uart1_rx_frame[n++] = buf[k];
|
||||
}
|
||||
uart1_rx_idx = n;
|
||||
uart1_rx_need = 0;
|
||||
if (n >= 3) {
|
||||
uint8_t lf = uart1_rx_frame[2];
|
||||
uint16_t need = (uint16_t)lf + 5;
|
||||
if (lf >= 1 && need <= LUP_MAX_PKG_LEN) {
|
||||
uart1_rx_need = need;
|
||||
} else {
|
||||
uart1_rx_idx = 0; /* LEN 非法: 残余作废 */
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* 残余中没有魔数 -> 全部丢弃 */
|
||||
}
|
||||
|
||||
/* ---- 定长收满 -> 校验 -> 分流 ---- */
|
||||
static void uart1_frame_ready(void)
|
||||
{
|
||||
uint16_t len = uart1_rx_idx;
|
||||
uart1_rx_idx = 0;
|
||||
uart1_rx_need = 0;
|
||||
if (lup_verify_checksum(uart1_rx_frame, len) == 0) {
|
||||
uart1_dispatch_frame(uart1_rx_frame, len);
|
||||
} else {
|
||||
g_uart1_badchk++;
|
||||
uart1_resync(uart1_rx_frame, len);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- UART1 字节流装配 ---- */
|
||||
static void uart1_feed_byte(uint8_t b)
|
||||
{
|
||||
uint8_t guard = 0;
|
||||
|
||||
if (uart1_rx_idx == 0) {
|
||||
if (b == UART1_MAGIC_LOOP || b == UART1_MAGIC_LOCAL) {
|
||||
uart1_rx_frame[0] = b;
|
||||
uart1_rx_idx = 1;
|
||||
}
|
||||
return; /* 非魔数字节: 帧间空闲, 直接丢 */
|
||||
}
|
||||
|
||||
if (uart1_rx_idx >= LUP_MAX_PKG_LEN) { /* 防御: 正常流程到不了 */
|
||||
uart1_rx_idx = 0;
|
||||
uart1_rx_need = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
uart1_rx_frame[uart1_rx_idx++] = b;
|
||||
|
||||
if (uart1_rx_idx == 3) { /* [7F][Addr][LEN] 到手 -> 定长 */
|
||||
uint8_t lf = uart1_rx_frame[2];
|
||||
uint16_t need = (uint16_t)lf + 5;
|
||||
if (lf < 1 || need > LUP_MAX_PKG_LEN) {
|
||||
uart1_rx_idx = 0; /* LEN 非法 -> 重新找魔数 */
|
||||
uart1_rx_need = 0;
|
||||
return;
|
||||
}
|
||||
uart1_rx_need = need;
|
||||
}
|
||||
|
||||
/* 校验失败后 resync 可能让残余已够长, 故用 while (guard 防跑飞) */
|
||||
while (uart1_rx_need != 0 && uart1_rx_idx >= uart1_rx_need && guard++ < 8) {
|
||||
uart1_frame_ready();
|
||||
}
|
||||
}
|
||||
|
||||
void uart1_dma_init(void)
|
||||
{
|
||||
GPIO_InitTypeDef GPIO_InitStructure = {0};
|
||||
USART_InitTypeDef USART_InitStructure = {0};
|
||||
DMA_InitTypeDef DMA_InitStructure;
|
||||
|
||||
/* PB6 = USART1_TX / PB7 = USART1_RX (USART1 重映射) */
|
||||
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
|
||||
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
|
||||
|
||||
GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE);
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
|
||||
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
|
||||
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; /* Tx */
|
||||
GPIO_Init(GPIOB, &GPIO_InitStructure);
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
|
||||
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; /* Rx */
|
||||
GPIO_Init(GPIOB, &GPIO_InitStructure);
|
||||
|
||||
USART_InitStructure.USART_BaudRate = 115200; /* 与 Air780 侧 uart1 一致 */
|
||||
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
|
||||
USART_InitStructure.USART_StopBits = USART_StopBits_1;
|
||||
USART_InitStructure.USART_Parity = USART_Parity_No;
|
||||
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
|
||||
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
|
||||
USART_Init(USART1, &USART_InitStructure);
|
||||
USART_Cmd(USART1, ENABLE);
|
||||
|
||||
/* RX 用 DMA1_Ch5 循环缓冲 (UART2 占用 Ch6/Ch7, 通道不冲突) */
|
||||
DMA_DeInit(DMA1_Channel5);
|
||||
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DATAR);
|
||||
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)uart1_dma_buf;
|
||||
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
|
||||
DMA_InitStructure.DMA_BufferSize = UART1_DMA_BUF_LEN;
|
||||
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
|
||||
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
|
||||
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
|
||||
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
|
||||
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
|
||||
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
|
||||
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
|
||||
DMA_Init(DMA1_Channel5, &DMA_InitStructure);
|
||||
|
||||
USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
|
||||
DMA_Cmd(DMA1_Channel5, ENABLE);
|
||||
USART_ITConfig(USART1, USART_IT_RXNE, DISABLE); /* DMA 接管, 逐字节中断不再需要 */
|
||||
|
||||
uart1_dma_last = 0;
|
||||
uart1_rx_idx = 0;
|
||||
uart1_rx_need = 0;
|
||||
}
|
||||
|
||||
/* 主循环每轮调用: 消费 DMA 环形缓冲新字节 -> 装配 -> 分流 */
|
||||
void uart1_dma_poll(void)
|
||||
{
|
||||
uint16_t cur = (uint16_t)(UART1_DMA_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel5));
|
||||
uint16_t n = (uint16_t)((cur + UART1_DMA_BUF_LEN - uart1_dma_last) & (UART1_DMA_BUF_LEN - 1));
|
||||
|
||||
if (n == 0) return;
|
||||
|
||||
/* 溢出保护: 未消费超过半缓冲 -> 已被 DMA 覆盖, 复位装配器重同步 */
|
||||
if (n > (UART1_DMA_BUF_LEN / 2)) {
|
||||
g_uart1_drop++;
|
||||
uart1_rx_idx = 0;
|
||||
uart1_rx_need = 0;
|
||||
uart1_dma_last = cur;
|
||||
return;
|
||||
}
|
||||
|
||||
while (n--) {
|
||||
uint8_t b = uart1_dma_buf[uart1_dma_last];
|
||||
uart1_dma_last = (uint16_t)((uart1_dma_last + 1) & (UART1_DMA_BUF_LEN - 1));
|
||||
uart1_feed_byte(b);
|
||||
}
|
||||
}
|
||||
|
||||
#else /* !UART1_AIR780_EN: 调试版 printf 占用 USART1 -> 空实现, 保证链接一致 */
|
||||
|
||||
void uart1_dma_init(void) { }
|
||||
void uart1_dma_poll(void) { }
|
||||
uint32_t g_uart1_fwd_to_air = 0;
|
||||
uint32_t g_uart1_fwd_to_loop = 0;
|
||||
uint32_t g_uart1_local_cnt = 0;
|
||||
uint32_t g_uart1_badchk = 0;
|
||||
uint32_t g_uart1_drop = 0;
|
||||
|
||||
#endif /* UART1_AIR780_EN */
|
||||
|
||||
void uart_init(void){
|
||||
GPIO_InitTypeDef GPIO_InitStructure = {0};
|
||||
USART_InitTypeDef USART_InitStructure = {0};
|
||||
@@ -151,6 +393,7 @@ void uart_init(void){
|
||||
USART_Cmd(USART2, ENABLE);
|
||||
|
||||
uart2_dma_init(); /* DMA 循环接收接管 (2026-08-17) */
|
||||
uart1_dma_init(); /* 4G 通道 UART1↔Air780 初始化 (协议 §6.1); DEBUG!=0 时为空实现 */
|
||||
|
||||
|
||||
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
|
||||
@@ -246,6 +489,14 @@ void uart_srv(void)
|
||||
// 其他: 校验后匹配挂起命令
|
||||
lup_process_frame(g_pkg_uart_2.pkg, g_pkg_uart_2.offset);
|
||||
|
||||
/* --- 4G 通道上行转发 (协议 §6.1) ---
|
||||
校验通过的完整帧原样送 Air780; 必须置于下方 InitPkgUart 消费之前。
|
||||
只转发 verify 通过的帧: Air780 靠 loop_state 变化沿判车, 残缺帧 = 多判一台车 */
|
||||
if (lup_verify_checksum(g_pkg_uart_2.pkg, g_pkg_uart_2.offset) == 0) {
|
||||
g_uart1_fwd_to_air++;
|
||||
UART1_SendString(g_pkg_uart_2.pkg, g_pkg_uart_2.offset);
|
||||
}
|
||||
|
||||
// --- 传感器上报 (0xC0) 分流 ---
|
||||
// 回调已处理 TCP 推送,此处仅处理 BLE 转发
|
||||
if(cmd == LUP_CMD_SENSOR_REPORT)
|
||||
@@ -291,10 +542,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,513 @@
|
||||
>
|
||||
> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接
|
||||
|
||||
## 2026-09-10 — BLE 分包机制核实 + net 229B 载荷可达性修正(§6.9 前置改造项)
|
||||
|
||||
> 触发:用户 2026-09-10 两问 ——「net 的 229B 是怎么计算出来的?」「我记得蓝牙小程序有分包机制,看下是不是」。
|
||||
> **性质:勘察 + 归档,本条目对 `vd960DBN` 源码零改动**;结论用于修订协议 §6.9 的落地前置条件。
|
||||
|
||||
### 1. BLE 分包机制 —— 确认存在,收发双向对称
|
||||
|
||||
分包头复用 **`pkg[1]`** 字段(原地是 Addr),把总包数与序号压进高/低 4 位:
|
||||
|
||||
| 位段 | 含义 |
|
||||
|------|------|
|
||||
| `pkg[1] >> 4` | 总包数 `pkg_amount` |
|
||||
| `pkg[1] % 0x10` | 当前包序号 `pkg_seq` |
|
||||
|
||||
**接收**(`dbn_ble_srv.c:466-504`,`unpack_packs()` 内):
|
||||
|
||||
- `high == 0` → 单包(不分包),整包直取 `_len - 1` 字节;
|
||||
- `low == 1` → 第一包,重置缓冲与偏移;
|
||||
- `low > 1` → 后续包,校验 `amount`/`cmd` 一致 + `seq` 连续,`dat_len += (_len - 1)` 并 `memcpy(&dat[_offset], ...)`;
|
||||
- `high == low` → 收包完成,进入命令处理。
|
||||
|
||||
**发送**(`dbn_ble_srv.c:420-421 / 436-437`):`_pkg_seq += 1`(**先自增再编码**,故线上第一包即 `seq == 1`)→ `buf[1] = (pkg_amount << 4) | pkg_seq`;单包时 `buf[1] = 0`;`dat_offset += _remain_len` 推进;发完 `pkg_amount == pkg_seq` 清缓冲。
|
||||
→ **与接收侧 `low == 1` 起点、`high == low` 收尾完全配对,设计自洽。**
|
||||
|
||||
**每包 dat 上限随 MTU 协商**(`ble_notify_chunk_max()`,`_limit = peripheral_get_mtu() - 9`,再被 `MAX_BLE_Notify_Buf_LEN - 6` 夹住;`BLE_BUFF_MAX_LEN = 100` @ `BLE/HAL/include/config.h:115`):
|
||||
|
||||
| MTU | 每包 dat | 4 位总包数上限(≤15 包) |
|
||||
|-----|----------|--------------------------|
|
||||
| 23(默认) | 14 B | 210 B |
|
||||
| 96 | 87 B | 1305 B |
|
||||
| **≥103(现用)** | **94 B** | **1410 B** |
|
||||
|
||||
> `dbn_ble_srv.h:81` 的 `//24` 注释是 `BLE_BUFF_MAX_LEN` 的**过期旧值**(原 24,现 100),不可据此推算分块。
|
||||
|
||||
### 2. 关键修正:229B **真的可达** ⇒ 越界**真的可达**
|
||||
|
||||
上一轮结论「`dat[132]` 装不下 229B,实际硬上限 131B,现场跑不到」**是错的** —— 分包让 `dat_len` 跨包累加,229B = 3 包 × ≤94B **可以送达**。
|
||||
|
||||
| 方向 | 代码事实 | 后果 |
|
||||
|------|----------|------|
|
||||
| 发送 | `set_response_iot_net()` 往 `dat[132]` 写最多 229B,`i` 全程无钳制 | **越界写**(同文件 `set_response_buf()` 有守卫,这两个没有) |
|
||||
| 接收 | `dat_len += (_len - 1)`、`memcpy(&dat[_offset], ...)` 全程无钳制 | 多包累加即越界 |
|
||||
| 通用 | `dat_len` / `_offset` / `_len` / `i` 均为 `uint8_t` | >255 回绕,解析循环行为错乱 |
|
||||
|
||||
定性由「理论上限、跑不到」升级为「**可达缺陷**」。**先撞的永远是 132 那道墙**(`pkg_amount` 4 位 ⇒ 15 包 = 1410B 的上限在更远处)。
|
||||
|
||||
### 3. 自我纠错(两处)
|
||||
|
||||
1. ❌「实际硬上限 131B」→ ✅ 131B 只是**缓冲区**上限;分包使更长载荷**可达**,越界非纸面。
|
||||
2. ❌ 曾疑「收方要求 `low == 1` 起、发方 `pkg_seq = 0` 起,序号基准不一致」→ ✅ 发送侧**先自增再编码**,线上第一包即 `seq = 1`,**配对正确,疑点作废**(不该写进文档)。
|
||||
|
||||
### 4. §6.9 落地前置改造项(新增,实现前必做)
|
||||
|
||||
| # | 项 | 依据 |
|
||||
|---|-----|------|
|
||||
| 1 | `g_buf_ble_response.dat` / `tmp_ble_buf` 由 132 B 扩至 **≥260 B** | 229B 载荷可达 ⇒ 现缓冲必越界(132B 当初为 offlog QUERY 129B 所定) |
|
||||
| 2 | `set_response_iot_net()` / `set_response_iot_topic()` 补长度钳制 | 照抄 `dbn_ble_srv.c:288` `set_response_buf()` 的守卫 |
|
||||
| 3 | `unpack_packs()` 接收侧累加加钳制 | `dat_len + (_len - 1) > MAX_BLE_DAT_BUF_LEN` 即丢包清缓冲,防 `uint8_t` 回绕 |
|
||||
| 4 | `unpack_packs()` 用起 `len` 形参 | 该形参在 263 行函数体里从未被引用,memcpy 长度全取自 `pkg[2]` |
|
||||
|
||||
改动项 1 需**两侧(DBN 与蓝牙小程序/MRS 工程)同步**,漏改一边即越界踩 `g_notify_buftemp` 一类相邻全局。
|
||||
|
||||
### 5. 待现场验证
|
||||
|
||||
- **小程序侧源码不在本机**(`find` 无命中)→ 其分包实现的**末包判定**与**是否同样按 MTU 协商 94 B 切包**无法核对,需上板抓包。
|
||||
- 现场是否已出现「host 写长了 MQTT 参数就乱」:按门槛(端口 4 位时 `host + clientid + username + password` 字符数 > 124 即越界)**应已偶发,只是未归因到缓冲区**。
|
||||
|
||||
### 6. 文件改动(本条目)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `vd960DBN` 源码 | **零改动**(`TaskLoop.c` 等一律未动) |
|
||||
| `docs/DLD960_IoT_MQTT协议.md` | §6.9.4.3 补分包承载说明;§6.9.9 加 4 条前置改造项;修订记录加 V1.15 |
|
||||
| skill `payload-sizing-and-buffer-audit.md` | 修正 §0/§3 定性、新增「BLE 分包机制」节、§5 补第 4 项 |
|
||||
| skill `dbn-air780-config-sync.md` | 补分包与缓冲边界指针 |
|
||||
|
||||
|
||||
## 2026-09-10 — 4G 配置同步协议定稿(《DLD960_IoT_MQTT协议》§6.9,0x7D 作废)
|
||||
|
||||
> 用户需求 2026-09-10:蓝牙小程序读写网络配置指令,哪些需要同步/转发给 4G(服务器域名/IP、端口、MQTT 账号密码、主题等)。
|
||||
> **关闭**:V1.12 条目"vd960DBN 侧开发计划"第 3 条(BLE 设置服务器/topic 时同步下发 Air780 配置);Air780 readme §2.2 / §3 的 0x7D 帧设计稿。
|
||||
|
||||
### 结论(协议 §6.9 定稿)
|
||||
|
||||
**权威源规则**:配置权威源 = **DBN 侧 flash**(`IOT_NET_INFO` / `IOT_Topic`,`cfig_flash.c` 持久化)。
|
||||
Air780 侧 fskv **仅作启动缓存**,唯一合法写入者 = **收到的 DBN 配置帧**;平台经 4G 下发配置一律拒绝
|
||||
(§6.6 回 `code=4`)——**BLE 是网络配置的唯一入口**,故同步不是可选优化,是 4G 通道能否工作的前提。
|
||||
|
||||
**载体**:复用 **0x8F 本机侧私有帧**(与 BLE 侧 `MAGIC_BYTE_DBN_DEFAULT` 同值)。语义自洽:
|
||||
`0x7F` = 业务帧(转发 UART2/Loop),`0x8F` = 链路两端本机侧私有(各自本地消费,不转发)。
|
||||
|
||||
| 项 | 定稿 |
|
||||
|----|------|
|
||||
| 命令 | 复用 BLE 侧既有命令码:`0x14`(GET_IOT_NET) / `0x16`(GET_IOT_TOPIC),双向对称(请求 DATA 空,应答带载荷) |
|
||||
| 载荷 | 复用 DBN 侧 `set_response_iot_net()` / `set_response_iot_topic()` 构造器 → **与 BLE 读响应逐字节同源**,零新增序列化代码 |
|
||||
| 拆分 | net / topic **两条独立帧 + 两条 fskv 记录**(net 最坏 229B、topic 129B,均 < fskv 单值 255B 上限) |
|
||||
| 交互 | **拉取为主**(Air780 上电先读 fskv 立即连,链路建立后拉权威值校准)+ **推送为辅**(BLE 写成功后链路可用则立即推) |
|
||||
| 状态 | DBN **不维护"待同步"标志位**:推送时链路不可用则等 Air780 下次拉取(无状态,天然自愈) |
|
||||
| 同步项 | 核心 8 项:`remote_addr` / `mqtt_port` / `client_id` / `username` / `password` / `clientid_enable` / `topic_pub` / `topic_sub` |
|
||||
| 边界项 | `UPDATE_DEV_SERIAL`(0x09) 必同步(Topic 族与 JSON `sn` 均由序列号派生);`iot_enable`(sub_code 0x22)必同步;`RW_UART_BAUD` **禁止在线改 UART1** |
|
||||
| 同步载荷不含 | `IOT_NET_INFO.mode`:实测**无 BLE 写入路径**、恒为 `0`(`IOT_Addr_IP_Mode`),仅 DBN 有线 MQTT 读取;Air780 侧按主机串自行判别(LuatOS `socket.connect` 对 IP/域名均兼容) |
|
||||
|
||||
### 勘察中发现的硬约束与隐患(均为代码事实)
|
||||
|
||||
| # | 发现 | 处置 |
|
||||
|---|------|------|
|
||||
| 1 | **`usart_biz.c` 帧缓冲 `uart1_rx_frame[LUP_MAX_PKG_LEN]` = 70B** → 单帧载荷上限 64B,**装不下 net 最坏 229B**,会被按 "LEN 非法" 丢弃 | `0x8F` 帧长上限**独立放宽到 260B**(LEN 1B ≤255);`0x7F` 业务帧维持 70B 纪律。**两侧帧缓冲需同步扩容** |
|
||||
| 2 | `iot_net_info` **是两条传输通道共享**的:`peripheral_main.c:248` 用它走 **DBN 自身 WCHNET 有线 MQTT**(`WCHNET_CreateTcpMqttSocket()`) | 更须保证 DBN 为唯一权威源 |
|
||||
| 3 | 该处 `else //DNS` **空分支** → 配置成域名时**有线通道 MQTT 不工作**(4G 不影响) | 已知缺口,是否补齐由产品决定(协议 §6.9.8 已标注) |
|
||||
| 4 | `CMD_DBN_RW_UART_BAUD` 可改**任意串口**波特率,含 UART1 这条 4G 链路 → 在线改而两侧不同步 = 永久失联,只能拆机重刷 | 建议固件侧对 `uart_num == 1` **直接拒绝** |
|
||||
| 5 | Air780 侧 `mqtt_main.lua` 在 require 期把配置读进 `local`,**运行时改 CFG 不生效** | 须改造为**重连时重读** + 断开重连重订阅 |
|
||||
| 6 | Air780 **已支持 fskv 持久化**(`components/fskv/`,littlefs,**掉电不丢**,key ≤64B / 单值 ≤255B,见官方 `demo/fskv`);原设计稿 §2.2 即已指向 fskv | 用作启动缓存;**仅在值变化时写入**(littlefs 擦写磨损) |
|
||||
|
||||
### 文件改动(文档口径)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `docs/DLD960_IoT_MQTT协议.md` | **+§6.9「4G 配置同步」**(9 个子节:背景与权威源 / 帧格式 / 命令 / 载荷 / 交互流程 / 其他触发命令 / 明确不同步项 / 异常处理 / 实施状态);§6.6 增补充说明;版本 V1.13→**V1.14**;修订记录加行 |
|
||||
| `docs/DLD960_IoT_MQTT协议.md` | 行尾核查:全文**纯 LF**(非 CRLF),改动按 LF 落盘,零行尾污染 |
|
||||
| vd960Air `readme.md` | §2.2 重写为权威源 + 拉取/推送设计;**§3「0x7D 帧设计稿 v0.2」整节作废**,改写为「0x7F 业务帧 + 0x8F 本机侧私有帧」定稿;3.1 对比表改成 0x7F vs 0x8F;待确认项/开发计划 P1 同步更新 |
|
||||
| vd960Air `uart_app.lua` / `config.lua` | 注释中的 0x7D 计划改为 0x8F 配置同步 |
|
||||
|
||||
**历史记录不动**:devlog V1.11/V1.12 条目、协议 V1.11 修订行中的 0x7D 描述均保留(记录彼时决定)。
|
||||
|
||||
### 待办(协议已定稿,两侧待实现)
|
||||
|
||||
- [ ] DBN:0x8F 配置请求**应答**(读 flash → 组帧)+ BLE 写成功后**主动推送**
|
||||
- [ ] DBN:`uart1_rx_frame` 扩容至 260B(含 TX 侧缓冲)
|
||||
- [ ] DBN:`RW_UART_BAUD` 拒绝 `uart_num == 1`
|
||||
- [ ] Air780:`frame_parser.lua` 增 `0x8F` 分支(本地消费,不转发)
|
||||
- [ ] Air780:fskv 缓存 + 上电即连 + 拉取校准 + 变化重连;配置读取改为重连时重读
|
||||
- [ ] Air780:`config.lua` 明文凭据处置(改占位符 + 非跟踪文件读取;⚠ 已在 git 历史中,商用前须换号)
|
||||
|
||||
|
||||
## 2026-09-10 — vd960DBN UART1 ↔ Air780 4G 通道实现(协议 §6.1 纯透传)
|
||||
|
||||
> 用户需求 2026-09-10:把 vd960DBN 与 vd960Air(Air780) 两侧的串口对接逻辑做出来 —— DBN 侧 UART1(PB6/PB7) 通道 + UART2↔UART1 双向透传。
|
||||
> **关闭**:V1.12 条目"vd960DBN 侧开发计划"第 1 条;V1.11 条目第 1、2 条(UART1 通道 / 双向透传)。
|
||||
|
||||
### 实现
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| `APP/usart_biz.c` | +242 行:UART1 通道整段(118-347);上行转发插入 `uart_srv()`(484-490) |
|
||||
| `APP/include/cmcng.h` | 声明 `uart1_dma_init/poll` + 5 个通道计数 |
|
||||
| `APP/peripheral_main.c` | 主循环加 `uart1_dma_poll()`(置于 `MK_UART_SRV` fault marker **之外**,不改变既有 marker 语义) |
|
||||
|
||||
**上行**(Loop → Air780):`uart_srv()` 内 `lup_process_frame()` 之后、**`InitPkgUart` 消费之前**插入转发,
|
||||
且**只转发 `lup_verify_checksum()==0` 的完整帧**。插入点必须在 BLE 分支改写 `pkg[0]=0x8F` 之前 ——
|
||||
否则会把已改魔数的帧发给 Air780。
|
||||
|
||||
**下行**(Air780 → DBN):UART1 收帧 → **魔数分流**:`0x7F` → `UART2_SendString()` 给 Loop;
|
||||
`0x8F` → **DBN(CH32V208GBU6) 私有指令**:只作用在自身读写配置上,**一般不转发**(用户 2026-09-10 澄清)。
|
||||
落地方式:直接复用 BLE 侧同一个分发器 `manage_dbn_ble_default()` —— 两边帧布局完全一致
|
||||
(`pkg[1]=Addr, pkg[2]=LEN, pkg[3]=CMD, pkg[4..]=Value`),零适配。响应仍走 `g_buf_ble_response`→BLE;
|
||||
不回 4G(Air780 侧 parser 只认 `0x7F`,回 `0x8F` 也没人接)。**OTA 期间拒绝**:私有指令会写自身配置(碰 flash),与刷写并发是自找麻烦。
|
||||
|
||||
### 三个关键设计决定(含理由)
|
||||
|
||||
1. **接收走 DMA(DMA1_Ch5 循环 + 512B),不用 RXNE 中断。** 理由与 UART2 方案A(2026-08-17) 完全相同:
|
||||
`PRINT` 临界区关中断 ~7.4ms / BLE 栈回调 / SPI 擦除 45ms 等长阻塞窗口会屏蔽 RXNE → 丢字节 → 校验失败。
|
||||
本通道"不丢帧"是 Air780 沿检测的硬前提。UART2 占 DMA1_Ch6/Ch7,**Ch5 空闲,无冲突**。
|
||||
2. **帧装配器独立于 `g_lup_parser`。** `lup_feed_byte()` 硬编码操作全局 `g_lup_parser`(专供 UART2/Loop 通道),
|
||||
两条字节流混进同一状态机必然互相残杀。故 UART1 用独立装配器,只复用纯函数 `lup_verify_checksum()`。
|
||||
3. **坏帧回找魔数(resync)。** 校验失败时在已收缓冲里从 idx=1 起回找下一个 `0x7F/0x8F`,残余搬到头部继续装配,
|
||||
防"单帧损坏 → 后续帧全部失步、链路长时间瞎掉"。
|
||||
|
||||
### 与 printf 共用 USART1 的处理(按用户指示)
|
||||
|
||||
用户 2026-09-10 指示:**printf 对应串口不动;不会同时使用 Debug 打印和 4G 通信,4G 通信时禁用 debug 即可。**
|
||||
|
||||
- 现状:`debug.c` 的 `USART_Printf_Init()` 把 USART1 重映射到 PB6 做 printf,且 `PRINT` 走 `__disable_irq()`。
|
||||
- 落地:本通道用编译开关 `UART1_AIR780_EN`(默认 `(DEBUG == 0)`)—— **4G 版本以 `-DDEBUG=0` 编译即自动启用**。
|
||||
此时 `#if(DEBUG)` 为假:`PRINT` 是空宏,且 `USART_Printf_Init()` 里 USART1 的配置分支整段不参与编译
|
||||
(已核对 `USART_Init/USART_Cmd` 均在 `#if(DEBUG == DEBUG_UART1)` 内)→ USART1 干净地交给本文件。
|
||||
- 调试版(DEBUG!=0)本通道编译为空实现,**`debug.h`/`debug.c` 一行未改**,printf 行为完全不变。
|
||||
- 已处理:`net_srv.c:125` 裸 `printf()`(错误路径)→ 改 `PRINT`;`DEBUG=0` 时随 `PRINT` 一起消失,
|
||||
不再出现"往未初始化的 USART1 写字节"这种恶心事。该文件本身已用 `PRINT` 53 处,无需补 include。
|
||||
|
||||
### 验证
|
||||
|
||||
- **host 单测**(本机无 RISC-V 工具链,无法编译固件):从 `usart_biz.c` **原样抽取**装配器代码 + 逐字复刻
|
||||
`loop_uart_proto.c` 的 XOR/SUM 校验,gcc 编译(`-Wall -Wextra -Werror` **零警告**)跑 13 组用例 **全部通过**:
|
||||
单帧 `0x7F`/`0x8F` 分流、带数据帧逐字节一致、70B 最大帧、校验和破坏不转发、
|
||||
**坏帧后紧跟好帧仍送达**、**截断帧后只交付 1 帧完整好帧**、乱码前缀、`LEN=0` 非法、`LEN` 超长、三帧连发、
|
||||
`0x9F`(Loop OTA 魔数) 忽略、上行闸门只放行 verify 通过的帧。
|
||||
- **待板级验证**:需 MounRiver 工具链编译 + 实机(本机无 riscv 工具链,固件编译尚未跑过)。
|
||||
|
||||
### 现场核对手段(比翻日志可靠)
|
||||
|
||||
新增 5 个全局计数(`cmcng.h` 已声明,可调试器 Watch):
|
||||
`g_uart1_fwd_to_air`(上行转发帧数)/ `g_uart1_fwd_to_loop`(下行 0x7F)/ `g_uart1_local_cnt`(下行 0x8F)/
|
||||
`g_uart1_badchk`(校验失败)/ `g_uart1_drop`(DMA 溢出)。
|
||||
|
||||
**判据:跑一段时间后 `g_uart1_fwd_to_air` 与 Air780 侧实收帧数必须相等。**
|
||||
|
||||
### 接线(供硬件核对)
|
||||
|
||||
- DBN `USART1` 重映射:**PB6 = TX / PB7 = RX**,115200 8N1
|
||||
- 与 Air780 必须**交叉**:`DBN_PB6(TX) → Air780_RX`,`DBN_PB7(RX) ← Air780_TX`
|
||||
- 波特率两侧一致:115200(Air780 侧日志 `Uart_ChangeBR uart1, 115200`)
|
||||
|
||||
### 已定案
|
||||
|
||||
- `0x7D` 帧:**删除/不理会**(用户 2026-09-10)—— V1.11 曾提的 DBN↔Air780 握手/配置同步构想作废,
|
||||
代码里本就没有实现,`docs/DLD960_IoT_MQTT协议.md` 正式协议也从未收录。历史条目保留不动(记录当时的想法)。
|
||||
|
||||
### 仍待办
|
||||
|
||||
- [ ] BLE 设置服务器/topic 时同步下发 Air780 配置(V1.12 第 2 条)
|
||||
- [ ] 通道切换策略(V1.12 第 3 条,待讨论;暂定默认 4G)
|
||||
- [ ] 板级联调:编译烧录 + 帧计数对齐 + 断线重连
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-31 — MQTT 协议 V1.13:initialize extra_info 增加 imei/iccid 可选字段
|
||||
|
||||
> 用户需求 2026-08-31:initialize 指令的 `extra_info` 增加可选字段 `imei` / `iccid`(4G 模块 IMEI / 流量卡 ICCID)。
|
||||
|
||||
### 协议变更(V1.12 → V1.13)
|
||||
|
||||
- §5.1 initialize 示例 + 字段表:`extra_info.imei` / `extra_info.iccid` 可选
|
||||
- 语义:无 4G 模块时省略或空串;**4G 通道由 Air780 填真实值**(§6.2 说明)
|
||||
- 用途:平台解析 initialize 即可拿到 4G 模块/流量卡标识(配合 4G 通道设备管理)
|
||||
|
||||
### 固件同步(vd960DBN iot_mqtt_srv.c iot_send_initialize)
|
||||
|
||||
- `extra_info` 增加 `imei`/`iccid` 字段(当前空串占位——DBN 固件无 4G 模块信息)
|
||||
- ⚠ 待 vd960DBN 实现"BLE→Air780 配置同步"后,可考虑从 Air780 回读 4G 信息填充(列入开发计划)
|
||||
|
||||
### 同步
|
||||
|
||||
- `README.md` 文档索引 V1.12 → V1.13
|
||||
- 协议文档 §5.1 + §6.2 + 修订记录 V1.13
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-31 — MQTT 协议 V1.12:4G 通道适配修订为方案 C(Air780 协议转换)
|
||||
|
||||
> **协议先行修订**(同日 V1.11 方案 B 未落地即修订,固件均未实现)。
|
||||
|
||||
### 修订动因
|
||||
|
||||
V1.11 曾定**方案 B**(vd960DBN 纯转发 + frame_report/frame_cmd hex 透传)。讨论后改为**方案 C**:Air780 解析 0x7F 帧并转换为**标准 JSON 命令**(loop_data / event_report / initialize / heartbeat),平台零改动、与有线通道一致。
|
||||
|
||||
### 四个决策点(用户 2026-08-31)
|
||||
|
||||
| # | 决策 | 结果 |
|
||||
|---|------|------|
|
||||
| 1 | event_report | Air780 以 Lua 复刻 DBN iot_evt_* 逻辑(沿检测 + ACK + 5s×3 + 16 深队列 + 跨重连同 msg_id),可行性已评估:全部逻辑块可映射 Lua,无硬障碍 |
|
||||
| 2 | 事件面 | **4G 仅线圈事件**(car_enter/car_leave/loop_cut/loop_restore),不含 DBN 内部网络事件(协议 §6.4 明确) |
|
||||
| 3 | 命令响应 | Air780 单命令状态机 + 超时回 code=5(与 DBN g_lup_cmd 同模式),协议 §6.5 |
|
||||
| 4 | frame_* 去留 | **保留为可选兜底**(Air780 未实现转换命令/未识别帧透传),协议 §6.8 |
|
||||
|
||||
### 协议变更(V1.11 → V1.12)
|
||||
|
||||
- §6 重写:方案 C 架构(Air780 协议转换)/ 命令面 = 标准 JSON / 转换职责表(0x7F↔JSON)/ 事件上报 / 命令响应链路 / link / 平台要求
|
||||
- §3 命令详表:frame_cmd/frame_report 标注改为"可选兜底"
|
||||
- 修订记录 V1.12
|
||||
|
||||
### vd960DBN 侧开发计划(固件未实现,不变)
|
||||
|
||||
- [ ] UART1(PB6/PB7) 通道 + UART2↔UART1 双向透传(⚠ 转发不丢帧是 Air780 沿检测的前提)
|
||||
- [ ] BLE 设置服务器/topic 参数时同步下发 Air780 配置
|
||||
- [ ] 通道切换策略(待讨论;暂定默认 4G)
|
||||
|
||||
### 同步
|
||||
|
||||
- `README.md` 文档索引 V1.11 → V1.12
|
||||
- 协议文档 §6 重写 + 修订记录 V1.12(V1.11 保留作历史)
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-31 — MQTT 协议 V1.11:4G 通道适配(方案 B:原始帧透传 + hex 封装)协议先行
|
||||
|
||||
> **本次为协议文档先行**(Air8781P 4G 兜底通道设计稿并入主协议;vd960DBN 固件未实现,列入开发计划)。
|
||||
|
||||
### 背景
|
||||
|
||||
vd960DBN 有线网络失效时需要 4G 兜底上报。接入方案定为 **方案 B:vd960DBN 纯转发**——Loop 传感数据(0x7F 帧)经 UART1 原样透传 Air780(Air8781P/Air780EPM, LuatOS vd960Air 工程),由 Air780 走 MQTT;4G 下行地感指令经 Air780 透传回 Loop。
|
||||
|
||||
### 协议变更(DLD960_IoT_MQTT协议 V1.10 → V1.11)
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 新增命令 | `frame_report`(dev→srv, 上行 0x7F 帧 hex 封装, 每条上行携带 link)+ `frame_cmd`(srv→dev, 下行 0x7F/0x8F 帧 hex) |
|
||||
| 4G 通道命令面 | 不使用有线标准 JSON 业务命令(loop_data/event_report);initialize 由 Air780 发(JSON + link, 用于上线识别 + report_config 时钟校准) |
|
||||
| 不适用命令 | ssc_net_*/iot_net_*/iot_topic_*(4G 通道下发回 code=4) |
|
||||
| link 对象 | imei/iccid/imsi/msisdn/csq/net(4G 特有字段, 流量卡管理/信号监控) |
|
||||
| 链路层 | 数据面 0x7F 帧字节流透传(魔数分流在 DBN 侧: 0x7F→UART2 / 0x8F→本地);0x7D 帧仅 DBN↔Air780 配置同步/握手 |
|
||||
| 平台侧 | 双通道区分解析 + 新增《DLD960Loop_串口通信协议》解析依赖 |
|
||||
|
||||
### vd960DBN 侧开发计划(固件未实现)
|
||||
|
||||
- [ ] UART1(PB6/PB7)通道: 0x7F 帧魔数分流
|
||||
- [ ] UART2↔UART1 双向透传(复用 manage_dbn_ble_transparent 模式)
|
||||
- [ ] BLE 设置服务器/topic 参数时同步下发 Air780 配置
|
||||
- [ ] 通道切换策略(待讨论: 动态自动 vs 人工;暂定默认 4G)
|
||||
|
||||
### 同步
|
||||
|
||||
- `README.md` 文档索引 MQTT V1.10 → V1.11
|
||||
- 协议文档 §3 命令详表 + §6 4G 通道适配章节 + 修订记录 V1.11
|
||||
- CHANGELOG 历史条目保留不动
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-31 — BLE 协议文档补全 V1.03:配置命令全表 + 未实现如实标注
|
||||
|
||||
> **本次为文档补录**(固件代码未变,不升固件版本):对照 `dbn_ble_srv.c`(GBK+CRLF)逐命令梳理,`DLD960_BLE协议.md` 由 V1.02(仅脱机日志/快照 6 命令)补齐为完整 BLE 协议。
|
||||
|
||||
### 背景
|
||||
|
||||
`DLD960_BLE协议.md` 此前只覆盖 0x25~0x2A(OFFLOG/SNAP 6 命令),既有的 BLE 配置命令(0x09~0x24 等)没有文档——小程序/APP 对接无从下手。本次从代码反向梳理全部命令。
|
||||
|
||||
### 文档补全内容
|
||||
|
||||
| 命令码 | 名称 | 实现状态(代码核实) |
|
||||
|--------|------|---------------------|
|
||||
| 0x09/0x10/0x11/0x12 | 序列码/设备信息/SSC 网络读写 | ✅ manage_dbn_ble_default |
|
||||
| 0x13/0x14/0x15/0x16 | IoT 网络/Topic 读写 | ✅ unpack_packs(字符串数组 0x00 分隔) |
|
||||
| 0x1C/0x1D | 验证/修改密码 | ✅ unpack_packs(0x1D 不在 switch,分包收尾处理) |
|
||||
| 0x1E/0x92 | 出厂初始化 | ✅ 回 [0] + factory_dev_info() |
|
||||
| 0x1F | 设备复位 | ✅ 无响应直接 NVIC_SystemReset |
|
||||
| 0x22 | 子功能码 | ✅ 2B LE,无响应 |
|
||||
| 0x23/0x24 | 车检器参数读写 | ⚠ 空壳(0x23 仅回 [0] 不处理参数;0x24 空响应) |
|
||||
| 0x31 | UART 波特率读写 | ✅(⚠ 写响应多 1B 重复低字节,文档已标注) |
|
||||
| 0x8A | Loop 灵敏度列表 | ⚠ 空响应 |
|
||||
| 0xC5 | 传感上报使能 | ✅ 设置生效无响应;⚠ report_sens_acs 无定义 → 0xC0 上报未实现 |
|
||||
| 0x17/0x18/0x20/0x21/0x87/0x88/0x89 | 交通参数/通知/Loop 采样等 | ⚠ 头文件有定义,switch 无 case(未实现) |
|
||||
| 0x7F/0x9F | 透传通道 | ✅ UART2 原样转发 / OTA 透传 |
|
||||
|
||||
### 关键发现
|
||||
|
||||
- **0x1D MODIFY_PASS 实际已实现**:不在 manage_dbn_ble_default 的 switch,而在 unpack_packs 分包收尾分支(头文件有定义、switch 无 case ≠ 未实现,须看 unpack_packs)
|
||||
- **0xC0 BLE 主动上报已废弃**:`report_sens_acs()` 仅 dbn_ble_srv.h 声明,全工程无定义无调用;0xC5 使能后无上报动作(上报走 MQTT/TCP)
|
||||
- **0x31 写响应固件 bug**:`tmp_ble_buf[i++] = g_storage_uart_baud;` 后紧跟 `tmp_ble_buf[i++] = (uint8_t)g_storage_uart_baud;` 重复写低字节,响应 10B 而非 9B——文档按语义描述 + 标注实际多 1B,待板级联调时确认是否修固件
|
||||
- 请求分包(unpack_packs)与响应分包(set_response_to_notify)格式一致:header 高 4 位=包数、低 4 位=序号(从 1 起)
|
||||
|
||||
### 同步
|
||||
|
||||
- `README.md` 文档索引 BLE 协议 V1.02 → V1.03(描述更新为完整命令表)
|
||||
- CHANGELOG 历史发布条目(V1.02 不变)保留不动
|
||||
- 待板级:0x31 写响应长度、0xC5 使能后无上报是否符合预期
|
||||
|
||||
---
|
||||
|
||||
## 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 存储适配两项修复)
|
||||
|
||||
### 内容
|
||||
@@ -1486,6 +1993,7 @@ 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 低字节匹配 (兼容多厂家) |
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
# vd960Loop 自动分频(Auto Frequency Selection)技术分析与开发计划
|
||||
|
||||
> 分析日期:2026-08-25
|
||||
> 场景:高速公路,每条车道 4 个线圈(前后串行),相邻车道也有线圈
|
||||
> 目标:检测到干扰时,四通道各自独立调节 4 档电容频率(33/43/66/76nF)错开干扰源
|
||||
|
||||
---
|
||||
|
||||
## 0. 现状盘点
|
||||
|
||||
| 能力 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 硬件切换 | ✅ 已具备 | 每通道 2 个 GPIO(FLPA1/2…FLPD1/2),2bit 编码 4 档电容 |
|
||||
| 基础软件 | ✅ 已具备 | `set_flp_level(loop_num, freq_level)` 已实现(storage.c:117) |
|
||||
| 配置字段 | ✅ 已具备 | `Loop_Cng_Unit.loopFreq_Level`(2bit),flash 持久化,协议字段 freq_level 已定义 |
|
||||
| 频率测量 | ✅ 已具备 | CAPVD ∝ 周期,上报实际频率(main.c 转换公式) |
|
||||
| 干扰检测 | ❌ 缺失 | 无"自动判断有干扰"的逻辑 |
|
||||
| 切换决策 | ❌ 缺失 | 无"切哪档、何时切"的逻辑 |
|
||||
| 切换时序 | ❌ 缺失 | 切档会打断检测,无软切换/基线重建流程 |
|
||||
| 频率规划 | ❌ 缺失 | 无多线圈错频分配策略 |
|
||||
|
||||
**结论:硬件和基础软件齐了,缺的是"自动"三件套:检测 → 决策 → 软切换。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 物理层分析:四档频率与错频裕量
|
||||
|
||||
### 1.1 四档频率(L₀≈59μH 时)
|
||||
|
||||
| 档位 | 电容 | 频率 | 相对上一档间隔 |
|
||||
|------|------|------|---------------|
|
||||
| 00 高频 | 33nF | ~114.1 kHz | — |
|
||||
| 01 中高 | 43nF | ~100.0 kHz | 14.1 kHz(14.1%) |
|
||||
| 10 中低 | 66nF | ~80.7 kHz | 19.3 kHz(19.3%) |
|
||||
| 11 低频 | 76nF | ~75.2 kHz | **5.5 kHz(7.3%)** ⚠ |
|
||||
|
||||
⚠ **关键发现:四档电容间隔不均匀(+30%/+53%/+15%),66nF 与 76nF 之间频率间隔只有 7.3%**。f ∝ 1/√C,这个比例与线圈 L 无关(恒为 √(76/66)=1.073)。若按行业惯例"相邻线圈频率差 ≥10%"要求,**66/76 这一对不达标**。
|
||||
|
||||
### 1.2 但真正的敌人不是"差拍大不大",而是"注入锁定"
|
||||
|
||||
错频抑制串扰的物理机制分两层:
|
||||
|
||||
1. **异频耦合 → 差拍**:本道 f1、邻道 f2,互感耦合产生 |f1−f2| 差拍。本道 10ms tick 采样,奈奎斯特 50Hz:
|
||||
- 差拍 >50Hz(如 5.5kHz):欠采样 → 随机高频抖动 → **IIR(τ≈32ms)天然压住**,无害
|
||||
- 差拍 <50Hz(近频):慢拍,可穿透 IIR → 有害
|
||||
- 差拍 →0(同频):注入锁定,最危险
|
||||
2. **同频/近频 → 注入锁定**:f1≈f2 时振荡器互相牵引,频率被拉偏、相位锁定,CAPVD 出现慢周期扰动,直接污染检测。
|
||||
|
||||
**结论:错频目标不是"差拍尽量大",而是"避免同档/近档导致注入锁定"。** 只要相邻线圈不同档(≥1 档间隔,即 ≥7.3% 频率差),差拍欠采样抖动可被滤波抑制。**7.3% 的最小档间隔对避免注入锁定足够,但余量偏小**,规划时优先避免 66/76 相邻组合。
|
||||
|
||||
### 1.3 串扰方向性(衔接上一轮分析)
|
||||
|
||||
- 错频配置下串扰为差拍抖动(正负交替,无稳定极性)
|
||||
- 单极性串扰只在同频/近频耦合时出现(错误配置)
|
||||
- 负向串扰进滑动平均会污染基线(真实危害)
|
||||
|
||||
---
|
||||
|
||||
## 2. 信号层:干扰检测算法("怎么知道有干扰")
|
||||
|
||||
### 2.1 干扰特征(候选)
|
||||
|
||||
| 特征 | 检测量 | 灵敏度 | 可靠性 |
|
||||
|------|--------|--------|--------|
|
||||
| **差拍抖动** | 无车时窗口内 variation 峰谷差/σ(复用 §10 健康度) | 高(差拍泄漏直接可见) | 高 |
|
||||
| **注入锁定慢拍** | CAPVD 出现 0.5~50Hz 周期性扰动(相邻样本差分谱) | 中 | 高(最隐蔽也最危险) |
|
||||
| **基线冻结频率** | 无车时 dev ≥ 4×dlt_ORG 的次数/占比(§10 冻结占比) | 中 | 高 |
|
||||
| **误触发率** | 无车误触发计数(单位时间) | 低(需要时间累积) | 中(最终后果指标) |
|
||||
|
||||
### 2.2 检测窗口约束(关键)
|
||||
|
||||
- **干扰检测必须在"本道无车"窗口做**——有车时 CAPVD 大变化是正常信号,不能当干扰
|
||||
- 但邻道车经过时差拍增强(邻道线圈被车扰动)→ 检测窗口应**包含"本道无车 + 邻道有车"时段**(无需知道邻道状态,本道无车时抖动超阈值即视为干扰)
|
||||
- 复用 §10 健康度框架:无车窗口峰谷差 > 阈值 × 连续 N 窗 → 判定"干扰存在"
|
||||
|
||||
### 2.3 干扰度评分(量化)
|
||||
|
||||
```
|
||||
干扰度 D = α·(抖动能量超标度) + β·(冻结占比) + γ·(误触发率归一化)
|
||||
D > D_HIGH → 需切换
|
||||
D > D_MID → 可疑,加强监测
|
||||
```
|
||||
|
||||
阈值需现场标定("待现场验证"),初值建议:无车窗口峰谷差 > 2× 正常噪声底(即 §10 健康度的漂移速率超基线 2 倍)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 调节时机策略(核心决策点)
|
||||
|
||||
### 方案 A:上电自适应(一次性探测)
|
||||
|
||||
```
|
||||
上电 → 各通道稳定期 → 分频探测(每档测 ~5s 噪声底)→ 选最优组合 → 固化 → 正常检测
|
||||
```
|
||||
|
||||
| 维度 | 评价 |
|
||||
|------|------|
|
||||
| 实现 | 简单,无运行中切换风险 |
|
||||
| 探测时长 | 4 通道 × 4 档 × 5s = 80s(交错并行可压到 ~20-30s)|
|
||||
| 缺点 1 | 探测期无检测功能(几十秒)|
|
||||
| 缺点 2 | **邻道可能未同时上电**,探测结果基于不完整邻居信息 |
|
||||
| 缺点 3 | 现场后期新增设备(护栏、新线圈、改造)不响应 |
|
||||
|
||||
### 方案 B:运行中自适应(动态切换)
|
||||
|
||||
```
|
||||
运行中持续监测干扰度 D → 超阈值 → 软切换(无车时执行)→ 切换后评估(改善则保持,恶化则回滚)
|
||||
```
|
||||
|
||||
| 维度 | 评价 |
|
||||
|------|------|
|
||||
| 实现 | 复杂,需软切换 + 回滚 |
|
||||
| 响应 | 实时适应现场变化 |
|
||||
| 风险 | 切换期间通道短暂不可用;切换震荡(需防抖:限次、评估、回滚)|
|
||||
| 硬约束 | **必须有车才能切**(防丢检测);切换后重建基线 |
|
||||
|
||||
### 方案 C:混合策略(推荐)
|
||||
|
||||
```
|
||||
上电探测建立初始分配(快速) + 运行中事件触发复测(慢速、保守) + 回滚保护
|
||||
```
|
||||
|
||||
- 上电:扫描选档(几十秒,覆盖"邻道未上电"风险用保守选档:选次优但间隔安全的档)
|
||||
- 运行中:干扰度 D 超阈值 或 定时(如每小时)复测 → 软切换 + 评估回滚
|
||||
- 切换频率限制:1 小时内最多 N 次(防震荡)
|
||||
|
||||
**切换时机细节(任何方案都需要):**
|
||||
|
||||
```
|
||||
前置条件: 该通道无车(VD_FLAG=0) 且 无脉冲输出中 且 非安全复位中
|
||||
执行序列: 挂起检测 → GPIO 切档 → 等待振荡稳定(~100ms, 实测标定) → 重建基线(稳定期 128样本≈1.3s) → 恢复检测
|
||||
安全约束: 切换期间继电器保持原状态; 四通道交错切换(防同时切换电源跌落)
|
||||
```
|
||||
|
||||
> 切换期间该通道约 1.5s 不可用——**这是方案取舍的核心代价,需用户确认可接受**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 频率规划:4 档怎么分给 8+ 个线圈
|
||||
|
||||
### 4.1 冲突定义
|
||||
|
||||
场景:每车道 4 线圈(前后)+ 相邻车道 4 线圈。冲突组合:
|
||||
- **同车道前后相邻**:线圈间距 ~几米,磁耦合强
|
||||
- **左右相邻车道最近线圈**:跨车道间距,耦合中等
|
||||
- 斜向/隔一档:耦合弱,可复用
|
||||
|
||||
### 4.2 复用策略(蜂窝式频率规划)
|
||||
|
||||
```
|
||||
同车道 4 线圈: 档位 1-2-3-4 顺序错开(每线圈独立档)
|
||||
相邻车道错相位: 车道 2 用 2-3-4-1 循环 → 左右最近线圈档位差 ≥1
|
||||
优先避免: 66/76(7.3% 间隔)相邻组合;同档复用距离 ≥2 个线圈位
|
||||
```
|
||||
|
||||
### 4.3 关键矛盾:车检器不知道邻道的档位
|
||||
|
||||
无通信前提下可选:
|
||||
|
||||
| 方案 | 做法 | 评价 |
|
||||
|------|------|------|
|
||||
| **现场配置基准**(推荐起步) | 协议/拨码设置每通道初始档位,各车道按约定错相位 | 简单可靠,依赖施工规范;为后续自动调整提供基准 |
|
||||
| **盲探测避让** | 扫描各档时测抖动能量,避开"差拍落入危险区"的档 | 不需邻道信息,但只能"避开坏的"不能"选最优" |
|
||||
| **差拍频率反推** | 从抖动频谱估计 f_beat,反推邻道档位 | 理论可行,工程复杂(欠采样谱估计),暂缓 |
|
||||
|
||||
**建议:初始档位现场配置(保证基本错频)+ 自动扫描避让(动态优化)+ 复用规则(防同档相邻)。**
|
||||
|
||||
---
|
||||
|
||||
## 5. 工程约束与风险清单
|
||||
|
||||
| 项 | 约束/风险 | 对策 |
|
||||
|----|----------|------|
|
||||
| 检测中断 | 切换 ~1.5s 不可用 | 无车才切;交错切换;切换评估回滚 |
|
||||
| 安全 | 有车时切档丢检测 → 砸车风险 | **硬约束:VD_FLAG=0 才可切**;继电器保持 |
|
||||
| 看门狗 | 探测/切换流程不能阻塞主循环 | 全部状态机化,单步 ≤50ms |
|
||||
| 基线 | 换档后 CAPVD 跳变 | 重建基线 + 稳定期(现有机制复用)|
|
||||
| 灵敏度 | 换档后阈值变化? | **归一化红利:SensTable 是相对量(×Origin/65536),换档后 Origin 变了但相对阈值不变,灵敏度自动保持** ✓ |
|
||||
| 持久化 | 新档位掉电丢失 | 写回 flash loopFreq_Level |
|
||||
| 切换震荡 | 频繁切换 | 限次(1h≤N)、评估回滚、Hysteresis |
|
||||
| 电源 | 多通道同时切电容冲击 | 交错切换 |
|
||||
| 协议联动 | 运行中档位变化后台未知 | 新增自动分频状态上报(含当前档位/干扰度/切换事件)|
|
||||
| 测试 | 无真实串扰环境难验证 | 实验室双线圈对测 + 工装注入模拟干扰 + 现场实测 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发计划
|
||||
|
||||
### 阶段 0:硬件标定(前置,依赖实验室/现场)
|
||||
|
||||
- [ ] 0.1 实测四档频率、切换 GPIO 到频率稳定的时间(示波器)
|
||||
- [ ] 0.2 标定振荡稳定等待时间(切档 → CAPVD 有效)
|
||||
- [ ] 0.3 两线圈对测:同档/相邻档/隔档的差拍形态与注入锁定距离
|
||||
- [ ] 0.4 标定干扰检测阈值初值(正常噪声底、抖动超标倍率)
|
||||
- 输出:参数表(写回代码宏)
|
||||
|
||||
### 阶段 1:干扰检测模块(先会"看病",不切换)
|
||||
|
||||
- [ ] 1.1 新建 `freq_monitor.c/h`:无车窗口抖动能量统计(复用 §10 峰谷差逻辑)
|
||||
- [ ] 1.2 注入锁定慢拍检测(相邻样本差分谱,0.5~50Hz 能量)
|
||||
- [ ] 1.3 干扰度评分 D(抖动 + 冻结占比 + 误触发)
|
||||
- [ ] 1.4 检测状态机:正常/可疑/干扰(带防抖、去程)
|
||||
- [ ] 1.5 协议上报:干扰状态字段(可选,先本地 PRINT)
|
||||
- [ ] 1.6 gcc 隔离单测(模拟差拍/锁定/车辆信号,验证不误报)
|
||||
- 验证:模拟数据注入,检测准确率;实车现场对比人工判断
|
||||
|
||||
### 阶段 2:选档决策(freq_planner)
|
||||
|
||||
- [ ] 2.1 候选档评估:每档噪声底 + 避让规则(差拍危险区)+ 复用约束(同车道不重复)
|
||||
- [ ] 2.2 决策状态机:保持/切换/回滚(带 Hysteresis + 限次)
|
||||
- [ ] 2.3 持久化:新档位写 flash
|
||||
- [ ] 2.4 单测:给定干扰特征序列 → 期望档位序列
|
||||
|
||||
### 阶段 3:软切换执行(freq_switch)
|
||||
|
||||
- [ ] 3.1 切档状态机:挂起检测 → GPIO 切档 → 等待稳定 → 重建基线 → 恢复
|
||||
- [ ] 3.2 与 vd1_task 集成(切档期间跳过检测、继电器保持)
|
||||
- [ ] 3.3 四通道交错切换调度
|
||||
- [ ] 3.4 切换前后 CAPVD 突变防护(切档瞬间不进入 IIR)
|
||||
- [ ] 3.5 单测:状态机全路径 + 异常分支(切换中有车进入 → 中止/回滚)
|
||||
|
||||
### 阶段 4:自适应策略整合(混合方案)
|
||||
|
||||
- [ ] 4.1 上电分频探测流程(4 通道交错扫描,~30s)
|
||||
- [ ] 4.2 运行中事件触发复测(干扰度超阈值)
|
||||
- [ ] 4.3 定时复测(如 1h,可配置)
|
||||
- [ ] 4.4 切换评估:切后 5 分钟干扰度对比,恶化回滚
|
||||
- [ ] 4.5 切换限次 + 防震荡
|
||||
|
||||
### 阶段 5:协议/后台联动 + 现场验证
|
||||
|
||||
- [ ] 5.1 协议新增:自动分频状态(当前档位/干扰度/切换事件),版本 bump
|
||||
- [ ] 5.2 vd960DBN 解析同步(若走 0xC0 传感帧或独立命令)
|
||||
- [ ] 5.3 现场测试方案:双车道对测、邻道干扰注入、长期稳定性(一周)
|
||||
- [ ] 5.4 devlog/产品文档同步
|
||||
|
||||
---
|
||||
|
||||
## 7. 需要用户拍板的决策点
|
||||
|
||||
1. **切换时机**:方案 A(仅上电)/ B(仅运行中)/ C(混合,推荐)?
|
||||
2. **初始档位来源**:现场配置基准(拨码/协议)vs 全自动扫描?(建议:配置基准 + 自动避让)
|
||||
3. **切换代价**:每通道 ~1.5s 检测中断,可接受?(有车不切,只影响无车时段,实际影响很小)
|
||||
4. **邻道协调**:相邻车道是否也是自家车检器?能否约定统一的错相位配置规范?
|
||||
5. **范围**:v1 只做"干扰检测 + 手动/半自动提示"(后台建议换档),v2 再做全自动?还是一次到位?
|
||||
@@ -4,6 +4,294 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-02 — V1.03:上电自检基准判稳同步 V4B V4.23(版本号 V1.03)
|
||||
|
||||
> **固件版本:V1.03**(cmcng.h:FIRMWARE_VER="1.03" + MAIN=1/SUB=3,SSUB=1 保持)。devlog 修订。
|
||||
|
||||
### 背景(源自 DLD154V4B 现场反馈,V4B V4.23 已实测通过)
|
||||
|
||||
绿灯上电自检快闪:原逻辑固定 128 样本(≈1.3 s)即判稳,**与基准值是否真稳定无关**;
|
||||
且 `stable_cnt` 只在 `init_vd_single` 清零,**有限存在超时 / 安全复位(LC_Reset=1)重学阶段不清零**,
|
||||
残留计数会让二次稳定期几乎瞬间判稳(绿灯自检形同虚设)。V4B V4.23 修复后本仓四路同步。
|
||||
|
||||
### 改动
|
||||
|
||||
- `TaskLoop.h`:新增 `STABLE_ORIGIN_PPT 1`(窗口 Origin 均值漂移 ≤ Origin×0.1%)/
|
||||
`STABLE_SETTLE_WINDOWS 2`(连续 2 窗判稳)/ `STABLE_MAX_SAMPLES 500`(≈5 s 硬兜底);
|
||||
`Loop154_Unit` 新增 `settle_cnt`
|
||||
- `TaskLoop.c`:
|
||||
- 稳定期:每窗(100 样本≈1 s)完成时比较 Origin 均值漂移,≤0.1% → `settle_cnt++` 否则清零;
|
||||
判稳 = 最少 128 样本 + 连续 2 窗稳定,或 500 样本硬兜底;判稳退出时 `stable_cnt/settle_cnt` 清零
|
||||
- `init_vd_single` / 有限存在超时 / 安全复位超时(重学路径)统一清零自检计数 → 二次稳定期从零开始
|
||||
- 绿灯 `poll_green_led` 条件不变:该路绿灯闪至**本路基准稳定**才停(四路独立)
|
||||
|
||||
### 验证
|
||||
|
||||
- `tests/test_stable_settle.c`:常量 → 200 样本判稳;首窗尖峰 → 400(扰动后需 2 个干净窗口);
|
||||
持续漂移 → 500 硬兜底;重学复位后 → 200(防呆:非瞬间判稳)ALL PASS
|
||||
|
||||
### 待现场验证
|
||||
|
||||
上电绿灯自检时长(干净启动 ≈2 s / 有扰动 3~4 s / 干扰大 5 s 兜底)与四路独立闪烁观感。
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-02 — V1.02:基线跟踪双向对称保护同步 V4B V4.20(版本号 V1.02)
|
||||
|
||||
> **固件版本:V1.02**(cmcng.h:FIRMWARE_VER="1.02" + MAIN=1/SUB=2,SSUB=1 保持)。devlog 修订。
|
||||
|
||||
### 背景(现场问题,源自 DLD154V4B 同步)
|
||||
|
||||
特殊形状铁块(磁导率主导)靠近线圈 → variation 为**负**(CAPVD 高于 Origin):
|
||||
- 原基线跟踪**单边保护**(只挡 `dev > +4×dlt`),负 variation 时基线照常更新 → **Origin 被污染抬高**
|
||||
- 铁块离开时 CAPVD 回落 → 跨过抬高的进入线 → **假进入** → 进入后 Origin 冻结 → 释放线抬高 → **锁死不释放**
|
||||
- DLD154V4B V4.20 已修复并现场验证(三次铁块测试 0 锁死),本仓同步
|
||||
|
||||
### 改动(TaskLoop.c,四路 unit 结构同步)
|
||||
|
||||
```
|
||||
原逻辑: dev < +4×dlt → 更新基线 ← 单边, 负variation漏网
|
||||
修复后: |dev| < 4×dlt → 更新基线 ← 对称窗口 [-4×dlt, +4×dlt]
|
||||
dev < 0 (正variation,疑似车) → 冻结 + FREEZE_TIMEOUT 超时兜底更新(保留)
|
||||
dev > 0 (负variation,铁块) → 只冻结, 永不超时更新Origin ← 核心
|
||||
```
|
||||
|
||||
- 对称窗口内正常跟踪时补 `loop_freeze_ref = 0`(残留清零,与 V4B 一致)
|
||||
- 权衡:长时间负温漂基线不跟随(安全方向,正车检测仍正常)
|
||||
- 逻辑与 DLD154V4B V4.20 `test_neg_variation.c` 6 项回归单测覆盖一致
|
||||
|
||||
### 版本
|
||||
|
||||
- cmcng.h:FIRMWARE_VER "1.01"→"1.02",FIRMWARE_VER_SUB 1→2
|
||||
- 同步仓:DLD154Pro(5f7082c)、DLD110SV4(fd6ee79)同期完成
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-28 — 看门狗升级:双保险喂狗模式 + 超时对齐 V4B(1.64s)
|
||||
|
||||
### 背景
|
||||
|
||||
与 DLD154V4B(同款 AT32F421)看门狗实现对照后发现三个问题:
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| **直喂模式缺 ISR 活性检测** | 原实现主循环每 10ms 无条件 `wdt_feed()`——TMR15 ISR 死掉但主循环活着时,看门狗照样被喂,系统"假活"(时序状态机全停但永不复位) |
|
||||
| **超时 3s 偏长** | `div=64, rld=1875` → 3s;V4B 已用 1.64s,复位自愈更快 |
|
||||
| **注释矛盾** | `main.c` 注释写"重载值=4687 超时≈3秒"(4687 是 7.5s 的算法),实际 1875→3s |
|
||||
|
||||
### 改动(对齐 DLD154V4B V2.10)
|
||||
|
||||
```c
|
||||
// TaskLoop.h
|
||||
#define WDT_COUNTER_LIMIT 100 // 喂狗周期 500ms (TMR15 5ms × 100)
|
||||
#define WDT_DIV 2 // IWDT 分频枚举值: 2 = WDT_CLK_DIV_16 (实际分频 16)
|
||||
#define WDT_RLD 4095 // 12bit 重装载 → 超时 ≈ 1.64s
|
||||
```
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/main.c` | `wdt_init()`:`WDT_CLK_DIV_64/1875` → `WDT_CLK_DIV_16/4095`(超时 3s→1.64s);`wdt_feed` 定义删除(移至 TaskLoop.c);注释修正 |
|
||||
| `src/TaskLoop.c` | +`g_wdg_counter` + `poll_wdg()`(500ms 才 reload);TMR15 ISR 5ms 递增;调用点 `wdt_feed()`→`poll_wdg()` |
|
||||
| `inc/TaskLoop.h` | +WDT 宏 + `poll_wdg` 声明 + `g_wdg_counter` extern(删 `wdt_feed`) |
|
||||
|
||||
### 验证
|
||||
|
||||
- 隔离测试(host gcc mock 3bit 寄存器语义)3 项断言全过:分频 16/重载 4095/超时 1638ms > 500ms 喂狗周期、10s 喂狗 20 次(500ms 精确)、ISR 死不喂狗
|
||||
- 双保险语义:ISR 死 → 计数不涨 → 永不喂狗;主循环死 → poll_wdg 不执行 → 超时复位
|
||||
- 待现场验证:LSI 实际频率偏差对复位时间的影响
|
||||
|
||||
### ⚠ 踩坑提醒(V4B 同源事故)
|
||||
|
||||
`wdt_divider_set()` 参数是**枚举值**不是分频数值(`WDT_CLK_DIV_16=0x02`,寄存器只取低 3 位)——V4B 曾把 `WDT_DIV=16` 当数值传入 → 实际分频 4 → 超时 410ms < 500ms 喂狗周期 → **不断复位重启**。本次 vd960Loop 直接沿用枚举写法,避免同坑。
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-27 — LOOP_MEASURE_BASE 归一化基数上报(协议 V1.09)
|
||||
|
||||
### 背景
|
||||
|
||||
跨产品 MEASUREMENT_BASE 对齐(2026-08-26 DLD110S ×4 事故:第三方小程序把 ΔL/L 报成 8.85%,真实 2.21%)。variation 是周期域计数值(`variation = Origin − CAPVD`,CAPVD ∝ √L),归一化必须除**设备自身**的 MEASUREMENT_BASE。960Loop 是产品线中唯一未上报基数的型号——0x64 读参响应裸发参数,第三方平台无从换算(写死 131072 恰好蒙对,写 524288 就 ÷4 失真)。
|
||||
|
||||
### 改动
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `docs/DLD960Loop_串口通信协议.md`(vd_960 仓库) | **V1.09**:0x64 响应尾部追加 3B LE `LOOP_MEASURE_BASE`(响应 34B→37B)+ 换算公式 + 修订记录 |
|
||||
| `inc/cmcng.h` | `#define LOOP_MEASURE_BASE 131072L`(注释标明与 TaskLoop.h MEASUREMENT_BASE 同步) |
|
||||
| `src/main.c` | `CMD_DBN_GET_MCJQ_PARAM` (0x64) handler for 循环后追加 3B LE(小端,响应 2+8×4+3=37B) |
|
||||
|
||||
### 换算公式(写入协议文档)
|
||||
|
||||
```
|
||||
Δf/f ≈ variation / LOOP_MEASURE_BASE (131072, AT32@120MHz)
|
||||
ΔL/L ≈ -2 × variation / LOOP_MEASURE_BASE (系数 2 来自 CAPVD ∝ √L 展开)
|
||||
```
|
||||
|
||||
### 设计决策
|
||||
|
||||
- 常量放 **0x64 查询响应**(低频),不进 0xC0 高频帧(每线圈 12B 已贴边,加字段触发分包)
|
||||
- 向后兼容:老客户端只读前 34B 不受影响;新客户端读 37B 拿基数
|
||||
- ⚠ 联动待办:DBN 侧对外(BLE/TCP/MQTT)暴露基数未落地——平台走网络协议评估时仍需加字段
|
||||
|
||||
### 验证
|
||||
|
||||
- ✅ `check_c_balance.py` 语法平衡 OK;编码保持 UTF-8+CRLF
|
||||
- [ ] 板级验证:0x64 响应长度 37B、尾部 3B = `00 00 02`(131072 LE)
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-25 — 灵敏度 4 级制修复(协议 V1.08)
|
||||
|
||||
### 背景
|
||||
|
||||
现场用金属块测感应高度,设置不同灵敏度档位**感应高度无区别**。排查发现三个根因:
|
||||
|
||||
| # | 根因 | 位置 |
|
||||
|---|------|------|
|
||||
| 1 | **运行中配置命令不同步检测单元**:`unpack_pkg_set_mcjq_param()` 只更新 `g_loop_cng_info.loop_cng[].sensitvity` + 写 flash,**没有同步 `g_loop_states.loop_unit[].loop_SensLevel`** → 改灵敏度不重启不生效(对比上电路径 `para_store_init()` 有同步) | main.c:576 |
|
||||
| 2 | **9级制映射 `& 0x03` 折叠**:协议 0~9 级(默认 7),`sensitvity & 0x03` 只取低 2 位 → 7→3、**3→3(从默认 7 改到 3 无效)**、8→0(反向折叠) | storage.c:184 |
|
||||
| 3 | 测试陷阱:金属块过大/过近信号饱和,4 档阈值都在饱和区 → 感应高度差异不可见(建议小金属块/测临界距离) | 测试方法 |
|
||||
|
||||
### 修复(方案 B:4 级制)
|
||||
|
||||
- 提取映射函数 `sens_level_from_config()`(storage.c/h):**4级制 0~3 一一对应**,9 级制后续实现时只改这一处
|
||||
- `unpack_pkg_set_mcjq_param()` 补 `loop_SensLevel` 同步 → 运行中配置立即生效
|
||||
- `para_store_init()` 上电路径改用同一函数(消除双处映射漂移)
|
||||
- 协议 V1.08:Sensitivity 语义 0~9 级 → **4 级制 0~3**(默认 2)
|
||||
- 新增 `tests/test_sens_mapping.c` gcc 隔离单测(mock flash.h/FreeRTOS.h/task.h + FLP GPIO 宏):验证 0~3 无折叠、兼容旧默认 7→3、SensTable 单调
|
||||
|
||||
### 改动
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `src/storage.c` | 新增 `sens_level_from_config()`;`para_store_init` 改用 |
|
||||
| `src/main.c` | `unpack_pkg_set_mcjq_param` 补 loop_SensLevel 同步(运行中生效) |
|
||||
| `inc/storage.h` | sensitvity 注释 4 级制;函数声明 |
|
||||
| `tests/test_sens_mapping.c` | 新增映射单测(gcc PASS) |
|
||||
| `tests/mock/` | flash.h/FreeRTOS.h/task.h 隔离测试 mock |
|
||||
| `docs/DLD960Loop_串口通信协议.md` | V1.08:灵敏度 4 级制 |
|
||||
|
||||
### 验证
|
||||
|
||||
gcc 单测 PASS:0→0, 1→1, 2→2, 3→3 无折叠;`7&0x0F → 3` 兼容旧默认。实机待金属块复测(建议小金属块测临界感应高度)。
|
||||
|
||||
### 9 级制灵敏度表设计(2026-08-25)
|
||||
|
||||
用户给行业典型范围:**0.01% ~ 5.00% ΔL/L**(跨度 546 倍)。9 级等比公比 2.198,方向 0=最低、8=最高(与 4 级一致)。
|
||||
|
||||
| SENS | sens_in | ΔL/L 进入 | sens_out | ΔL/L 离开 | out/in |
|
||||
|------|---------|-----------|----------|-----------|--------|
|
||||
| 0 | 1638 | 5.00% | 819 | 2.50% | 50% |
|
||||
| 1 | 745 | 2.27% | 373 | 1.14% | 50% |
|
||||
| 2 | 339 | 1.03% | 170 | 0.52% | 50% |
|
||||
| 3 | 154 | 0.47% | 100 | 0.31% | 65% |
|
||||
| 4 | 70 | 0.21% | 46 | 0.14% | 65% |
|
||||
| 5 | 32 | 0.098% | 21 | 0.064% | 65% |
|
||||
| 6 | 15 | 0.046% | 14 | 0.043% | 90% |
|
||||
| 7 | 7 | 0.021% | 6 | 0.018% | 90% |
|
||||
| 8 | 3 | 0.01% | 3 | 0.01% | 100% |
|
||||
|
||||
设计要点:
|
||||
- 滞回渐变 50%→65%→90%/100%:高档 out 低于噪声底会不释放(对齐 DLD154Pro 最高档 86% 经验)
|
||||
- 4 级→9 级迁移(就近):0→3、1→4、2→5、3→7(端点不再保留,需现场复测)
|
||||
- 工程警示:SENS=8 dlt≈6 counts 贴噪声底(仅低噪声环境);SENS=0 dlt≈3275(只认重型车)
|
||||
- 溢出检查:Origin×1638 = 214M < 2³² ✓
|
||||
|
||||
落地:
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `docs/variation-analysis.md` | §9.3.1 9 级制表(设计稿) |
|
||||
| `tools/variation_calc.py` | `--sens-table 9` 支持 9 级显示(4 级默认兼容) |
|
||||
|
||||
验证:工具 9 级端点 0.0092%/4.9988% ≈ 0.01%/5.00% ✓。代码落地(set_factory_param/sens_level_from_config)待 9 级立项。
|
||||
|
||||
### 补充根因(同日):INIT_VDs 硬编码覆盖配置 + flash 坏数据
|
||||
|
||||
现场串口日志(`Read_Cng_Store`)显示:配置 4 通道 SENS=3/2/1/0,但检测时 `Car_In` 全部打印 `sens_in:36`(SENS=2 表值)——4 通道检测全用 SENS=2。定位两个根因:
|
||||
|
||||
**① `init_vd_single()` 硬编码 `loop_SensLevel = 2`(TaskLoop.c:111)**
|
||||
|
||||
时序:`main → para_store_init()`(同步 loop_SensLevel=配置)→ 任务启动 → `INIT_VDs()` → `init_vd_single()` **把 loop_SensLevel 重置为 2**,覆盖配置值 → 全通道 SENS=2。
|
||||
|
||||
修复:`init_vd_single` 改为从配置读——`sens_level_from_config(g_loop_cng_info.loop_cng[unit->loop_num].sensitvity)`(unit->loop_num 由 INIT_VDs 先设置)。
|
||||
|
||||
**② flash 灵敏度表 sens[0]={512,400} 是 0x8A 配置值(更正:非坏数据)**
|
||||
|
||||
解析 `Read_Cng_Store` flash 数据(0x40 起小端 2B):`{512,400},{108,72},{36,18},{10,9}`。**512/400 是 `CMD_DBN_LOOP_SENS_LIST (0x8A)` 配置的自定义 SENS=0 档阈值**(main.c:946:写 → `memcpy(g_loop_sens_list.sens)` → `storage_dev()` 持久化 → `para_store_init()` 重读),链路完整。值大 = 阈值高 = 更不灵敏(0.78% vs 出厂 0.33% Δf/f),属于用户自定义,**勿恢复出厂(会清掉配置)**。修复 ① 后 loop_3(SENS=0)将使用 512/400 配置值,符合预期。
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-25 — variation ↔ ΔL/ΔL-L 物理对应分析与工具
|
||||
|
||||
### 背景
|
||||
|
||||
后台需要把上报的 `variation`(CAPVD 计数域)换算成物理电感变化量,用于标定灵敏度、分析车辆信号强度。
|
||||
|
||||
### 核心推导
|
||||
|
||||
CAPVD ∝ 周期 T = 2π√(LC),即 **CAPVD ∝ √L 而非 L**:
|
||||
|
||||
```
|
||||
variation/Origin = 1 − √(1 + ΔL/L₀) 精确
|
||||
ΔL/L₀ ≈ −2 × variation/Origin 一阶(残差 <0.2%)
|
||||
Δf/f ≈ variation/Origin 频域对应
|
||||
```
|
||||
|
||||
- 系数 2 来源于 √(1+x) ≈ 1+x/2 展开
|
||||
- 符号:车辆进入 ΔL<0 → variation 恒为正,与 V1.05 协议语义一致
|
||||
- **电容档无关性**:Origin 与 CAPVD 均 ∝ √C,variation/Origin 中 C 消掉 → 33/43/66/76nF 四档共用同一换算表,现场换档不换标定
|
||||
|
||||
### 灵敏度档位 → ΔL/L 触发阈值(SensTable {216,108,36,10})
|
||||
|
||||
| SENS | Δf/f 进入 (=SensTable/65536) | ΔL/L 进入 ≈ −2× |
|
||||
|------|------------------------------|-----------------|
|
||||
| 0 低 | 0.330% | −0.659% |
|
||||
| 1 中 | 0.165% | −0.330% |
|
||||
| 2 高 | 0.055% | −0.110% |
|
||||
| 3 最高 | 0.015% | −0.031% |
|
||||
|
||||
交叉验证:DLD154Pro 技术文档 sens_in {108,54,28,14} 标注 ΔL/L = 0.330%/0.165%/0.085%/0.043%,恰为 SensTable/65536 的 2 倍——产品线口径一致。
|
||||
|
||||
### 改动
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `docs/variation-analysis.md` | 新增 §9(物理链路/公式/档位表/电容无关性/示例/注意事项) |
|
||||
| `tools/variation_calc.py` | 新增换算工具:正向(freq+cap+variation+origin→L₀,ΔL,ΔL/L)、反推、3B LE 补码解析(V1.05)、灵敏度对照、交互模式 |
|
||||
|
||||
### 验证
|
||||
|
||||
工具四模式实测通过,正反算自洽(ΔL/L=0.33% ↔ variation=216 @ Origin=131072)。
|
||||
|
||||
### 协议文字勘误(协议 V1.07)
|
||||
|
||||
用户指出 §5.6.3 记录的口径差异应直接改协议:原文 `±8388607` 不对称,负向边界少了 1。统一为 **−8388608 ~ +8388607**(3B 补码标准值域 −2²³ ~ +2²³−1,对齐代码负向饱和边界 0x800000),后台按文档实现时不会对 0x800000 边界犯嘀咕。
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `DLD960Loop_串口通信协议.md` | variation 字段值域文字修正 + 修订记录 V1.07 |
|
||||
| `variation-analysis.md` | 头部版本引用 → V1.07;§1 值域;§5.1 表格;§5.6.3 口径小注改写 |
|
||||
|
||||
代码无改动(`main.c` 饱和边界本就是 −8388608 ~ +8388607)。
|
||||
|
||||
### 环境健康度算法(§10)
|
||||
|
||||
用户提出零成本环境健康度方案:无车时 variation 锯齿的峰 = 漂移信号,后台逐窗统计画健康度曲线。模拟验证后修正两处口径并落地:
|
||||
|
||||
1. **绝对峰 ≠ 窗口漂移**:Origin 更新到窗口均值(滞后半窗 2.5s),稳态绝对峰 = **1.5×** 窗口漂移 → 改用**峰谷差**(max−min,误差 <1%)
|
||||
2. **600ms 采样系统性低估 ~12%**:峰恒在窗口末(500 tick),末采样点 480 tick 永远错过 → 单边偏差,趋势可用,绝对标定 ×1.14
|
||||
3. **冻结态失效**:漂移 > 4×dlt_ORG(中档 0.13%/s)→ 锯齿消失,速率口径失效 → 双指标:`漂移速率 = 峰谷差/Origin/5s` + `冻结占比`
|
||||
4. 冻结判定双判据:跨窗跳变消失(|jump|≈0)+ 峰谷差突变(>4×前 4 窗中位数),最后一窗也可判
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| `docs/variation-analysis.md` | 新增 §10(口径修正表/双指标/冻结判定/工具用法) |
|
||||
| `tools/drift_health.py` | 新增参考实现:CSV 输入 + 逐窗统计 + 冻结检测 + JSON 输出 + 自测 |
|
||||
|
||||
自测:前 6 窗慢漂移 0.02%/s 全正常,后 2 窗快漂移正确标冻结,平均速率/冻结占比准确。
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-14 — variation 上报量 2B→3B 有符号 (协议 V1.05)
|
||||
|
||||
### 背景
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 分析对象:`uart_report_packet_loop_acs()` 中上报的 **变化量 variation**
|
||||
> 涉及文件:`src/main.c`(组包)、`src/TaskLoop.c`(基线/IIR 更新)、`inc/TaskLoop.h`(参数宏)
|
||||
> 协议版本:与《DLD960Loop_串口通信协议.md》**V1.05** 同步(2026-07-14:variation 无符号 2B → **有符号 3B**,见 §5)
|
||||
> 协议版本:与《DLD960Loop_串口通信协议.md》**V1.07** 同步(2026-07-14 V1.05:variation 无符号 2B → **有符号 3B**,见 §5;2026-08-25 V1.07:值域表述勘误统一为 **−8388608 ~ +8388607**)
|
||||
> 结论层次:物理层 → 信号层 → 数据层 → 上报调度层
|
||||
|
||||
---
|
||||
@@ -38,7 +38,7 @@ g_pkg_uart_report.pkg[i++] = (uint8_t)((variation >> 16) & 0xFF);
|
||||
```
|
||||
|
||||
- **带符号带方向**:正 = 检测裕量方向(与旧版有车时的数值语义连续),负 = 反向漂移
|
||||
- 3 字节小端补码,值域 ±2²³−1,发送端 **饱和限幅,不会回绕**
|
||||
- 3 字节小端补码,值域 −2²³ ~ +2²³−1,发送端 **饱和限幅,不会回绕**
|
||||
- 旧版(≤V1.04)为 2B 无符号绝对值,缺陷与变更动机见 **§5**
|
||||
|
||||
---
|
||||
@@ -136,7 +136,7 @@ variation: ╱| ╱| ╱| ╱| ← 锯齿波!
|
||||
|---|---|---|
|
||||
| 计算式 | `\|Origin − CAPVD\|` 取绝对值 | `Origin − CAPVD` 带符号 |
|
||||
| 编码 | 2 字节无符号,小端 | **3 字节有符号补码**,小端 |
|
||||
| 值域 | 0~65535,**溢出回绕** | ±8388607,**发送端饱和限幅** |
|
||||
| 值域 | 0~65535,**溢出回绕** | −8388608 ~ +8388607,**发送端饱和限幅** |
|
||||
| 方向信息 | 无 | 正 = 车/金属进入,负 = 反向漂移 |
|
||||
| 每通道单元 | 11 字节 | **12 字节** |
|
||||
| 帧 Len / 总长(4 路无分包) | 0x2F=47 / 52B | **0x33=51 / 56B**(仍 < `BUFF_STACK_SIZE=64`,缓冲安全) |
|
||||
@@ -182,7 +182,7 @@ if (v & 0x800000) v |= 0xFF000000; // bit23 = 1 → 负数,符号扩展
|
||||
|
||||
1. **Loop 与 DBN 固件必须同版本发布**。单元步长 11↔12 死绑定,任一端用旧固件,4 通道数据整体错位。**且帧级 XOR/SUM 校验依然通过**(校验的是整帧,不感知单元边界)——这是**静默数据损坏**,不会报错,只会得到一堆"看起来合理"的错值。
|
||||
2. 上位机若想兼容新旧固件,可用 **Len 字节区分**:4 路无分包时 Len=0x2F(47) 为旧格式(2B 无符号),Len=0x33(51) 为新格式(3B 有符号),按帧动态选解析器。
|
||||
3. 口径小注:协议文字写值域 ±8388607,代码负向饱和到 −8388608(0x800000)。解析端符号扩展后无歧义,且实际物理量远到不了该量级(CAPVD 全量程才 ~131072,3B 有 **~64 倍余量**),不构成问题,仅作记录。
|
||||
3. 口径说明(V1.07 已勘误):协议文字与代码统一为 **−8388608 ~ +8388607**(0x800000 是合法负边界)。解析端符号扩展后无歧义;实际物理量远到不了该量级(CAPVD 全量程才 ~131072,3B 有 **~64 倍余量**),边界仅作记录。
|
||||
|
||||
### 5.7 对分析工作的直接增益
|
||||
|
||||
@@ -218,3 +218,215 @@ if (v & 0x800000) v |= 0xFF000000; // bit23 = 1 → 负数,符号扩展
|
||||
- 将 variation 改为上报 **斜率 / 趋势** 而非瞬时差,削掉锯齿混叠
|
||||
- ~~补 variation 饱和保护~~ → V1.05 已完成
|
||||
- **随包上报当前基线状态(跟踪 / 冻结 / 有车)**,让后台区分锯齿谷与真空闲、冻结大值与真车
|
||||
|
||||
---
|
||||
|
||||
## 9. variation ↔ ΔL / ΔL-L 物理对应(2026-08-25 补充)
|
||||
|
||||
> 配套工具:`tools/variation_calc.py`(正向/反推/3B 补码解析/灵敏度对照,纯 stdlib)
|
||||
|
||||
### 9.1 物理链路:CAPVD 正比于 √L,不是 L
|
||||
|
||||
由 `main.c` 频率转换公式反推:
|
||||
|
||||
```c
|
||||
freq = sclk_freq × input_div × LPCNT / CAPVD → CAPVD ∝ 1/freq = T(周期)
|
||||
```
|
||||
|
||||
LC 振荡器周期 `T = 2π√(LC)`,因此:
|
||||
|
||||
```
|
||||
CAPVD = A·√(LC) (A = 系统归一化常数,含时钟/分频/MEASUREMENT_BASE=131072)
|
||||
```
|
||||
|
||||
**CAPVD 与周期成正比,与 √L 成正比**——这是 variation 换算 ΔL 的关键前提(平方根关系,不是线性)。
|
||||
|
||||
### 9.2 换算公式
|
||||
|
||||
设基线电感 L₀,有车后电感 L₀+ΔL:
|
||||
|
||||
```
|
||||
Origin = A·√(L₀·C)
|
||||
CAPVD = Origin·√(1 + ΔL/L₀)
|
||||
variation = Origin − CAPVD = Origin·[1 − √(1 + ΔL/L₀)]
|
||||
```
|
||||
|
||||
| 口径 | 公式 | 适用 |
|
||||
|------|------|------|
|
||||
| **精确** | `ΔL/L₀ = (1 − variation/Origin)² − 1 = (variation/Origin)² − 2(variation/Origin)` | 大信号(如大车全覆盖) |
|
||||
| **一阶近似** | `ΔL/L₀ ≈ −2 × variation/Origin` | 检测阈值区间,残差 <0.2% |
|
||||
| 频域对应 | `Δf/f ≈ variation/Origin`(CAPVD ∝ 1/f) | 与验收标准 §3 频偏口径衔接 |
|
||||
|
||||
符号语义:车辆进入 → 涡流 → ΔL<0 → CAPVD<Origin → **variation 恒为正**,与 V1.05 协议一致;负值 = 反向漂移 / 基线污染。
|
||||
|
||||
**一阶近似的误差上界**:二阶残差 ≈ (variation/Origin)²,最低灵敏度档 0.33% 时仅 0.0011%,相对误差 <0.2%——系数 2 直接可用。
|
||||
|
||||
### 9.3 灵敏度档位 → ΔL/L 触发阈值
|
||||
|
||||
进入条件 `CAPVD < Origin − dlt_ORG`,进入瞬间 `variation ≈ dlt_ORG = Origin×SensTable/65536`,逐档对应:
|
||||
|
||||
| SENS | 进入表 | 离开表 | Δf/f 进入 (=vr) | **ΔL/L 进入 ≈ −2×vr** | ΔL/L 离开 |
|
||||
|------|--------|--------|-----------------|----------------------|-----------|
|
||||
| 0 低 | 216 | 108 | 0.3296% | **−0.659%** | −0.330% |
|
||||
| 1 中 | 108 | 72 | 0.1648% | **−0.330%** | −0.220% |
|
||||
| 2 高 | 36 | 18 | 0.0549% | **−0.110%** | −0.055% |
|
||||
| 3 最高 | 10 | 9 | 0.0153% | **−0.031%** | −0.027% |
|
||||
|
||||
> 交叉验证:DLD154Pro 技术文档 sens_in {108,54,28,14} 标注 ΔL/L = 0.330%/0.165%/0.085%/0.043%,恰为 SensTable/65536 的 2 倍——产品线已统一使用 `ΔL/L ≈ 2×SensTable/65536` 口径。
|
||||
|
||||
### 9.3.1 9 级制灵敏度表(设计稿 2026-08-25)
|
||||
|
||||
行业典型范围:**0.01% ~ 5.00% ΔL/L**(跨度 546 倍,9 级等比公比 2.198)。方向:SENS **0=最低灵敏度、8=最高灵敏度**(值越大越灵敏,与 4 级一致)。
|
||||
|
||||
**sens_in(车来触发)**:
|
||||
|
||||
| SENS | sens_in | Δf/f | **ΔL/L** | 典型用途 |
|
||||
|------|---------|------|----------|----------|
|
||||
| 8(最高) | 3 | 0.005% | **0.01%** | 摩托车/低噪声极限档 |
|
||||
| 7 | 7 | 0.011% | 0.021% | 摩托车 |
|
||||
| 6 | 15 | 0.023% | 0.046% | 小型车/电动车 |
|
||||
| 5 | 32 | 0.049% | 0.098% | 轿车 |
|
||||
| 4 | 70 | 0.107% | 0.214% | 轿车/SUV |
|
||||
| 3 | 154 | 0.235% | 0.470% | 常规 |
|
||||
| 2 | 339 | 0.517% | 1.034% | 大车优先 |
|
||||
| 1 | 745 | 1.137% | 2.273% | 大车/货车 |
|
||||
| 0(最低) | 1638 | 2.500% | **5.00%** | 只认重型车/强干扰环境 |
|
||||
|
||||
**sens_out(车走释放,滞回渐变 50%→65%→90%/100%)**:
|
||||
|
||||
| SENS | sens_out | out/in | ΔL/L 离开 |
|
||||
|------|----------|--------|-----------|
|
||||
| 0 | 819 | 50% | 2.50% |
|
||||
| 1 | 373 | 50% | 1.14% |
|
||||
| 2 | 170 | 50% | 0.52% |
|
||||
| 3 | 100 | 65% | 0.31% |
|
||||
| 4 | 46 | 65% | 0.14% |
|
||||
| 5 | 21 | 65% | 0.064% |
|
||||
| 6 | 14 | 90% | 0.043% |
|
||||
| 7 | 6 | 90% | 0.018% |
|
||||
| 8 | 3 | 100% | 0.01% |
|
||||
|
||||
- **滞回渐变原因**:高档 sens_in 很小,out 若按 50% 会低于噪声底 → 车走不释放。6/7/8 档收窄到 90%/100%(对齐 DLD154Pro 最高档 out/in=86% 经验)
|
||||
- **4 级→9 级迁移**(就近):4级 SENS 0→9级3、1→4、2→5、3→7(4 级端点不再保留,迁移后阈值小幅变化,需现场复测)
|
||||
- **工程警示**:SENS=8(0.01%)dlt≈6 counts 贴着噪声底,仅限低噪声环境;SENS=0(5%)dlt≈3275,只认重型车
|
||||
- **乘法溢出检查**:Origin(131072)×1638 = 214M < 2³² ✓
|
||||
- 代码落地:`set_factory_param` total=9 填上表;`sens_level_from_config` 改 `sensitvity & 0x0F`(0~8 直接索引);协议 sensitvity 0~8
|
||||
|
||||
### 9.4 电容档位无关性(33/43/66/76nF)
|
||||
|
||||
四档电容只改绝对工作频率 f₀ = 1/(2π√(L₀C)):
|
||||
|
||||
| C 档 | 频率 @ L₀≈59μH |
|
||||
|------|----------------|
|
||||
| 33nF | ~114 kHz |
|
||||
| 43nF | ~100 kHz |
|
||||
| 66nF | ~81 kHz |
|
||||
| 76nF | ~75 kHz |
|
||||
|
||||
但 **Origin 与 CAPVD 均 ∝ √C,比值 variation/Origin 中 C 被消掉**:
|
||||
|
||||
```
|
||||
variation/Origin = 1 − √(1 + ΔL/L₀) ← 与 C 无关
|
||||
```
|
||||
|
||||
**结论:四档电容共用同一套 ΔL/L 换算表,现场换档不换标定。** 这是 LPCNT = MEASUREMENT_BASE/Xn 自适应归一化的直接收益。
|
||||
|
||||
### 9.5 绝对电感与 ΔL 完整换算
|
||||
|
||||
```
|
||||
① 绝对电感: L₀ = 1/(4π²·f₀²·C) ← f₀ 用上报频率(3B),C 用当前档位电容
|
||||
② 相对变化: ΔL/L₀ ≈ −2 × variation/Origin
|
||||
③ 绝对变化: ΔL = L₀ × ΔL/L₀
|
||||
```
|
||||
|
||||
示例(C=43nF, f₀=100kHz → L₀≈58.9μH;SENS=1 触发,variation=216, Origin=131072):
|
||||
|
||||
```
|
||||
variation/Origin = 0.1648%
|
||||
ΔL/L₀ ≈ −0.330%
|
||||
ΔL ≈ −58.9μH × 0.330% ≈ −0.194 μH
|
||||
```
|
||||
|
||||
### 9.6 使用注意事项(后台/上位机口径)
|
||||
|
||||
1. **必须归一化**:variation 是 CAPVD 计数域(~131072 量级),直接拿绝对值当 ΔL 会差 4~5 个数量级。所有换算先除 Origin。
|
||||
2. **只有有车冻结期的 variation 干净**:有车时 Origin 冻结,variation 忠实反映信号强度,此刻换算 ΔL/L 最可信;无车时 Origin 5s 阶跃 → variation 呈锯齿,锯齿峰 = 5s 漂移量,不能当车辆信号。
|
||||
3. **基线重锁识别**:冻结超时(10s 稳定)后 Origin 强制 = CAPVD,variation 突降归零——后台应识别为"基线重锁"事件,勿当车辆离开。
|
||||
4. **文档口径 2 倍陷阱**:SensTable/65536 直接读是 Δf/f(0.33%),标成 ΔL/L 是 0.66%。验收/规格文档必须注明口径(推荐统一用 ΔL/L)。
|
||||
5. **绝对 ΔL 依赖 C 档信息**:协议帧未带 C 档位,后台需按现场配置查表,或由 f₀ 与档位映射反推(f₀ 落在哪档频率区间即哪档)。
|
||||
|
||||
### 9.7 工具用法速查
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
python3 variation_calc.py --freq 100 --cap 43 --variation 216 --origin 131072 # 正向
|
||||
python3 variation_calc.py --freq 100 --cap 43 --dl-rel 0.33 # 反推
|
||||
python3 variation_calc.py --parse-var F6FFFF # 3B LE 补码解析
|
||||
python3 variation_calc.py --sens-table # 四档对照
|
||||
python3 variation_calc.py --interactive # 交互
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 环境健康度算法:无车漂移监测(2026-08-25 补充)
|
||||
|
||||
> 配套工具:`tools/drift_health.py`。零成本方案——不改协议、不改固件,直接用现有 `variation` 锯齿。
|
||||
|
||||
### 10.1 思路
|
||||
|
||||
无车时 Origin 每 5s 阶跃更新(窗口均值),CAPVD 缓慢漂移 → variation 呈锯齿。**锯齿就是漂移信号**:峰谷差 = 窗口内漂移量,后台逐窗口统计即可画环境健康度曲线(湿度突变、变频器干扰加剧提前可见)。
|
||||
|
||||
### 10.2 正确口径(踩坑修正)
|
||||
|
||||
| 口径 | 关系 | 结论 |
|
||||
|------|------|------|
|
||||
| 绝对峰 \|variation\| | = **1.5×** 窗口漂移量 | ❌ 高估 50%——Origin 更新到**窗口均值**(滞后半窗 2.5s),峰含半窗累积 |
|
||||
| **峰谷差**(窗口内 max−min) | ≈ 窗口漂移量(误差 <1%) | ✅ 正确 |
|
||||
| 600ms 采样(8 点/窗) | 峰谷差**系统性低估 ~12%** | ⚠ 峰恒在窗口末(500 tick),末采样点 480 tick 永远错过它;实测 −11%~−14% |
|
||||
|
||||
**推荐公式:**
|
||||
|
||||
```
|
||||
漂移速率(%/s) = (窗口内 max − min) / Origin / 5s × 100
|
||||
```
|
||||
|
||||
- 方向保留:V1.05 带符号,正向锯齿(CAPVD 下降/频率上升)= 正速率,反向为负
|
||||
- 采样低估 ~12% 为单边已知偏差:趋势曲线看相对变化可直接接受;要绝对标定乘 1.14
|
||||
|
||||
### 10.3 失效场景:冻结态(必须双指标)
|
||||
|
||||
`dev ≥ 4×dlt_ORG` 基线冻结 → **锯齿消失**,variation 跨窗口单调累积不归零,"每窗口取峰"变成取历史累积,速率口径失效:
|
||||
|
||||
| 灵敏度档 | 冻结门槛(窗口漂移) | 对应速率 |
|
||||
|---------|-------------------|---------|
|
||||
| 0 低 | 1.32% / 5s | 0.26%/s |
|
||||
| 1 中 | 0.66% / 5s | **0.13%/s** |
|
||||
| 3 最高 | 0.06% / 5s | 0.012%/s |
|
||||
|
||||
变频器启动、湿度突变轻松超中档 0.13%/s → 冻结。**冻结本身 + 累积 variation 就是更强的恶化信号**,健康度曲线用双指标:
|
||||
|
||||
```
|
||||
① 漂移速率 = 峰谷差 / Origin / 5s ← 慢漂移场景(温/湿度),慢且稳
|
||||
② 冻结占比 = 冻结 tick / 总 tick ← 快干扰场景(变频器),触发即预警
|
||||
```
|
||||
|
||||
**冻结判定**(后台可用跨窗回落检测):正常锯齿窗口边界 variation **跳回谷值**,跳变幅度 ≈ 峰谷差;冻结时 Origin 不动,窗口边界无跳变、variation 继续同向爬升。
|
||||
|
||||
```
|
||||
jump = variation[下窗起点] − variation[本窗终点]
|
||||
正常: |jump| ≈ 峰谷差(窗口边界跳回谷值)
|
||||
冻结: |jump| ≈ 0(Origin 不动, 连续爬升)
|
||||
```
|
||||
|
||||
### 10.4 工具用法
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
python3 drift_health.py --selftest # 模拟数据自测
|
||||
python3 drift_health.py --csv drift.csv --window 500 # CSV: tick,origin,variation
|
||||
python3 drift_health.py --csv drift.csv --window 500 --json # JSON 输出
|
||||
```
|
||||
|
||||
### 10.5 与 §8"改上报斜率"的关系
|
||||
|
||||
当前零成本方案精度受上报窗限制(600ms 采样 vs 固件 10ms tick,斜率分辨率差 60 倍)。§8 的固件侧斜率字段是**远期升级**:先跑通后台算法验证指标价值,确认值得后,再由固件直接上报 dCAPVD/dt 消除采样混叠与相位滞后。
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/* mock FreeRTOS.h — gcc 隔离测试用空壳 */
|
||||
#ifndef MOCK_FREERTOS_H
|
||||
#define MOCK_FREERTOS_H
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
/* mock flash.h — gcc 隔离测试用 (storage.c 依赖的 flash API + 宏) */
|
||||
#ifndef MOCK_FLASH_H
|
||||
#define MOCK_FLASH_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define DLD_BUFEER_SIZE 100
|
||||
#define DLD_FLASH_ADDRESS_START (0x08000000 + 1024 * 63 + 512)
|
||||
#define DLD_FLASH_MAX_SIZE (0x08000000 + 1024 * 64)
|
||||
#define MAX_Store_Size DLD_BUFEER_SIZE
|
||||
|
||||
void flash_write(uint32_t addr, uint16_t *buf, uint16_t len);
|
||||
void flash_read(uint32_t addr, uint16_t *buf, uint16_t len);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,4 @@
|
||||
/* mock task.h — gcc 隔离测试用空壳 (TaskHandle_t 由 TaskLoop.h 自行定义) */
|
||||
#ifndef MOCK_TASK_H
|
||||
#define MOCK_TASK_H
|
||||
#endif
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* test_sens_mapping.c — 灵敏度映射 gcc 隔离单测 (vd960Loop)
|
||||
*
|
||||
* 验证对象: storage.c 的 sens_level_from_config() (4级制 0~3 一一对应)
|
||||
* 背景: 2026-08-25 修复 "设置不同灵敏度感应高度无区别"
|
||||
* - 根因1: 运行中配置命令未同步 loop_SensLevel (main.c unpack_pkg_set_mcjq_param)
|
||||
* - 根因2: 9级制映射 &0x03 折叠 (7→3, 8→0 错乱) → 收敛为 4级制
|
||||
* 本测试保护: 9级制后续实现时, 0~3 映射不得破坏
|
||||
*
|
||||
* 编译运行:
|
||||
* gcc -I tests/mock -I utilities/at32f421_freertos_demo/inc \
|
||||
* tests/test_sens_mapping.c -o /tmp/test_sens_mapping && /tmp/test_sens_mapping
|
||||
*/
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- 被测试对象: include storage.c 使 static 逻辑同编译单元可见 ---- */
|
||||
/* 硬件 GPIO 宏 mock (storage.c 的 set_flp_level 使用, 空实现) */
|
||||
#define FLPA1_HIGH do { } while (0)
|
||||
#define FLPA1_LOW do { } while (0)
|
||||
#define FLPA2_HIGH do { } while (0)
|
||||
#define FLPA2_LOW do { } while (0)
|
||||
#define FLPB1_HIGH do { } while (0)
|
||||
#define FLPB1_LOW do { } while (0)
|
||||
#define FLPB2_HIGH do { } while (0)
|
||||
#define FLPB2_LOW do { } while (0)
|
||||
#define FLPC1_HIGH do { } while (0)
|
||||
#define FLPC1_LOW do { } while (0)
|
||||
#define FLPC2_HIGH do { } while (0)
|
||||
#define FLPC2_LOW do { } while (0)
|
||||
#define FLPD1_HIGH do { } while (0)
|
||||
#define FLPD1_LOW do { } while (0)
|
||||
#define FLPD2_HIGH do { } while (0)
|
||||
#define FLPD2_LOW do { } while (0)
|
||||
|
||||
/* storage.c 引用的系统函数 (消除隐式声明 warning) */
|
||||
void nvic_system_reset(void);
|
||||
void delay_ms(uint32_t ms);
|
||||
|
||||
#include "../utilities/at32f421_freertos_demo/src/storage.c"
|
||||
|
||||
/* ---- 外部符号 stub ---- */
|
||||
#include "flash.h"
|
||||
void flash_write(uint32_t addr, uint16_t *buf, uint16_t len) { (void)addr; (void)buf; (void)len; }
|
||||
void flash_read(uint32_t addr, uint16_t *buf, uint16_t len) { (void)addr; (void)buf; (void)len; }
|
||||
void nvic_system_reset(void) { abort(); }
|
||||
void delay_ms(uint32_t ms) { (void)ms; }
|
||||
|
||||
const uint16_t SensTable[4] = {216, 108, 36, 10};
|
||||
const uint16_t SensTable_1[4] = {108, 72, 18, 9};
|
||||
Loop154_States g_loop_states; /* TaskLoop.c 的全局, 此处提供定义 */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* 4级制: 0~3 一一对应, 无折叠 */
|
||||
assert(sens_level_from_config(0) == 0);
|
||||
assert(sens_level_from_config(1) == 1);
|
||||
assert(sens_level_from_config(2) == 2);
|
||||
assert(sens_level_from_config(3) == 3);
|
||||
|
||||
/* 配置命令链路: pkg 低4位 & 0x0F 后传入 (main.c unpack 语义) */
|
||||
/* 上位机发 0x00~0x03 → 档位 0~3 */
|
||||
assert(sens_level_from_config(0x00 & 0x0F) == 0);
|
||||
assert(sens_level_from_config(0x01 & 0x0F) == 1);
|
||||
assert(sens_level_from_config(0x02 & 0x0F) == 2);
|
||||
assert(sens_level_from_config(0x03 & 0x0F) == 3);
|
||||
/* 兼容旧默认 7 (旧 9 级制默认): 7&0x0F=7 → &0x03=3 最高档, 行为连续 */
|
||||
assert(sens_level_from_config(0x07 & 0x0F) == 3);
|
||||
|
||||
/* 出厂默认 loops_cng_default[0]=2 → SENS=2 (高灵敏度档) */
|
||||
assert(sens_level_from_config(2) == 2);
|
||||
|
||||
/* 灵敏度表逐档单调: sens_in 递减 = 档位越高阈值越低 (越灵敏) */
|
||||
assert(SensTable[0] > SensTable[1]);
|
||||
assert(SensTable[1] > SensTable[2]);
|
||||
assert(SensTable[2] > SensTable[3]);
|
||||
|
||||
printf("PASS: sens_level_from_config 4级映射无折叠\n");
|
||||
printf(" SENS 0→%d, 1→%d, 2→%d, 3→%d\n",
|
||||
sens_level_from_config(0), sens_level_from_config(1),
|
||||
sens_level_from_config(2), sens_level_from_config(3));
|
||||
printf(" SensTable: {216,108,36,10} 单调递减 ✓\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* test_stable_settle.c — 上电自检基准判稳 gcc 隔离单测 (vd960Loop V1.03)
|
||||
*
|
||||
* 验证对象: TaskLoop.c vd1_task_per_channel() 稳定期判稳逻辑
|
||||
* (对齐 DLD154V4B V4.23, 2026-09-02 实测通过后同步)
|
||||
*
|
||||
* 语义:
|
||||
* 绿灯上电自检快闪 → 仅当"基准值真正稳定"才停止:
|
||||
* - 每窗(100样本≈1s)完成时比较窗口平均 Origin 漂移, ≤ Origin×0.1% → settle++
|
||||
* - 判稳 = 最少 128 样本 + 连续 2 窗漂移达标; 硬兜底 500 样本(≈5s)
|
||||
* - 判稳退出/init_vd_single/有限存在超时/安全复位(LC_Reset=1) 均清零计数
|
||||
* → 二次稳定期从零开始 (修掉原"残留计数 1 样本瞬间判稳")
|
||||
*
|
||||
* 编译运行:
|
||||
* gcc tests/test_stable_settle.c -o /tmp/test_stable_settle && /tmp/test_stable_settle
|
||||
*/
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- 与 TaskLoop.h / TaskLoop.c 一致的宏与结构 (隔离副本) ---- */
|
||||
#define STABLE_SAMPLES 128 // 稳定期最少样本数 (128×10ms≈1.3s)
|
||||
#define STABLE_ORIGIN_PPT 1 // 基准稳定判据: 窗口均值漂移 ≤ Origin×0.1%
|
||||
#define STABLE_SETTLE_WINDOWS 2 // 连续 2 窗(100样本/窗≈1s)漂移达标 → 判稳
|
||||
#define STABLE_MAX_SAMPLES 500 // 硬兜底 ≈5s
|
||||
|
||||
typedef struct {
|
||||
uint32_t loop_Origin;
|
||||
uint32_t loop_Value;
|
||||
uint32_t loop_CAPVD;
|
||||
uint32_t loop_ORG_SUM;
|
||||
uint16_t loop_ORG_CNT;
|
||||
uint16_t stable_cnt;
|
||||
uint16_t settle_cnt;
|
||||
uint8_t loop_stable;
|
||||
} Unit;
|
||||
|
||||
/* ---- 与 TaskLoop.c update_moving_average 同逻辑副本 ---- */
|
||||
static uint8_t update_moving_average(uint32_t *p_sum, uint16_t *p_cnt,
|
||||
uint32_t *p_origin, uint32_t new_value,
|
||||
uint16_t window)
|
||||
{
|
||||
*p_sum += new_value;
|
||||
(*p_cnt)++;
|
||||
if (*p_cnt >= window) {
|
||||
*p_origin = *p_sum / window;
|
||||
*p_cnt = 0;
|
||||
*p_sum = 0;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* init_vd_single 等价 (含 V1.03 清零) */
|
||||
static void unit_init(Unit *u, uint32_t origin)
|
||||
{
|
||||
u->loop_Origin = origin;
|
||||
u->loop_Value = 0;
|
||||
u->loop_CAPVD = 0;
|
||||
u->loop_ORG_SUM = 0;
|
||||
u->loop_ORG_CNT = 0;
|
||||
u->stable_cnt = 0;
|
||||
u->settle_cnt = 0;
|
||||
u->loop_stable = 0;
|
||||
}
|
||||
|
||||
/* vd1_task_per_channel() 稳定期分支的精确副本 (V1.03)
|
||||
* 含函数入口守卫: Origin==0 直接 return (首个测量窗确立 Origin 前不推进) */
|
||||
static void stable_step(Unit *u, uint32_t value)
|
||||
{
|
||||
uint32_t _prev_origin = u->loop_Origin;
|
||||
uint8_t _win_done;
|
||||
|
||||
if (u->loop_Origin == 0) return;
|
||||
|
||||
u->loop_CAPVD = value;
|
||||
|
||||
_win_done = update_moving_average(&u->loop_ORG_SUM, &u->loop_ORG_CNT,
|
||||
&u->loop_Origin, u->loop_CAPVD, 100);
|
||||
u->stable_cnt++;
|
||||
|
||||
if (_win_done) {
|
||||
uint32_t _drift = (u->loop_Origin > _prev_origin)
|
||||
? (u->loop_Origin - _prev_origin)
|
||||
: (_prev_origin - u->loop_Origin);
|
||||
uint32_t _band = (uint32_t)(u->loop_Origin * STABLE_ORIGIN_PPT / 1000);
|
||||
if (_drift <= _band) {
|
||||
if (u->settle_cnt < 0xFFFF) u->settle_cnt++;
|
||||
} else {
|
||||
u->settle_cnt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ((u->stable_cnt >= STABLE_SAMPLES &&
|
||||
u->settle_cnt >= STABLE_SETTLE_WINDOWS) ||
|
||||
u->stable_cnt >= STABLE_MAX_SAMPLES) {
|
||||
u->loop_stable = 1;
|
||||
u->stable_cnt = 0;
|
||||
u->settle_cnt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 打样本直至判稳, 返回判稳时的累计样本数; 上限 5000 防死循环 */
|
||||
static int run_until_stable(Unit *u, uint32_t (*gen)(Unit *u, int i))
|
||||
{
|
||||
int i;
|
||||
for (i = 1; i <= 5000; i++) {
|
||||
stable_step(u, gen(u, i));
|
||||
if (u->loop_stable) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static uint32_t gen_const(Unit *u, int i) { (void)u; (void)i; return 100000UL; }
|
||||
static uint32_t gen_spike(Unit *u, int i) { (void)u; return (i == 50) ? 200000UL : 100000UL; }
|
||||
static uint32_t gen_drift(Unit *u, int i) { (void)u; return 100000UL + 2UL * (uint32_t)i; }
|
||||
|
||||
static int expect_stable(int n, int exp, const char *tag)
|
||||
{
|
||||
printf("%-24s 判稳样本=%d (期望 %d)%s\n", tag, n, exp,
|
||||
(n == exp) ? "" : " <-- MISMATCH");
|
||||
return n == exp;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
Unit u;
|
||||
int n, fail = 0;
|
||||
|
||||
/* 1. 常量基准: 窗1@100 settle=1, 窗2@200 settle=2 → 200 判稳 */
|
||||
unit_init(&u, 100000UL);
|
||||
n = run_until_stable(&u, gen_const);
|
||||
fail += !expect_stable(n, 200, "1. 常量值");
|
||||
|
||||
/* 2. 首窗尖峰(第50样本 200000): 窗1均值101000 漂移>0.1% 清零;
|
||||
* 窗2均值100000 与101000差1000 仍>band → 清零; 窗3/窗4 漂移0
|
||||
* settle=1@300, settle=2@400 → 400 判稳 (扰动后需2个干净窗口) */
|
||||
unit_init(&u, 100000UL);
|
||||
n = run_until_stable(&u, gen_spike);
|
||||
fail += !expect_stable(n, 400, "2. 首窗尖峰");
|
||||
|
||||
/* 3. 持续漂移(每样本+2): 每窗漂移200 > band(≈100) → 永远不达标,
|
||||
* 500 样本硬兜底强制判稳 */
|
||||
unit_init(&u, 100000UL);
|
||||
n = run_until_stable(&u, gen_drift);
|
||||
fail += !expect_stable(n, 500, "3. 持续漂移(硬兜底)");
|
||||
|
||||
/* 4. 二次稳定期防呆: 判稳后模拟 有限存在/安全复位 重学路径
|
||||
* (loop_stable=0 + 计数清零, 即 C2/C3 新增的两处清零),
|
||||
* 常量基准须重新满 200 样本判稳, 绝非 1 样本瞬间判稳 */
|
||||
unit_init(&u, 100000UL);
|
||||
n = run_until_stable(&u, gen_const);
|
||||
if (n == 200) {
|
||||
u.loop_stable = 0; /* 重学复位 (同 LC_Reset=1 路径) */
|
||||
u.stable_cnt = 0;
|
||||
u.settle_cnt = 0;
|
||||
u.loop_ORG_CNT = 0;
|
||||
u.loop_ORG_SUM = 0;
|
||||
n = run_until_stable(&u, gen_const);
|
||||
fail += !expect_stable(n, 200, "4. 二次稳定期(重学复位)");
|
||||
} else {
|
||||
printf("4. 二次稳定期 前置失败 (首轮 n=%d)\n", n);
|
||||
fail++;
|
||||
}
|
||||
|
||||
/* 5. Origin==0 边界 (vd1 入口 return, 稳定期不应推进) */
|
||||
unit_init(&u, 0UL);
|
||||
stable_step(&u, 100000UL);
|
||||
if (u.stable_cnt != 0 || u.settle_cnt != 0) {
|
||||
printf("5. Origin=0 守卫 FAIL (stable_cnt=%u settle_cnt=%u)\n",
|
||||
u.stable_cnt, u.settle_cnt);
|
||||
fail++;
|
||||
} else {
|
||||
printf("%-24s OK (不推进自检计数)\n", "5. Origin=0 守卫");
|
||||
}
|
||||
|
||||
if (fail == 0) {
|
||||
printf("ALL PASS (test_stable_settle)\n");
|
||||
return 0;
|
||||
}
|
||||
printf("FAILED (%d)\n", fail);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
drift_health.py — vd960Loop 无车漂移环境健康度分析(零成本)
|
||||
|
||||
原理(docs/variation-analysis.md §10):
|
||||
无车时 Origin 每 5s 阶跃更新(窗口均值)→ variation 呈锯齿
|
||||
锯齿峰谷差 = 窗口内漂移量 (绝对峰 = 1.5×窗口漂移, Origin 滞后半窗)
|
||||
漂移速率(%/s) = 峰谷差 / Origin / 窗口秒数 × 100
|
||||
|
||||
输入: CSV (tick,origin,variation 三列) 或 --selftest 模拟数据
|
||||
输出: 逐窗口统计 + 汇总(平均速率/方向/冻结占比)
|
||||
|
||||
注意:
|
||||
- 600ms 上报采样会使峰谷差系统性低估 ~12%(峰恒在窗口末, 末采样点错过)
|
||||
- 漂移 > 4×dlt_ORG 时基线冻结, 锯齿消失 → 冻结占比是第二指标
|
||||
- 冻结判定: 跨窗回落检测 (正常窗口边界回落≈峰谷差; 冻结继续同向爬升)
|
||||
|
||||
用法:
|
||||
python3 drift_health.py --selftest
|
||||
python3 drift_health.py --csv drift.csv --window 500
|
||||
python3 drift_health.py --csv drift.csv --window 500 --json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
|
||||
WINDOW_DEFAULT = 500 # tick, 5s @10ms
|
||||
TICK_S_DEFAULT = 0.01 # 10ms
|
||||
|
||||
|
||||
def analyze(variation, origins, window, tick_s):
|
||||
"""逐窗口统计。返回 (窗口列表, 汇总字典)"""
|
||||
import statistics
|
||||
n = len(variation)
|
||||
nw = n // window
|
||||
rows = []
|
||||
pkpk_hist = []
|
||||
for k in range(nw):
|
||||
a = k * window
|
||||
b = a + window
|
||||
seg = variation[a:b]
|
||||
oseg = origins[a:b]
|
||||
vmax = max(seg)
|
||||
vmin = min(seg)
|
||||
pkpk = vmax - vmin # 峰谷差 = 窗口漂移量
|
||||
origin = sum(oseg) / len(oseg)
|
||||
# 跨窗回落: jump = 下窗起点 - 本窗终点
|
||||
jump = None
|
||||
if k + 1 < nw:
|
||||
jump = variation[b] - variation[b - 1]
|
||||
# 冻结判定 (双判据):
|
||||
# ① 跨窗跳变消失: 正常 |jump| ≈ 峰谷差, 冻结 |jump| ≈ 0
|
||||
# ② 峰谷差突变: 当前窗 pkpk > 4× 前 4 窗中位数 (冻结=漂移持续累积)
|
||||
# 判据②让最后一窗(无 jump)也能判冻结, 实时滚动同样适用
|
||||
baseline = statistics.median(pkpk_hist[-4:]) if pkpk_hist else 0.0
|
||||
frozen = False
|
||||
if pkpk > 0:
|
||||
if jump is not None and abs(jump) < 0.3 * pkpk:
|
||||
frozen = True
|
||||
if baseline > 0 and pkpk > 4 * baseline:
|
||||
frozen = True
|
||||
pkpk_hist.append(pkpk)
|
||||
# 速率 %/s (按峰谷差)
|
||||
rate = pkpk / origin / (window * tick_s) * 100.0 if origin > 0 else 0.0
|
||||
rows.append({
|
||||
"window": k,
|
||||
"max": round(vmax, 4),
|
||||
"min": round(vmin, 4),
|
||||
"pkpk": round(pkpk, 4),
|
||||
"origin": round(origin, 1),
|
||||
"rate_pct_per_s": round(rate, 4),
|
||||
"jump": round(jump, 4) if jump is not None else None,
|
||||
"frozen": frozen,
|
||||
})
|
||||
if not rows:
|
||||
return [], {}
|
||||
rates = [r["rate_pct_per_s"] for r in rows if not r["frozen"]]
|
||||
frozen_cnt = sum(1 for r in rows if r["frozen"])
|
||||
summary = {
|
||||
"windows": len(rows),
|
||||
"frozen_ratio_pct": round(frozen_cnt / len(rows) * 100, 1),
|
||||
"avg_rate_pct_per_s": round(sum(rates) / len(rates), 4) if rates else None,
|
||||
"max_rate_pct_per_s": round(max(rates), 4) if rates else None,
|
||||
"last_direction": "正(频率上升/CAPVD下降)" if rows[-1]["pkpk"] > 0 and rows[-1]["max"] >= abs(rows[-1]["min"]) else "负(频率下降/CAPVD上升)",
|
||||
}
|
||||
return rows, summary
|
||||
|
||||
|
||||
def gen_selftest(window=WINDOW_DEFAULT, tick_s=TICK_S_DEFAULT):
|
||||
"""生成模拟数据: 前 6 窗慢漂移(0.02%/s, 正常锯齿), 后 2 窗快漂移(0.5%/s, 冻结)
|
||||
归一化坐标: Origin≈1.0, 漂移为相对比例"""
|
||||
r_slow = 2e-6 # 0.02%/s → 窗口漂移 0.1%
|
||||
r_fast = 5e-5 # 0.5%/s → 窗口漂移 2.5%, 远超 4×dlt_ORG → 冻结
|
||||
n = 8 * window
|
||||
capvd = []
|
||||
for i in range(n):
|
||||
if i < 6 * window:
|
||||
capvd.append(1.0 + i * r_slow)
|
||||
else:
|
||||
capvd.append(1.0 + 6 * window * r_slow + (i - 6 * window) * r_fast)
|
||||
# Origin: 窗口 k 期间 = 窗口 k-1 均值 (k>=1); 冻结窗(6,7) 锁在窗口 5 均值
|
||||
origin = [0.0] * n
|
||||
base6 = sum(capvd[5 * window:6 * window]) / window
|
||||
for i in range(n):
|
||||
k = i // window
|
||||
if k == 0:
|
||||
origin[i] = capvd[0]
|
||||
elif k < 6:
|
||||
origin[i] = sum(capvd[(k - 1) * window:k * window]) / window
|
||||
else:
|
||||
origin[i] = base6
|
||||
variation = [origin[i] - capvd[i] for i in range(n)]
|
||||
return variation, origin
|
||||
|
||||
|
||||
def fmt_table(rows, summary):
|
||||
lines = []
|
||||
lines.append(f"{'窗口':<5}{'max':>10}{'min':>10}{'峰谷差':>10}{'Origin':>12}{'速率%/s':>10}{'跨窗jump':>10} 状态")
|
||||
for r in rows:
|
||||
state = "冻结" if r["frozen"] else "正常"
|
||||
jump = f"{r['jump']:+.4f}" if r["jump"] is not None else " -"
|
||||
lines.append(f"{r['window']:<5}{r['max']:>10.4f}{r['min']:>10.4f}{r['pkpk']:>10.4f}"
|
||||
f"{r['origin']:>12.1f}{r['rate_pct_per_s']:>10.4f}{jump:>10} {state}")
|
||||
lines.append("-" * 66)
|
||||
lines.append(f"平均速率: {summary['avg_rate_pct_per_s']} %/s "
|
||||
f"最大速率: {summary['max_rate_pct_per_s']} %/s "
|
||||
f"冻结占比: {summary['frozen_ratio_pct']}% "
|
||||
f"末窗方向: {summary['last_direction']}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(
|
||||
description="vd960Loop 无车漂移环境健康度分析(零成本, 用现有 variation 锯齿)",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
|
||||
p.add_argument("--csv", help="CSV 文件, 表头含 tick,origin,variation")
|
||||
p.add_argument("--window", type=int, default=WINDOW_DEFAULT, help=f"窗口 tick (默认 {WINDOW_DEFAULT}=5s)")
|
||||
p.add_argument("--tick-s", type=float, default=TICK_S_DEFAULT, help=f"tick 秒数 (默认 {TICK_S_DEFAULT}=10ms)")
|
||||
p.add_argument("--json", action="store_true", help="JSON 输出")
|
||||
p.add_argument("--selftest", action="store_true", help="模拟数据自测")
|
||||
args = p.parse_args()
|
||||
|
||||
if args.selftest:
|
||||
variation, origin = gen_selftest(args.window, args.tick_s)
|
||||
rows, summary = analyze(variation, origin, args.window, args.tick_s)
|
||||
if args.json:
|
||||
print(json.dumps({"rows": rows, "summary": summary}, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print("模拟: 前 6 窗慢漂移 r=0.0002/tick (0.1%/窗 → 0.02%/s), 后 2 窗快漂移 r=0.002 (冻结)")
|
||||
print(fmt_table(rows, summary))
|
||||
return
|
||||
|
||||
if not args.csv:
|
||||
p.error("需要 --csv 或 --selftest")
|
||||
|
||||
variations, origins = [], []
|
||||
with open(args.csv, newline="", encoding="utf-8") as f:
|
||||
for row in csv.DictReader(f):
|
||||
variations.append(float(row["variation"]))
|
||||
origins.append(float(row["origin"]))
|
||||
if len(variations) < args.window:
|
||||
p.error(f"数据不足一个窗口 ({len(variations)} < {args.window})")
|
||||
|
||||
rows, summary = analyze(variations, origins, args.window, args.tick_s)
|
||||
if args.json:
|
||||
print(json.dumps({"rows": rows, "summary": summary}, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(fmt_table(rows, summary))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
variation_calc.py — vd960Loop variation ↔ ΔL / ΔL-L 换算工具
|
||||
|
||||
物理关系(推导见 docs/variation-analysis.md §9):
|
||||
CAPVD ∝ T = 2π·√(LC) (周期域, 归一化到 MEASUREMENT_BASE≈131072)
|
||||
variation = Origin − CAPVD (V1.05 起 3B 有符号补码, 正=金属进入方向)
|
||||
variation/Origin = 1 − √(1 + ΔL/L0) (精确)
|
||||
ΔL/L0 ≈ −2 × variation/Origin (一阶, |ΔL/L| ≪ 1 时, 残差 <0.2%)
|
||||
Δf/f ≈ variation/Origin (频域对应: CAPVD ∝ 1/f)
|
||||
L0 = 1/(4π²·f0²·C) (绝对电感, f0=上报频率, C=档位电容)
|
||||
|
||||
电容档位无关性: Origin 与 CAPVD 均 ∝ √C, variation/Origin 中 C 消掉,
|
||||
四档电容(33/43/66/76nF)共用同一套 ΔL/L 换算表。
|
||||
|
||||
用法示例:
|
||||
# 正向: 频率+电容+variation+origin → L0, ΔL, ΔL/L
|
||||
python3 variation_calc.py --freq 100 --cap 43 --variation 216 --origin 131072
|
||||
# 反推: 给定 ΔL/L 看需要多大 variation
|
||||
python3 variation_calc.py --freq 100 --cap 43 --dl-rel 0.33
|
||||
# 解析协议 3B LE 有符号补码 (V1.05 variation 字段)
|
||||
python3 variation_calc.py --parse-var F6FFFF
|
||||
# 灵敏度档位对照表
|
||||
python3 variation_calc.py --sens-table
|
||||
# 交互模式
|
||||
python3 variation_calc.py --interactive
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import sys
|
||||
|
||||
# ---------------------------------------------------------------- 常量
|
||||
CAP_OPTIONS = (33, 43, 66, 76) # 线圈内部电容四档 (nF)
|
||||
ORIGIN_DEFAULT = 131072 # MEASUREMENT_BASE = 2^17
|
||||
SENS_TABLE = {0: (216, 108), 1: (108, 72), 2: (36, 18), 3: (10, 9)}
|
||||
SENS_TABLE_9 = { # 9级制设计稿 (0=最低, 8=最高, 0.01%~5.00% ΔL/L)
|
||||
0: (1638, 819), 1: (745, 373), 2: (339, 170), 3: (154, 100), 4: (70, 46),
|
||||
5: (32, 21), 6: (15, 14), 7: (7, 6), 8: (3, 3),
|
||||
}
|
||||
SENS_NAMES = {0: "0(低)", 1: "1(中)", 2: "2(高)", 3: "3(最高)"}
|
||||
SENS_NAMES_9 = {0: "0(最低)", 1: "1", 2: "2", 3: "3", 4: "4",
|
||||
5: "5", 6: "6", 7: "7", 8: "8(最高)"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 核心计算
|
||||
def inductance(freq_hz, cap_nf):
|
||||
"""绝对电感 L0 = 1/(4π²·f²·C), 返回 μH"""
|
||||
c = cap_nf * 1e-9
|
||||
return 1.0 / (4.0 * math.pi * math.pi * freq_hz * freq_hz * c) * 1e6
|
||||
|
||||
|
||||
def variation_ratio(variation, origin):
|
||||
"""variation/Origin (有符号)"""
|
||||
return variation / origin
|
||||
|
||||
|
||||
def delta_ll(vr):
|
||||
"""ΔL/L: (精确, 一阶), 返回相对值(负=电感减小)"""
|
||||
exact = vr * vr - 2.0 * vr
|
||||
approx = -2.0 * vr
|
||||
return exact, approx
|
||||
|
||||
|
||||
def variation_from_dl_rel(dl_rel_pct, origin):
|
||||
"""反推: 给定 |ΔL/L|(%) → 需要的 variation (计数域)"""
|
||||
r = dl_rel_pct / 100.0 # ΔL/L = −r
|
||||
vr = 1.0 - math.sqrt(1.0 - r) # 精确
|
||||
return vr * origin, vr
|
||||
|
||||
|
||||
def sens_hit(vr):
|
||||
"""当前 variation/Origin 落在哪档进入阈值 (SensTable/65536)"""
|
||||
for sens in (0, 1, 2, 3):
|
||||
thr = SENS_TABLE[sens][0] / 65536.0
|
||||
if abs(vr) >= thr:
|
||||
return sens, thr
|
||||
return None, None
|
||||
|
||||
|
||||
def parse_var_3byte(hex_str):
|
||||
"""V1.05 协议 variation 字段: 3B LE 有符号补码 → int32
|
||||
F6FF FF → −10; 000001 → 65536
|
||||
"""
|
||||
h = hex_str.strip()
|
||||
if len(h) != 6:
|
||||
raise ValueError("需要 6 位 hex (3 字节 LE), 如 F6FFFF")
|
||||
b0 = int(h[0:2], 16)
|
||||
b1 = int(h[2:4], 16)
|
||||
b2 = int(h[4:6], 16)
|
||||
v = b0 | (b1 << 8) | (b2 << 16)
|
||||
if v & 0x800000:
|
||||
v |= 0xFF000000
|
||||
if v >= 0x80000000:
|
||||
v -= 0x100000000
|
||||
return v
|
||||
|
||||
|
||||
def fmt_sens_table(sens_count=4):
|
||||
"""打印灵敏度档位对照表 (4级或9级)"""
|
||||
table = SENS_TABLE if sens_count == 4 else SENS_TABLE_9
|
||||
names = SENS_NAMES if sens_count == 4 else SENS_NAMES_9
|
||||
lines = [f"灵敏度档位 → ΔL/L 触发阈值 ({sens_count}级制, 一阶 ΔL/L≈−2×vr):"]
|
||||
lines.append(f"{'SENS':<10}{'进入表':<8}{'离开表':<8}{'Δf/f进入':<12}{'ΔL/L进入':<12}{'ΔL/L离开'}")
|
||||
for sens in sorted(table):
|
||||
sin, sout = table[sens]
|
||||
vr_in = sin / 65536.0
|
||||
vr_out = sout / 65536.0
|
||||
lines.append(f"{names[sens]:<10}{sin:<8}{sout:<8}"
|
||||
f"{vr_in * 100:>7.4f}%{'':<4}{-2 * vr_in * 100:>8.4f}%{'':<4}{-2 * vr_out * 100:>8.4f}%")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def forward(freq, cap, variation, origin):
|
||||
"""正向计算并打印"""
|
||||
f_hz = freq * 1000.0 if freq < 1e6 else freq
|
||||
l0 = inductance(f_hz, cap)
|
||||
vr = variation_ratio(variation, origin)
|
||||
exact, approx = delta_ll(vr)
|
||||
dl = l0 * exact
|
||||
|
||||
sens, thr = sens_hit(vr)
|
||||
|
||||
out = []
|
||||
out.append("=" * 62)
|
||||
out.append("正向换算: 频率+电容+variation+Origin")
|
||||
out.append("=" * 62)
|
||||
out.append(f" 上报频率 f0 : {f_hz / 1000.0:>10.3f} kHz")
|
||||
out.append(f" 电容档 C : {cap:>10d} nF")
|
||||
out.append(f" 绝对电感 L0 : {l0:>10.3f} μH (L=1/(4π²f²C))")
|
||||
out.append(f" Origin : {origin:>10d} (CAPVD 基线)")
|
||||
out.append(f" variation : {variation:>10d} (3B 有符号)")
|
||||
out.append(f" variation/Origin : {vr * 100:>9.4f} % (≈ Δf/f)")
|
||||
out.append("-" * 62)
|
||||
out.append(f" ΔL/L 一阶 : {-2 * vr * 100:>9.4f} % (≈ −2×vr)")
|
||||
out.append(f" ΔL/L 精确 : {exact * 100:>9.4f} % (vr²−2vr)")
|
||||
out.append(f" ΔL (绝对) : {dl:>9.4f} μH")
|
||||
out.append("-" * 62)
|
||||
if sens is not None:
|
||||
out.append(f" ≥ SENS={SENS_NAMES[sens]} 进入阈值 (vr_thr={thr * 100:.4f}%)")
|
||||
else:
|
||||
out.append(f" 低于全部进入阈值 (最小档 SENS=3: {10 / 65536 * 100:.4f}%)")
|
||||
out.append("=" * 62)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def reverse(freq, cap, dl_rel_pct, origin):
|
||||
"""反推: 给定 |ΔL/L|% → variation"""
|
||||
f_hz = freq * 1000.0 if freq < 1e6 else freq
|
||||
l0 = inductance(f_hz, cap)
|
||||
variation, vr = variation_from_dl_rel(dl_rel_pct, origin)
|
||||
|
||||
out = []
|
||||
out.append("=" * 62)
|
||||
out.append(f"反推: ΔL/L = −{dl_rel_pct}% 需要的 variation")
|
||||
out.append("=" * 62)
|
||||
out.append(f" L0 = {l0:.3f} μH (f0={f_hz / 1000:.3f}kHz, C={cap}nF)")
|
||||
out.append(f" ΔL = {l0 * (-dl_rel_pct / 100.0):.4f} μH")
|
||||
out.append(f" variation/Origin = {vr * 100:.4f} %")
|
||||
out.append(f" variation = {variation:.0f} (Origin={origin})")
|
||||
out.append(f" 一阶估算 = {origin * dl_rel_pct / 200.0:.0f} (vr≈r/2)")
|
||||
out.append("=" * 62)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def interactive():
|
||||
print("vd960Loop variation 换算工具 — 交互模式 (Ctrl+C 退出)")
|
||||
cap = int(input("电容档 (33/43/66/76 nF): "))
|
||||
if cap not in CAP_OPTIONS:
|
||||
print(f"警告: {cap}nF 不在标准档位 {CAP_OPTIONS} 中")
|
||||
freq = float(input("上报频率 (kHz, 如 100): "))
|
||||
origin = int(input(f"Origin (默认 {ORIGIN_DEFAULT}): ") or ORIGIN_DEFAULT)
|
||||
while True:
|
||||
try:
|
||||
variation = int(input("variation (有符号, q 退出): "))
|
||||
except ValueError:
|
||||
break
|
||||
print()
|
||||
print(forward(freq, cap, variation, origin))
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(
|
||||
description="vd960Loop variation ↔ ΔL/ΔL-L 换算工具",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__)
|
||||
p.add_argument("--freq", type=float, help="上报频率 (默认 kHz, <1e6 视为 kHz)")
|
||||
p.add_argument("--cap", type=int, choices=CAP_OPTIONS,
|
||||
help="电容档位 nF: 33/43/66/76")
|
||||
p.add_argument("--variation", type=int, help="variation (3B 有符号, 计数域)")
|
||||
p.add_argument("--origin", type=int, default=ORIGIN_DEFAULT,
|
||||
help=f"Origin 基线 (默认 {ORIGIN_DEFAULT})")
|
||||
p.add_argument("--dl-rel", type=float,
|
||||
help="反推模式: 给定 |ΔL/L|%% → 所需 variation")
|
||||
p.add_argument("--parse-var", metavar="HEX6",
|
||||
help="解析 V1.05 协议 3B LE 有符号补码 (如 F6FFFF = −10)")
|
||||
p.add_argument("--sens-table", nargs="?", const="4", type=int, choices=[4, 9],
|
||||
help="打印灵敏度档位对照表 (4=4级制默认, 9=9级制设计稿)")
|
||||
p.add_argument("--interactive", action="store_true", help="交互模式")
|
||||
args = p.parse_args()
|
||||
|
||||
if args.sens_table:
|
||||
print(fmt_sens_table(args.sens_table))
|
||||
return
|
||||
|
||||
if args.parse_var:
|
||||
v = parse_var_3byte(args.parse_var)
|
||||
print(f"3B LE 补码 {args.parse_var.upper()} → {v}")
|
||||
return
|
||||
|
||||
if args.interactive:
|
||||
interactive()
|
||||
return
|
||||
|
||||
if args.dl_rel is not None:
|
||||
if not args.freq or not args.cap:
|
||||
p.error("反推模式需要 --freq 和 --cap")
|
||||
print(reverse(args.freq, args.cap, args.dl_rel, args.origin))
|
||||
return
|
||||
|
||||
if None in (args.freq, args.cap, args.variation):
|
||||
p.error("需要 --freq --cap --variation (或 --dl-rel / --sens-table / --interactive)")
|
||||
print(forward(args.freq, args.cap, args.variation, args.origin))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -33,7 +33,10 @@
|
||||
#define ALFA_CAP1 79 // IIR α = 79/256 ≈ 0.31 (@10ms → τ≈32ms)
|
||||
#define MAX_SLOPE_RATE 5 // 斜率限幅: 单次最大变化 5%
|
||||
#define ENTRY_CONFIRM 3 // 进入确认: 连续 N 次低于阈值
|
||||
#define STABLE_SAMPLES 128 // 稳定期样本数
|
||||
#define STABLE_SAMPLES 128 // 稳定期最少样本数 (128×10ms≈1.3s)
|
||||
#define STABLE_ORIGIN_PPT 1 // 基准稳定判据: 窗口均值漂移 ≤ Origin×0.1%
|
||||
#define STABLE_SETTLE_WINDOWS 2 // 连续 2 窗(100样本/窗≈1s)漂移达标 → 判稳
|
||||
#define STABLE_MAX_SAMPLES 500 // 硬兜底 ≈5s (防环境长期不稳绿灯无限自检)
|
||||
|
||||
/*===========================================================================
|
||||
* 基线冻结超时 (V2.3~V2.5)
|
||||
@@ -41,6 +44,15 @@
|
||||
#define FREEZE_TIMEOUT 1000 // 冻结超时: ~10s @ 10ms/tick
|
||||
#define FREEZE_STABILITY_RATE 2 // 稳定性窗口: 参考值的 ±2%
|
||||
|
||||
/*===========================================================================
|
||||
* 看门狗 (IWDT) - 对齐 DLD154V4B V2.10
|
||||
* 双保险模式: TMR15 ISR 5ms 递增 g_wdg_counter, 主循环 poll_wdg() 每 500ms 喂狗
|
||||
* 超时 = WDT_DIV 枚举分频 x (WDT_RLD+1) / LSI(约40kHz) 约 1.64s
|
||||
*===========================================================================*/
|
||||
#define WDT_COUNTER_LIMIT 100 // 喂狗周期 500ms (TMR15 5ms x 100)
|
||||
#define WDT_DIV 2 // IWDT 分频枚举值: 2 = WDT_CLK_DIV_16 (实际分频 16; 勿传 16!)
|
||||
#define WDT_RLD 4095 // 12bit 重装载 -> 超时约 1.64s
|
||||
|
||||
/*===========================================================================
|
||||
* 离开检测: 1 = 平坦性三条件 (CN200910309382), 0 = cnt_release 防抖
|
||||
*===========================================================================*/
|
||||
@@ -173,7 +185,8 @@ typedef struct {
|
||||
uint16_t fault_tick;
|
||||
|
||||
/*--- 稳定期 ---*/
|
||||
uint16_t stable_cnt;
|
||||
uint16_t stable_cnt; // 稳定期累计样本 (每次 CAP_OK +1)
|
||||
uint16_t settle_cnt; // 基准稳定连续窗口计数 (窗=100样本≈1s, 漂移≤0.1%则+1)
|
||||
|
||||
/*--- 调试 ---*/
|
||||
uint32_t xn_counter;
|
||||
@@ -228,7 +241,8 @@ uint8_t update_moving_average(uint32_t* p_sum, uint16_t* p_cnt,
|
||||
void set_loops_relay_on(uint8_t loop_num);
|
||||
void set_loops_relay_off(uint8_t loop_num);
|
||||
|
||||
void wdt_feed(void);
|
||||
void poll_wdg(void);
|
||||
extern volatile uint16_t g_wdg_counter; // 看门狗活性计数 (TMR15 5ms 递增)
|
||||
|
||||
extern TaskHandle_t usart_task_handler;
|
||||
|
||||
|
||||
@@ -16,15 +16,20 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define PRODUCT_MODEL "DLD960"
|
||||
#define FIRMWARE_VER "1.00"
|
||||
#define FIRMWARE_VER "1.03"
|
||||
#define HARDWARE_VER "1.00"
|
||||
#define FIRMWARE_VER_MAIN 0
|
||||
#define FIRMWARE_VER_SUB 30
|
||||
#define FIRMWARE_VER_SSUB 0
|
||||
#define FIRMWARE_VER_MAIN 1
|
||||
#define FIRMWARE_VER_SUB 3
|
||||
#define FIRMWARE_VER_SSUB 1
|
||||
#define HARDWARE_VER_MAIN 1
|
||||
#define HARDWARE_VER_SUB 0
|
||||
#define HARDWARE_VER_SSUB 0
|
||||
|
||||
// LOOP_MEASURE_BASE: variation 归一化基数 (协议 0x64 响应尾部 3B LE 上报)
|
||||
// ⚠ 必须与 TaskLoop.h 的 MEASUREMENT_BASE 同步 (AT32F421 @120MHz = 2^17 = 131072)
|
||||
// 第三方评估换算: Δf/f ≈ variation/LOOP_MEASURE_BASE; ΔL/L ≈ -2×variation/LOOP_MEASURE_BASE
|
||||
#define LOOP_MEASURE_BASE 131072L
|
||||
|
||||
|
||||
#define FUNCTION_A 0x01 // 二次判断
|
||||
#define FUNCTION_B 0x02 // 二次判断增强条件判断
|
||||
|
||||
@@ -111,7 +111,7 @@ typedef enum
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint8_t sensitvity; // 高四位表示灵敏度的数量,低四位表示当前灵敏度的序号(从0开始)
|
||||
uint8_t sensitvity; // 灵敏度序号: 4级制 0~3 (值越大越灵敏, 默认2); 9级制后续版本实现
|
||||
uint8_t delay_time;
|
||||
uint8_t output_mode;
|
||||
uint8_t direction_mode; // 方向判别模式,0 触发模式, 非0 表示方向判别模式
|
||||
@@ -171,6 +171,9 @@ typedef struct _DBN_BLE_STATE_
|
||||
|
||||
void set_flp_level(uint8_t loop_num, uint8_t freq_level);
|
||||
|
||||
/* 灵敏度配置 → 检测档位映射 (4级制: 0~3 一一对应; 9级制后续实现) */
|
||||
uint8_t sens_level_from_config(uint8_t sensitvity);
|
||||
|
||||
void set_factory_param(void);
|
||||
void storage_dev(void);
|
||||
void para_store_init(void);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "task.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "storage.h"
|
||||
|
||||
/*===========================================================================
|
||||
* 灵敏度表 — 对齐 DLD154V4B / M1H
|
||||
@@ -23,6 +24,7 @@ Loop154_States g_loop_states = {0};
|
||||
uint32_t g_sys_freq = 0;
|
||||
uint8_t g_input_div = 1;
|
||||
uint32_t g_safe_max_cnt = LC_HOLD_TIME;
|
||||
volatile uint16_t g_wdg_counter = 0; // 看门狗活性计数 (TMR15 ISR 5ms 递增)
|
||||
|
||||
/*===========================================================================
|
||||
* 一阶 IIR 低通滤波器(对齐 DLD154V4B)
|
||||
@@ -107,7 +109,8 @@ void init_vd_single(Loop154_Unit *unit)
|
||||
unit->loop_CAP_OK = 0;
|
||||
unit->loop_CAPVD = 0;
|
||||
|
||||
unit->loop_SensLevel = 2; // 默认中灵敏度
|
||||
unit->loop_SensLevel = sens_level_from_config(
|
||||
g_loop_cng_info.loop_cng[unit->loop_num].sensitvity); // 从配置读, 不再硬编码 2 (修复: INIT_VDs 覆盖配置值)
|
||||
unit->Flt_Reg = ALFA_CAP1;
|
||||
|
||||
unit->loop_entry_cnt = 0;
|
||||
@@ -151,6 +154,7 @@ void init_vd_single(Loop154_Unit *unit)
|
||||
unit->SET_SAFE = 1;
|
||||
|
||||
unit->stable_cnt = 0;
|
||||
unit->settle_cnt = 0; // V1.03: 自检计数从零开始
|
||||
unit->xn_counter = 0;
|
||||
}
|
||||
|
||||
@@ -250,6 +254,9 @@ void TMR15_GLOBAL_IRQHandler(void)
|
||||
|
||||
if (tmr_interrupt_flag_get(TMR15, TMR_OVF_FLAG) != RESET) {
|
||||
|
||||
/*--- 看门狗活性计数 (5ms) ---*/
|
||||
g_wdg_counter++;
|
||||
|
||||
/*--- 50ms tick 分频 ---*/
|
||||
TM1cnt++;
|
||||
if (TM1cnt >= 10) {
|
||||
@@ -312,6 +319,8 @@ void TMR15_GLOBAL_IRQHandler(void)
|
||||
unit->loop_INI_LOOP = 1;
|
||||
unit->loop_LOOP_OK0 = 0;
|
||||
unit->loop_stable = 0;
|
||||
unit->stable_cnt = 0; // V1.03: 重学自检计数从零开始
|
||||
unit->settle_cnt = 0;
|
||||
unit->loop_ORG_CNT = 0;
|
||||
unit->loop_ORG_SUM = 0;
|
||||
unit->last_exit_tick = g_loop_states.misc_counter;
|
||||
@@ -331,6 +340,8 @@ void TMR15_GLOBAL_IRQHandler(void)
|
||||
unit->loop_INI_LOOP = 1;
|
||||
unit->loop_LOOP_OK0 = 0;
|
||||
unit->loop_stable = 0;
|
||||
unit->stable_cnt = 0; // V1.03: 二次稳定期从零开始
|
||||
unit->settle_cnt = 0;
|
||||
#if USE_FLATNESS_EXIT
|
||||
unit->exit_state = 0;
|
||||
unit->max_slope = 0;
|
||||
@@ -453,15 +464,40 @@ void vd1_task_per_channel(Loop154_Unit *unit)
|
||||
unit->loop_CAPVD = get_flt_value(clamped_value, unit->loop_CAPVD);
|
||||
}
|
||||
|
||||
/*--- 2. 稳定期:绕过 IIR 和斜率限幅,小窗口快速收敛 ---*/
|
||||
/*--- 2. 稳定期:绕过 IIR 和斜率限幅,小窗口快速收敛;
|
||||
* 基准值稳定(连续窗口 Origin 漂移达标)后才结束自检 (V1.03, 对齐V4B V4.23) ---*/
|
||||
if (!unit->loop_stable) {
|
||||
uint32_t _prev_origin = unit->loop_Origin;
|
||||
uint8_t _win_done;
|
||||
|
||||
unit->loop_CAPVD = unit->loop_Value;
|
||||
|
||||
update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
|
||||
&unit->loop_Origin, unit->loop_CAPVD, 100);
|
||||
_win_done = update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
|
||||
&unit->loop_Origin, unit->loop_CAPVD, 100);
|
||||
unit->stable_cnt++;
|
||||
if (unit->stable_cnt >= STABLE_SAMPLES) {
|
||||
|
||||
/* 每窗(100样本≈1s)完成时比较 Origin 均值漂移;
|
||||
* 漂移 ≤ Origin×STABLE_ORIGIN_PPT/1000 → settle 计数+1, 否则清零
|
||||
* (首个窗口常与捕获首窗 Origin 偏差大, 自然多等一窗再判稳) */
|
||||
if (_win_done) {
|
||||
uint32_t _drift = (unit->loop_Origin > _prev_origin)
|
||||
? (unit->loop_Origin - _prev_origin)
|
||||
: (_prev_origin - unit->loop_Origin);
|
||||
uint32_t _band = (uint32_t)(unit->loop_Origin * STABLE_ORIGIN_PPT / 1000);
|
||||
if (_drift <= _band) {
|
||||
if (unit->settle_cnt < 0xFFFF) unit->settle_cnt++;
|
||||
} else {
|
||||
unit->settle_cnt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 判稳条件: 最少样本 + 连续 2 窗基准稳定; 硬兜底 STABLE_MAX_SAMPLES */
|
||||
if ((unit->stable_cnt >= STABLE_SAMPLES &&
|
||||
unit->settle_cnt >= STABLE_SETTLE_WINDOWS) ||
|
||||
unit->stable_cnt >= STABLE_MAX_SAMPLES) {
|
||||
unit->loop_stable = 1;
|
||||
unit->stable_cnt = 0; // 计数清零: 重连/安全复位二次稳定期从零开始
|
||||
unit->settle_cnt = 0;
|
||||
PRINT("Loop%d stable, Origin:%d\n", unit->loop_num + 1, unit->loop_Origin);
|
||||
}
|
||||
return;
|
||||
@@ -472,14 +508,21 @@ void vd1_task_per_channel(Loop154_Unit *unit)
|
||||
* 无车状态
|
||||
*================================================================*/
|
||||
|
||||
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * SensTable[unit->loop_SensLevel]) >> 16;
|
||||
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * g_loop_sens_list.sens[unit->loop_SensLevel].sens_in) >> 16;
|
||||
{
|
||||
/* V4B V4.20 同步: 双向对称保护 — 负variation(铁块磁导率)也冻结基线
|
||||
* 原单边只挡 dev>+4×dlt, 负variation会把Origin污染抬高 → 离开时假进入 → 锁死不释放
|
||||
* 修复: ① 对称窗口 [-4×dlt, +4×dlt] ② 负偏差只冻结永不超时更新 */
|
||||
int32_t dev = (int32_t)unit->loop_CAPVD - (int32_t)unit->loop_Origin;
|
||||
if (dev < (int32_t)(unit->loop_dlt_ORG * 4)) {
|
||||
int32_t freeze_band = (int32_t)(unit->loop_dlt_ORG * 4);
|
||||
if (dev < freeze_band && dev > -freeze_band) {
|
||||
/* 对称窗口内 → 正常基线跟踪 */
|
||||
unit->loop_freeze_cnt = 0;
|
||||
unit->loop_freeze_ref = 0;
|
||||
update_moving_average(&unit->loop_ORG_SUM, &unit->loop_ORG_CNT,
|
||||
&unit->loop_Origin, unit->loop_CAPVD, WINDOW_ORIGIN);
|
||||
} else {
|
||||
} else if (dev < 0) {
|
||||
/* 正variation (疑似车) → 冻结, 保留超时兜底更新 */
|
||||
if (unit->loop_freeze_cnt == 0) {
|
||||
unit->loop_freeze_ref = unit->loop_CAPVD;
|
||||
} else {
|
||||
@@ -503,6 +546,15 @@ void vd1_task_per_channel(Loop154_Unit *unit)
|
||||
unit->loop_ORG_CNT = 0;
|
||||
unit->loop_ORG_SUM = 0;
|
||||
}
|
||||
} else {
|
||||
/* 负variation (CAPVD高于Origin, 特殊铁块磁导率主导) → 只冻结, 永不更新 Origin */
|
||||
if (unit->loop_freeze_cnt == 0) {
|
||||
unit->loop_freeze_ref = unit->loop_CAPVD;
|
||||
}
|
||||
unit->loop_freeze_cnt++;
|
||||
if (unit->loop_freeze_cnt > FREEZE_TIMEOUT) unit->loop_freeze_cnt = FREEZE_TIMEOUT;
|
||||
unit->loop_ORG_CNT = 0;
|
||||
unit->loop_ORG_SUM = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +642,7 @@ void vd1_task_per_channel(Loop154_Unit *unit)
|
||||
} else {
|
||||
int32_t dev = (int32_t)unit->loop_CAPVD - (int32_t)unit->loop_Origin;
|
||||
int32_t cond1 = (dev >= 0) ? dev : -dev;
|
||||
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * SensTable_1[unit->loop_SensLevel]) >> 16;
|
||||
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * g_loop_sens_list.sens[unit->loop_SensLevel].sens_out) >> 16;
|
||||
if (cond1 < (int32_t)unit->loop_dlt_ORG && abs_fd < (int32_t)unit->delta2
|
||||
&& abs_sd < (int32_t)unit->delta3) {
|
||||
unit->flat_ok_cnt++;
|
||||
@@ -624,7 +676,7 @@ void vd1_task_per_channel(Loop154_Unit *unit)
|
||||
/*================================================================
|
||||
* 有车状态 — cnt_release 防抖离开
|
||||
*================================================================*/
|
||||
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * SensTable_1[unit->loop_SensLevel]) >> 16;
|
||||
unit->loop_dlt_ORG = ((uint32_t)unit->loop_Origin * g_loop_sens_list.sens[unit->loop_SensLevel].sens_out) >> 16;
|
||||
|
||||
if ((unit->loop_Origin - unit->loop_dlt_ORG) < unit->loop_CAPVD) {
|
||||
unit->loop_cnt_release++;
|
||||
@@ -655,6 +707,20 @@ void vd1_task_per_channel(Loop154_Unit *unit)
|
||||
|
||||
/*===========================================================================
|
||||
* loop_task_function — FreeRTOS 主任务(对齐 DLD154V4B main 循环)
|
||||
/*===========================================================================
|
||||
* 看门狗喂狗 (双保险, 对齐 DLD154V4B V2.10)
|
||||
* TMR15 ISR 5ms 递增 g_wdg_counter; 计数达 100 (500ms) 才 reload
|
||||
* ISR 死 -> 计数不涨 -> 永不喂狗; 主循环死 -> 本函数不执行 -> 超时复位
|
||||
*===========================================================================*/
|
||||
void poll_wdg(void)
|
||||
{
|
||||
if (g_wdg_counter >= WDT_COUNTER_LIMIT) { // 100 x 5ms = 500ms
|
||||
g_wdg_counter = 0;
|
||||
wdt_counter_reload();
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* 10ms 周期,独立处理每路线圈。
|
||||
*===========================================================================*/
|
||||
void loop_task_function(void *pvParameters)
|
||||
@@ -703,7 +769,7 @@ void loop_task_function(void *pvParameters)
|
||||
}
|
||||
}
|
||||
|
||||
wdt_feed();
|
||||
poll_wdg();
|
||||
vTaskDelay(10); // 10ms tick, M4 优化: 双路 IIR + 进入确认 + 斜率限幅
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,8 @@ __IO uint32_t TimingDelayDec;
|
||||
TaskHandle_t loop_task_handler;
|
||||
|
||||
// 看门狗相关
|
||||
#define WDT_TIMEOUT_MS 3000 // 看门狗超时时间 3秒
|
||||
#define WDT_TIMEOUT_MS 1600 // 看门狗超时时间: IWDT div16 rld4095, LSI约40kHz -> ~1.64s
|
||||
void wdt_init(void);
|
||||
void wdt_feed(void);
|
||||
|
||||
tmr_output_config_type tmr_oc_init_structure;
|
||||
|
||||
@@ -104,20 +103,19 @@ void InitPkgUart(Pkg_Uart * pkg){
|
||||
* @param none
|
||||
* @retval none
|
||||
* @note 超时时间 = (预分频系数 × 重载值) / LSI频率
|
||||
* LSI频率约 40kHz,预分频64,重载值=4687,超时≈ 3秒
|
||||
* LSI频率约 40kHz,预分频16(枚举 WDT_CLK_DIV_16),重载值=4095,超时≈ 1.64秒
|
||||
*/
|
||||
void wdt_init(void)
|
||||
{
|
||||
// 使能看门狗寄存器写访问
|
||||
wdt_register_write_enable(TRUE);
|
||||
|
||||
// 设置预分频系数为 64
|
||||
wdt_divider_set(WDT_CLK_DIV_64);
|
||||
// 设置预分频: 必须用枚举 WDT_CLK_DIV_16 (=0x02), 勿传数值 16! (V4B 踩坑)
|
||||
wdt_divider_set(WDT_CLK_DIV_16);
|
||||
|
||||
// 设置重载值
|
||||
// 超时时间 = (64 × 4687) / 40000 ≈ 7.5秒
|
||||
// 如果需要 3秒: (64 × 1875) / 40000 = 3秒
|
||||
wdt_reload_value_set(1875);
|
||||
// 设置重载值 4095 (12bit 最大)
|
||||
// 超时时间 = (16 × 4096) / 40000 ≈ 1.64秒
|
||||
wdt_reload_value_set(4095);
|
||||
|
||||
// 重载计数器
|
||||
wdt_counter_reload();
|
||||
@@ -128,15 +126,6 @@ void wdt_init(void)
|
||||
PRINT("Watchdog initialized, timeout: %d ms\n", WDT_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 喂狗(重载看门狗计数器)
|
||||
* @param none
|
||||
* @retval none
|
||||
*/
|
||||
void wdt_feed(void)
|
||||
{
|
||||
wdt_counter_reload();
|
||||
}
|
||||
|
||||
uint16_t timer_period = 0;
|
||||
uint16_t channel1_pulse = 0;
|
||||
@@ -584,8 +573,8 @@ static void unpack_pkg_set_mcjq_param(uint8_t *pkg, uint8_t len)
|
||||
for(j = 0; j < _amount; j++)
|
||||
{
|
||||
k = pkg[i++];
|
||||
unit->sensitvity = k & 0x0F;
|
||||
unit->loopFreq_Level = k >> 4;
|
||||
unit->sensitvity = k & 0x0F; // 低4位: 灵敏度序号 (4级制 0~3)
|
||||
unit->loopFreq_Level = k >> 4; // 高4位: 频率档
|
||||
unit->delay_time = pkg[i++];
|
||||
k = pkg[i++];
|
||||
unit->output_mode = k & 0x03;
|
||||
@@ -593,6 +582,9 @@ static void unpack_pkg_set_mcjq_param(uint8_t *pkg, uint8_t len)
|
||||
unit->exist_mode = pkg[i++];
|
||||
unit->direction_mode = pkg[i++];
|
||||
|
||||
// 同步灵敏度到检测单元 (修复: 运行中配置必须立即生效, 不依赖重启)
|
||||
g_loop_states.loop_unit[j].loop_SensLevel = sens_level_from_config(unit->sensitvity);
|
||||
|
||||
unit++;
|
||||
}
|
||||
|
||||
@@ -836,6 +828,11 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len)
|
||||
_cng++;
|
||||
}
|
||||
|
||||
// LOOP_MEASURE_BASE: variation 归一化基数 (3B LE, 与 TaskLoop.h MEASUREMENT_BASE 同步)
|
||||
tmp_ble_buf[i++] = (uint8_t)(LOOP_MEASURE_BASE & 0xFF);
|
||||
tmp_ble_buf[i++] = (uint8_t)((LOOP_MEASURE_BASE >> 8) & 0xFF);
|
||||
tmp_ble_buf[i++] = (uint8_t)((LOOP_MEASURE_BASE >> 16) & 0xFF);
|
||||
|
||||
response_uart_pkg(_cmd, tmp_ble_buf, i);
|
||||
|
||||
} break;
|
||||
|
||||
@@ -113,6 +113,12 @@ void storage_dev(void)
|
||||
|
||||
}
|
||||
|
||||
/* 灵敏度配置 → 检测档位映射 (4级制: 0~3 一一对应; 9级制后续实现) */
|
||||
uint8_t sens_level_from_config(uint8_t sensitvity)
|
||||
{
|
||||
return sensitvity & 0x03;
|
||||
}
|
||||
|
||||
// set frequent level
|
||||
void set_flp_level(uint8_t loop_num, uint8_t freq_level)
|
||||
{
|
||||
@@ -181,7 +187,7 @@ void para_store_init(void)
|
||||
set_flp_level(i, unitout->loopFreq_Level);
|
||||
|
||||
// 同步灵敏度到检测单元
|
||||
g_loop_states.loop_unit[i].loop_SensLevel = unitout->sensitvity & 0x03;
|
||||
g_loop_states.loop_unit[i].loop_SensLevel = sens_level_from_config(unitout->sensitvity);
|
||||
g_loop_states.loop_unit[i].SET_PLUS = unitout->output_mode & 0x01;
|
||||
g_loop_states.loop_unit[i].SET_DLY = (unitout->output_mode >> 1) & 0x01;
|
||||
g_loop_states.loop_unit[i].hold_time = unitout->exist_mode * 20 * 5;
|
||||
|
||||
Reference in New Issue
Block a user