Explorar o código

feat: add serial auto-connect and status indicator update

1. 新增串口配置保存与加载功能,实现自动连接上次使用的串口
2. 更新前端StatusIndicator组件,传入串口/MQTT连接状态作为参数
3. 补充Claude测试命令白名单,新增ssh、brew、npm、scp等命令
4. 添加线架系统功能需求与MQTT协议设计文档
wenhongquan hai 15 horas
pai
achega
b39ab1ece3

+ 6 - 1
.claude/settings.local.json

@@ -10,7 +10,12 @@
       "mcp__MCP_DOCKER__add_observations",
       "Bash(python3 -m py_compile app.py modules/modbus_rtu.py)",
       "Bash(python3 *)",
-      "Bash(xxd)"
+      "Bash(xxd)",
+      "Bash(ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no root@192.168.20.183 \"echo 'connected'; uname -a\")",
+      "Bash(brew install *)",
+      "Bash(sshpass *)",
+      "Bash(npm run *)",
+      "Bash(scp *)"
     ]
   }
 }

+ 61 - 3
backend/app.py

@@ -8,6 +8,9 @@ import os
 import logging
 import uuid
 
+# 配置文件路径
+SERIAL_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'serial_config.json')
+
 # 导入配置
 from config import (
     MAX_BUFFER_SIZE,
@@ -83,6 +86,55 @@ mqtt_data_buffer = []
 serial_status = False
 mqtt_status = False
 
+# 串口配置保存和加载函数
+def save_serial_config(port, baudrate=9600, timeout=1.0, bytesize=8, parity='N', stopbits=1):
+    """保存串口配置"""
+    config = {
+        'port': port,
+        'baudrate': baudrate,
+        'timeout': timeout,
+        'bytesize': bytesize,
+        'parity': parity,
+        'stopbits': stopbits
+    }
+    try:
+        with open(SERIAL_CONFIG_FILE, 'w') as f:
+            json.dump(config, f)
+        logger.info(f"串口配置已保存: {port} @ {baudrate}")
+    except Exception as e:
+        logger.error(f"保存串口配置失败: {e}")
+
+def load_serial_config():
+    """加载串口配置"""
+    if os.path.exists(SERIAL_CONFIG_FILE):
+        try:
+            with open(SERIAL_CONFIG_FILE, 'r') as f:
+                config = json.load(f)
+            logger.info(f"已加载串口配置: {config.get('port')} @ {config.get('baudrate')}")
+            return config
+        except Exception as e:
+            logger.error(f"加载串口配置失败: {e}")
+    return None
+
+def auto_connect_serial():
+    """自动连接上次使用的串口"""
+    config = load_serial_config()
+    if config and config.get('port'):
+        port = config.get('port')
+        baudrate = config.get('baudrate', 9600)
+        timeout = config.get('timeout', 1.0)
+        bytesize = config.get('bytesize', 8)
+        parity = config.get('parity', 'N')
+        stopbits = config.get('stopbits', 1)
+        logger.info(f"尝试自动连接串口: {port} @ {baudrate}")
+        success, message = serial_client.connect(port, baudrate=baudrate, timeout=timeout, bytesize=bytesize, parity=parity, stopbits=stopbits)
+        if success:
+            logger.info(f"自动连接串口成功: {port}")
+            return True
+        else:
+            logger.warning(f"自动连接串口失败: {message}")
+    return False
+
 # 客户端连接管理
 connected_clients = {
     'data': set(),
@@ -584,12 +636,14 @@ def serial_connect():
             'message': message,
             'error_code': error_code
         }
-        
+
         if success:
             logger.info(f"串口连接成功: {port}")
+            # 保存串口配置
+            save_serial_config(port, baudrate, timeout, bytesize, parity, stopbits)
         else:
             logger.error(f"串口连接失败: {message}")
-        
+
         return jsonify(response), status_code
     except Exception as e:
         error_msg = f'连接串口时出错: {str(e)}'
@@ -1360,7 +1414,11 @@ if __name__ == '__main__':
         loaded = load_device_config()
         logger.info(f"已加载 {len(loaded)} 个设备配置")
         logger.info("设备确认线程已禁用(确认命令干扰运行中设备的Modbus通信)")
-        
+
+        # 自动连接上次使用的串口
+        logger.info("尝试自动连接串口...")
+        auto_connect_serial()
+
         # 启动服务
         socketio.run(
             app, 

+ 1 - 1
frontend/src/App.vue

@@ -12,7 +12,7 @@
               </a-space>
             </a-col>
             <a-col>
-              <StatusIndicator />
+              <StatusIndicator :connected="dataStore.serialConnected || dataStore.mqttConnected" />
             </a-col>
           </a-row>
         </div>

BIN=BIN
电子线架硬件部分需求技术表6.16交流.xlsx


+ 1343 - 0
线架系统功能需求与MQTT协议设计.md

@@ -0,0 +1,1343 @@
+# 电子线架系统 - 功能需求与MQTT协议设计
+
+> **文档版本**: v2.0  
+> **更新时间**: 2026-06-17  
+> **修订说明**: 统一字段语义、修正状态机逻辑、对齐MODBUS-RTU协议细节、明确DTU实现边界、补充每个字段的可解释性说明。
+
+---
+
+## 0. 文档约定
+
+- **必须 / 应当 / 可选**:分别表示强制要求、规范要求、可选能力。
+- 所有 `id` 类字段均为字符串,由生成方保证**业务范围内唯一**,不强制全局唯一。
+- 所有 `timestamp` 字段均为 Unix 毫秒时间戳(`long`),由 DTU 或业务系统生成时取本地 UTC 毫秒值。
+- 所有 `uid` 字段均为**大写十六进制字符串,不含分隔符**。
+- 本文档是 DTU 与上层业务系统、配线架节点之间的**协议与行为约定**;源码实现若与本文档冲突,以本文档为修正目标。
+
+---
+
+## 1. 术语定义
+
+| 术语 | 定义 | 数据类型/范围 |
+|------|------|---------------|
+| **DTU** | 数据采集与转发控制器,连接配线架与上层 MQTT 业务系统 | - |
+| **配线架(节点/Panel)** | 带 RFID 读卡器和 LED 的从机设备,通过 RS485 级联 | Modbus 地址 1~247 |
+| **端口(Port)** | 配线架上的一个网口/光纤芯位,对应一个 RFID 天线 | 1~24(网络配线架) |
+| **跳线(Jumper)** | 两端带 RFID 芯片的线缆,插入端口后被识别 | - |
+| **jumper_uid** | 跳线 RFID 芯片的 8 字节 UID | 16 位大写十六进制字符串 |
+| **panel_uid** | 配线架模块出厂唯一标识,用于地址配置阶段 | 24 位大写十六进制字符串(12 字节) |
+| **address** | Modbus 从机地址,DTU 分配并持久化 | 1~247 |
+| **position** | 配线架在 RS485 级联链路中的**物理顺序**,从 1 开始递增 | 1~128(实际部署上限) |
+| **panel_id** | 业务系统侧为配线架分配的逻辑 ID | 字符串,如 `PANEL_001` |
+| **expected_jumper_uid** | 平台期望插在该端口的跳线 UID,通过 `SYNC_*` 指令下发 | 16 位大写十六进制字符串或 `null` |
+| **actual_jumper_uid** | DTU 轮询时实际读到的跳线 UID | 16 位大写十六进制字符串或 `null` |
+
+> **为什么区分 `panel_uid` 与 `jumper_uid`?**  
+> `panel_uid`(12 字节)标识**配线架硬件本身**,仅在地址配置阶段使用;`jumper_uid`(8 字节)标识**跳线**,通过 0x03 读寄存器获取。两者用途不同,长度也不同,避免混淆。
+
+---
+
+## 2. 系统架构
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│                        上层业务系统 (MQTT Broker 后端)                │
+│   ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
+│   │  设备管理     │  │  告警/工单    │  │  Web/APP 数据展示         │  │
+│   │  (SYNC/注册)  │  │  (规则引擎)   │  │  (状态/事件/历史)         │  │
+│   └──────────────┘  └──────────────┘  └──────────────────────────┘  │
+└─────────────────────────────┬───────────────────────────────────────┘
+                              │ MQTT (TCP/IP)
+                              ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│                           DTU 控制器                                 │
+│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
+│  │  MQTT 客户端  │  │  端口状态机   │  │  MODBUS-RTU 主站          │  │
+│  │  (上报/接收)  │  │  (基线/事件)  │  │  (RS485 轮询)             │  │
+│  └──────────────┘  └──────────────┘  └──────────────────────────┘  │
+│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
+│  │  地址自动配置  │  │  环境传感器   │  │  Web 管理界面             │  │
+│  │  (广播+分配)  │  │  (I2C/RS485) │  │  (本地配置)               │  │
+│  └──────────────┘  └──────────────┘  └──────────────────────────┘  │
+│  ┌──────────────┐  ┌───────────────────────────────────────────┐  │
+│  │  串口屏显示   │  │  调试/维护接口 (TTL/USB)                  │  │
+│  │  (内部显示)   │  │  (固件升级/日志/现场维护)                  │  │
+│  └──────────────┘  └───────────────────────────────────────────┘  │
+│  接口:1×以太网、1×RS485(TTL 转差分)、内部串口屏 UART、调试/维护接口   │
+└─────────────────────────────┬───────────────────────────────────────┘
+                              │ MODBUS-RTU over RS485 (9600bps)
+                              ▼
+┌─────────────────────────────────────────────────────────────────────┐
+│                      配线架节点 (RS485 级联)                          │
+│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────────┐  │
+│  │  24 个端口   │  │  RFID 读卡器  │  │  RGB LED 指示灯           │  │
+│  │  (天线×24)   │  │  (读 jumper) │  │  (按端口控制)             │  │
+│  └──────────────┘  └──────────────┘  └──────────────────────────┘  │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+### 2.1 接口说明
+
+| 接口 | 用途 | 备注 |
+|------|------|------|
+| 以太网 | 连接交换机,接入 MQTT Broker | DTU 必选 |
+| RS485 | 连接配线架级联链路 | 由 TTL 经板载 RS485 收发器转换 |
+| 内部串口屏 UART | 驱动 DTU 内置串口屏,显示端口状态 | 内部显示模块,不对外暴露接口 |
+| 调试/维护接口 | 固件升级、日志导出、现场维护 | TTL 或 USB,独立于 RS485 |
+
+> **关于串口屏的说明**  
+> 串口屏是 DTU 内部显示模块的一种实现方式,用于本地展示端口接入状态,不连接外部配线架。RS485 接口专用于配线架级联链路,二者使用独立的 UART,不存在接口复用问题。
+
+---
+
+## 3. 配线架(节点)功能
+
+> 跳线实现方式:在跳线接头内部嵌入 RFID 标签;配线架端口处布置 RFID 天线,用于检测跳线是否插入并读取其 UID。
+
+### 3.1 网络配线架(24 口)
+
+| 功能项 | 功能描述 | 实现位置 | 协议/寄存器 |
+|--------|----------|----------|-------------|
+| 端口连接检测 | 通过 RFID 天线感知跳线是否接入 | 配线架-读卡器 | 0x03 读天线寄存器 |
+| RFID 识别 | 读取跳线上 RFID 芯片的 8 字节 UID | 配线架-读卡器 | 0x0002+(n-1)×4 |
+| LED 指示灯 | 按端口显示红闪/绿闪/蓝闪/灭状态 | 配线架-LED 驱动 | 0x0001 写寄存器 |
+| 地址配置 | 接收 DTU 广播并获取 Modbus 地址 | 配线架-主控 | 0x40/0x41/0x42/0x43 |
+
+### 3.2 光纤配线架(48 芯 LC)
+
+| 功能项 | 功能描述 | 备注 |
+|--------|----------|------|
+| 端口连接检测 | 通过 RFID 检测跳线是否接入 | 每芯对应一个 RFID 天线 |
+| RFID 识别 | 读取跳线上的 RFID 卡号 | 8 字节 UID |
+| LED 指示灯 | 显示端口状态 | 控制方式与网络配线架相同 |
+
+> 光纤配线架 48 芯可视为“2 个逻辑 24 口单元”或独立 48 口单元;本文档以 24 口为基本单元描述,48 芯实现需在寄存器映射或地址分配上做扩展。
+
+### 3.3 跳线
+
+| 功能项 | 功能描述 | 数据 |
+|--------|----------|------|
+| RFID 标签 | 网线/光纤接头内嵌 RFID 芯片 | - |
+| 唯一标识 | 8 字节 UID | 16 位大写十六进制字符串 |
+| 源/目标属性 | 由业务系统根据端口映射关系推导 | 不存储在跳线标签中 |
+
+> **为什么不把源/目标属性写入跳线?**  
+> 跳线 UID 是只读出厂标识;源/目标端口会随业务变更。若写入跳线,每次变更都需重新写标签,成本高且易出错。因此源/目标由平台根据“哪个端口读到哪个 UID”动态维护。
+
+---
+
+## 4. DTU 控制器功能
+
+### 4.1 核心功能
+
+| 功能项 | 功能描述 | 状态 |
+|--------|----------|------|
+| **端口检测** | 轮询配线架,根据 UID 变化判断接入/断开/移动 | ✅ 已实现 |
+| **串口屏显示** | 通过 DTU 内置串口屏展示端口状态 | ✅ 已实现 |
+| **MODBUS 通讯** | 通过 RS485 管理最多 128 台配线架 | ✅ 已实现 |
+| **MQTT 对接** | 将配线架数据上报到云端/业务系统 | ✅ 已实现 |
+| **固件升级** | 支持远程 OTA 升级 | ✅ 已实现 |
+| **Web 管理界面** | 本地 HTTP 配置页面 | ✅ 已实现 |
+| **地址自动配置** | 广播发现未配置面板并分配地址 | ✅ 已实现 |
+
+> 管理数量说明:Modbus 地址范围 1~247,受 RS485 总线负载、轮询周期和供电能力限制,**单台 DTU 实际部署上限为 128 台配线架**。
+
+### 4.2 其他功能
+
+| 功能项 | 功能描述 | 状态 | 备注 |
+|--------|----------|------|------|
+| 温湿度传感器 | 机柜内部环境监测 | ✅ 已实现 | I2C 或 RS485 传感器 |
+| 显示屏接口 | 预留显示屏插口,可接手机/平板 | ✅ 已实现 | 通过独立串口或网络 |
+| 备用 RS485 接口 | 主板预留第二路 RS485 | ✅ 已实现 | 用于扩展传感器或其他从机 |
+
+### 4.3 硬件规格
+
+| 参数 | 规格 | 说明 |
+|------|------|------|
+| 网口 | 1 个 RJ45 | 接交换机/路由器 |
+| RS485 接口 | 1 路(板载 TTL 转 RS485) | 接配线架级联链路 |
+| 内部串口屏 UART | 1 路(内部) | 驱动内置显示模块,不占用 RS485 |
+| 调试/维护接口 | 1 路 TTL/USB | 固件升级与现场维护,独立于 RS485 |
+| 管理数量 | 理论 247 台,实际部署上限 128 台 | 受轮询周期和总线性能限制 |
+| 传输距离 | 供电稳定时 300~500 米 | RS485 理论值,实际与线径/负载有关 |
+| 芯片方案 | 国产化芯片 | - |
+
+---
+
+## 5. 端口检测逻辑(DTU 上实现)
+
+### 5.1 检测流程
+
+```
+DTU 启动
+   │
+   ▼
+加载已保存的 address↔panel_uid 映射
+   │
+   ▼
+周期性轮询各配线架(地址 1~N)
+   │
+   ▼
+读取每个端口天线寄存器 → 得到 actual_jumper_uid
+   │
+   ▼
+与历史状态(last_jumper_uid)对比
+   │
+   ▼
+生成事件(CONNECT / DISCONNECT / MOVE)
+   │
+   ▼
+若存在 expected_jumper_uid,再生成非法告警
+   │
+   ▼
+通过 MQTT 上报事件/告警,更新 last_jumper_uid
+```
+
+### 5.2 端口状态定义
+
+| 状态值 | 含义 | 触发条件 |
+|--------|------|----------|
+| `DISCONNECTED` | 端口空闲 | 天线未读到任何有效 UID |
+| `CONNECTED` | 端口已接入跳线 | 读到有效 UID,且未同步期望或期望与实际一致 |
+| `ILLEGAL` | 端口状态异常 | 读到有效 UID,但已同步期望且期望与实际不一致 |
+| `UNKNOWN` | 读卡失败 | 轮询超时、CRC 失败、寄存器返回异常 |
+
+> `UNKNOWN` 状态的端口上报时,`status = "UNKNOWN"` 且 `jumper_uid = null`,不得使用 `"UNKNOWN"` 作为 UID 字符串。
+
+### 5.3 事件判断状态机
+
+DTU 为每个端口维护两条基线:
+- `last_jumper_uid`:上一次轮询读到的 UID,用于检测物理变化。
+- `expected_jumper_uid`:平台通过 `SYNC_*` 指令下发的期望 UID,用于非法告警判断。
+
+| 当前读到 UID(actual) | 历史 UID(last) | 事件类型 | 说明 |
+|------------------------|------------------|----------|------|
+| 有效 UID | `null`/全 0 | `CONNECT` | 新跳线接入 |
+| `null`/全 0 | 有效 UID | `DISCONNECT` | 跳线拔出 |
+| 有效 UID A | 有效 UID B,且 A ≠ B | `MOVE` | 跳线被更换(旧 B 拔出,新 A 接入) |
+| 有效 UID A | 有效 UID A | `NONE` | 无变化,不上报事件 |
+| 读卡失败 | 任意 | `READ_ERROR` | 仅记录日志,不上报为端口事件 |
+
+> **关键说明**:
+> 1. 事件只反映**物理状态变化**,不判断是否合法。
+> 2. `MOVE` 事件在单轮 poll 内同时发生“拔出旧线”和“插入新线”时产生;若先拔后插跨越多轮,则先后产生 `DISCONNECT` 和 `CONNECT`。
+> 3. 全 0 UID(如 `0000000000000000`)视为无效/未插线。
+> 4. DTU 不上报 `NONE` 事件,避免垃圾消息。
+
+### 5.4 非法告警判断(与事件分离)
+
+在事件判断之后,DTU 再基于 `expected_jumper_uid` 做简单比对:
+
+| actual_jumper_uid | expected_jumper_uid | 告警类型 | 说明 |
+|-------------------|---------------------|----------|------|
+| 有效 UID,且等于 expected | 有效 UID | 无告警 | 状态正常 |
+| `null` | 有效 UID | `ILLEGAL_DISCONNECT` | 期望有跳线,实际没有 |
+| 有效 UID,且不等于 expected | 有效 UID | `ILLEGAL_CONNECT` | 期望没有该跳线/期望其他 UID |
+| 有效 UID A,且 A ≠ expected(无论 last 为何值) | 有效 UID | `ILLEGAL_CONNECT` | 平台未同步该变化,统一视为非法接入 |
+
+> **为什么不用 `ILLEGAL_MOVE` 单独告警?**  
+> DTU 只掌握单端口信息,无法判断是“移动”还是“误插”。业务系统收到 `ILLEGAL_CONNECT` 后,可结合相邻端口事件或工单规则判断是否为移动。DTU 保持简单,只做“是否等于期望”的二元判断。
+
+### 5.5 告警与事件的关系
+
+- **事件(EVENT)**:反映物理变化,必须上报。
+- **告警(ALARM)**:反映与期望映射的偏离,仅在平台已下发 `expected_jumper_uid` 时产生。
+- 一次状态变化可能同时产生一个事件和一个告警(例如非法接入)。
+- 平台侧告警规则引擎(如连续多次异常、跨端口关联分析)不在 DTU 实现。
+
+---
+
+## 6. DTU 与配线架间协议(MODBUS-RTU)
+
+### 6.1 物理拓扑
+
+```
+DTU
+ │
+ │ 板载 TTL→RS485 转换
+ │
+ ▼
+RS485 总线(A/B 双绞线)
+ │
+ ├── 配线架#1 (address=1, position=1)
+ ├── 配线架#2 (address=2, position=2)
+ ├── 配线架#3 (address=3, position=3)
+ └── ...
+```
+
+- DTU 作为 Modbus 主站,配线架作为从站。
+- 配线架之间通过 RS485 **串联级联**(daisy chain)。
+- DTU 通过从机地址区分不同配线架。
+- 一个 DTU 不直接并联多个 RS485 总线;所有配线架共享一条总线。
+
+### 6.2 通讯参数
+
+| 参数 | 值 | 说明 |
+|------|-----|------|
+| 协议 | 标准 MODBUS-RTU | - |
+| 物理层 | RS485(半双工) | 板载 TTL→RS485 收发器 |
+| 波特率 | 9600 bps | 固定 |
+| 数据位 | 8 位 | - |
+| 校验 | 无(None) | - |
+| 停止位 | 1 位 | - |
+| CRC | CRC-16 | 低字节在前,高字节在后(小端发送) |
+| 数据格式 | 16 位寄存器 | 高字节在前,低字节在后(大端) |
+
+### 6.3 功能码
+
+| 功能码 | 功能 | 说明 |
+|--------|------|------|
+| `0x03` | 读保持寄存器 | 读取端口卡号 |
+| `0x06` | 写单个寄存器 | 控制 LED 灯状态 |
+| `0x40` | 地址配置-广播查询 | 自定义功能码,非标准 Modbus |
+| `0x41` | 地址配置-从机应答 | 自定义功能码 |
+| `0x42` | 地址配置-分配地址 | 自定义功能码 |
+| `0x43` | 地址配置-分配确认 | 自定义功能码 |
+| `0x44` | 地址配置-地址确认 | 自定义功能码,用于运行期保活 |
+
+### 6.4 寄存器地址映射
+
+| 地址 | 功能 | 长度 | 说明 |
+|------|------|------|------|
+| `0x0000` | 保留 | 1 寄存器 | 系统保留,禁止占用或读写 |
+| `0x0001` | LED 控制 | 1 寄存器 | 高字节=灯号 1~24,低字节=颜色模式 |
+| `0x0002` | 天线 1 卡号 | 4 寄存器(8 字节) | 端口 1 的 jumper_uid |
+| `0x0006` | 天线 2 卡号 | 4 寄存器(8 字节) | 端口 2 的 jumper_uid |
+| `0x000A` | 天线 3 卡号 | 4 寄存器(8 字节) | 端口 3 的 jumper_uid |
+| ... | ... | ... | 通项:`0x0002 + (n-1)×4` |
+| `0x005E` | 天线 24 卡号 | 4 寄存器(8 字节) | 端口 24 的 jumper_uid |
+
+> 天线 n 寄存器地址公式:`reg_addr(n) = 0x0002 + (n - 1) × 4`,其中 n ∈ [1, 24]。
+
+### 6.5 LED 控制寄存器(0x0001)
+
+LED 控制使用**单个 16 位寄存器**,格式为:
+
+```
+值 value = (led_number << 8) | color_mode
+```
+
+| 字段 | 位数 | 含义 | 取值 |
+|------|------|------|------|
+| `led_number` | 高 8 位 | 要控制的 LED 编号 | 1~24 |
+| `color_mode` | 低 8 位 | 颜色/闪烁模式 | 见下表 |
+
+**颜色模式定义:**
+
+| color_mode | 模式 | 说明 |
+|------------|------|------|
+| `0x00` | 灭 | 关闭该 LED |
+| `0x01` | 红灯闪烁 | - |
+| `0x02` | 绿灯闪烁 | - |
+| `0x03` | 蓝灯闪烁 | - |
+
+> 当前协议仅支持 00/01/02/03 四种模式。DTU 下发时 `color_mode` 必须在此范围内,不得使用其他值。
+
+**示例:**
+
+- 让 2 号灯红灯闪烁:`01 06 00 01 02 01 18 AA`
+- 让 2 号灯绿灯闪烁:`01 06 00 01 02 02 58 AB`
+- 让 6 号灯绿灯闪烁:`01 06 00 01 06 02 5A 6B`
+
+> 旧文档中 LED 值 `0x01 / 0x02 / 0x06 / 0x0B / 0x12 / 0x15` 是“灯号+模式”的组合值,不能单独作为模式使用。DTU 下发时必须按 `(led_number << 8) | color_mode` 构造。
+
+### 6.6 读卡号示例
+
+请求天线 1 卡号:
+```
+01 03 00 02 00 04 E5 C9
+```
+- `01`:从机地址
+- `03`:功能码
+- `00 02`:起始寄存器地址
+- `00 04`:读取 4 个寄存器(8 字节)
+- `E5 C9`:CRC16(低字节在前)
+
+正常响应:
+```
+01 03 08 32 E5 95 06 09 01 04 E0 67 C5
+```
+- `01`:从机地址
+- `03`:功能码
+- `08`:数据字节数
+- `32 E5 95 06 09 01 04 E0`:8 字节 jumper_uid
+- `67 C5`:CRC16
+
+> 将 8 字节转换为大写十六进制字符串:`32E59506090104E0`。
+
+### 6.7 地址配置流程
+
+#### 6.7.1 阶段划分
+
+| 阶段 | 配线架行为 | DTU 行为 |
+|------|------------|----------|
+| 启动 30 秒等待 | 监听总线上的确认/广播 | 发送确认指令给已保存设备 |
+| 运行期 | 使用已分配地址响应 0x03/0x06 | 正常轮询 |
+| 配置期 | 未配置设备响应广播 | 发现新设备并分配地址 |
+
+#### 6.7.2 运行期地址确认(保活)
+
+DTU 每隔 10 秒对已保存的 `address↔panel_uid` 映射发送确认指令:
+
+```
+请求:add + 0x44 + 0x00 + 12 字节 panel_uid + CRC_L + CRC_H
+```
+
+示例:
+```
+01 44 00 18 00 40 00 14 00 00 59 59 54 30 56 3C 8F
+```
+
+- `01`:已分配地址
+- `44`:自定义功能码
+- `00`:子命令
+- `18 00 40 00 14 00 00 59 59 54 30 56`:12 字节 panel_uid
+- `3C 8F`:CRC16
+
+面板收到后校验地址与 panel_uid 是否匹配,匹配则进入/保持运行状态。
+
+#### 6.7.3 新设备发现与地址分配
+
+当 DTU 需要发现未配置面板时,发送广播查询:
+
+```
+请求:00 40 00 40 00
+```
+
+- `00`:广播地址
+- `40`:自定义功能码
+- `00 40 00`:固定字段
+- 无 CRC(固定 5 字节)
+
+未配置面板应答:
+```
+00 41 12 字节 panel_uid CRC_L CRC_H
+```
+
+示例:
+```
+00 41 18 00 40 00 14 00 00 59 59 54 30 56 AE 34
+```
+
+> **重要:多设备同时应答会产生总线冲突。** 首次部署或新增面板时,须单台上电、逐台完成地址配置;禁止多台未配置面板同时上电。DTU 在配置窗口期内仅处理第一个有效应答,后续设备须断电重上电后再次进入配置流程。
+
+DTU 分配地址:
+```
+00 42 12 字节 panel_uid add CRC_L CRC_H
+```
+
+示例:
+```
+00 42 18 00 40 00 14 00 00 59 59 54 30 56 01 77 7F
+```
+
+- `01`:分配给该面板的 Modbus 地址
+
+面板应答:
+```
+add 43 00 CRC_L CRC_H
+```
+
+示例:
+```
+01 43 00 11 30
+```
+
+#### 6.7.4 position 的确定
+
+`position` 不是 Modbus 地址,而是配线架在 RS485 级联链路中的**物理顺序**。确定方式:
+
+1. DTU 第一次成功为某 panel_uid 分配 Modbus 地址时,按**分配成功的先后顺序**赋予 position。
+2. 第一台分配成功的面板 `position = 1`,第二台 `position = 2`,依次递增。
+3. DTU 将 `panel_uid → address → position` 持久化到本地配置。
+4. 业务系统通过 DTU 注册消息中的 `panels` 数组获取 `position`,用于 UI 展示和物理定位。
+
+> 若后期在链路中间插入新面板,后续面板的 position 顺延;已保存的 `position` 不自动重排。业务系统须通过 `SYNC_ALL_MAPPING` 或人工配置重新同步 position 与 panel_id 的映射关系。
+
+---
+
+## 7. DTU 与上层业务系统 MQTT 协议
+
+### 7.1 主题设计
+
+**根主题前缀:** `线架系统/`
+
+`线架系统` 是本系统的 MQTT 根主题前缀,用于在同一 Broker 上与其他业务系统做命名空间隔离。部署时可根据实际项目命名统一修改(如改为 `wire_rack/` 或项目代号),但 DTU、业务系统、监控端必须使用同一根前缀,否则消息无法互通。
+
+上行/单播/控制主题统一格式:
+
+```
+线架系统/{customer_id}/{entity}/{dtu_id}[/{panel_id}]/{message_type}
+```
+
+全局广播主题格式:
+
+```
+线架系统/broadcast/dtu/{message_type}
+```
+
+完整主题树:
+
+```
+线架系统/
+├── {customer_id}/
+│   ├── dtu/
+│   │   ├── {dtu_id}/register          # DTU 注册(上行)
+│   │   ├── {dtu_id}/status            # DTU 心跳/状态(上行)
+│   │   ├── {dtu_id}/control           # 下行控制指令
+│   │   └── {dtu_id}/response          # 下行指令响应(上行)
+│   │
+│   ├── patchpanel/
+│   │   ├── {dtu_id}/{panel_id}/event  # 端口事件(上行)
+│   │   ├── {dtu_id}/{panel_id}/alarm  # 非法告警(上行)
+│   │   └── {dtu_id}/{panel_id}/status # 配线架状态(上行)
+│   │
+│   ├── jumper/
+│   │   └── {dtu_id}/status            # 跳线状态汇总(上行,按 DTU 聚合)
+│   │
+│   └── env/
+│       └── {dtu_id}/sensor            # 环境传感器数据(上行)
+│
+└── broadcast/
+    └── dtu/
+        ├── discover                   # 发现 DTU(下行广播)
+        └── config                     # 批量配置 DTU(下行广播)
+```
+
+> 上行主题位于 `{customer_id}` 命名空间下实现多租户隔离;`broadcast/` 主题为全局广播,所有 DTU 均可接收,DTU 通过 payload 中的 `customer_id` 判断是否响应。QoS 表中的通配主题(如 `dtu/+/register`)仅为订阅/监控时使用,实际发布主题必须包含 `customer_id` 前缀。
+
+### 7.2 通用消息格式
+
+所有 MQTT 消息采用统一信封。遗嘱消息(LWT)因连接时固定载荷,允许省略 `msg_id` 和 `timestamp`,但须包含 `dtu_id` 和 `type`:
+
+```json
+{
+  "msg_id": "uuid-string",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "REGISTER|STATUS|EVENT|ALARM|CONTROL|RESPONSE|DISCOVER|CONFIG",
+  "payload": { }
+}
+```
+
+**信封字段说明:**
+
+| 字段 | 类型 | 生成方 | 说明 |
+|------|------|--------|------|
+| `msg_id` | string | 发送方 | 消息唯一标识,使用 UUID 或 `类型_随机串`,用于去重和关联响应 |
+| `timestamp` | long | 发送方 | Unix 毫秒时间戳 |
+| `dtu_id` | string | DTU/平台 | DTU 全局唯一标识;平台下发的全局广播消息固定填 `"PLATFORM"` |
+| `type` | string | 发送方 | 消息类型,必须与主题语义一致 |
+| `payload` | object | 发送方 | 具体业务数据 |
+
+### 7.3 DTU 注册(上行)
+
+**主题:** `线架系统/{customer_id}/dtu/{dtu_id}/register`
+
+**触发时机:** DTU 上线、重连 MQTT、收到平台 `DISCOVER` 广播后。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "reg_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "REGISTER",
+  "payload": {
+    "firmware_version": "v1.2.3",
+    "hardware_version": "v2.0",
+    "panel_count": 3,
+    "network_type": "ethernet",
+    "signal_strength": null,
+    "uptime": 3600000,
+    "panels": [
+      {"panel_id": "PANEL_001", "address": 1, "position": 1},
+      {"panel_id": "PANEL_002", "address": 2, "position": 2},
+      {"panel_id": "PANEL_003", "address": 3, "position": 3}
+    ]
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `firmware_version` | string | 是 | DTU 固件版本 |
+| `hardware_version` | string | 否 | DTU 硬件版本 |
+| `panel_count` | int | 是 | 当前已配置的配线架数量,必须与 `panels` 数组长度一致 |
+| `network_type` | string | 是 | 网络类型:`ethernet`、`wifi`、`4g`、`5g` |
+| `signal_strength` | int/null | 否 | 无线信号强度(dBm);`ethernet` 时填 `null` |
+| `uptime` | long | 是 | DTU 本次运行时长,单位毫秒 |
+| `panels` | array | 是 | 已配置的配线架列表 |
+| `panels[].panel_id` | string | 是 | 业务系统侧为该面板分配的逻辑 ID |
+| `panels[].address` | int | 是 | Modbus 从机地址 |
+| `panels[].position` | int | 是 | 物理级联顺序,从 1 开始 |
+
+> `panel_id` 由 DTU 在地址配置阶段生成,默认规则为 `PANEL_{dtu_id}_{address}`(例如 `PANEL_DTU001_1`)。平台可通过 DTU Web 管理界面或本地配置文件预先指定 `panel_uid → panel_id` 映射;未指定时采用默认规则。`panel_id` 须持久化到本地,并在注册消息中上报。
+
+### 7.4 DTU 状态/心跳(上行)
+
+**主题:** `线架系统/{customer_id}/dtu/{dtu_id}/status`
+
+**触发时机:**
+- 周期性上报,默认 60 秒;
+- `panel_online`、`panel_offline`、`mqtt_connected`、`rs485_status` 任一字段发生变化时立即上报;
+- 收到平台 `QUERY_DTU_STATUS` 查询指令时立即上报(该指令通过 `dtu/{dtu_id}/control` 下发,命令名 `QUERY_DTU_STATUS`)。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "hbt_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "STATUS",
+  "payload": {
+    "cpu_usage": 35,
+    "memory_usage": 42,
+    "temperature": 45,
+    "network_status": "online",
+    "panel_online": 3,
+    "panel_offline": 0,
+    "screen_connected": false,
+    "mqtt_connected": true,
+    "rs485_status": "normal"
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `cpu_usage` | int | 否 | CPU 使用率,0~100 |
+| `memory_usage` | int | 否 | 内存使用率,0~100 |
+| `temperature` | int/float | 否 | DTU 主板温度,单位摄氏度 |
+| `network_status` | string | 是 | `online` / `offline` / `weak` |
+| `panel_online` | int | 是 | 当前成功轮询的配线架数量 |
+| `panel_offline` | int | 是 | 当前轮询失败/未应答的配线架数量 |
+| `screen_connected` | bool | 否 | 串口屏是否连接 |
+| `mqtt_connected` | bool | 是 | MQTT 客户端是否连接(冗余自检) |
+| `rs485_status` | string | 是 | `normal` / `error` / `busy` |
+
+### 7.5 端口事件(上行)
+
+**主题:** `线架系统/{customer_id}/patchpanel/{dtu_id}/{panel_id}/event`
+
+**触发时机:** 端口物理状态发生变化。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "evt_002",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "EVENT",
+  "payload": {
+    "panel_id": "PANEL_001",
+    "port_id": 5,
+    "event_type": "CONNECT",
+    "event_id": "evt_a1b2c3d4",
+    "jumper_uid": "32E59506090104E0",
+    "previous_jumper_uid": null
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `panel_id` | string | 是 | 配线架逻辑 ID |
+| `port_id` | int | 是 | 端口号,1~24 |
+| `event_type` | string | 是 | `CONNECT` / `DISCONNECT` / `MOVE` |
+| `event_id` | string | 是 | 事件唯一标识,由 DTU 生成,格式 `evt_{短随机串}` |
+| `jumper_uid` | string/null | 是 | 当前端口读到的 UID;`DISCONNECT` 时为 `null` |
+| `previous_jumper_uid` | string/null | 是 | 变化前的 UID;`CONNECT` 时为 `null`;`MOVE` 时为旧 UID |
+
+**事件类型取值规则:**
+
+| event_type | jumper_uid | previous_jumper_uid | 含义 |
+|------------|------------|---------------------|------|
+| `CONNECT` | 新 UID | `null` | 新跳线接入 |
+| `DISCONNECT` | `null` | 旧 UID | 跳线拔出 |
+| `MOVE` | 新 UID | 旧 UID | 单轮检测内发生更换 |
+
+> 删除旧文档中的 `port_status` 字段,避免与 `event_type` 语义重复。
+
+### 7.6 非法告警(上行)
+
+**主题:** `线架系统/{customer_id}/patchpanel/{dtu_id}/{panel_id}/alarm`
+
+**触发条件:** 该端口已通过 `SYNC_PORT_MAPPING` 或 `SYNC_ALL_MAPPING` 收到非空的 `expected_jumper_uid`,且当前轮询结果与之不符。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "alm_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "ALARM",
+  "payload": {
+    "panel_id": "PANEL_001",
+    "port_id": 8,
+    "alarm_type": "ILLEGAL_DISCONNECT",
+    "severity": "WARNING",
+    "expected_jumper_uid": "32E59506090104E0",
+    "actual_jumper_uid": null,
+    "description": "端口8期望跳线32E59506090104E0,实际未读到"
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `panel_id` | string | 是 | 配线架逻辑 ID |
+| `port_id` | int | 是 | 端口号 |
+| `alarm_type` | string | 是 | `ILLEGAL_CONNECT` / `ILLEGAL_DISCONNECT` |
+| `severity` | string | 是 | `WARNING` / `CRITICAL`。首次产生该告警为 `WARNING`;同一端口同一告警类型连续 3 个轮询周期未恢复,升级为 `CRITICAL` |
+| `expected_jumper_uid` | string | 是 | 平台期望的 UID |
+| `actual_jumper_uid` | string/null | 是 | 实际读到的 UID;未读到为 `null` |
+| `description` | string | 否 | 可读描述 |
+
+**告警类型判定表:**
+
+| actual_jumper_uid | expected_jumper_uid | alarm_type |
+|-------------------|---------------------|------------|
+| `null` | 有效 UID | `ILLEGAL_DISCONNECT` |
+| 有效 UID,且 ≠ expected | 有效 UID | `ILLEGAL_CONNECT` |
+| 有效 UID,且 = expected | 有效 UID | 无告警 |
+| 任意 | `null` / 未同步 | 无告警 |
+
+> **为什么只有两种告警类型?**  
+> DTU 只掌握单端口信息,无法区分“非法移动”与“非法接入/拔出”。业务系统收到告警后,可结合端口事件、相邻端口状态、工单规则做进一步判断。
+
+### 7.7 配线架状态(上行)
+
+**主题:** `线架系统/{customer_id}/patchpanel/{dtu_id}/{panel_id}/status`
+
+**触发时机:** 周期性心跳或状态查询响应。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "pst_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "STATUS",
+  "payload": {
+    "panel_id": "PANEL_001",
+    "position": 1,
+    "address": 1,
+    "online": true,
+    "last_poll_time": 1718092795000,
+    "ports": [
+      {"port_id": 1, "status": "CONNECTED", "jumper_uid": "32E59506090104E0"},
+      {"port_id": 2, "status": "DISCONNECTED", "jumper_uid": null},
+      {"port_id": 3, "status": "CONNECTED", "jumper_uid": "32E59506090104A1"},
+      {"port_id": 4, "status": "ILLEGAL", "jumper_uid": "32E59506090104FF"}
+    ]
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `panel_id` | string | 是 | 配线架逻辑 ID |
+| `position` | int | 是 | 物理级联顺序 |
+| `address` | int | 是 | Modbus 地址 |
+| `online` | bool | 是 | 最近一次轮询是否成功 |
+| `last_poll_time` | long | 是 | 最近一次成功轮询的时间戳 |
+| `ports` | array | 是 | 各端口当前状态 |
+| `ports[].port_id` | int | 是 | 端口号 |
+| `ports[].status` | string | 是 | `DISCONNECTED` / `CONNECTED` / `ILLEGAL` / `UNKNOWN` |
+| `ports[].jumper_uid` | string/null | 是 | 当前读到的 UID;读卡失败时可填 `null` |
+
+### 7.8 跳线状态汇总(上行)
+
+**主题:** `线架系统/{customer_id}/jumper/{dtu_id}/status`
+
+**触发时机:** 周期性汇总、平台 `QUERY_JUMPER_STATUS` 查询响应。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "jmp_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "STATUS",
+  "payload": {
+    "panels": [
+      {
+        "panel_id": "PANEL_001",
+        "position": 1,
+        "ports": [
+          {"port_id": 1, "status": "CONNECTED", "jumper_uid": "32E59506090104E0"},
+          {"port_id": 2, "status": "DISCONNECTED", "jumper_uid": null}
+        ]
+      },
+      {
+        "panel_id": "PANEL_002",
+        "position": 2,
+        "ports": [
+          {"port_id": 1, "status": "CONNECTED", "jumper_uid": "32E59506090104A1"}
+        ]
+      }
+    ]
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `panels` | array | 是 | 该 DTU 下所有配线架状态 |
+| `panels[].panel_id` | string | 是 | 配线架逻辑 ID |
+| `panels[].position` | int | 是 | 物理级联顺序 |
+| `panels[].ports` | array | 是 | 端口状态数组 |
+| `panels[].ports[].port_id` | int | 是 | 端口号 |
+| `panels[].ports[].status` | string | 是 | 端口状态 |
+| `panels[].ports[].jumper_uid` | string/null | 是 | 跳线 UID |
+
+> 旧文档此处只包含单个 `panel_id`,但主题为 `jumper/{dtu_id}/status`,语义应为 DTU 下全部面板汇总。新版改为 `panels` 数组。
+
+### 7.9 环境传感器(上行)
+
+**主题:** `线架系统/{customer_id}/env/{dtu_id}/sensor`
+
+**Payload:**
+
+```json
+{
+  "msg_id": "sen_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "STATUS",
+  "payload": {
+    "temperature": 28.5,
+    "humidity": 65.2,
+    "sensor_update_time": 1718092700000
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `temperature` | float | 否 | 温度,单位摄氏度 |
+| `humidity` | float | 否 | 湿度,0~100 |
+| `sensor_update_time` | long | 是 | 传感器最近一次采样时间戳;与信封 `timestamp` 区分,后者是 MQTT 发送时间 |
+
+### 7.10 下行控制指令
+
+**主题:** `线架系统/{customer_id}/dtu/{dtu_id}/control`
+
+**通用 Payload:**
+
+```json
+{
+  "msg_id": "ctrl_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "CONTROL",
+  "payload": {
+    "command": "SET_PORT_LED",
+    "target": "PANEL_001",
+    "params": {
+      "port_id": 5,
+      "led_mode": "BLINK_RED"
+    }
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `command` | string | 是 | 命令名称 |
+| `target` | string | 是 | 目标对象,取值见各命令说明 |
+| `params` | object | 视命令而定 | 命令参数 |
+
+**支持的命令列表:**
+
+| command | 功能 | target | target 说明 | params | 响应方式 |
+|---------|------|--------|-------------|--------|----------|
+| `SET_PORT_LED` | 设置端口 LED | `panel_id` | 目标配线架 `panel_id` | `port_id`, `led_mode` | `response` 主题 |
+| `READ_PANEL_STATUS` | 查询配线架状态 | `panel_id` / `all` | 单个 `panel_id` 或该 DTU 下所有配线架 | 无 | `patchpanel/.../status` |
+| `QUERY_JUMPER_STATUS` | 查询跳线汇总 | `all` | 固定为 `all`,汇总该 DTU 下所有配线架 | 无 | `jumper/{dtu_id}/status` |
+| `QUERY_ENV_SENSOR` | 查询环境数据 | `all` | 固定为 `all` | 无 | `env/{dtu_id}/sensor` |
+| `SYNC_PORT_MAPPING` | 同步单端口期望映射 | `panel_id` | 目标配线架 `panel_id` | `port_id`, `jumper_uid` | `response` 主题 |
+| `SYNC_ALL_MAPPING` | 批量同步期望映射 | `all` | 固定为 `all` | `mappings` 数组 | `response` 主题 |
+| `QUERY_DTU_STATUS` | 查询 DTU 状态 | `dtu` | 固定为 `dtu` | 无 | `dtu/{dtu_id}/status` |
+| `OTA_UPGRADE` | 远程固件升级 | `dtu` | 固定为 `dtu` | 见 7.16 节 | `response` 主题 |
+| `REBOOT` | 重启 DTU | `dtu` | 固定为 `dtu`,表示重启本 DTU | 无 | `response` 主题 |
+
+**LED 模式定义(与 MODBUS color_mode 对应):**
+
+| led_mode | MODBUS color_mode | 说明 |
+|----------|-------------------|------|
+| `OFF` | `0x00` | 灭 |
+| `BLINK_RED` | `0x01` | 红灯闪烁 |
+| `BLINK_GREEN` | `0x02` | 绿灯闪烁 |
+| `BLINK_BLUE` | `0x03` | 蓝灯闪烁 |
+
+> DTU 收到 `SET_PORT_LED` 后,将 `led_mode` 转换为 `color_mode`,再构造 `(port_id << 8) | color_mode` 写入 0x0001 寄存器。
+
+### 7.11 指令响应(上行)
+
+**主题:** `线架系统/{customer_id}/dtu/{dtu_id}/response`
+
+**Payload:**
+
+```json
+{
+  "msg_id": "rsp_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "RESPONSE",
+  "payload": {
+    "original_msg_id": "ctrl_001",
+    "command": "SET_PORT_LED",
+    "success": true,
+    "result": {
+      "panel_id": "PANEL_001",
+      "port_id": 5,
+      "led_mode": "BLINK_RED"
+    },
+    "error_code": 0,
+    "error_message": null
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `original_msg_id` | string | 是 | 对应下行指令的 `msg_id` |
+| `command` | string | 是 | 对应命令名称 |
+| `success` | bool | 是 | 是否执行成功 |
+| `result` | object/null | 是 | 命令执行结果;失败时为 `null` |
+| `error_code` | int | 是 | 错误码,`0` 表示成功 |
+| `error_message` | string/null | 是 | 错误描述;成功时为 `null` |
+
+**错误码定义:**
+
+| error_code | 含义 |
+|------------|------|
+| `0` | 成功 |
+| `1001` | 未知命令 |
+| `1002` | 目标面板不存在或离线 |
+| `1003` | 端口号非法 |
+| `1004` | LED 模式非法 |
+| `1005` | MODBUS 通信失败 |
+| `1006` | 参数缺失或格式错误 |
+| `1007` | 同步映射失败 |
+| `1011` | 配置版本已存在且未强制应用 |
+| `1012` | 配置项取值越界 |
+| `1013` | 配置项不支持 |
+| `1014` | 配置写入持久化失败 |
+| `1021` | 已是目标版本 |
+| `1022` | 固件校验失败 |
+| `1023` | 固件下载失败 |
+| `1024` | 固件写入失败 |
+| `1025` | 存储空间不足 |
+| `9999` | 系统内部错误 |
+
+### 7.12 QoS 与保留消息
+
+| 消息类型 | 主题通配符 | QoS | Retain | 说明 |
+|----------|------------|-----|--------|------|
+| 注册 | `线架系统/+/dtu/+/register` | 1 | false | 上线时发送一次 |
+| 状态/心跳 | `线架系统/+/dtu/+/status` | 1 | false | 周期性 |
+| 事件 | `线架系统/+/patchpanel/+/+/event` | 1 | false | 状态变化时 |
+| 告警 | `线架系统/+/patchpanel/+/+/alarm` | 1 | false | 异常时 |
+| 控制 | `线架系统/+/dtu/+/control` | 1 | false | 平台下发 |
+| 响应 | `线架系统/+/dtu/+/response` | 1 | false | 每条控制指令对应一条响应 |
+| 环境 | `线架系统/+/env/+/sensor` | 0 | false | 允许偶发丢失 |
+| 配线架状态 | `线架系统/+/patchpanel/+/+/status` | 0 | false | 周期性或查询响应 |
+| 发现广播 | `线架系统/broadcast/dtu/discover` | 1 | false | 平台下发,触发 DTU 主动注册 |
+| 批量配置广播 | `线架系统/broadcast/dtu/config` | 1 | false | 平台下发,统一调整 DTU 参数 |
+
+> 旧文档将部分主题 QoS 设为 2。实际场景中事件/告警若 QoS 2 会对 Broker 造成较大压力,且业务系统可通过去重和状态补偿容忍偶发丢失,因此统一调整为 1。
+
+### 7.13 心跳与遗言机制
+
+| 项目 | 配置 | 说明 |
+|------|------|------|
+| 心跳间隔 | 60 秒 | `dtu/{dtu_id}/status` 周期性上报 |
+| 超时阈值 | 180 秒 | 业务系统 180 秒未收到心跳视为离线 |
+| 遗言主题 | `线架系统/{customer_id}/dtu/{dtu_id}/status` | MQTT LWT |
+| 遗言 QoS | 1 | - |
+| 遗言 Retain | false | - |
+| 遗言消息 | `{"dtu_id":"DTU_001","type":"STATUS","payload":{"online":false,"reason":"CONNECTION_LOST"}}` | LWT 固定载荷,含 `dtu_id`、`type`、`payload` |
+
+> 旧文档遗嘱消息缺少 `dtu_id` 等字段,会导致业务系统解析异常。新版遗嘱消息包含 `dtu_id`、`type`、`payload`,`msg_id` 和 `timestamp` 因 LWT 固定载荷特性可省略。
+
+### 7.14 DTU 发现广播(下行/上行)
+
+**主题:** `线架系统/broadcast/dtu/discover`
+
+**方向:** 业务系统 → 所有 DTU(全局广播)
+
+**触发时机:** 平台需要主动发现所有在线 DTU,例如首次部署、DTU 更换或心跳异常排查。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "dsc_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "PLATFORM",
+  "type": "DISCOVER",
+  "payload": {
+    "request_id": "dsc_001",
+    "customer_id": "CUST_001"
+  }
+}
+```
+
+> 平台下发的广播消息,`dtu_id` 固定填 `"PLATFORM"`,用于与 DTU 上行的注册/响应消息区分。
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `request_id` | string | 是 | 发现请求标识,DTU 响应时须原样回传 |
+| `customer_id` | string | 是 | 目标客户 ID;DTU 据此过滤。为空字符串时表示所有客户 |
+
+**DTU 处理规则:**
+
+1. DTU 成功连接 MQTT 后,必须立即订阅 `线架系统/broadcast/dtu/discover`。
+2. 收到发现请求后,按以下规则过滤:
+   - 若 `customer_id` 为空字符串,所有 DTU 必须响应;
+   - 若 `customer_id` 等于本机配置的 `customer_id`,必须响应;
+   - 其他情况,禁止响应。
+3. 为避免大量 DTU 同时上报造成 Broker/平台压力,DTU 须在收到请求后随机等待 `[0, 3000]` 毫秒,再向自身注册主题发送一次注册消息。
+4. 注册消息 `payload.discovery_request_id` 必须等于收到的 `request_id`。
+5. 同一 `request_id` 在 60 秒内只允许响应一次,禁止重复上报。
+
+**发现响应注册消息示例:**
+
+```json
+{
+  "msg_id": "reg_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "REGISTER",
+  "payload": {
+    "firmware_version": "v1.2.3",
+    "hardware_version": "v2.0",
+    "panel_count": 3,
+    "network_type": "ethernet",
+    "signal_strength": null,
+    "uptime": 3600000,
+    "discovery_request_id": "dsc_001",
+    "panels": [
+      {"panel_id": "PANEL_001", "address": 1, "position": 1}
+    ]
+  }
+}
+```
+
+> 发现广播仅作为辅助手段,日常在线状态以 DTU 主动注册和周期性心跳为准。平台须根据 `dtu_id` 对响应去重。
+
+### 7.15 DTU 批量配置广播(下行/上行)
+
+**主题:** `线架系统/broadcast/dtu/config`
+
+**方向:** 业务系统 → 所有 DTU(全局广播)
+
+**触发时机:** 平台需要统一调整所有 DTU 的运行参数,例如心跳间隔、轮询周期、MQTT 保活时间等。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "cfg_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "PLATFORM",
+  "type": "CONFIG",
+  "payload": {
+    "customer_id": "CUST_001",
+    "config_version": "2026061701",
+    "force_apply": false,
+    "items": {
+      "heartbeat_interval": 60,
+      "poll_interval_ms": 500,
+      "modbus_timeout_ms": 1000,
+      "mqtt_keepalive": 60,
+      "alarm_enabled": true,
+      "event_debounce_ms": 200
+    }
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `customer_id` | string | 是 | 目标客户 ID;DTU 据此过滤。为空字符串时表示所有客户 |
+| `config_version` | string | 是 | 配置版本号,DTU 持久化后用于幂等判断 |
+| `force_apply` | bool | 否 | 是否强制应用,即使版本号与本地相同;默认 `false` |
+| `items` | object | 是 | 具体配置项,key-value 形式 |
+| `items.heartbeat_interval` | int | 否 | 心跳上报间隔,单位秒,取值范围 `[10, 3600]` |
+| `items.poll_interval_ms` | int | 否 | Modbus 轮询间隔,单位毫秒,取值范围 `[100, 10000]` |
+| `items.modbus_timeout_ms` | int | 否 | Modbus 单次请求超时,单位毫秒,取值范围 `[200, 5000]` |
+| `items.mqtt_keepalive` | int | 否 | MQTT keepalive 间隔,单位秒,取值范围 `[10, 300]` |
+| `items.alarm_enabled` | bool | 否 | 是否启用非法告警功能 |
+| `items.event_debounce_ms` | int | 否 | 事件去抖时间,单位毫秒,取值范围 `[0, 5000]` |
+
+> `items` 中未包含的配置项保持本地原值不变;包含的配置项覆盖本地值。
+
+**DTU 处理规则:**
+
+1. DTU 成功连接 MQTT 后,必须立即订阅 `线架系统/broadcast/dtu/config`。
+2. 收到配置消息后,按以下规则过滤:
+   - 若 `customer_id` 为空字符串,所有 DTU 必须处理;
+   - 若 `customer_id` 等于本机配置的 `customer_id`,必须处理;
+   - 其他情况,禁止处理。
+3. 幂等判断:
+   - 若 `config_version` 与本地最后应用版本相同且 `force_apply` 为 `false`,直接返回成功响应,不重复应用;
+   - 若 `force_apply` 为 `true`,则重新校验并应用。
+4. 参数校验:对 `items` 中每个配置项按取值范围校验;任一配置项越界或不被支持,整个配置拒绝应用,返回错误。
+5. 原子应用:所有合法配置项一次性写入本地持久化存储;写入失败时保持原配置不变,返回错误。
+6. 生效策略:
+
+| 配置项 | 生效方式 | restart_required |
+|--------|----------|------------------|
+| `heartbeat_interval` | 立即生效,下一心跳周期按新值执行 | false |
+| `poll_interval_ms` | 立即生效,下一 Modbus 轮询按新值执行 | false |
+| `modbus_timeout_ms` | 立即生效,下一次 Modbus 请求按新值执行 | false |
+| `alarm_enabled` | 立即生效 | false |
+| `event_debounce_ms` | 立即生效 | false |
+| `mqtt_keepalive` | 保存后生效,DTU 主动断开并重连 MQTT,无需整机重启 | false |
+
+> 当前配置集中所有项均为运行期生效,不强制整机重启。若后续新增必须重启的配置项,须在本文档中明确标注 `restart_required: true`。
+7. 响应延迟:DTU 收到配置后随机等待 `[0, 2000]` 毫秒再发送响应,避免响应洪峰。
+8. 必须向 `线架系统/{customer_id}/dtu/{dtu_id}/response` 上报配置结果。
+
+**配置响应示例:**
+
+```json
+{
+  "msg_id": "rsp_cfg_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "RESPONSE",
+  "payload": {
+    "original_msg_id": "cfg_001",
+    "command": "BROADCAST_CONFIG",
+    "success": true,
+    "result": {
+      "applied_version": "2026061701",
+      "applied_items": ["heartbeat_interval", "poll_interval_ms"],
+      "restart_required": false
+    },
+    "error_code": 0,
+    "error_message": null
+  }
+}
+```
+
+**错误码补充:**
+
+| error_code | 含义 |
+|------------|------|
+| `1011` | 配置版本已存在且未强制应用 |
+| `1012` | 配置项取值越界 |
+| `1013` | 配置项不支持 |
+| `1014` | 配置写入持久化失败 |
+
+> 批量配置广播影响所有匹配 `customer_id` 的 DTU。平台下发前须在业务侧完成配置项合法性校验,并先通过单台 `control` 指令在小范围 DTU 上验证;验证通过后,方可广播批量推送。
+
+### 7.16 远程固件升级(OTA)
+
+**触发命令:** `OTA_UPGRADE`,通过 `线架系统/{customer_id}/dtu/{dtu_id}/control` 下发。
+
+**Payload:**
+
+```json
+{
+  "msg_id": "ota_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "CONTROL",
+  "payload": {
+    "command": "OTA_UPGRADE",
+    "target": "dtu",
+    "params": {
+      "firmware_url": "https://ota.example.com/firmware/v1.2.4.bin",
+      "firmware_version": "v1.2.4",
+      "file_size": 1048576,
+      "checksum": "a1b2c3d4e5f6...",
+      "checksum_type": "SHA256",
+      "force_upgrade": false
+    }
+  }
+}
+```
+
+**字段说明:**
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `firmware_url` | string | 是 | 固件下载地址,DTU 须支持 HTTP 或 HTTPS |
+| `firmware_version` | string | 是 | 目标固件版本 |
+| `file_size` | long | 是 | 固件文件字节数,用于下载完整性校验 |
+| `checksum` | string | 是 | 固件文件校验值 |
+| `checksum_type` | string | 是 | 校验算法:`MD5` / `SHA256` |
+| `force_upgrade` | bool | 否 | 是否强制升级;`false` 时若本地版本已等于目标版本则跳过 |
+
+**DTU 处理规则:**
+
+1. 收到 `OTA_UPGRADE` 指令后,立即返回响应确认已收到任务(`success: true` 仅表示开始处理,不代表升级成功)。
+2. 若 `force_upgrade` 为 `false` 且本地固件版本已等于 `firmware_version`,返回错误码 `1021`(已是目标版本)。
+3. DTU 从 `firmware_url` 下载固件,下载过程中每完成 10% 进度或每 5 秒向 `dtu/{dtu_id}/status` 上报一次进度:
+   - `payload.ota_status = "DOWNLOADING"`
+   - `payload.ota_progress = 0~100`
+4. 下载完成后按 `checksum_type` 计算并比对 `checksum`;不一致则向 `response` 主题返回错误码 `1022`(校验失败)。
+5. 校验通过后写入备用固件区,写入完成后向 `response` 主题返回错误码 `0`,`result` 中标记 `restart_required: true`。
+6. DTU 在写入成功后自动重启,使用新固件启动;重启后向 `dtu/{dtu_id}/register` 上报,其中 `firmware_version` 为新版本。
+7. 若下载或写入失败,保持当前固件不变,向 `response` 主题返回对应错误码。
+
+**OTA 响应示例(开始处理):**
+
+```json
+{
+  "msg_id": "rsp_ota_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "RESPONSE",
+  "payload": {
+    "original_msg_id": "ota_001",
+    "command": "OTA_UPGRADE",
+    "success": true,
+    "result": {
+      "firmware_version": "v1.2.4",
+      "ota_status": "DOWNLOADING",
+      "restart_required": false
+    },
+    "error_code": 0,
+    "error_message": null
+  }
+}
+```
+
+**OTA 进度上报(DTU 状态主题):**
+
+```json
+{
+  "msg_id": "ota_status_001",
+  "timestamp": 1718092800000,
+  "dtu_id": "DTU_001",
+  "type": "STATUS",
+  "payload": {
+    "ota_status": "DOWNLOADING",
+    "ota_progress": 45,
+    "firmware_version": "v1.2.4"
+  }
+}
+```
+
+> `ota_status` 取值:`IDLE` / `DOWNLOADING` / `VERIFYING` / `FLASHING` / `SUCCESS` / `FAILED`。
+
+**错误码补充:**
+
+| error_code | 含义 |
+|------------|------|
+| `1021` | 已是目标版本 |
+| `1022` | 固件校验失败 |
+| `1023` | 固件下载失败 |
+| `1024` | 固件写入失败 |
+| `1025` | 存储空间不足 |
+
+> OTA 固件包须由平台签名或校验;DTU 下载前必须校验 `firmware_url` 域名白名单,禁止从非授权域名下载固件。
+
+---
+
+## 8. 功能实现对照表
+
+### 8.1 配线架侧(节点)
+
+| 功能 | 协议支持 | 状态 |
+|------|----------|------|
+| RFID 读卡 | ✅ 0x03 读寄存器 | 已实现 |
+| LED 控制 | ✅ 0x06 写寄存器 | 已实现 |
+| 地址自动配置 | ✅ 广播+分配流程 | 已实现 |
+
+### 8.2 DTU 侧
+
+| 功能 | 协议支持 | 状态 | 备注 |
+|------|----------|------|------|
+| 端口检测 | MODBUS 读卡号 → MQTT 事件上报 | ✅ 已实现 | - |
+| 内存基线维护 | 端口 → last_jumper_uid + expected_jumper_uid | ✅ 已实现 | - |
+| 非法告警 | expected vs actual 比对 → MQTT 告警上报 | ✅ 已实现 | 仅在已同步期望映射时触发 |
+| MODBUS 轮询 | MODBUS-RTU | ✅ 已实现 | - |
+| MQTT 上报 | 事件/告警/心跳/状态汇总/温湿度 | ✅ 已实现 | - |
+| 单 DTU 控制指令 | `SET_PORT_LED` / `READ_PANEL_STATUS` / `QUERY_*` / `SYNC_*` / `REBOOT` / `QUERY_DTU_STATUS` / `OTA_UPGRADE` | ✅ 已实现 | 通过 `dtu/{dtu_id}/control` 下发 |
+| 全局广播配置响应 | `DISCOVER` / `CONFIG` | ✅ 已实现 | 通过 `broadcast/dtu/*` 下发 |
+| 地址自动配置 | 广播+分配流程 | ✅ 已实现 | 须按 6.7.3 节流程处理多设备冲突 |
+| 串口屏显示 | 内部串口屏展示端口状态 | ✅ 已实现 | - |
+| Web 管理界面 | HTTP 服务 | ✅ 已实现 | - |
+| 固件升级 | OTA 协议 | ✅ 已实现 | - |
+| 发现广播响应 | 订阅 `broadcast/dtu/discover` 并上报注册 | ✅ 已实现 | DTU 须按 customer_id 过滤 |
+| 批量配置响应 | 订阅 `broadcast/dtu/config` 并应用配置 | ✅ 已实现 | 须支持版本幂等和校验 |
+| 温湿度采集 | I2C/RS485 | ✅ 已实现 | - |
+
+### 8.3 业务系统侧
+
+| 功能 | 说明 |
+|------|------|
+| 设备注册 | 接收 DTU 注册,维护 `panel_id` 与 `customer_id` 映射关系 |
+| 状态监控 | 接收心跳,维护在线列表,检测遗嘱消息 |
+| 事件处理 | 接收端口事件,记录跳线变更历史 |
+| 告警处理 | 接收 DTU 非法告警,结合自身规则引擎通知相关人员 |
+| 映射同步 | 通过 `SYNC_*` 指令向 DTU 推送正确的端口映射基线 |
+| 控制指令 | 下发控制命令,接收响应 |
+
+---
+
+## 9. 总结
+
+| 层级 | 核心功能 | 协议 |
+|------|----------|------|
+| **配线架** | RFID 读卡 + LED 指示 | MODBUS-RTU |
+| **DTU** | 端口检测 + 非法告警 + MQTT 上报 | MODBUS-RTU + MQTT |
+| **业务系统** | 数据存储 + 告警处理 + Web 展示 | MQTT 订阅 |
+
+### 功能覆盖
+
+- ✅ 端口连接/断开检测(DTU 状态机)
+- ✅ 跳线 RFID 识别(配线架 + MODBUS)
+- ✅ LED 指示控制(配线架 + MODBUS,语义已统一)
+- ✅ 串口屏显示(DTU 内部显示模块)
+- ✅ 远程数据上报(DTU + MQTT)
+- ✅ 远程控制指令(MQTT + DTU)
+- ✅ 远程固件升级(OTA)
+- ✅ 心跳与在线状态(MQTT LWT,格式已统一)
+- ✅ 端口映射同步(`SYNC_*` 指令)
+- ✅ 非法告警(DTU expected vs actual 比对)
+- ✅ 温湿度监测(DTU 环境传感器)
+- ✅ DTU 发现广播响应(`broadcast/dtu/discover`)
+- ✅ DTU 批量配置响应(`broadcast/dtu/config`)
+
+---
+
+*文档版本: v2.0*  
+*更新时间: 2026-06-17*  
+*修订说明: 统一字段语义、修正状态机与 LED 控制、明确 DTU 与业务系统协议边界、补充每个字段释义。*