From 8eb2b7c68c0a11766fc6adb75eb3c3571b31d078 Mon Sep 17 00:00:00 2001
From: 12451 <12451@example.com>
Date: Mon, 21 Sep 2026 13:41:17 +0800
Subject: [PATCH] =?UTF-8?q?docs(protocol):=20hex=20=E5=AD=97=E9=9D=A2?=
=?UTF-8?q?=E9=87=8F=E5=AD=97=E6=AF=8D=E5=85=A8=E9=83=A8=E5=A4=A7=E5=86=99?=
=?UTF-8?q?=EF=BC=88=E5=90=AB=207=20=E5=A4=84"=E5=BB=BA=E8=AE=AE=E5=B0=8F?=
=?UTF-8?q?=E5=86=99"=E7=9A=84=E8=A7=84=E8=8C=83=E8=A1=A8=E8=BF=B0?=
=?UTF-8?q?=E8=AE=A2=E6=AD=A3=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 用户要求
「tcp 文档里的 hex 部分字母全部要大写」。
## 改法(⛔ 不是全局替换 a-f → A-F)
全局替换会误伤英文正文(face / define 里都有 a-f)。先普查再定向改,
只处理**明确是 hex 字面量**的三种形态,且仅当确实含小写时才改写:
| 形态 | 改前普查 | 改写处数 |
|---|---|---|
| JSON 字符串 hex `"828255152c01f40143"` | 95 处 / 16 种,含小写 6 种 | 24 |
| 反引号 hex `` `3957b959` `` | 47 处 / 30 种,含小写 7 种 | 11 |
| `0x` 前缀 | 21 处 / 9 种,**已全大写** | 0 |
共 **35 处**。危险面(2 位字母数字,可能误伤正文)普查为 **0**;
`864720562557021`(IMEI) / `8986012520983901841`(ICCID) 纯数字,不受影响。
## 大小写安全性(改前已核实)
平台侧解析全部走 `u8::from_str_radix(s, 16)`,**大小写不敏感**:
- `report_worker.rs:838/860/1152/1270/2401`
- `routes/mod.rs:637`、`routes/cabinet_tree.rs:1086`
故文档改大写**不影响设备上报与平台解析**。
## 验证:逐行证明"只是大小写"
git diff 共 28 行改动(订正规范表述前),剥掉行首 +/- 后与原行
**逐行做大写比对,非大小写差异行数 = 0** —— 证明没有误伤任何内容。
## ⚠️ 机械替换破坏了一处对照性表述(已修)
原 L584 是**故意的大小写对照**:
> 平台解析**不区分大小写**(`0e10` 与 `0E10` 等价);**建议固件统一小写**
被机械替换后两边都成 `0E10`,对照失效。已改为:
> 平台解析**不区分大小写**(`0e10` 与 `0E10` 等价,本文示例统一用**大写**);**建议固件也统一用大写**
并复查全文档:无其它同类被破坏的对照。
## 顺带订正 7 处矛盾规范
文档里还有 7 处**规范性表述仍写"小写"**,与现在的全大写示例直接矛盾
(这是交付给固件的指令,不能留矛盾):
| 位置 | 原文 |
|---|---|
| `sign` 字段表 | 8 位小写十六进制签名 |
| 签名算法伪码 | hex = 小写十六进制字符串(64 字符) |
| 签名要求 | `sign` 为 8 位小写十六进制字符 |
| 固件接入自查项 | `status1`/`status2`(hex 2 位小写) |
| 常见错误表 | hex 字符串、固定 2 位小写 |
| 签名步骤 | 8 位小写十六进制 |
| C 代码注释 | SHA256(...) 的小写 hex |
统一改为"十六进制(本文示例用**大写**)"。`snake_case`、`err` 英文小写
与 hex 无关,未动。
## 复核
- 残留含小写的 hex 字面量:**1 处**,即上面故意保留的 `0e10` 对照示例;
- 全部目标值已到位(`828255152C01F40143` 8 处、`80825515180320012A` 3 处、
`80005F15180000012A` 3 处、`81026415540000012A` 5 处、`269C249D` 7 处、
`3957B959` 4 处、`012A0128` 2 处、`ABCD1234` 1 处)。
## 同批发布
官网移动端 canvas 防黑边兜底(`.stage canvas` 铺浅色背景)已发布并实测生效
(线上 HTML 含 `linear-gradient(155deg`)。GSAP 技能按用户要求**不装**;
表号按用户要求**不管**。
---
docs/protocol/柜控板固件对接文档.md | 72 ++++++++++++++---------------
1 file changed, 36 insertions(+), 36 deletions(-)
diff --git a/docs/protocol/柜控板固件对接文档.md b/docs/protocol/柜控板固件对接文档.md
index 8ba5bd6..e535ffa 100644
--- a/docs/protocol/柜控板固件对接文档.md
+++ b/docs/protocol/柜控板固件对接文档.md
@@ -191,7 +191,7 @@
| `err` | string | 仅 `suc=0` 的应答,**必带** | 异常原因 |
| `id` | string | 见各指令 | 寻址标识(**动作目标**),**不含 IMEI**,见 3.7 |
| `timestamp` | uint64 | 仅 `login` | 秒级 Unix 时间戳 |
-| `sign` | string | 仅 `login` | 8 位小写十六进制签名 |
+| `sign` | string | 仅 `login` | 8 位十六进制签名(本文示例用**大写**;平台解析不区分大小写) |
| `auth_str` | string | 仅 `auth_str` 的应答 | 8 位安全码 `[A-Za-z0-9]`,**仅下发一次**,须持久保存 |
| `spec` | string(4 位) | 仅 `login` 的应答 | **柜子规格**:前 2 位 = 仓控板数、后 2 位 = 每板通道数。`"0606"` = 6 板 × 每板 6 通道。**由平台按该柜已配置拓扑在每次 `login` 时现算**,**恒为 4 位数字、不会是 `"0000"`**(见 5.2) |
| `work_mode` | string | 仅 `login` 的应答 | **充电工作模式**:`"0"`=平台指令模式(默认)/ `"1"`=自动充电工作模式。**平台侧可配置**,见 5.2 |
@@ -483,7 +483,7 @@
### 5.2 `login` —— 登录验签
```json
-→ {"act":"login","msg_id":2,"dev_id":"864720562557021","sign":"269c249d","timestamp":1750000000,"iccid":"8986012520983901841"}
+→ {"act":"login","msg_id":2,"dev_id":"864720562557021","sign":"269C249D","timestamp":1750000000,"iccid":"8986012520983901841"}
← {"act":"login","msg_id":2,"suc":1,"spec":"0606","work_mode":"0"}
```
@@ -534,9 +534,9 @@
→ {"act":"status_post","msg_id":3,"rssi":85,
"cabinet":{"status1":"00","status2":"00","temp":29800,"meter":125500},
"boards":[
- {"board":1,"channels":["828255152c01f40143","008200000000000000",
- "828255152c01f40143","008200000000000000",
- "828255152c01f40143","008200000000000000"]},
+ {"board":1,"channels":["828255152C01F40143","008200000000000000",
+ "828255152C01F40143","008200000000000000",
+ "828255152C01F40143","008200000000000000"]},
{"board":2,"channels":["008200000000000000","008200000000000000",
"008200000000000000","008200000000000000",
"008200000000000000","008200000000000000"]}
@@ -581,7 +581,7 @@
| 项 | 规则 |
|---|---|
| 形式 | **字符串数组**,每个元素 = **恰好 18 个 hex 字符** |
-| **大小写** | 平台解析**不区分大小写**(`0e10` 与 `0E10` 等价);**建议固件统一小写** |
+| **大小写** | 平台解析**不区分大小写**(`0e10` 与 `0E10` 等价,本文示例统一用**大写**);**建议固件也统一用大写** |
| **空仓** | **byte0 = `00`(无电池)**;**byte1 照实报电源状态**(电源与是否有电池**无关**);byte2–8 数值全 `0`。
例:空仓但电源正常 → `"008200000000000000"` |
| **⚠️ 两个状态相互独立** | **电池状态(byte0)与电源状态(byte1)是两回事**,平台前端也**分两个指示分开显示**——不要因为"没电池"就把 电源状态 写成 0 |
| **顺序(通道号怎么来)** | **通道号由数组下标决定,下标从 0 开始**:**第 1 个元素(下标 0)= 通道 1**,第 2 个元素(下标 1)= 通道 2 …… 依此类推。**数组顺序不得打乱** |
@@ -604,7 +604,7 @@
**解析示例**(取自硬件文档的示例值)
```
-"828255152c01f40143"
+"828255152C01F40143"
82 电池状态 = 有电池 + 正在充电(bit7 + bit1)
82 电源状态 = 有电源 + 电源打开(bit7 + bit1)
55 SOC = 85%
@@ -676,8 +676,8 @@
"battery_id":"SN123456",
"brand_code":"1","model_code":"1","material_code":"1",
"cell_count":15,"rated_voltage":48000,"rated_capacity":24000,
- "info":"80825515180320012a",
- "cells":"0e100e150e0e0e120e170e0b0e110e140e0f0e130e160e0d0e100e120e0e0e170e0b012a0128"
+ "info":"80825515180320012A",
+ "cells":"0E100E150E0E0E120E170E0B0E110E140E0F0E130E160E0D0E100E120E0E0E170E0B012A0128"
}}
← {"act":"bat_in","msg_id":5,"suc":1}
```
@@ -780,7 +780,7 @@ MCU 约定:**先 `off` 再 `bat_out`**。
```json
→ {"act":"bat_out","msg_id":6,"id":"01-3",
- "battery":{"battery_id":"SN123456","info":"80005f15180000012a"}}
+ "battery":{"battery_id":"SN123456","info":"80005F15180000012A"}}
← {"act":"bat_out","msg_id":6,"suc":1}
```
@@ -860,11 +860,11 @@ MCU 约定:**先 `off` 再 `bat_out`**。
```json
→ {"act":"off","msg_id":8,"id":"01-3",
- "battery":{"battery_id":"SN123456","info":"81026415540000012a"}}
+ "battery":{"battery_id":"SN123456","info":"81026415540000012A"}}
← {"act":"off","msg_id":8,"suc":1}
```
-> 上例 `info="81026415540000012a"` 解析:byte0 `81` = 有电池 + 充满;byte1 `02` = 电源打开;SOC `64` = 100%;电压 `1554` = 5460×0.01V = 54.60V;电流 `0000`;温度 `012a` = 298K。(位定义见 **3.8**;**以上均为 hex 内的原始硬件值,平台再按 3.1 换算**)
+> 上例 `info="81026415540000012A"` 解析:byte0 `81` = 有电池 + 充满;byte1 `02` = 电源打开;SOC `64` = 100%;电压 `1554` = 5460×0.01V = 54.60V;电流 `0000`;温度 `012A` = 298K。(位定义见 **3.8**;**以上均为 hex 内的原始硬件值,平台再按 3.1 换算**)
**`off` 是「订单结束」的触发之一**(另一个是 `bat_out`)。
@@ -925,21 +925,21 @@ MCU 约定:**先 `off` 再 `bat_out`**。
```
input = dev_id + auth_str + timestamp // 三段直接拼接,无分隔符,无空格
hash = SHA256(input) // 32 字节
-hex = 小写十六进制字符串(64 字符)
+hex = 十六进制字符串(64 字符,本文示例用**大写**)
sign = hex[56..64] // 第 57~64 个字符,共 8 位
```
- `dev_id`:IMEI 字符串
- `auth_str`:`auth_str` 接口返回的 8 位安全码,原样拼接
- `timestamp`:本次 `login` 发送的秒级时间戳,按十进制数字拼接
-- **`sign` 为 8 位小写十六进制字符**
+- **`sign` 为 8 位十六进制字符**(本文示例用**大写**)
### 6.2 自测向量(可直接用于固件单元测试)
| dev_id | auth_str | timestamp | SHA256(input) | 期望 sign |
|--------|----------|-----------|---------------|-----------|
-| `123456789012345` | `abcd1234` | `1719800000` | `23e2f229ca7cd67cc1c74a0219aa69cfbf2b267b1974a922ddae6e453957b959` | **`3957b959`** |
-| `864720562557021` | `Ab3xY9Qz` | `1750000000` | `a44d44fbbf7bccffc5c5068d987ea4e89f17e9a9bae865635921afcc269c249d` | **`269c249d`** |
+| `123456789012345` | `ABCD1234` | `1719800000` | `23E2F229CA7CD67CC1C74A0219AA69CFBF2B267B1974A922DDAE6E453957B959` | **`3957B959`** |
+| `864720562557021` | `Ab3xY9Qz` | `1750000000` | `A44D44FBBF7BCCFFC5C5068D987EA4E89F17E9A9BAE865635921AFCC269C249D` | **`269C249D`** |
> 固件自测时把这两个输入喂进签名函数,输出必须逐字符相等。**任何一位偏差都会导致登录失败。**
@@ -962,14 +962,14 @@ sign = hex[56..64] // 第 57~64 个字符,共 8 位
固件保存安全码
【上电 / 断线重连后】
-→ {"act":"login","msg_id":2,"dev_id":"864720562557021","sign":"269c249d","timestamp":1750000000,"iccid":"..."}
+→ {"act":"login","msg_id":2,"dev_id":"864720562557021","sign":"269C249D","timestamp":1750000000,"iccid":"..."}
← {"act":"login","msg_id":2,"suc":1,"spec":"0606","work_mode":"0"}
连接转为已登录
【周期上报】每 60 秒(全量)
→ {"act":"status_post","msg_id":3,"rssi":85,
"cabinet":{"status1":"00","status2":"00","temp":29800,"meter":125500},
- "boards":[{"board":1,"channels":["828255152c01f40143","008200000000000000", ...]}, ...]}
+ "boards":[{"board":1,"channels":["828255152C01F40143","008200000000000000", ...]}, ...]}
← {"act":"status_post","msg_id":3,"suc":1}
```
@@ -1023,12 +1023,12 @@ sign = hex[56..64] // 第 57~64 个字符,共 8 位
【充满,设备主动上报】
→ {"act":"off","msg_id":6,"id":"01-3",
- "battery":{"battery_id":"SN123456","info":"81026415540000012a"}}
+ "battery":{"battery_id":"SN123456","info":"81026415540000012A"}}
← {"act":"off","msg_id":6,"suc":1}
【电池拔出】
→ {"act":"bat_out","msg_id":7,"id":"01-3",
- "battery":{"battery_id":"SN123456","info":"80005f15180000012a"}}
+ "battery":{"battery_id":"SN123456","info":"80005F15180000012A"}}
← {"act":"bat_out","msg_id":7,"suc":1}
平台结束订单
```
@@ -1097,7 +1097,7 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
**签名与登录**
- [ ] `auth_str` 返回 8 位 `[A-Za-z0-9]`
-- [ ] 用 6.2 节向量验签,期望值逐字符相等(`3957b959` / `269c249d`)
+- [ ] 用 6.2 节向量验签,期望值逐字符相等(`3957B959` / `269C249D`)
- [ ] `sign` 取 **8 位**(`hex[56..64]`)
- [ ] `timestamp` 为**秒**级,与平台时差在 **±60 秒内**
- [ ] `login` 成功收到 `spec`、`work_mode`;`spec` **恒为 4 位数字、不会是 `"0000"`**(平台按该柜拓扑生成,拓扑未配置时按柜型兜底)
@@ -1114,7 +1114,7 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
- [ ] 超时后迟到的应答被丢弃、不补配;已作废的 `msg_id` 不被立即复用
**状态上报**
-- [ ] `cabinet.status1` / `status2`(hex 2 位小写)与通道 hex 的 **byte0 / byte1** 均按 **3.8** 位定义构造
+- [ ] `cabinet.status1` / `status2`(hex 2 位,本文示例用**大写**)与通道 hex 的 **byte0 / byte1** 均按 **3.8** 位定义构造
- [ ] 每个通道 hex **恰好 18 个字符**、多字节**大端**;**通道号由数组下标决定(第 1 个元素 = 通道 1)**,数组顺序不得打乱
- [ ] **数值口径**(见 **3.1** 统一规则):`cabinet.temp` 送 **K×100**、`cabinet.meter` 送 **Wh**(**设备换算**);`channels[]` / `info` / `cells` 一律是 **hex 原始硬件单位**(**4G 层零换算**,换算由平台做)
- [ ] 上报间隔 **固定 60 秒/次**,且每次为**全量**(所有仓控板 × 全部通道,无增量、无分片)
@@ -1139,7 +1139,7 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
|---|------|--------|
| 1 | **接入地址** | 测试 **`118.31.119.206:3001`**;正式 **`118.31.119.206:9001`**。TCP 由平台 TCP 服务直接监听,**不经域名/nginx** |
| 2 | **`status_post` 的板号与通道** | `boards[].board` 取 **1–15**(当前 36 仓柜型实装 6 块,按 `spec` 前 2 位上报)。
**`channels` 是字符串数组**(每个元素 = 该通道的 **18 字符 hex**),**元素本身没有 `channel` 字段** —— **通道号由数组下标决定**:**第 1 个元素(下标 0)= 通道 1**,第 2 个元素(下标 1)= 通道 2 ……,数组长度 = 每板通道数(当前柜型为 6,即 1–8 / 1–15 是产品设计冗余),**顺序不得打乱** |
-| 3 | **状态字节格式** | 柜体状态 2 字节(`cabinet.status1` / `status2`)用 **hex 字符串、固定 2 位小写**;通道状态则**整体为一串 18 字符 hex**(其中 **byte0 = 电池状态、byte1 = 电源状态**)。位定义与解析示例见 **3.8**;通道 hex 的字节布局见 **5.3**。保留位填 0 或 1 平台均忽略 |
+| 3 | **状态字节格式** | 柜体状态 2 字节(`cabinet.status1` / `status2`)用 **hex 字符串、固定 2 位**(本文示例用**大写**);通道状态则**整体为一串 18 字符 hex**(其中 **byte0 = 电池状态、byte1 = 电源状态**)。位定义与解析示例见 **3.8**;通道 hex 的字节布局见 **5.3**。保留位填 0 或 1 平台均忽略 |
| 4 | **上报节奏与内容** | **`status_post` 固定每 60 秒一次,每次全量上报**(所有仓控板 × 全部通道;不区分空闲/充电,不做增量、不分片)。任意上行间隔必须 < 300 秒(平台 300 秒无上行即判离线) |
| 5 | **柜门与充电的关系** | **本产品为工厂用集中充电柜,柜门开 / 关是独立动作,与充电逻辑互不联动**:开门不触发充电;充电启停也不依赖柜门状态。柜门实际状态由 `status_post` 的 `cabinet.status1` bit4 上报(0=关闭 / 1=打开) |
| 6 | **柜门"开 / 关"的实现方式** | **开**:平台下发 `open`(**解锁**),门由**人拉开**——门不会自动打开;**关**:**人手动关闭**。本协议**不提供"关门"指令,任何柜型均如此**。开 / 关状态都经 `status_post` 回传,**最大延迟 60 秒**,平台侧不得当作实时量 |
@@ -1161,7 +1161,7 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
## 附:本文档采用的约定(如有异议请提出)
-1. `sign` 取 `hex[56..64]`,**8 位**小写十六进制
+1. `sign` 取 `hex[56..64]`,**8 位**十六进制(本文示例用**大写**)
2. `suc`:`1`=正常,`0`=异常,`suc=0` 必须带 `err`
3. 应答**必带 `act`**,值等于被应答报文的 `act`
4. `msg_id` 在单条 TCP 连接内由各方向独立自增,应答原样回带;**两个方向的编号允许重叠,不需要约定号段**
@@ -1224,8 +1224,8 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
```jsonc
→ {"act":"login","msg_id":2,"dev_id":"864720562557021",
- "sign":"269c249d","timestamp":1750000000,"iccid":"8986012520983901841"}
-// sign : SHA256(dev_id + auth_str + timestamp) 的小写 hex,取 hex[56..64] = 8 位
+ "sign":"269C249D","timestamp":1750000000,"iccid":"8986012520983901841"}
+// sign : SHA256(dev_id + auth_str + timestamp) 的 hex,取 hex[56..64] = 8 位(本文示例用大写)
// timestamp : 秒级 Unix 时间戳(不是毫秒),与平台时差须在 ±60 秒内
// iccid : 可选;上报后平台自动比对,SIM 卡变更会被记录
@@ -1262,9 +1262,9 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
```jsonc
→ {"act":"status_post","msg_id":3,"rssi":85,
"cabinet":{"status1":"00","status2":"00","temp":29800,"meter":125500},
- "boards":[{"board":1,"channels":["828255152c01f40143","008200000000000000",
- "828255152c01f40143","008200000000000000",
- "828255152c01f40143","008200000000000000"]}]}
+ "boards":[{"board":1,"channels":["828255152C01F40143","008200000000000000",
+ "828255152C01F40143","008200000000000000",
+ "828255152C01F40143","008200000000000000"]}]}
// rssi : 4G 信号强度,整数 0–99(不是 dBm,不可为负)
// cabinet : 柜体状态,柜级只带一次
// status1 / status2 → 柜体状态 2 字节(hex 2 位,位定义见 §3.8)
@@ -1321,8 +1321,8 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
"battery_id":"SN123456",
"brand_code":"1","model_code":"1","material_code":"1",
"cell_count":15,"rated_voltage":48000,"rated_capacity":24000,
- "info":"80825515180320012a",
- "cells":"0e100e150e0e0e120e170e0b0e110e140e0f0e130e160e0d0e100e120e0e0e170e0b012a0128"}}
+ "info":"80825515180320012A",
+ "cells":"0E100E150E0E0E120E170E0B0E110E140E0F0E130E160E0D0E100E120E0E0E170E0B012A0128"}}
// id : 两段【仓控板-通道】(仓控板补零 01–15、通道 1–8 不补零),段数必须为 2
// battery_id: 电池序列号(ASCII ≤30),电池唯一标识;**BMS 读不到时传空串 ""**(不影响充电)
// brand_code / model_code / material_code
@@ -1332,7 +1332,7 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
// rated_voltage : mV(48000 = 48.0V)——【JSON 字段,设备按硬件 0.1V ×100 换算后上报】
// rated_capacity : mAh(24000 = 24.0Ah)——【JSON 字段,设备按硬件 0.1AH ×100 换算后上报】
// info : **18 个 hex 字符** = 该通道的 9 字节电池信息(布局同 §5.3)
-// "80825515180320012a" 解析:
+// "80825515180320012A" 解析:
// byte0 80 = 有电池、尚未充电(位定义见 §3.8)
// byte1 82 = 有电源 + 电源打开(位定义见 §3.8)
// SOC 55 = 85% 电压 1518 = 54.00V
@@ -1354,7 +1354,7 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
```jsonc
→ {"act":"bat_out","msg_id":6,"id":"01-3",
- "battery":{"battery_id":"SN123456","info":"80005f15180000012a"}}
+ "battery":{"battery_id":"SN123456","info":"80005F15180000012A"}}
// MCU 约定:先发 off,再发 bat_out
// battery 至少带 battery_id,建议带末帧动态 info(18 字符 hex,规则同 §5.3;hex 内为原始硬件值,平台换算)
@@ -1383,8 +1383,8 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
```jsonc
→ {"act":"off","msg_id":6,"id":"01-3",
- "battery":{"battery_id":"SN123456","info":"81026415540000012a"}}
-// info="81026415540000012a" → byte0 81 = 有电池 + 充满;byte1 02 = 电源打开;
+ "battery":{"battery_id":"SN123456","info":"81026415540000012A"}}
+// info="81026415540000012A" → byte0 81 = 有电池 + 充满;byte1 02 = 电源打开;
// SOC 64 = 100%;电压 1554 = 54.60V(位定义见 §3.8)
// 不带 suc → 这是设备【新发起】的上报(不是对平台指令的回执)
// → 平台结束订单并回包
@@ -1528,5 +1528,5 @@ t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完
| 版本 | 日期 | 变更内容 |
|------|------|----------|
| **v1.1.1** | **2026-09-20** | ㊳ **交付前文档一致性修正(第二轮,逐条回代码核对)—— 只改文档自身的错误 / 矛盾 / 缺失说明,协议行为(`act`、字段名、位定义、签名、`id` 规则、上报节奏、超时语义)一字未动**:
① **§7.3 时序图纠错**:`bat_in` 后的说明"平台创建充电订单"与 §5.5 / §5.7 / §9 / 附 A.3.1 矛盾(`bat_in` **不建单**,建单唯一入口是 `on`)→ 改为"登记/更新电池台账(不建单)"。
② **登录门控补全"下行"方向**(§2.2 表格重写、§2.2 新增双向规则说明、§4.2 前置条件、§8.1 末尾、§8.3、§9 清单、§10 第 10 条、附 A.5 头部、附 A.1.2 注释):原表述只覆盖"上行丢弃",未说明**平台在未登录连接上也不投递任何下行报文**;现明确"**`login` 成功前设备收不到任何字节(唯一例外是 `auth_str`/`login` 自己的应答)**",与 §4.1 的同一规则对齐。
③ **§3.1 纠错**:删去"平台应答中会额外附带 `id`"——**平台应答不带 `id`**(§5.8 / §8.3 与实现一致),原句与 §8.3 自相矛盾。
④ **§2.3 纠错**:"平台对每条上行都会回 `suc=1`"改为"对每条**请求类**上行回包;**回执类上行不回包**",消除与 §3.5 / §4.1 的矛盾。
⑤ **§4.1 表格纠错**:第 7 / 8 / 9 条的"是否必带 `id`"由"回带原 `id`"改为"**上报形态:是(仓体级);回执形态:可选**"——`open`/`pow_on`/`pow_off`/整柜 `reset` **本就无 `id`**,原写法不可能满足(与 §3.7 / §5.8 对齐)。
⑥ **§3.3 `invalid id` 语义补全**:除"格式非法"外,**格式合法但该 `id` 在平台上查不到**(该柜未配置该板/通道)也回同一个 `err`,并给出固件应对。
⑦ **§2.2 新增"同一 IMEI 只保留一条连接"**:新连接会**接管并断开旧连接**,旧连接收不到任何通知报文(固件不得并存两条连接);§9 清单同步加一条自测项。
⑧ **§8.1 新增一行"平台内部处理异常"**:数据库/缓存故障时该条报文可能得不到任何应答,固件按 60 秒超时处理、不重发业务动作;并补充"平台侧静默不投递下行"的表现说明。
⑨ **§5.3 示例标注"截断写法"**(只列了 2 块板,实际须按 `spec` 上报全部板 × 全部通道)、附 A.2.1 注释同步;**附 A.3.1 修正 JSON 多出一个 `}`**、**附 A.8 去掉 `...` 使报文可直接解析**;附 A.6 头部与 A.6.1 注释措辞修正(回执必带 `suc`;`id` 为可选)。
⑩ **§2.2 / §5.2 三处"会话中保存的安全码"改为"平台侧保存的该设备安全码(持久保存)"**,与 ⑨「DB 权威 + Redis 缓存」一致,避免被误读为随连接失效。
⑪ **文首新增「这 7 项的平台侧落地情况」表**:明确第 **13 / 17 / 18** 项的平台侧行为**已落地**、第 **14 / 15** 项平台侧已按"原值透传 / 只取 SOC"实现、第 **16 / 19** 项**仍未定**(保持"待确认")。
⑫ 文首版本号与本文档自检(围栏配对 / 表格列数 / 报文 JSON 可解析)同步更新。 |
-| v1.1 | 2026-09-14 | ① 签名定稿为 **8 位**(`hex[56..64]`),自测向量更新为 `3957b959` / `269c249d` ② 明确 `act` 双向必带、`suc=0` 必带 `err` ③ 报文性质判定改为**按有无 `suc`**,配对要求 `msg_id` + `act` 双条件 ④ 明确**每个交互 60 秒超时、超时作废、迟到应答丢弃、不重试** ⑤ 新增 `login` 失败分类处理与**安全码自愈闭环**(重置后设备自动重新申领)⑥ 接入端口定稿:测试 `3001`、正式 `9001`,明确 TCP **不经 nginx/域名** ⑦ 第 10 节遗留项全部定稿;新增附 A 带注释报文示例 ⑧ **`login` 失败后平台主动断开 TCP 连接**,设备须重连后重新握手 ⑨ 安全码存储明确为 **DB 权威 + Redis 缓存**(缓存未命中回源 DB,Redis 故障不再导致设备无法登录)⑩ 上报节奏定稿:`status_post` **固定每 60 秒一次、每次全量**(取消原"空闲 60s / 充电 15s"与按板分片设想);单行上限 **4096 → 65536 字节** ⑪ **上报字段按硬件权威数据字典重构**:`status_post` 改为 `cabinet`(柜体状态2 字节 + 柜温 + 电表)+ `boards[].batteries[]`(2 个状态字节 + SOC/电压/电流/温度);`bat_in` / `bat_out` / `off` 的 `bms` 改为 `battery`(属性 + 动态 + 电芯);新增 **§3.8 状态字节 hex 解析表**;`id` 量程改为仓控板 **1–15** / 电池组 **1–8**;`soh` / `power` 移除 ⑫ **柜门整章定稿**:柜门与充电**完全解耦**(§7.2/§7.3 拆分)、`open` 明确为**解锁**(门由人拉开)、**无"关门"指令(任何柜型)**、柜门状态经 `status_post` ≤60 秒回传;`rssi` 口径由 dBm 改为 **0–99 信号等级** ⑬ **挂起 4 项待确认**(`pow_fail` `info` 语义、`energy` 数据源、代码表取值内容、电芯数据字节数)并在**文首新增「⚠️ 待确认事项」**集中列出;删除内部重复文档 `docs/柜控板-平台TCP接口文档.md` ⑭ **`login` 成功应答新增 `spec` 与 `work_mode`**:`spec`(4 位,前 2 位仓控板数 + 后 2 位每板通道数,当前柜型 `"0606"`)作为**柜子规格/型号**由平台下发;`work_mode`(`"0"`=平台指令模式 / `"1"`=自动充电工作模式)下发**充电工作模式**,固件据此决定是否等平台 `on` 指令——**平台侧可配置、后台可改**,每次 `login` 以最新配置为准 ⑮ **新增连接登录门控**:**只有 `auth_str` / `login` 可在未登录状态发送**,其余 `act` 必须先 `login` 成功,否则平台丢弃且不回包;**未登录的 TCP 连接不予保持**(建连后 30 秒未 `login` 成功即主动断开)。同时 `err` 文案全部改为**英文小写短语**(§3.3 新增取值全集表),`id` 段数规则明确(一段=柜级 / 两段=仅 `reset` 单板 / 三段=仓体级)**(⚠️ 该 `id` 段数规则与最终定稿完全相反,已被 ㉛ 修正并经 ㉟ 确认,作废;当前生效规则见 §3.7)** ⑯ **补充 `pow_on` / `pow_off` 业务语义**:远程通断柜内总电(接触器)——**`pow_off` 分闸后仓控板随之下电**(仓体/电池数据不可用,并因此产生 `pow_fail` `info:"0"` 直流停电),属运维动作;`pow_fail` 与 `cabinet.status1` bit0 的关系(事件 vs 状态)在 §5.4 说明 **(⚠️ 本条中的 `pow_fail` / `info:"0"` 已被 ⑱ 取代,作废)** ⑰ **`pow_fail` 的 `info` 区分定稿**(判定依据 = **"谁掉电"**):`"0"`=直流停电(**接触器分闸**→仓控板下电,柜控板仍有电)/ `"1"`=交流停电(**柜控板自身断电**,靠**板载电容余电抢发最后一条**,与接触器无关);该条由「待确认」转入「约定定稿」第 11 条,待确认事项由 4 项缩减为 **3 项** **(⚠️ 已被 ⑱ 取代,作废:`pow_fail` 与 `info` 字段均已取消)** ⑱ **停电上报更正为两条独立指令**:**取消 `pow_fail`(及 `info` 字段)**,改为 **`pow_fail_dc`**(直流停电:接触器分闸→仓控板下电,柜控板仍有电)与 **`pow_fail_ac`**(交流停电:柜控板自身断电,靠板载电容余电抢发最后一条,与接触器无关)两条独立 `act`,**均无附加字段**;`act` 名与平台 Redis 字段 `pow_fail_dc` / `pow_fail_ac` 一一对应。代码侧 `report_worker` 分发与 `handle_pow_fail` 同步改为按 `act` 判定 ⑲ **新增待确认第 12 项**(**该编号后调整为第 16 项,见文首「待确认事项」**):**仓控板离线(485 芯片坏 / 线松 / 掉电)如何上报** —— **4G 层读不到该板数据时该怎么报尚未约定**(省略该板?照报全 0 的板?另有在线标志?),**以硬件答复为准、本文不预设结论**(§5.3 同步加警示) ⑳ **`login` 失败统一为单一应答**:取消 `invalid timestamp` / `auth_str not found`,**只返回 `invalid signature`**(不区分失败原因,避免外部试探系统状态——安全设计);同时修正 `spec` 的定位为「平台对柜子的规格定义(型号)」,**与仓控板实际能否通信无关** **(⚠️ 其中"型号"的措辞已在 ㊲ 更正:`spec` 由平台按拓扑现算、**不承诺"型号存储"**,见 §5.2 / §10 第 9 条)**;`work_mode` 明确**仅两种、不再扩展** ㉑ **`status_post` 字段改名**:`batteries[]` → **`channels[]`**、内层 `group` → **`channel`**(那是**仓板的通道**、可能空着,不是纯电池信息;同时与平台侧 `channel_id` 对齐)**(⚠️ 该 `channel` 字段已在 ㉒ 随之取消:`channels[]` 改为**字符串数组**,元素内不含任何字段名,**通道号由数组下标决定**,见 §5.3 / §10 第 2 条)**;`id` 第三段说明改为「通道」;全文「电池组」按语境改为「通道」(硬件表名「电池组属性」保留) ㉒ **通道状态改为 hex 透传(大幅瘦身)**:`status_post.channels[]` 由"每通道 6 个命名字段"改为 **每通道 18 个 hex 字符**(9 字节原样拼接,**4G 层零转换**),**数组下标 = 通道号**(**精确定义:数组下标从 0 开始,第 1 个元素 = 通道 1**,见 §5.3 / §10 第 2 条);`bat_in`/`bat_out`/`off` 的动态数据同步改为 `info`(同一种 18 字符 hex);定字节布局与**大端**规则,§3.8/§5.3/§5.5/§7/§9/§10/附A 全面同步。体积:6 板 × 6 通道由 ~3.4KB 降至 **~0.8KB** ㉓ **修正空仓写法**:**空仓 ≠ 全 0** —— byte0 = `00`(无电池)但 **byte1 照实报电源状态**(电源与有无电池无关),例 `"008200000000000000"`;明确**电池状态与电源状态是两个独立维度**,平台前端**分两个指示分开显示** ㉔ **字段改名**:`bat_in`/`bat_out`/`off` 里的电池数据字段 `dyn` → **`info`**(它是**这块电池的信息**,不是"充电数据");同时明确 hex **大小写不敏感**(平台两种都解析,建议固件统一小写),示例 hex 全部改小写 ㉕ **`cells` 同样改拼接 hex**:由"JSON 数组 + 4 个统计字段"改为**整段拼接**的 hex 字符串,长度 = **`cell_count × 4 + 16`**(N 节电压 + 最高/最低电压 + 最高/最低温度,大端),15 节 = 76 字符、20 节 = 96、22 节 = 104(**与硬件文档「52 字节」吻合**) ㉖ **`battery_id` 允许为空串**:BMS 读不到序列号时传 `""`,**不影响充电与订单记录**;平台侧 `battery_id` 为空时**不做"换电池"判断、跳过电池台账**,订单照常创建(只是不知道是哪块电池) ㉗ **`bat_in` 重复上报不新建订单**:明确 **`bat_out` 是订单结束标志**——没有 `bat_out` 就说明订单还在进行;因此该通道已有活跃订单且 `battery_id` **相同**(含两边都为空串)时**不新建订单**,仅当 `battery_id` **不同且都非空**才 `forced` 结束旧订单并新建。**依据:一个通道只可能有一块电池**,`bat_in` 带不同 `battery_id` 必然是换了电池 → 老订单必须先结束(⚠️ 该条后续在 ㉝ 修正:`bat_in` 最终定为**与订单无关**,建单只在 `on`) ㉘ **新增订单超时兜底**:设备可能**停电/离线期间电池被取走**而永远发不出 `bat_out` → 平台侧**充电订单"进行中"超过 10 小时即自动结束**(`reason='timeout'`,依据"充电不可能超过 10 小时");固件**不必补报历史事件** ㉙ **补充两条待确认(第 18/19 项)**:**① 人在没充满时强制拔电池**——MCU 能否检测、能否保证"先 `off` 再 `bat_out`"(§5.6 已把该顺序列为**硬性要求**);**② 停电恢复后如何兜底**——掉电期间事件无法上报,恢复后是补报、还是以当前状态为准由平台对账 ㉚ **报文瘦身:`dev_id` 只在 `auth_str` / `login` 带** —— 这两条用于把 TCP 连接与设备绑定,**之后所有报文都不再带 `dev_id`**(设备身份由连接确定;断开重连才需重新 `login`)。同步更新 §3.1 字段总表、§5.3~§5.8、§7 时序、§8.1、§9 清单、附 A 全部示例;平台侧 `device-server` 转发时按连接注入设备身份(内部改动、固件无感) ㉛ **`id` 也去掉 IMEI 前缀**:既然设备身份来自连接,`id` 只表示**动作目标** —— **不带**=柜级、**一段**=单板(仅 `reset`,如 `"01"`)、**两段**=仓体级(如 `"01-3"`);仓体级段数校验由 `≠3` 改为 `≠2`;§3.7 / §4.2 / §5.5 / 附 A 全部示例与注释同步。平台侧 `device-server` 下发时需按目标连接**补回 IMEI 前缀**(内部改动) **(⚠️ 仅本条末句被 ㉟ 取代、作废:`id` 不下发 IMEI,"补回 IMEI 前缀"的做法已废弃,改由**内部路由字段 `_dev_id`** 定位目标设备;`id` 只表示动作目标、不含 IMEI 的规则本身仍然生效,见 §3.7)** ㉜ **`msg_id` 编号规则写实**:**两个方向各自独立计数、都每连接从 `1` 起自增**(`0` 不使用),重连归零;**明确不约定号段**(方向靠有无 `suc` 区分),并说明"同一数字在两个方向同时出现是正常的"。原示例中的大号(`2147483648` 等,共 25 处)全部改为从 `1` 起的小号;删除原先"平台从高位起"的不实描述 ㉝ **订单生命周期定稿(关键修正)**:**`bat_in` 与订单无关**(只登记电池台账,**不建单**);**建立订单的唯一入口是 `on`**(`work_mode="0"` 平台下发 `on` 时建单、`work_mode="1"` 设备上报 `on` 时建单、**回执形态不建单**);**结束** = `bat_out` / `off` / **10 小时订单超时**。建单判定:该通道无活跃订单→新建;有活跃订单且**电池 id 没变**→不新建;**变了**→老订单 `forced` + 新建(依据"一个通道只可能有一块电池");起始 SOC 取最近一次 `bat_in` 的 `info`。**`off` 到达时无活跃订单 → 平台不处理但照常回 `suc:1`**。并在 §5.7 加注释**告知硬件侧**:除充满外,**故障 / 过温 / 电池异常等停止也应上报 `off`** ㉞ **`reset` 回执语义明确**:`reset` 只是**一个动作**,回执表示"**已收到并开始执行(重启)**"= **下发成功**,**不表示重启成功**;固件**必须先回执、再重启**(整柜重启后就没机会回执);**重启是否成功平台不看回执**,而是看设备**是否重新 `login`**;回执的 `id` 明确为**可选**(平台配对只靠 `msg_id` + `act`) ㉟ **平台应答补上 `act`(实现对齐协议)**:`ServerResponse` 新增 `act`(**必带,与请求一致**),新增 `success_for` / `error_for` 从**请求报文**取 `act`+`msg_id`(协议 §3.1 要求 `act` 双向必带、§3.4 要求配对含 `act`,原实现应答无 `act` 属实现缺陷);连带修复 device-server 的「应答 vs 指令」判据(**只看有没有 `suc`**,不再看 `act`)与**敏感指令门控**(只拦指令 —— 否则 `auth_str` 的应答会被自己拦掉 → 首次配网死锁);并新增**内部路由字段 `_dev_id`**(`id` 去掉 IMEI 后 device-server 无法再从 `id` 推导目标设备,改用该字段;下发前摘除、**设备侧不可见**,§8.3 同步说明) **(✅ 本条取代 ㉛ 末句"下发时需按目标连接补回 IMEI 前缀";当前生效口径:`id` 只表示动作目标、**不含 IMEI**,设备身份由 TCP 连接确定,平台内部路由字段一律不下发 —— 与 §3.7 / §8.3 一致)** ㊱ **补两条边界说明**:① **未登记的 IMEI 发 `auth_str` 时平台静默丢弃**(不发码/不写缓存/不回包,见 §5.1 与 §8.1 表)—— 合法设备必然已建档,正常固件不会遇到;② **电池属性按原值入库**(`brand_code`/`model_code`/`material_code`/`cell_count`/`rated_voltage`/`rated_capacity` 均入电池台账,映射出的中文名留待代码表确认后回填,与 §10 定稿第 7 条一致) ㊲ **交付前一致性修正(本文与实现对齐,不改动任何已冻结的协议行为)**:
① **新增 §3.1「数值口径的统一规则」并全文贯彻** —— **hex 字节流(`channels[]` / `info` / `cells`)= 原始硬件单位、由平台换算;JSON 数值字段(`cabinet.temp` / `cabinet.meter` / `rated_voltage` / `rated_capacity`)= 平台单位、由设备换算**;据此改正 §5.3 `temp`/`meter`(设备送 K×100 / Wh,平台原样存库)、§5.5 `info`/`cells`/额定值的说明,使"字段说明"与"JSON 示例"完全自洽。
② **`spec` 口径更正**:由平台**每次 `login` 按该柜已配置的仓控板/通道拓扑现算**(**不再**写"作为柜子型号存储"),**拓扑未配置/不完整时按柜型兜底**,**恒为 4 位数字、不会是 `"0000"`**;§5.2 / §9 自测清单 / §10 第 9 条同步补上"**固件应以收到的 `spec` 决定上报几块板、每板几路通道**;收到与预期不符应先按 `spec` 上报并记日志反馈平台(不得自造新 `act`)"。
③ **附 A.5.3 注释更正**:`open` 为柜级指令**不带 `id`**(原注释"只写 IMEI"与 §3.7 矛盾);并明确 ㉛ 末句"下发时补回 IMEI 前缀"**已被 ㉟ 取代(作废)**,⑮ 的 `id` 段数规则、⑯/⑰ 的 `pow_fail` `info` 规则均**在原处标注作废**、保留可追溯;⑲ 的"第 12 项"标注为**后调整为第 16 项**。
④ **§10 第 2 条更正**:`channels` 是**字符串数组**、元素**没有 `channel` 字段**,**通道号由数组下标决定**(第 1 个元素 = 通道 1);§5.3 同段措辞统一为同一句话。
⑤ **「无电池」状态兜底写实**(§5.6 与 §10 新增第 14 条):判定依据 `batt_status` bit7、**连续 2 次(约 2 分钟)**、后果为 **`forced` 异常结单**,并写明与待确认第 16/18 项的关系及"照报全 0 的板会误判"的风险。
⑥ **结构修复**:文首「⚠️ 待确认事项」表中被引用块切断的 7 行表格恢复连续(引用块移至表格之后);§5.1 示例中不可解析的 `msg_id:N` 占位改为具体编号;附 A.3.1 清理陈旧注释(`soc/voltage/current/temp`、重复的 `cells` 行) |
+| v1.1 | 2026-09-14 | ① 签名定稿为 **8 位**(`hex[56..64]`),自测向量更新为 `3957B959` / `269C249D` ② 明确 `act` 双向必带、`suc=0` 必带 `err` ③ 报文性质判定改为**按有无 `suc`**,配对要求 `msg_id` + `act` 双条件 ④ 明确**每个交互 60 秒超时、超时作废、迟到应答丢弃、不重试** ⑤ 新增 `login` 失败分类处理与**安全码自愈闭环**(重置后设备自动重新申领)⑥ 接入端口定稿:测试 `3001`、正式 `9001`,明确 TCP **不经 nginx/域名** ⑦ 第 10 节遗留项全部定稿;新增附 A 带注释报文示例 ⑧ **`login` 失败后平台主动断开 TCP 连接**,设备须重连后重新握手 ⑨ 安全码存储明确为 **DB 权威 + Redis 缓存**(缓存未命中回源 DB,Redis 故障不再导致设备无法登录)⑩ 上报节奏定稿:`status_post` **固定每 60 秒一次、每次全量**(取消原"空闲 60s / 充电 15s"与按板分片设想);单行上限 **4096 → 65536 字节** ⑪ **上报字段按硬件权威数据字典重构**:`status_post` 改为 `cabinet`(柜体状态2 字节 + 柜温 + 电表)+ `boards[].batteries[]`(2 个状态字节 + SOC/电压/电流/温度);`bat_in` / `bat_out` / `off` 的 `bms` 改为 `battery`(属性 + 动态 + 电芯);新增 **§3.8 状态字节 hex 解析表**;`id` 量程改为仓控板 **1–15** / 电池组 **1–8**;`soh` / `power` 移除 ⑫ **柜门整章定稿**:柜门与充电**完全解耦**(§7.2/§7.3 拆分)、`open` 明确为**解锁**(门由人拉开)、**无"关门"指令(任何柜型)**、柜门状态经 `status_post` ≤60 秒回传;`rssi` 口径由 dBm 改为 **0–99 信号等级** ⑬ **挂起 4 项待确认**(`pow_fail` `info` 语义、`energy` 数据源、代码表取值内容、电芯数据字节数)并在**文首新增「⚠️ 待确认事项」**集中列出;删除内部重复文档 `docs/柜控板-平台TCP接口文档.md` ⑭ **`login` 成功应答新增 `spec` 与 `work_mode`**:`spec`(4 位,前 2 位仓控板数 + 后 2 位每板通道数,当前柜型 `"0606"`)作为**柜子规格/型号**由平台下发;`work_mode`(`"0"`=平台指令模式 / `"1"`=自动充电工作模式)下发**充电工作模式**,固件据此决定是否等平台 `on` 指令——**平台侧可配置、后台可改**,每次 `login` 以最新配置为准 ⑮ **新增连接登录门控**:**只有 `auth_str` / `login` 可在未登录状态发送**,其余 `act` 必须先 `login` 成功,否则平台丢弃且不回包;**未登录的 TCP 连接不予保持**(建连后 30 秒未 `login` 成功即主动断开)。同时 `err` 文案全部改为**英文小写短语**(§3.3 新增取值全集表),`id` 段数规则明确(一段=柜级 / 两段=仅 `reset` 单板 / 三段=仓体级)**(⚠️ 该 `id` 段数规则与最终定稿完全相反,已被 ㉛ 修正并经 ㉟ 确认,作废;当前生效规则见 §3.7)** ⑯ **补充 `pow_on` / `pow_off` 业务语义**:远程通断柜内总电(接触器)——**`pow_off` 分闸后仓控板随之下电**(仓体/电池数据不可用,并因此产生 `pow_fail` `info:"0"` 直流停电),属运维动作;`pow_fail` 与 `cabinet.status1` bit0 的关系(事件 vs 状态)在 §5.4 说明 **(⚠️ 本条中的 `pow_fail` / `info:"0"` 已被 ⑱ 取代,作废)** ⑰ **`pow_fail` 的 `info` 区分定稿**(判定依据 = **"谁掉电"**):`"0"`=直流停电(**接触器分闸**→仓控板下电,柜控板仍有电)/ `"1"`=交流停电(**柜控板自身断电**,靠**板载电容余电抢发最后一条**,与接触器无关);该条由「待确认」转入「约定定稿」第 11 条,待确认事项由 4 项缩减为 **3 项** **(⚠️ 已被 ⑱ 取代,作废:`pow_fail` 与 `info` 字段均已取消)** ⑱ **停电上报更正为两条独立指令**:**取消 `pow_fail`(及 `info` 字段)**,改为 **`pow_fail_dc`**(直流停电:接触器分闸→仓控板下电,柜控板仍有电)与 **`pow_fail_ac`**(交流停电:柜控板自身断电,靠板载电容余电抢发最后一条,与接触器无关)两条独立 `act`,**均无附加字段**;`act` 名与平台 Redis 字段 `pow_fail_dc` / `pow_fail_ac` 一一对应。代码侧 `report_worker` 分发与 `handle_pow_fail` 同步改为按 `act` 判定 ⑲ **新增待确认第 12 项**(**该编号后调整为第 16 项,见文首「待确认事项」**):**仓控板离线(485 芯片坏 / 线松 / 掉电)如何上报** —— **4G 层读不到该板数据时该怎么报尚未约定**(省略该板?照报全 0 的板?另有在线标志?),**以硬件答复为准、本文不预设结论**(§5.3 同步加警示) ⑳ **`login` 失败统一为单一应答**:取消 `invalid timestamp` / `auth_str not found`,**只返回 `invalid signature`**(不区分失败原因,避免外部试探系统状态——安全设计);同时修正 `spec` 的定位为「平台对柜子的规格定义(型号)」,**与仓控板实际能否通信无关** **(⚠️ 其中"型号"的措辞已在 ㊲ 更正:`spec` 由平台按拓扑现算、**不承诺"型号存储"**,见 §5.2 / §10 第 9 条)**;`work_mode` 明确**仅两种、不再扩展** ㉑ **`status_post` 字段改名**:`batteries[]` → **`channels[]`**、内层 `group` → **`channel`**(那是**仓板的通道**、可能空着,不是纯电池信息;同时与平台侧 `channel_id` 对齐)**(⚠️ 该 `channel` 字段已在 ㉒ 随之取消:`channels[]` 改为**字符串数组**,元素内不含任何字段名,**通道号由数组下标决定**,见 §5.3 / §10 第 2 条)**;`id` 第三段说明改为「通道」;全文「电池组」按语境改为「通道」(硬件表名「电池组属性」保留) ㉒ **通道状态改为 hex 透传(大幅瘦身)**:`status_post.channels[]` 由"每通道 6 个命名字段"改为 **每通道 18 个 hex 字符**(9 字节原样拼接,**4G 层零转换**),**数组下标 = 通道号**(**精确定义:数组下标从 0 开始,第 1 个元素 = 通道 1**,见 §5.3 / §10 第 2 条);`bat_in`/`bat_out`/`off` 的动态数据同步改为 `info`(同一种 18 字符 hex);定字节布局与**大端**规则,§3.8/§5.3/§5.5/§7/§9/§10/附A 全面同步。体积:6 板 × 6 通道由 ~3.4KB 降至 **~0.8KB** ㉓ **修正空仓写法**:**空仓 ≠ 全 0** —— byte0 = `00`(无电池)但 **byte1 照实报电源状态**(电源与有无电池无关),例 `"008200000000000000"`;明确**电池状态与电源状态是两个独立维度**,平台前端**分两个指示分开显示** ㉔ **字段改名**:`bat_in`/`bat_out`/`off` 里的电池数据字段 `dyn` → **`info`**(它是**这块电池的信息**,不是"充电数据");同时明确 hex **大小写不敏感**(平台两种都解析,建议固件统一小写),示例 hex 全部改小写 ㉕ **`cells` 同样改拼接 hex**:由"JSON 数组 + 4 个统计字段"改为**整段拼接**的 hex 字符串,长度 = **`cell_count × 4 + 16`**(N 节电压 + 最高/最低电压 + 最高/最低温度,大端),15 节 = 76 字符、20 节 = 96、22 节 = 104(**与硬件文档「52 字节」吻合**) ㉖ **`battery_id` 允许为空串**:BMS 读不到序列号时传 `""`,**不影响充电与订单记录**;平台侧 `battery_id` 为空时**不做"换电池"判断、跳过电池台账**,订单照常创建(只是不知道是哪块电池) ㉗ **`bat_in` 重复上报不新建订单**:明确 **`bat_out` 是订单结束标志**——没有 `bat_out` 就说明订单还在进行;因此该通道已有活跃订单且 `battery_id` **相同**(含两边都为空串)时**不新建订单**,仅当 `battery_id` **不同且都非空**才 `forced` 结束旧订单并新建。**依据:一个通道只可能有一块电池**,`bat_in` 带不同 `battery_id` 必然是换了电池 → 老订单必须先结束(⚠️ 该条后续在 ㉝ 修正:`bat_in` 最终定为**与订单无关**,建单只在 `on`) ㉘ **新增订单超时兜底**:设备可能**停电/离线期间电池被取走**而永远发不出 `bat_out` → 平台侧**充电订单"进行中"超过 10 小时即自动结束**(`reason='timeout'`,依据"充电不可能超过 10 小时");固件**不必补报历史事件** ㉙ **补充两条待确认(第 18/19 项)**:**① 人在没充满时强制拔电池**——MCU 能否检测、能否保证"先 `off` 再 `bat_out`"(§5.6 已把该顺序列为**硬性要求**);**② 停电恢复后如何兜底**——掉电期间事件无法上报,恢复后是补报、还是以当前状态为准由平台对账 ㉚ **报文瘦身:`dev_id` 只在 `auth_str` / `login` 带** —— 这两条用于把 TCP 连接与设备绑定,**之后所有报文都不再带 `dev_id`**(设备身份由连接确定;断开重连才需重新 `login`)。同步更新 §3.1 字段总表、§5.3~§5.8、§7 时序、§8.1、§9 清单、附 A 全部示例;平台侧 `device-server` 转发时按连接注入设备身份(内部改动、固件无感) ㉛ **`id` 也去掉 IMEI 前缀**:既然设备身份来自连接,`id` 只表示**动作目标** —— **不带**=柜级、**一段**=单板(仅 `reset`,如 `"01"`)、**两段**=仓体级(如 `"01-3"`);仓体级段数校验由 `≠3` 改为 `≠2`;§3.7 / §4.2 / §5.5 / 附 A 全部示例与注释同步。平台侧 `device-server` 下发时需按目标连接**补回 IMEI 前缀**(内部改动) **(⚠️ 仅本条末句被 ㉟ 取代、作废:`id` 不下发 IMEI,"补回 IMEI 前缀"的做法已废弃,改由**内部路由字段 `_dev_id`** 定位目标设备;`id` 只表示动作目标、不含 IMEI 的规则本身仍然生效,见 §3.7)** ㉜ **`msg_id` 编号规则写实**:**两个方向各自独立计数、都每连接从 `1` 起自增**(`0` 不使用),重连归零;**明确不约定号段**(方向靠有无 `suc` 区分),并说明"同一数字在两个方向同时出现是正常的"。原示例中的大号(`2147483648` 等,共 25 处)全部改为从 `1` 起的小号;删除原先"平台从高位起"的不实描述 ㉝ **订单生命周期定稿(关键修正)**:**`bat_in` 与订单无关**(只登记电池台账,**不建单**);**建立订单的唯一入口是 `on`**(`work_mode="0"` 平台下发 `on` 时建单、`work_mode="1"` 设备上报 `on` 时建单、**回执形态不建单**);**结束** = `bat_out` / `off` / **10 小时订单超时**。建单判定:该通道无活跃订单→新建;有活跃订单且**电池 id 没变**→不新建;**变了**→老订单 `forced` + 新建(依据"一个通道只可能有一块电池");起始 SOC 取最近一次 `bat_in` 的 `info`。**`off` 到达时无活跃订单 → 平台不处理但照常回 `suc:1`**。并在 §5.7 加注释**告知硬件侧**:除充满外,**故障 / 过温 / 电池异常等停止也应上报 `off`** ㉞ **`reset` 回执语义明确**:`reset` 只是**一个动作**,回执表示"**已收到并开始执行(重启)**"= **下发成功**,**不表示重启成功**;固件**必须先回执、再重启**(整柜重启后就没机会回执);**重启是否成功平台不看回执**,而是看设备**是否重新 `login`**;回执的 `id` 明确为**可选**(平台配对只靠 `msg_id` + `act`) ㉟ **平台应答补上 `act`(实现对齐协议)**:`ServerResponse` 新增 `act`(**必带,与请求一致**),新增 `success_for` / `error_for` 从**请求报文**取 `act`+`msg_id`(协议 §3.1 要求 `act` 双向必带、§3.4 要求配对含 `act`,原实现应答无 `act` 属实现缺陷);连带修复 device-server 的「应答 vs 指令」判据(**只看有没有 `suc`**,不再看 `act`)与**敏感指令门控**(只拦指令 —— 否则 `auth_str` 的应答会被自己拦掉 → 首次配网死锁);并新增**内部路由字段 `_dev_id`**(`id` 去掉 IMEI 后 device-server 无法再从 `id` 推导目标设备,改用该字段;下发前摘除、**设备侧不可见**,§8.3 同步说明) **(✅ 本条取代 ㉛ 末句"下发时需按目标连接补回 IMEI 前缀";当前生效口径:`id` 只表示动作目标、**不含 IMEI**,设备身份由 TCP 连接确定,平台内部路由字段一律不下发 —— 与 §3.7 / §8.3 一致)** ㊱ **补两条边界说明**:① **未登记的 IMEI 发 `auth_str` 时平台静默丢弃**(不发码/不写缓存/不回包,见 §5.1 与 §8.1 表)—— 合法设备必然已建档,正常固件不会遇到;② **电池属性按原值入库**(`brand_code`/`model_code`/`material_code`/`cell_count`/`rated_voltage`/`rated_capacity` 均入电池台账,映射出的中文名留待代码表确认后回填,与 §10 定稿第 7 条一致) ㊲ **交付前一致性修正(本文与实现对齐,不改动任何已冻结的协议行为)**:
① **新增 §3.1「数值口径的统一规则」并全文贯彻** —— **hex 字节流(`channels[]` / `info` / `cells`)= 原始硬件单位、由平台换算;JSON 数值字段(`cabinet.temp` / `cabinet.meter` / `rated_voltage` / `rated_capacity`)= 平台单位、由设备换算**;据此改正 §5.3 `temp`/`meter`(设备送 K×100 / Wh,平台原样存库)、§5.5 `info`/`cells`/额定值的说明,使"字段说明"与"JSON 示例"完全自洽。
② **`spec` 口径更正**:由平台**每次 `login` 按该柜已配置的仓控板/通道拓扑现算**(**不再**写"作为柜子型号存储"),**拓扑未配置/不完整时按柜型兜底**,**恒为 4 位数字、不会是 `"0000"`**;§5.2 / §9 自测清单 / §10 第 9 条同步补上"**固件应以收到的 `spec` 决定上报几块板、每板几路通道**;收到与预期不符应先按 `spec` 上报并记日志反馈平台(不得自造新 `act`)"。
③ **附 A.5.3 注释更正**:`open` 为柜级指令**不带 `id`**(原注释"只写 IMEI"与 §3.7 矛盾);并明确 ㉛ 末句"下发时补回 IMEI 前缀"**已被 ㉟ 取代(作废)**,⑮ 的 `id` 段数规则、⑯/⑰ 的 `pow_fail` `info` 规则均**在原处标注作废**、保留可追溯;⑲ 的"第 12 项"标注为**后调整为第 16 项**。
④ **§10 第 2 条更正**:`channels` 是**字符串数组**、元素**没有 `channel` 字段**,**通道号由数组下标决定**(第 1 个元素 = 通道 1);§5.3 同段措辞统一为同一句话。
⑤ **「无电池」状态兜底写实**(§5.6 与 §10 新增第 14 条):判定依据 `batt_status` bit7、**连续 2 次(约 2 分钟)**、后果为 **`forced` 异常结单**,并写明与待确认第 16/18 项的关系及"照报全 0 的板会误判"的风险。
⑥ **结构修复**:文首「⚠️ 待确认事项」表中被引用块切断的 7 行表格恢复连续(引用块移至表格之后);§5.1 示例中不可解析的 `msg_id:N` 占位改为具体编号;附 A.3.1 清理陈旧注释(`soc/voltage/current/temp`、重复的 `cells` 行) |
| v1.0 | 2026-09-14 | 首版:基于平台 TCP 服务实现逐项核对生成 |