协议 §5.3 的 cabinet.meter 字段表原本只写了要求「平台原样存库」。现补上落地情况: - 已落 cabinets.meter_wh / meter_at(最新读数) - 已在 cabinet_meter_readings 表**只在读数变化时**追加留痕(即抄表记录,可直接算日用电量) - 并注明此前只写 Redis(TTL 300s)⇒ 上报即被清掉、历史不可回溯,属实现缺口 - 明确 meter(整柜累计电能)与 energy(单订单充电量,协议无此字段)不是一回事 实测证据:4 次同值上报只在留痕表留下 1 行;读数由 125500 改为 125600 后新增 1 行且 cabinets.meter_wh 同步。
125 KiB
柜控板固件对接文档(TCP 接口)
| 项目 | 内容 |
|---|---|
| 文档版本 | v1.1.1(2026-09-14 定稿,2026-09-20 文档修订) 修订只改文档自身的错误 / 矛盾 / 缺失说明,协议行为( act、字段名、位定义、签名、id 规则、上报节奏、超时语义)一字未动(见附 B 变更记录 ㊳) |
| 交付对象 | 柜控板固件开发工程师 |
| 接口定义方 | 平台侧(本接口由平台方定义,固件按本文实现) |
| 协议性质 | 已冻结:报文格式、字段名、act 取值、签名算法一经确认不再变更 |
本文档只描述固件需要实现的内容。平台内部实现(存储、队列、业务逻辑)不在本文范围内。
⚠️ 待确认事项(请硬件侧确认;编号与第 10 节一致)
除下表 7 项外,本文其余内容均已定稿。 确认前固件可按"现状"(即本文当前写法)实现; 我们拿到答复后会同步更新本文并通知。这 7 项都不阻塞 4G 层开工。
📌 其中第 13 / 17 / 18 项的「平台侧行为」已经落地(不依赖硬件答复),第 14 / 15 项平台侧也已按"原值透传 / 只取 SOC"实现 —— 逐项状态见本表之后的「这 7 项的平台侧落地情况」。硬件侧只需按"现状"发报文即可,不必等我们改平台。
| # | 事项 | 现状(本文当前写法) | 需硬件确认 | 影响范围 |
|---|---|---|---|---|
| 13 | energy(单订单累计充电量,Wh) |
本协议暂未包含 energy 字段 |
硬件是否有该数据源?若无,订单电量由平台侧按 电压 × 电流 × 时间 积分计算 |
§5.5 / §5.6 / §5.7 是否补该字段 |
| 14 | 代码表的取值内容 | 仅知 厂家代码 01=星恒、电池型号 01=磷酸铁锂 |
① 四个字段(厂家代码 / 电池型号 / 电芯材料 / 电芯节数)的完整取值表 ② 「电芯材料」与「电池型号」是否写反("型号"的值给的是"磷酸铁锂",那是材料) ③ 代码几位、几进制(1 还是 01) |
仅影响 §5.5 代码字段能否显示为中文名 |
| 15 | 电芯数据的字节数 / 项数 | 硬件文档写「电芯数据 52 个字节」,但其示例为「20 节电芯(20×2)」,按字段算 20×2+2+2+2+2 = 48 |
到底是 48 字节 / 20 项,还是 52 字节 / 22 项,或 52 字节里含未列出的 4 字节字段? | §5.5 cells 的解析与缓冲分配 |
| 16 | 仓控板离线(485 芯片坏 / 线松 / 板子掉电)如何上报? | 本文暂未定义:4G 层读不到该板数据时该怎么报还没有约定(省略这块板?照报一块数值全 0 的板?还是另有在线/离线标志?)。硬件字典的「柜体状态1/2」只覆盖柜级(断电/烟/水/倾倒/门/风扇/接触器/喷淋/声光/门超时),没有"仓控板通信状态" |
硬件侧能提供什么形式的"仓控板在线/离线"信息?例如: ① 每块仓控板一个在线位(如放在 boards[] 里)② 柜体数据里加一个「仓控板离线位图」(15 块板各一位) ③ 柜控板给出"应在线 N 块 / 实际失联 M 块" ④ 或明确回答:硬件无法检测 并请说明:板离线时 4G 层是省略该板、还是照报全 0 的板? |
§5.3 status_post 需新增字段;平台「仓控板故障」告警完全依赖它;并且该答复会直接影响 §5.6 的「无电池」状态兜底 —— 若"板离线时照报全 0 的板",平台会把整板离线误判为"电池被拔走"并提前异常结单(详见 §5.6 / §10 第 14 条) |
| 17 | 直流停电是否还需要 pow_fail_dc 这条指令? |
现在直流停电有两条路径:① 事件指令 pow_fail_dc(接触器分闸时抢发,见 §5.4.1)② status_post 的 cabinet.status1 bit0「断电」(每 60 秒一个状态位)。而直流停电时柜控板本身仍有电,状态位理论上足以表达 |
直流停电能不能只用 status1 bit0 表达、取消 pow_fail_dc 这条指令?请确认:接触器分闸时, status1 bit0 是否会置 1?· 若会 → pow_fail_dc 冗余,建议删掉(协议少一条 act)· 若不会(bit0 只表示外部市电断)→ pow_fail_dc 必须保留 |
§5.4.1 / §4.1 / §10 / 附 A.2.2;平台停电状态的数据来源 |
| 18 | 人在没充满时把电池直接拔走(强制拔出)能否检测? | §5.6 已把「无论是否充满,被拔时必须先 off 再 bat_out」列为硬性要求;但未充满时人为直接拔走,MCU 不一定能感知 |
① MCU 能否检测未充满时的强制拔出(电池状态 bit7 由 1→0)? ② 检测到后能否保证先发 off 再发 bat_out?(平台订单正确结束依赖它)③ 若检测不到,请说明 —— 平台侧已实现兜底: status_post 的 batt_status **bit7 连续 2 次为 0(约 2 分钟)**即判定"电池已被取走",提前异常结单(forced),并提供 10 小时订单超时双保险(详见 §5.6) |
§5.6;订单能否正常结束(漏报时由平台兜底) |
| 19 | 停电恢复后如何兜底? | 停电期间设备完全失电,期间发生的电池拔出/插入等事件无法上报;恢复后设备重启,平台侧会留下不一致(例如挂着未结束的订单、台账里的电池已不在柜内) | 停电恢复后硬件侧如何处理?例如: ① 设备恢复后补报停电期间发生的事件? ② 还是以当前实际状态为准——重连后发一次全量 status_post,由平台自行对账?③ 或硬件不做特殊处理(平台侧靠 10 小时订单超时兜底) |
§5.5 / §5.6 订单;掉电期间的状态对账机制 |
关于第 13 项的兜底已落地:平台侧已按"积分计算"实现该兜底 —— 每条
status_post的通道电压/电流按 60 秒积分累加(协议固定 60 秒一次上报), 订单结束时写入订单电量并生成充电记录。硬件侧无需为此改协议; 若硬件后续确认能提供energy字段,平台改为直接采用硬件值即可(不影响报文结构以外的任何东西)。
关于第 14 项:「代码 → 名称」的映射责任已定稿 —— 4G 层按字符串原值透传(不补零、不转义、不改写),映射在平台侧完成(见第 10 节第 7 条)。 所以第 14 项只缺"取值内容",不改变
brand_code/model_code/material_code的字段格式,不阻塞 4G 层实现。
这 7 项的平台侧落地情况(给硬件侧的输入:哪些已经不用等)
| # | 事项 | 平台侧现状 | 还需要硬件确认什么 |
|---|---|---|---|
| 13 | energy 字段 |
✅ 已落地(兜底方案已生效):平台按 电压 × 电流 × 时间(固定 60 秒)积分算每单电量 |
只问"硬件是否有该数据源";有则平台改为直接取硬件值,没有则现状即可,固件两种情况下都不用改报文 |
| 14 | 代码表取值内容 | ✅ 原值透传 / 入库已落地:brand_code / model_code / material_code 原值存电池台账;⏳ 中文名映射未落地(batteries.brand / model 列为空,待取值表确认后回填) |
只缺取值内容(及"型号/材料是否写反"、几位几进制);字段格式与报文结构不变;⚠️ 交付时不要理解为"平台已能显示中文名" |
| 15 | 电芯数据字节数 | ✅ 平台侧无影响:平台只从 info 取 SOC,不解析 cells 的内容与长度(整段原样留痕) |
该问题只影响固件侧的解析与缓冲分配;平台不依赖 48 / 52 的结论 |
| 16 | 仓控板离线如何上报 | ⏳ 仍未定,且平台侧确实没有实现:平台目前没有任何"仓控板离线 / 仓控板故障"的判定与告警(status_post 里也没有可承载它的字段) |
仍需硬件答复"能否提供在线/离线信息"以及"板离线时是省略该板还是照报全 0 的板"(见 §5.6 的误判风险) |
| 17 | 是否保留 pow_fail_dc |
✅ 两条路径平台都已实现:① pow_fail_dc 事件指令(收到即置停电状态)② status_post 的 status1 bit0(bit0=0 时平台清除停电状态) |
仍需硬件确认"接触器分闸时 status1 bit0 是否置 1",以决定要不要取消这条 act;在答复前请照现状发 pow_fail_dc |
| 18 | 未充满强制拔出能否检测 | ✅ 已落地(兜底已生效):batt_status bit7 连续 2 次为 0 → 平台判定"电池已被取走",提前异常结单(forced);另有 10 小时订单超时双保险 |
仍需硬件答复"能否检测、检测到能否保证先发 off 再发 bat_out";能则订单更准,不能则靠上述兜底 |
| 19 | 停电恢复后如何兜底 | ⏳ 仍未定,平台侧按"以当前状态为准"运行:恢复后设备重连 login + 发全量 status_post,平台据此刷新状态;status1 bit0=0 时清停电标记;遗留订单靠 10 小时超时兜底(不补报停电期间的事件) |
仍需硬件确认恢复后是否补报;若硬件选择"不补报",平台侧现状即为最终方案,固件无需额外动作 |
1. 接入信息
| 项目 | 值 |
|---|---|
| 连接方式 | 裸 TCP 长连接(无 TLS、无 WebSocket) |
| 角色 | 柜控板 = client,平台 = server;由柜控板主动发起连接 |
| 测试环境 | 118.31.119.206:3001 |
| 正式环境 | 118.31.119.206:9001 |
| 传输编码 | UTF-8 |
⚠️ TCP 由平台侧TCP 服务(device-server)直接监听端口,不经过 nginx、也不使用域名。 请直接连接上表的
IP:端口;app.anzhizhichong.com/test.anzhizhichong.com只用于 HTTPS(Web/API),与 TCP 无关。 联调请先连测试环境118.31.119.206:3001。
2. 通信基础
2.1 帧格式
- 一条报文 = 一行紧凑 JSON + LF(
\n,0x0A) 结尾。 - 平台同样以
JSON + \n下行。 - 单行最大 65536 字节(64 KB)(不含 LF)。超出会导致平台断开连接,固件需重连。
- JSON 内禁止出现换行;键名一律
snake_case(小写 + 下划线)。 - 不需要发送空行。
- 平台按 LF 切分,天然处理 TCP 粘包/分包,固件侧按同样规则处理即可。
2.2 连接建立后的第一步
连接建立后,第一条报文必须是 auth_str(本地无安全码时)或 login(本地已有安全码时)——这两条有先后关系,顺序不可颠倒:
| 设备本地状态 | 第一步必须发 | 之后 |
|---|---|---|
| 无安全码(首次开机 / 换板 / 安全码被清) | auth_str |
收到安全码 → 持久保存 → 紧接着发 login |
| 已有安全码(重启、断线重连) | login |
— |
- 首次开机必须先
auth_str:拿到安全码并保存后,才能发login。本地没有安全码时无法计算sign,因此固件不可能、也不应该发送login。 - 因此平台对
login只做签名比对:与平台侧保存的该设备安全码一致 → 成功;不一致 →invalid signature。平台无法判断设备本地是否持有安全码,也不需要判断。 (该安全码由平台持久保存,不随 TCP 连接重建或平台服务重启失效,见 5.1 / 5.2。) - 安全码必须持久保存、掉电不丢。
- 平台在收到
auth_str/login时把 TCP 连接与dev_id绑定,并且只认这条连接上出现的第一个dev_id。一条 TCP 连接只服务一个dev_id。
⛔ 登录门控:除 auth_str / login 外,其余指令必须先 login 成功
| 连接状态 | 允许的 act(上行:设备 → 平台) |
上行其余 act 的处理 |
下行(平台 → 设备) |
|---|---|---|---|
未登录(连接刚建立,尚未 login 成功) |
只有 auth_str、login |
平台一律丢弃,且不回任何应答 | 只投递 auth_str / login 自己的应答;其余报文(on/off/open/pow_on/pow_off/reset 等指令,以及任何其它应答)一律不投递 → 设备收不到任何字节 |
已登录(收到 suc=1 且带 spec 的 login 应答后) |
全部 act |
正常处理 | 全部报文正常投递 |
- 未登录连接尚未与
dev_id建立可信绑定,平台无法路由应答,所以被丢弃的报文不会收到任何回应(固件表现为"发了没反应")。 - ⛔ 门控是双向的同一条规则 —— 「上行一律丢弃、下行一律不投递」(设备侧收不到任何字节),不是两份清单:
- 上行:未登录时除
auth_str/login外的报文被丢弃(上表右二列); - 下行:未登录时平台不会向该连接发送任何报文 —— 唯一例外是
auth_str/login自己的应答(否则首次配网就死锁了)。 - 因此固件可以这样理解:
login成功之前,这条 TCP 上除握手报文外不会有任何字节;业务指令(含on/off)也不会提前到达。
- 上行:未登录时除
- ⚠️ 未登录的 TCP 连接平台不予保持:连接建立后 30 秒内未完成
login成功,平台会主动断开。固件应在建连后立即发auth_str/login,不要空闲等待。 - ⚠️ 同一个 IMEI 只保留一条连接:若同一
dev_id又建了一条新连接(例如固件异常重连、而旧连接在平台侧还没被感知断开),平台让新连接接管、并主动关闭旧连接;旧连接上不会收到任何通知报文(设备侧表现为读到 EOF / 连接被关)。因此固件必须保证同一时刻只维持一条连接:一旦发现连接被关闭就应重连(并重新login),不要在旧连接上继续发报文(平台对它的上行一律不再处理)。 - 设备侧表现:连上后马上
auth_str/login→ 收到suc=1→ 才发status_post等业务报文。
login 失败的分类处理(含自愈,必须实现)
设备收到 login 应答后,必须按 err 分类处理,不能一律重试:
login 应答 |
含义 | 设备动作 |
|---|---|---|
{"suc":1,"spec":"0606","work_mode":"0"} |
登录成功 | 进入正常业务流程 |
{"suc":0,"err":"invalid signature"} |
登录失败(唯一一种失败应答)。平台刻意不区分失败原因 | 重新发 auth_str 申领新码 → 保存 → 重新 login(详见下方说明) |
⛔ 平台对
login失败只返回一种err:invalid signature。 不区分是"安全码不一致"、"时间戳超窗"还是"平台无该设备记录"——这是有意设计,避免向外部泄露判定细节。 因此固件不要去猜失败原因。
失败后的处理顺序(固件必须实现)
- 重连(平台发出失败应答后会立即断开,见下方)
- 重连后先发
auth_str申领新码:- 拿到新码 → 保存 →
login - 返回
auth_str already issued(说明码没被重置)→ 停止重试,用本地已有安全码login
- 拿到新码 → 保存 →
- 若仍持续失败,检查本地时钟是否准确(时间戳超窗也会表现为
invalid signature);建议设备由 4G 网络校时,避免因时钟漂移导致永远登录不上
重新申领的边界
- 设备未被重置、且平台仍持有安全码时,请求
auth_str返回:
{"act":"auth_str","msg_id":1,"suc":0,"err":"auth_str already issued"}
此时停止重试,改为按重连间隔周期性重试 login,等待平台管理员重置。
- 重新申领有频率限制:同一 IMEI 5 次 / 5 分钟,超限返回
{"act":"auth_str","msg_id":1,"suc":0,"err":"rate limit exceeded"},需退避后重试。
⚠️ login 失败后平台会主动断开 TCP 连接
login 返回 suc=0 之后,平台会立即断开该 TCP 连接。设备必须:
- 重新建立 TCP 连接(
msg_id在新连接上重新计数) - 按上面的"失败后的处理顺序"继续(先
auth_str,或直接login)
- 连接被断开后不要在旧连接上继续发报文。
- 重连建议按退避间隔递增(10s / 30s / 60s / 300s,见 8.3),避免
login持续失败时高频重连。- 该断开是平台行为,不是网络故障;设备不应将其计入网络异常统计。
平台重置安全码后的预期行为(自愈闭环)
管理员在后台点「重置安全码」
↓ 平台立即用新码替换旧码(旧码立即失效)
设备仍用本地旧码 login → 收到 {"suc":0,"err":"invalid signature"}
↓ 平台主动断开 TCP 连接
设备重新连接
↓ 按上表触发重新申领
设备发 auth_str → 收到 {"suc":1,"auth_str":"<新码>"}
↓ 持久保存
设备用新码 login → {"suc":1,"spec":"0606","work_mode":"0"} 登录成功
全程无需人工到现场操作设备。
2.3 保活 —— 由 status_post 承担(无独立心跳包)
- 本协议不设独立的心跳 / PING 包。
status_post即保活报文:平台对每条请求类上行(auth_str/login/status_post/pow_fail_dc/pow_fail_ac/bat_in/bat_out/ 设备自发的on/off)都会回一条{"act":"<同请求>","msg_id":N,"suc":1}作为存活确认(回执类上行平台不回包,见 3.5 / 4.1)。 - 平台以「300 秒内是否收到过该设备的任意报文」判定在线 / 离线(依据 Redis
device:{imei}的 TTL)。 - 上报节奏:固定每 60 秒一次
status_post,每次全量上报(不区分空闲 / 充电状态,不做增量——每次携带全部仓控板及其全部通道的状态)。该间隔远小于 300 秒的离线判定阈值,同时保证 4G 网络的 NAT 映射不被回收。 - 设备不得自行增加自定义心跳指令:
act取值是本协议的一部分,平台只识别清单内的act,其余一律回{"suc":0,"err":"unknown act"}。
3. 报文通用规则
3.1 字段总表
| 字段 | 类型 | 出现位置 | 说明 |
|---|---|---|---|
act |
string | 双向全部报文必带 | 动作类型 |
msg_id |
uint32 | 双向全部报文必带 | 交互号,见 3.4 |
dev_id |
string | 仅 auth_str / login 必带(用于把 TCP 连接与设备绑定) |
柜控板 IMEI,15 位十进制。 其余报文一律不带 —— 连接建立后设备身份即由该 TCP 连接确定(见 2.2) |
suc |
uint8 | 仅应答 | 1=正常,0=异常 |
err |
string | 仅 suc=0 的应答,必带 |
异常原因 |
id |
string | 见各指令 | 寻址标识(动作目标),不含 IMEI,见 3.7 |
timestamp |
uint64 | 仅 login |
秒级 Unix 时间戳 |
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 |
rssi |
int32 | status_post |
4G 信号强度,整数 0–99,数值越大信号越好(不是 dBm,不可为负) |
cabinet |
object | status_post |
柜体状态块,见 5.3 |
boards |
array | status_post |
仓控板及通道状态(全量),见 5.3 |
battery |
object | bat_in/bat_out/off |
电池数据块,见 5.5 / 5.6 / 5.7 |
iccid |
string | login(可选) |
SIM 卡 ICCID |
平台会忽略未定义的字段;固件解析下行报文时也必须忽略未知字段。 ⚠️ 平台的应答不带
id——id只出现在平台下发的指令与设备上行报文里(见 3.7 / 5.8 / 8.3)。
📏 数值口径的统一规则(全文唯一口径,务必按此实现)
| 数据形态 | 出现在哪 | 数值口径 | 谁负责换算 |
|---|---|---|---|
| hex 字节流 | status_post.channels[] 元素、bat_in/bat_out/off 的 info、bat_in 的 cells |
原始硬件单位(SOC 1% / 电压 0.01V / 电流 0.01A / 温度 1K …,多字节大端) | 平台换算(×10 / ×10 / ×10 / ×100,见 5.3 字节布局表)。固件原样拼接 hex,不做任何换算 |
| JSON 数值字段 | cabinet.temp / cabinet.meter / battery.rated_voltage / battery.rated_capacity 等 |
平台单位(K×100 / Wh / mV / mAh …) | 设备换算后上报。平台原样存库、不再换算 |
一句话记住:hex 字节流里的数值 = 原始硬件单位,由平台换算;JSON 数值字段 = 平台单位,由设备换算。 本文示例都按各自口径给出,不要互相套用:
"temp":29800= 已是换算结果(设备送的 K×100);channels里的0143= 硬件原始值(1K 的 323),由平台 ×100 得到 32300(K×100)。
3.2 act —— 双向必带
- 上行:缺少
act的报文会被平台丢弃,且不返回任何应答(固件会一直等不到回复)。 - 下行:平台的应答同样带
act,值为被应答报文的act。
3.3 suc / err —— 只有两个态
suc = 1:正常。不带err。suc = 0:异常。必须同时给出err说明原因。- 只有应答带
suc;请求与指令都不带。 err一律为英文(小写短语,无标点)。固件请按字符串精确匹配,不要做翻译或模糊匹配。
err 取值全集(平台侧下发)
err |
含义 | 固件应做什么 |
|---|---|---|
unknown act |
act 不在本协议清单内 |
检查报文;不得自定义新指令 |
invalid signature |
login 失败的唯一应答(平台刻意不区分:安全码不一致 / 时间戳超窗 / 无记录) |
重连 → 先 auth_str;若返回 already issued 则停止重试并检查本地时钟 |
auth_str already issued |
安全码已下发过(未重置),拒绝重复申领 | 停止重试,用本地已有的码 login,等管理员重置 |
rate limit exceeded |
auth_str 申领超过 5 次 / 5 分钟 |
退避后重试 |
invalid id |
id 目标非法,两种情况平台都回这个错:① 格式不对(仓体级段数 ≠ 2、段内非数字、超出量程 —— 仓控板非 1–15 / 通道非 1–8);② 格式合法但该 id 在平台上查不到(该柜未配置这块仓控板 / 这个通道) |
先核对 id 格式(仓控板-通道,如 "01-3");格式确实有误 → 修正后重发;格式无误(仍收到 invalid id)→ 属平台侧该柜的通道配置问题,经运维渠道反馈,不要重试同一错误报文 |
设备侧回执报错时(如
open执行失败),err也请用英文短语,例如door lock not engaged。
示例:
// 平台 → 设备(应答)
{"act":"login","msg_id":2,"suc":1,"spec":"0606","work_mode":"0"}
{"act":"login","msg_id":2,"suc":0,"err":"invalid signature"}
// 设备 → 平台(回执:suc=0 时必须带 err)
{"act":"open","msg_id":1,"suc":0,"err":"door lock not engaged"}
3.4 msg_id —— 配对键
| 规则 | 说明 |
|---|---|
| 作用域 | 单个 TCP 连接内。连接重建后重新计数 |
| 计数 | 两个方向各自独立计数,都每连接从 1 起自增(0 不使用) |
| 唯一性 | 同一连接内、同一方向不允许两个未完成交互使用同一个号 |
| 方向区分 | 不靠号段 —— 判断一条下行报文是"应答"还是"新指令",只看有没有 suc(见 3.5),与编号大小无关 |
| 配对 | 应答的 msg_id 必须等于被应答报文的 msg_id(配对还需 act 一致) |
| 归属 | 平台下发的指令,其 msg_id 由平台分配,固件原样回带,不要自己改号 |
⚠️ 同一个数字在两个方向同时出现是正常的:例如设备发出的
login用msg_id=2,平台随后下发的on也可能用msg_id=2——两者互不干扰,因为方向不同。 固件只需在自己的「未完成请求表」里按msg_id(+act)查自己发出去的号;收到下行报文时先看有没有suc,再决定它是"应答"还是"新指令"。
3.5 报文性质判定 —— 看有没有 suc(关键)
固件收到任何下行报文后,按下表判定:
| 下行报文 | 判定 | 固件动作 |
|---|---|---|
带 suc |
这是平台对固件某条请求的应答 | 在「未完成请求表」中按 msg_id 且 act 一致配对;命中则完成并清除。应答到此结束,不要再回包 |
不带 suc |
这是平台新下发的指令 | 执行指令 → 回执(act 与 msg_id 原样回带) |
带 suc 但 msg_id 在表中查不到 |
迟到的应答(本地已超时丢弃) | 丢弃,不做任何响应 |
平台侧对上行报文采用完全相同的判据:设备的回执带 suc → 是回执;设备的请求(status_post/bat_in/…)不带 suc → 是新请求。
✅ 该判据不依赖编号大小,因此两个方向的
msg_id即使取到相同数字,指令也不会被误当成应答。 ✅ 配对必须同时校验msg_id与act(两者都与挂起项一致才算命中)。仅凭msg_id配对,在两个方向编号重叠时可能把别的交互的应答错配到自己的请求上。 ⛔ 禁止只凭"有act"判定——平台应答也带act(见 3.2)。 ⛔ 收到应答后不要再回包(双方都遵守,链条才是单向的;若双方都对"应答"再回包,会互相应答形成回环)。
3.6 超时 —— 每个交互 60 秒
发送方对每一条发出的报文计时 60 秒,超时未收到对应应答即作废该交互。双向都适用。
| 角色 | 等待什么 | 时限 | 超时后动作 |
|---|---|---|---|
| 固件 | 自己发出的每条请求的应答 | 60 秒 | 丢弃该 msg_id 的等待,不重试 |
| 平台 | 自己下发的每条指令的回执 | 60 秒 | 丢弃该 msg_id 的挂起项,不重传,记 WARN |
超时后即使应答到达,也一律丢弃
- 该
msg_id已作废:之后收到的应答不再配对、不补入挂起表,直接丢弃 - 双方都不得因迟到的应答而回滚、重做或补记业务动作
- 已作废的
msg_id不得立即复用:否则迟到应答可能撞上复用后的新交互。固件应让msg_id单调自增,回绕前确保旧号已彻底作废 - 配合 3.5 的
msg_id+act双条件配对,可进一步避免误配
固件侧的隐含义务:60 秒内必须回执
平台对每条下发的指令只等 60 秒。固件收到指令后须在 60 秒内完成执行并回执,否则平台判超时,该次操作在管理端标记为"未确认"(⚠️ 该管理端状态待实现 —— 平台侧目前只在服务端记 WARN,尚未在管理端界面展示这个状态;这不影响固件的回执义务)。
超时 ≠ 未执行
- 固件侧:超时只说明"没等到应答",平台可能已收到并处理(例如
bat_in已登记台账、on已建单)→ 不得据此重发同一业务动作 - 平台侧:超时只说明"没等到回执",柜子可能已经执行(例如门已开)→ 管理端不得据此断言失败
3.7 id —— 寻址标识
id只表示"动作目标",不含 IMEI —— 设备身份由 TCP 连接确定(见 2.2),所以不必在每条报文里重复。
| 动作粒度 | id 格式 |
示例 |
|---|---|---|
柜级(open / pow_on / pow_off,及 reset 整柜) |
不带 id(省略该字段) |
— |
单仓控板级(仅 reset) |
仓控板(一段) |
"01" |
仓体级(bat_in / bat_out / on / off) |
仓控板-通道(两段) |
"01-3" |
- 仓控板地址:1–15(当前 36 仓柜型实装 6 块;下发时按 2 位补零,如
01) - 通道号:1–8(当前 36 仓柜型每板 6 通道;不是 0–7);不补零,如第 3 通道写
3- 平台解析对
03这类补零写法也兼容,但固件请统一按不补零发
- 平台解析对
- 段数规则:不带 = 柜级、一段 = 仅
reset的单板重启、两段 = 仓体级- 仓体级
id段数必须恰好为 2,否则平台返回{"suc":0,...,"err":"invalid id"}
- 仓体级
3.8 状态字节(hex)解析表
以下 4 个字段一律用 hex 字符串表示:cabinet.status1 / cabinet.status2 各 2 位;
batt_status / pow_status 是通道 18 字符 hex 里的 byte0 / byte1(见 5.3)。一个字节按位承载多个状态。
cabinet.status1(柜体状态1)
| 位 | 掩码 | =0 | =1 |
|---|---|---|---|
| bit0 | 0x01 |
正常 | 断电 |
| bit1 | 0x02 |
正常 | 烟雾 |
| bit2 | 0x04 |
正常 | 水位 |
| bit3 | 0x08 |
正常 | 倾倒 |
| bit4 | 0x10 |
柜门关闭 | 柜门打开 |
| bit5 | 0x20 |
加热风扇关闭 | 加热风扇打开 |
| bit6 | 0x40 |
接触器导通 | 接触器断开 |
| bit7 | 0x80 |
喷淋关闭 | 喷淋打开 |
cabinet.status2(柜体状态2)
| 位 | 掩码 | =0 | =1 |
|---|---|---|---|
| bit0 | 0x01 |
正常 | 声光报警 |
| bit1 | 0x02 |
正常 | 柜门打开超时 |
| bit2–bit7 | — | 硬件保留,平台忽略 | — |
「柜门打开超时」的判定阈值由硬件侧定义(硬件文档未给出具体秒数),平台只做呈现,不自行判定。
batt_status(电池状态)
| 位 | 掩码 | =0 | =1 |
|---|---|---|---|
| bit0 | 0x01 |
电池不满 | 充满 |
| bit1 | 0x02 |
停止充电 | 正在充电 |
| bit4 | 0x10 |
正常 | 过欠压 |
| bit5 | 0x20 |
正常 | 过流 |
| bit6 | 0x40 |
正常 | 热失控 |
| bit7 | 0x80 |
无电池 | 有电池 |
| bit2–bit3 | — | 硬件保留,平台忽略 | — |
pow_status(电源状态)
| 位 | 掩码 | =0 | =1 |
|---|---|---|---|
| bit0 | 0x01 |
电源正常 | 电源故障 |
| bit1 | 0x02 |
电源关闭 | 电源打开 |
| bit2 | 0x04 |
允许打开 | 禁止打开 |
| bit7 | 0x80 |
无电源 | 有电源 |
| bit3–bit6 | — | 硬件保留,平台忽略 | — |
解析示例
| 字段 | hex | 二进制 | 解析结果 |
|---|---|---|---|
| 任意 | "00" |
0000 0000 |
全 0:正常 / 无电池 |
batt_status |
"82" |
1000 0010 |
bit7 有电池 + bit1 正在充电 |
pow_status |
"82" |
1000 0010 |
bit7 有电源 + bit1 电源打开 |
cabinet.status1 |
"82" |
1000 0010 |
bit7 喷淋打开 + bit1 烟雾报警 |
cabinet.status1 |
"10" |
0001 0000 |
柜门打开 |
cabinet.status2 |
"01" |
0000 0001 |
声光报警 |
batt_status |
"90" |
1001 0000 |
bit7 有电池 + bit4 过欠压 |
保留位不做要求:硬件填 0 或 1 平台都忽略,固件不必为保留位纠结。
4. 指令清单
4.1 上行(柜控板 → 平台)
⛔ 登录门控:下表中只有
auth_str/login可以在未登录状态发送;其余act必须先login成功,否则平台丢弃且不回任何应答(见 2.2)。
| # | act |
说明 | 是否必带 id |
需要应答 |
|---|---|---|---|---|
| 1 | auth_str |
首次获取安全码 | 否 | ✅ |
| 2 | login |
登录验签 | 否 | ✅ |
| 3 | status_post |
周期状态上报(每 60 秒全量:柜体 + 所有仓控板 × 全部通道) | 否 | ✅ |
| 4 | pow_fail_dc / pow_fail_ac |
停电上报(两条独立指令:直流 / 交流,见 5.4) | 否 | ✅ |
| 5 | bat_in |
电池插入(属性 + 动态首帧 + 电芯数据) | 是 | ✅ |
| 6 | bat_out |
电池拔出(末帧动态) | 是 | ✅ |
| 7 | on |
开始充电 —— 两种形态:① 不带 suc = 自动充电工作模式下设备自发上报(需应答)② 带 suc = 对下行 on 的回执(不回包) |
上报形态:是(仓体级);回执形态:可选(有则原样回带) | 视形态 |
| 8 | off |
两种形态:① 不带 suc = 充满自动停止(设备新请求,需应答)② 带 suc = 对下行 off 的回执(不回包) |
上报形态:是(仓体级);回执形态:可选(有则原样回带) | 视形态 |
| 9 | open/pow_on/pow_off/reset |
下行指令的执行回执 | 可选(有则原样回带;open/pow_on/pow_off 与整柜 reset 本就无 id,见 3.7 / 5.8) |
❌ 回执不回包(见 3.5) |
回执类上行(第 7、9 条,及第 8 条的
suc形态)平台一律不回包,固件发出即结束,不要等待应答。
4.2 下行(平台 → 柜控板)
act |
说明 | id 粒度 |
前置条件 |
|---|---|---|---|
on |
开始充电 | 仓体级 | 需设备在线(隐含已 login) |
off |
停止充电 | 仓体级 | 需设备在线(隐含已 login) |
open |
柜门解锁(下发后由人拉开,门不会自动开) | 柜级 | 需已 login 成功 |
pow_on |
接触器合闸 | 柜级 | 需已 login 成功 |
pow_off |
接触器分闸 | 柜级 | 需已 login 成功 |
reset |
重启整柜 / 单仓控板 | 柜级(不带 id)/ 仓控板(一段) |
需已 login 成功 |
⛔ 登录门控(双向同一条规则):未
login成功前,平台上行丢弃、下行不投递 —— 下表中的on/off/open/pow_on/pow_off/reset一条都不会到达设备,设备收不到任何字节(唯一例外是auth_str/login自己的应答,见 2.2)。 平台侧拦截,固件无需为此做任何处理。
⚠️ 还有一道「设备在线」前置:平台侧要求该 IMEI 的设备节点在线(即
device-server侧存在已登录的连接),否则一律不下发,管理端会提示「设备不在线」。离线时的表现同样是"固件收不到任何报文"。
⚠️ 柜门是整柜共用的,只有一扇:
open作用在整柜的柜门上,对应cabinet.status1的 bit4。 本协议没有"按仓体(舱门)开门"的指令,也没有"关门"指令。
pow_on / pow_off —— 远程通断柜内总电(接触器)
| 指令 | 动作 | 后果 |
|---|---|---|
pow_on |
接触器合闸 | 整柜上电(含各仓控板),仓体/电池数据恢复 |
pow_off |
接触器分闸 | 整柜下电:仓控板随之下电 → 该柜所有仓体/电池数据不可用;设备会因此上报 pow_fail_dc(直流停电,见 5.4.1) |
- 这是运维动作(检修 / 停用 / 复位总电),不是充放电业务,与柜门、单仓充电是不同层级
pow_off期间该柜无法充电、仓体状态不可读,恢复需pow_on- 接触器当前状态由
status_post的cabinet.status1bit6 体现(0=导通 / 1=断开)
柜门的完整闭环(开门 = 平台指令,关门 = 人为动作)
平台下发 open → 柜控板【解锁】 → 人拉开柜门 → cabinet.status1 bit4 = 1
(最迟 60 秒内随 status_post 上报)
人手动关闭柜门 → cabinet.status1 bit4 = 0
(最迟 60 秒内随 status_post 上报,平台据此得知已关门)
open是解锁动作:门不会自动打开,仍需人拉(下发后未拉开,bit4 保持 0,属正常)- 关门不在平台侧:人关闭,平台只通过
status_post感知,最大延迟 60 秒 - 因此平台侧"柜门状态"永远来自
status_post,不要把它当成实时量
5. 上行报文详情
5.1 auth_str —— 首次获取安全码(重置后可再次获取)
→ {"act":"auth_str","msg_id":1,"dev_id":"864720562557021"}
← {"act":"auth_str","msg_id":1,"suc":1,"auth_str":"Ab3xY9Qz"}
- 返回 8 位安全码,字符集
[A-Za-z0-9]。固件需存入非易失存储,用于后续login签名。 - 只能获取一次。已申领过之后再请求,返回:
← {"act":"auth_str","msg_id":1,"suc":0,"err":"auth_str already issued"}
- 要再次获取,必须由平台管理员在后台执行「重置安全码」。重置后旧安全码立即失效,设备可再申领一次新码。
- 收到「auth_str already issued」时,固件应停止重试,继续用本地已保存的安全码
login;若login也失败,则按重连间隔周期性重试,等待管理员重置。 - 频率限制:同一 IMEI 5 次 / 5 分钟,超限返回:
← {"act":"auth_str","msg_id":1,"suc":0,"err":"rate limit exceeded"}
⚠️ 若该 IMEI 未在平台登记(未建档的柜子、或非法设备):平台不发码、不写缓存、不回任何包(属 8.1 的静默丢弃)。 合法设备必然已在平台建档(录入 IMEI),因此正常固件不会遇到这种情况;若遇到,说明设备未录入或 IMEI 写错了。
5.2 login —— 登录验签
→ {"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"}
login 成功应答的字段
| 字段 | 类型 | 说明 |
|---|---|---|
suc |
int | 1 = 登录成功 |
spec |
string(固定 4 位) | 柜子规格:前 2 位 = 仓控板数量,后 2 位 = 每块板的通道数。"0606" = 6 板 × 每板 6 通道(即 6 块仓控板、每板 6 仓)。由平台在每次 login 时按该柜已配置的仓控板 / 通道拓扑现算;恒为 4 位数字、不会是 "0000"(拓扑未配置时平台按柜型兜底,见下方说明) |
work_mode |
string | 充电工作模式(平台侧可配置,仅这两种、不再扩展):"0" = 平台指令模式(默认)——充电由平台下发 on 指令发起,设备执行后回执"1" = 自动充电工作模式——设备检测到电池后自行开始充电,并主动上报 on(不带 suc) |
固件必须按 spec / work_mode 行事
-
spec:平台按该柜已配置的仓控板 / 通道拓扑实时生成(不是"设备型号"的持久存储字段),如"0606"= 6 块仓控板 × 每板 6 仓。生成规则:- 该柜的仓控板 / 通道拓扑已配置 → 按拓扑统计(仓控板数 × 每板通道数);
- 拓扑未配置 / 不完整 → 平台按柜型兜底(按该柜的仓控板数量 × 每板 6 通道生成)。
- 因此
spec恒为 4 位数字,永远不会是"0000"—— 固件不需要为"收到 0000"做任何特殊处理。
-
固件必须以收到的
spec决定上报结构:前 2 位 = 上报几块仓控板,后 2 位 = 每板上报几路通道("0606"→boards[]6 项、每项channels[]6 个元素)。- 若
spec与本机实际读到的板数 / 通道数不一致:先按spec的结构上报(板数与每板通道数都按spec),把该不一致记入本地日志并经运维渠道反馈平台。 - ⛔ 不要自造"规格异常"上行指令 ——
act取值是本协议的一部分,平台只识别清单内的act(见 2.3)。
- 若
-
spec与仓控板实际能否通信无关 —— 某块板扫不到(485 芯片坏 / 线松)属于硬件故障,不由本字段表达。 -
work_mode:"0"→ 等平台on指令再开始充电;收到后执行并回执"1"→ 检测到电池即自行开始充电,并主动上报on(不带suc);平台仍可能下发on,此时按回执处理- 每次
login都以本次应答为准(平台改了配置,重连后即生效)
-
timestamp为秒级(不是毫秒),与平台时差须在 ±60 秒内。 -
iccid可选;上报后平台自动比对,SIM 卡更换会被记录。 -
平台只有在收到
suc=1且含spec的应答后,才把该连接视为已登录(这是唯一判据:login成功前平台上行丢弃、下行不投递,见 2.2 / 10 第 10 条)。 -
平台对
login只做签名比对:与平台侧保存的该设备安全码(持久保存,不随连接重建 / 平台服务重启失效)一致 → 成功;否则 →invalid signature。平台不判断、也无法判断设备本地是否持有安全码。 -
失败应答只有一种:
| 场景(平台内部逻辑,不对外区分) | err |
固件动作 |
|---|---|---|
| 时间戳超出 ±60 秒 / 签名与平台侧保存的该设备安全码不一致 / 平台 DB 中没有该设备的安全码记录 | 一律 invalid signature |
重连 → 先 auth_str 申领新码;若返回 already issued → 停止重试并检查本地时钟 |
⛔ 为什么不区分:若分别返回"时间戳无效 / 签名错误 / 无记录",外部就能据此试探系统状态(例如判断某 IMEI 是否已登记、安全码是否被重置)。统一返回
invalid signature是刻意的安全设计,不要改动。
⚠️
login返回suc=0后,平台会立即断开该 TCP 连接(见 2.2)。设备需重新建连后再走上面的固件动作。
签名算法:见第 6 节(含可直接用于固件单元测试的自测向量)。
5.3 status_post —— 定时状态上报(每 60 秒全量)
每 60 秒上报一次,每次全量:携带所有仓控板及其全部通道的状态,不做增量、不分片。空仓也要上报(byte0 = 00 无电池,byte1 照实报电源状态,数值全 0)。
→ {"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":2,"channels":["008200000000000000","008200000000000000",
"008200000000000000","008200000000000000",
"008200000000000000","008200000000000000"]}
]}
← {"act":"status_post","msg_id":3,"suc":1}
上例中
"008200000000000000"= 空仓:byte000无电池;byte182有电源 + 电源打开(电源状态与有没有电池无关);SOC/电压/电流/温度全 0。 ⚠️ 示例为截断写法(只列了board1、2 两块板):实际必须按login应答的spec上报全部仓控板与每板全部通道 ——"0606"→boards[]6 项、每项channels[]6 个元素(见 5.2 / 10 第 2、9 条)。省略任何一块板,平台侧那块板的通道状态就停留在旧值 / 缺失。
顶层字段
| 字段 | 类型 | 说明 |
|---|---|---|
rssi |
整数 | 4G 信号强度,0–99,数值越大越好(不是 dBm、不可为负) |
cabinet |
object | 柜体状态,柜级、每条报文只带一次 |
boards |
array | 所有仓控板;每项含 board(地址 1–15)与 channels(该板全部通道) |
cabinet 块(对应硬件「柜体数据」8 字节)
| 字段 | 类型 | 说明 |
|---|---|---|
status1 |
hex 字符串(2 位) | 柜体状态1 字节,位定义见 3.8 |
status2 |
hex 字符串(2 位) | 柜体状态2 字节,位定义见 3.8 |
temp |
整数 | 柜体温度。⚠️ 这是 JSON 数值字段 = 平台单位,由设备换算后上报:送 K×100(即硬件 1K 的读数 ×100,29800 = 298.00K)。平台原样存库、不再 ×100;管理端按 temp/100 − 273.15 显示为 ℃ |
meter |
整数 | 电表读数。⚠️ 这是 JSON 数值字段 = 平台单位,由设备换算后上报:送 Wh(即硬件 0.1kWh 的读数 ×100,125500 = 125.5 kWh = 125500 Wh)。平台原样存库、不再 ×100 |
✅ 平台侧落地情况(2026-09-20):本字段已按此要求落库 —— cabinets.meter_wh(最新读数)+ cabinets.meter_at(收到时刻), 并在 cabinet_meter_readings 表只在读数变化时追加一条留痕(即抄表记录,可直接用于按日统计用电量)。 此前平台只把它写进 Redis(TTL 300 秒)⇒ 上报 5 分钟后即被清掉、历史无法回溯,属实现缺口,现已补齐。 ⚠️ 与订单电量 nergy 不是一回事:meter 是整柜累计电能(电表读数,天然累加值),nergy 是单订单充电量(协议无此字段,见待确认第 13 项的兜底)。
⚠️
temp/meter与channels[]不是同一套口径,千万别混(全文统一规则见 3.1):
temp/meter是 JSON 数值字段 → 设备换算:示例里的29800/125500就是最终值,固件不要再乘 100。channels[]是 hex 字节流 → 平台换算:同一条报文里的0143是硬件原始值(323K),平台会 ×100 得到 32300(K×100)。
channels —— 每通道 18 个 hex 字符(9 字节),原样透传
一个通道 = 一个仓(可能空着、也可能插着电池)。不拆字段、直接拼接 hex——4G 层把硬件那 9 个字节按顺序拼成 18 个 hex 字符即可,无需做任何转换。 ⚠️ 因此 hex 里的数值一律是原始硬件单位(SOC 1% / 电压 0.01V / 电流 0.01A / 温度 1K),换算在平台侧做(见 3.1 统一规则)。
| 项 | 规则 |
|---|---|
| 形式 | 字符串数组,每个元素 = 恰好 18 个 hex 字符 |
| 大小写 | 平台解析不区分大小写(0e10 与 0E10 等价);建议固件统一小写 |
| 空仓 | byte0 = 00(无电池);byte1 照实报电源状态(电源与是否有电池无关);byte2–8 数值全 0。例:空仓但电源正常 → "008200000000000000" |
| ⚠️ 两个状态相互独立 | 电池状态(byte0)与电源状态(byte1)是两回事,平台前端也分两个指示分开显示——不要因为"没电池"就把 电源状态 写成 0 |
| 顺序(通道号怎么来) | 通道号由数组下标决定,下标从 0 开始:第 1 个元素(下标 0)= 通道 1,第 2 个元素(下标 1)= 通道 2 …… 依此类推。数组顺序不得打乱 |
| 长度 | 数组元素个数 = 该板通道数(当前 36 仓柜型为 6),由 login 应答的 spec 后 2 位决定 |
| 端序 | 多字节字段一律大端(高字节在前) |
每 18 个字符的字节布局(与硬件「充电动态数据」9 字节逐字节一致)
左半是设备侧(照抄硬件原始字节),右半是平台侧换算(统一规则见 3.1)。
| 字节位(hex 字符位) | 含义 | 设备侧:硬件单位(原样拼进 hex) | 平台侧:换算 |
|---|---|---|---|
| byte0(第 1–2 字符) | 电池状态 | — | 位定义见 3.8 batt_status |
| byte1(第 3–4 字符) | 电源状态 | — | 位定义见 3.8 pow_status |
| byte2(第 5–6 字符) | SOC | 1% | ×10 → ‰ |
| byte3–4(第 7–10 字符) | 充电电压 | 0.01V,大端 | ×10 → mV |
| byte5–6(第 11–14 字符) | 充电电流 | 0.01A,大端 | ×10 → mA |
| byte7–8(第 15–18 字符) | 电芯最高温度 | 1K,大端 | ×100 → K×100 |
解析示例(取自硬件文档的示例值)
"828255152c01f40143"
82 电池状态 = 有电池 + 正在充电(bit7 + bit1)
82 电源状态 = 有电源 + 电源打开(bit7 + bit1)
55 SOC = 85%
152C 电压 = 5420 × 0.01V = 54.20V
01F4 电流 = 500 × 0.01A = 5.00A
0143 温度 = 323K
以上全是硬件原始值(设备侧原样拼接,不换算)。平台换算后的入库值为:SOC
850‰、电压54200mV、电流5000mA、温度32300(K×100)。
cabinet.status1/cabinet.status2,以及通道 hex 里的 byte0 / byte1(电池状态、电源状态)——这 4 个字节的位定义与解析示例统一见 3.8 状态字节(hex)解析表。
⚠️ 仓控板离线如何上报 —— 待硬件侧确认(待确认事项第 16 项) 若某块仓控板的 485 通信中断(芯片坏 / 线松 / 板子掉电),4G 层将读不到该板的任何数据。 这种情况下 4G 层应如何上报,本文暂未定义 —— 例如:省略这块板?照报一块数值全
0的板?还是另有在线/离线标志? 平台能否把"某块板离线"与"该板通道都空着"区分开,完全取决于硬件侧怎么报 —— 以硬件答复为准,本文不做任何预设。 在硬件答复前,平台侧「仓控板故障」这类告警不做实现。 ⚠️ 该答复还会直接影响 §5.6 的**「无电池」状态兜底**:若选择"板离线时照报一块数值全 0 的板", 平台会把整板离线误判成"电池被拔走"(详见 §5.6),请一并确认。
5.4 pow_fail_dc / pow_fail_ac —— 停电上报(两条独立指令)
停电上报是两条独立的 act,没有 info 字段——看到 act 就知道是哪种停电。
5.4.1 pow_fail_dc —— 直流停电
→ {"act":"pow_fail_dc","msg_id":4}
← {"act":"pow_fail_dc","msg_id":4,"suc":1}
- 场景:柜内接触器分闸(含平台远程
pow_off)→ 仓控板下电 - 特征:柜控板本身仍有电,可正常上报,不依赖电容
⏳ 这一条是否还需要,待硬件确认(见文首待确认事项第 17 项): 直流停电时柜控板仍有电,而
status_post的cabinet.status1**bit0「断电」**本身就是一个状态位—— 若接触器分闸时 bit0 会置 1,则本指令与 bit0 重复,可考虑取消。
5.4.2 pow_fail_ac —— 交流停电
→ {"act":"pow_fail_ac","msg_id":5}
← {"act":"pow_fail_ac","msg_id":5,"suc":1}
- 场景:柜控板自身断电(市电 380V 断)→ 靠板载电容余电抢发这最后一条
- 特征:与接触器无关;发完柜子即完全失电
两条的共同规则
| 项 | 说明 |
|---|---|
| 字段 | 只需 act / msg_id,无附加字段(设备身份由连接确定) |
| 性质 | 事件上报(一次性,非周期):平台收到即记录,并置为该柜的停电状态(pow_fail_dc / pow_fail_ac),前端显示;不与其他业务逻辑联动 |
| 时机 | 检测到停电的瞬间立即发出 |
| 应答 | {"act":"<同请求>","msg_id":N,"suc":1}(pow_fail_ac 发出后设备可能已失电,收不到应答属正常) |
| 恢复 | 供电恢复后消除停电状态(前端不再显示) |
平台内部映射(
act名与字段名一一对应):pow_fail_dc→ Redisdevice:{imei}.pow_fail_dc;pow_fail_ac→ Redisdevice:{imei}.pow_fail_ac。
5.5 bat_in —— 电池插入
电池插入时上报一次,携带电池组属性(硬件 38 字节)+ 动态数据首帧(硬件 9 字节)+ 电芯数据。
(注:这里的"电池组属性"是硬件文档的表名,指"插在该通道里的这块电池"的属性;status_post 里的通道状态才是按通道组织。)
→ {"act":"bat_in","msg_id":5,"id":"01-3",
"battery":{
"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"
}}
← {"act":"bat_in","msg_id":5,"suc":1}
📌 上例是15 串磷酸铁锂体系,内部自洽:
15 × 3.6V = 54.0V,与info里的电压一致;cell_count与cells的长度必须匹配(cell_count × 4 + 16)。
id 段数不是 2(仓体级要求 仓控板-通道)时:
← {"act":"bat_in","msg_id":5,"suc":0,"err":"invalid id"}
标识与属性(硬件「电池组属性」38 字节)
| 字段 | 类型 | 说明 |
|---|---|---|
battery_id |
string | 电池序列号(ASCII ≤30),作为电池唯一标识。 BMS 读不到序列号时传空串 "" —— 不影响充电与订单记录,只是平台不知道是哪块电池 |
brand_code |
string | 厂家代码,原值透传,由平台解析为名称("1" = 星恒) |
model_code |
string | 电池型号,原值透传,由平台解析为名称("1" = 磷酸铁锂) |
material_code |
string | 电芯材料,原值透传,由平台解析 |
cell_count |
整数 | 电芯节数 |
rated_voltage |
整数 | 额定电压。JSON 数值字段 = 平台单位,由设备换算后上报:送 mV(硬件 0.1V 的读数 ×100,48000 = 48.0V)。平台原样存库 |
rated_capacity |
整数 | 额定容量。JSON 数值字段 = 平台单位,由设备换算后上报:送 mAh(硬件 0.1AH 的读数 ×100,24000 = 24.0Ah)。平台原样存库 |
动态数据(硬件「充电动态数据」9 字节)
| 字段 | 类型 | 说明 |
|---|---|---|
info |
string(恰好 18 个 hex 字符) | 该通道的 9 字节电池信息(硬件「充电动态数据」)。布局与解析规则完全同 5.3 的 channels 元素:byte0 电池状态 / byte1 电源状态 / byte2 SOC / byte3–4 电压 / byte5–6 电流 / byte7–8 电芯最高温度,多字节大端。⚠️ hex 内是原始硬件单位(SOC 1% / 电压 0.01V / 电流 0.01A / 温度 1K),由平台换算(见 3.1 统一规则),固件不换算 |
info与status_post.channels[]里的元素是同一种 18 字符 hex,只是这里只针对"刚插入的这个通道"。
电芯数据(硬件「电芯数据」表)—— 同样拼接 hex,不拆字段
| 字段 | 类型 | 说明 |
|---|---|---|
cells |
string | 电芯数据整段拼接的 hex 字符串,长度 = cell_count × 4 + 16 个字符 |
cells 的段布局(按硬件「电芯数据」表顺序,多字节一律大端)
⚠️
cells也是 hex 字节流 = 原始硬件单位(电压、温度均为硬件原值),由平台换算,固件只负责按cell_count顺序拼接(见 3.1 统一规则)。
| 段 | 长度(hex 字符) | 含义 | 设备侧:硬件单位(原值) |
|---|---|---|---|
| 电芯电压 × N | N × 4(N = cell_count) |
各电芯电压,依次排列,每节 2 字节 | 硬件原值(示例按 mV 对照) |
| 最高电压 | 4 | 所有电芯中的最高电压,2 字节 | 硬件原值 |
| 最低电压 | 4 | 最低电压,2 字节 | 硬件原值 |
| 最高温度 | 4 | 电芯最高温度,2 字节 | 1K |
| 最低温度 | 4 | 电芯最低温度,2 字节 | 1K |
长度对照(cell_count × 4 + 16)
| 电芯节数 | hex 字符数 | 字节数 |
|---|---|---|
| 15 | 76 | 38 |
| 20 | 96 | 48 |
| 22 | 104 | 52 ← 与硬件文档写的「52 个字节」吻合 |
示例解析(15 节)
"0e100e150e0e0e120e170e0b0e110e140e0f0e130e160e0d0e100e120e0e | 0e17 | 0e0b | 012a | 0128"
└────────── 15 节电芯电压(每节 4 字符,共 60 字符)──────────┘ 最高 最低 最高温 最低温
0e10 = 3600 0e15 = 3605 0e0e = 3598 …(硬件原始值) =3607 =3595 =298K =296K
上例各段均为硬件原始值,平台侧再按 3.1 的规则换算(温度 ×100 → K×100)。
⚠️ 待确认第 15 项:硬件文档写「电芯数据 52 个字节」,但其示例写「20 节电芯」——按 20 节算是 48 字节,若按 22 节算正好 52 字节。 固件请按实际
cell_count拼接,不要按固定 52 字节处理。
cells块只随bat_in上报一次,不进 60 秒周期上报。
平台行为
⛔
bat_in与订单无关 —— 它只是"插入了一块电池"这个事件。平台在这里不创建订单(订单只由on建立,见 5.7)。
- 校验
id→ 解析出仓控板与通道(非法 →err: invalid id) - 登记 / 更新电池台账(
status=1 使用中;battery_id为空时跳过台账) - 记录插入事件(
device_logs) - 回复
suc:1
订单从
on开始、到bat_out(或off)结束;bat_out是订单结束的标志 —— 没有bat_out就说明这笔订单还在进行中。 插入电池后没有开始充电(没有on),就不会有订单。
⚠️
energy(单订单累计充电量,Wh)协议不包含该字段(待确认第 13 项)。 平台侧已按兜底方案实现:对每条status_post的通道电压/电流按 60 秒积分累加, 订单结束时写入订单电量与充电记录;硬件侧不需要为本条改动任何报文。 ⚠️ 代码类字段(brand_code/model_code/material_code):4G 层原值透传、平台侧负责映射(见第 10 节第 7 条);取值表内容待硬件提供(现仅知01)。
5.6 bat_out —— 电池拔出
MCU 约定:先 off 再 bat_out。
⛔ 硬性要求:无论电池是否充满,被拔出时都必须「先
off、再bat_out」 这条包含人在没充满时直接把电池拔走(强制拔出)的情况。 检测到电池移除后,先上报off(停止充电),再上报bat_out(电池拔出)。 否则平台那条订单只能靠兜底收尾(10 小时订单超时 / 状态兜底,见下方)。 该场景 MCU 能否检测到,见文首待确认事项第 18 项。
→ {"act":"bat_out","msg_id":6,"id":"01-3",
"battery":{"battery_id":"SN123456","info":"80005f15180000012a"}}
← {"act":"bat_out","msg_id":6,"suc":1}
battery至少带battery_id;建议同时带末帧电池信息info(18 字符 hex,规则见 5.3;hex 内是原始硬件单位,由平台换算)。- 平台结束该仓体活跃订单(
reason='user_stop'),并把电池台账置空闲。
平台兜底:订单不会永远挂着
设备可能来不及/无法上报结束事件——例如停电或离线期间,人把电池取走了(设备根本没机会发 bat_out)。为此平台侧有兜底:
| 兜底 | 规则 |
|---|---|
| 订单超时兜底(主要) | 充电订单处于"进行中"超过 10 小时未结束 → 平台自动结束该订单(reason='timeout')。依据:充电不可能超过 10 小时(再久电池会出问题) |
| 「无电池」状态兜底(次要,平台侧已实现并生效,固件无需实现) | 该通道已有活跃订单、而 status_post 报该通道 batt_status bit7 = 0(无电池)连续 2 次 → 平台判定"电池已被取走"并提前结束该订单。阈值、后果与风险见下表 |
「无电池」状态兜底细则(平台侧行为,固件不需要做任何事)
| 项 | 内容 |
|---|---|
| 判定依据 | status_post 里该通道 18 字符 hex 的 byte0 batt_status bit7 = 0(0=无电池 / 1=有电池,位定义见 3.8) |
| 阈值 | 连续 2 次上报为 0。status_post 固定每 60 秒一次 → 约 2 分钟;平台对账任务每 5 分钟跑一次,故实际最迟约 5–7 分钟内结单。单次瞬时抖动不会触发 |
| 前提 | 该通道已存在活跃订单(空仓、没建单的通道不受影响) |
| 后果 | 平台把该订单提前结束,结束原因记为 forced(异常结束),充电记录状态记为异常;电池台账置空闲。人工可在充电记录里识别这批异常单 |
| 与待确认 #18 的关系 | 这正是**第 18 项(未充满强制拔出能否检测)**的平台侧兜底:MCU 检测不到、发不出 off/bat_out 时,靠它避免订单一直挂到 10 小时超时 |
| ⚠️ 与待确认 #16 的关系(给硬件侧的输入) | 若 #16 最终选择「板离线时照报一块数值全 0 的板」,则该板所有通道的 bit7 都会是 0 → 平台会把「整板离线」误判成「电池被拔走」,把该板上所有活跃订单提前异常结单。 若选择「省略该板」,平台读不到该板通道 → 兜底不触发(该通道订单退回 10 小时超时兜底)。 请在答复 #16 时一并说明离线时的上报方式。 |
固件侧不必为"补报历史事件"负责:漏报的结束事件由上面两条兜底收尾。 但停电恢复后是否需要设备/平台做一次状态对账,仍待硬件侧答复(见文首待确认事项第 19 项)。
5.7 on / off —— 设备侧上报
on(开始充电)—— 两种形态,靠有无 suc 区分
设备支持两种充电启动方式:
| 工作模式 | 谁发起充电 | 设备发出的 on |
|---|---|---|
| 平台指令模式(默认) | 平台下发 on 指令 |
回执形态:带 suc,msg_id 回带平台号 |
| 自动充电工作模式 | 设备检测到电池后自行开始充电 | 上报形态:不带 suc,msg_id 用设备自己的计数 |
当前处于哪种模式,由
login成功应答里的work_mode下发(见 5.2):"0"=平台指令模式、"1"=自动充电工作模式。 平台侧可配置、后台可改;每次login都以本次应答为准(改了配置,设备重连后即生效)。
// ① 平台指令模式:平台先下发 on,设备执行后回执
← {"act":"on","msg_id":2,"id":"01-3"}
→ {"act":"on","msg_id":2,"id":"01-3","suc":1}
// ② 自动充电工作模式:设备自行开始充电后主动上报
→ {"act":"on","msg_id":7,"id":"01-3"}
← {"act":"on","msg_id":7,"suc":1}
- 判定与
off完全一致:带suc= 回执(平台不回包);不带suc= 设备上报(平台按新请求处理并回包)(见 3.5) - 平台两种都收:回执用于配对;上报用于记录"充电已开始"
on 是建立订单的【唯一入口】
| 工作模式 | 建单时机 |
|---|---|
平台指令模式(work_mode="0") |
平台下发 on 指令时运行建单逻辑(这是平台自己的动作,不必等回执) |
自动充电工作模式(work_mode="1") |
设备上报 on(不带 suc)时建单 |
| — | 回执形态的 on(带 suc)不建单 —— 平台指令模式下单已在下发时建了 |
建单判定(该通道当前状态)
| 该通道当前状态 | 平台行为 |
|---|---|
| 没有活跃订单 | 新建订单(status=0) |
| 有活跃订单,电池 id 没变 | 不新建 —— 判定为同一次充电 |
| 有活跃订单,电池 id 变了 | 老订单 status=2、reason='forced' 结束 → 新建订单 |
- 起始 SOC:取该通道最近一次
bat_in的info里的 SOC(on报文本身不带info);若拿不到则记0- 依据:一个通道只可能有一块电池 —— 所以电池 id 变了必然是换了新电池,老订单必须先结束
off(充电停止)
→ {"act":"off","msg_id":8,"id":"01-3",
"battery":{"battery_id":"SN123456","info":"81026415540000012a"}}
← {"act":"off","msg_id":8,"suc":1}
上例
info="81026415540000012a"解析:byte081= 有电池 + 充满;byte102= 电源打开;SOC64= 100%;电压1554= 5460×0.01V = 54.60V;电流0000;温度012a= 298K。(位定义见 3.8;以上均为 hex 内的原始硬件值,平台再按 3.1 换算)
off 是「订单结束」的触发之一(另一个是 bat_out)。
⚠️ 需告知硬件侧:除"充满"外,故障 / 过温 / 电池异常等造成充电停止时,也应当上报
off—— 否则平台那条订单只能等 10 小时订单超时 兜底,期间一直显示"充电中"。异常原因不需要在off里单独说明,status_post的batt_status故障位会体现。
若 off 到达时该通道没有活跃订单:平台不做任何处理,但照常回 suc:1(固件收到 suc:1 即视为已送达)。
可能的原因有几种,平台侧不追究:老订单已被 10 小时超时结束、或停电期间状态错乱等。
注意区分:
off也可能是平台下发的指令(管理端手动停止充电)。区分方式见 3.5:下行报文不带suc的就是指令,固件应执行停止并回执(见 5.8),而不是当成自己的上报。
5.8 下行指令的执行回执
收到 open / pow_on / pow_off / reset / on / off 指令后,固件执行完必须回执:
→ {"act":"open","msg_id":1,"suc":1}
执行异常时:
→ {"act":"open","msg_id":1,"suc":0,"err":"door lock not engaged"}
回执要求(逐条对照检查)
| 字段 | 要求 |
|---|---|
act |
必带,与收到的指令一致 |
msg_id |
必带,与收到的指令完全一致(原样回带) |
dev_id |
不带 —— 回执走的是同一条连接,平台已知道是哪台设备 |
id |
可选(平台配对只靠 msg_id + act,不依赖 id);回带只为便于日志排查 |
suc |
执行结果:1 正常 / 0 异常 |
err |
suc=0 时必带 |
⛔ 回执发出后即结束,不要再等待"回执的应答"——平台对回执不再回包。若固件把平台后续的任何报文当成"对回执的应答",会破坏
msg_id配对。
reset 的回执语义(特殊,务必按此实现)
reset 只是一个动作,回执的含义是"已收到并开始执行(重启)",不表示重启成功:
| 项 | 说明 |
|---|---|
| 顺序 | 固件必须先发回执、再执行重启 —— 整柜重启后就没机会回执了 |
| 回执内容 | {"act":"reset","msg_id":N,"suc":1} —— 表示下发成功,不需要体现"重启成功与否" |
| 重启是否成功 | 平台不看回执;平台看设备是否重新 login —— 重连并 login 成功即证明重启完成 |
| 平台侧表现 | 收到 suc:1 → 管理端显示"已下发";随后设备重连 login → 恢复正常在线 |
也就是说:
reset收到suc:1只代表指令送达并已执行,不代表柜子/仓控板已经重启完成。
6. 签名算法与自测向量
6.1 算法
input = dev_id + auth_str + timestamp // 三段直接拼接,无分隔符,无空格
hash = SHA256(input) // 32 字节
hex = 小写十六进制字符串(64 字符)
sign = hex[56..64] // 第 57~64 个字符,共 8 位
dev_id:IMEI 字符串auth_str:auth_str接口返回的 8 位安全码,原样拼接timestamp:本次login发送的秒级时间戳,按十进制数字拼接sign为 8 位小写十六进制字符
6.2 自测向量(可直接用于固件单元测试)
| dev_id | auth_str | timestamp | SHA256(input) | 期望 sign |
|---|---|---|---|---|
123456789012345 |
abcd1234 |
1719800000 |
23e2f229ca7cd67cc1c74a0219aa69cfbf2b267b1974a922ddae6e453957b959 |
3957b959 |
864720562557021 |
Ab3xY9Qz |
1750000000 |
a44d44fbbf7bccffc5c5068d987ea4e89f17e9a9bae865635921afcc269c249d |
269c249d |
固件自测时把这两个输入喂进签名函数,输出必须逐字符相等。任何一位偏差都会导致登录失败。
7. 典型交互时序
⚠️ 本产品为工厂用集中充电柜:柜门开关与充电逻辑【互不联动】。
- 柜门开 / 关是独立动作,与电池插入、充电开始 / 结束之间没有因果关系,也没有先后要求。
- 充电的开始 / 结束只由电池的插入 / 拔出与充满决定(
bat_in→on→off→bat_out)。- 下面把两条流程分开列出:二者可任意交错,也可只发生其中一条。
- 不要把"开门"当成充电流程的前置步骤。
7.1 公共前提:连接与保活
【首次配网(仅一次)】
→ {"act":"auth_str","msg_id":1,"dev_id":"864720562557021"}
← {"act":"auth_str","msg_id":1,"suc":1,"auth_str":"Ab3xY9Qz"}
固件保存安全码
【上电 / 断线重连后】
→ {"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", ...]}, ...]}
← {"act":"status_post","msg_id":3,"suc":1}
上例为截断写法(
...):实际必须带全部仓控板 × 每板全部通道(含空仓)。
7.2 柜门动作(独立流程,与充电无关)
【平台下发解锁(开门)】
← {"act":"open","msg_id":1}
→ {"act":"open","msg_id":1,"suc":1}
柜控板解开门锁;门不会自动打开,需【人拉开】
【人拉开柜门 → 状态变化,最迟 60 秒内上报】
→ {"act":"status_post","msg_id":9,"rssi":85,
"cabinet":{"status1":"10","status2":"00","temp":29800,"meter":125500}, ...}
// status1 = "10" → bit4=1 柜门打开(§3.8)
【人手动关闭柜门 → 状态变化,最迟 60 秒内上报】
→ {"act":"status_post","msg_id":10,"rssi":85,
"cabinet":{"status1":"00","status2":"00","temp":29800,"meter":125500}, ...}
// status1 = "00" → bit4=0 柜门关闭
- 柜门开 / 关不触发任何充电动作;充电的启停也不依赖柜门状态。
- 关门:本协议不提供"关门"指令(任何柜型均如此),柜门由人手动关闭;关闭后状态经
status_post(≤60 秒)回到平台。- 硬件侧另有约定:「柜门由打开→关闭」这一跳变由 4G 层本地检测并用于触发电池激活(平台不参与、无需下发任何指令)。
7.3 充电流程(独立流程,与柜门无关)
【电池插入 → 设备上报】
→ {"act":"bat_in","msg_id":4,"id":"01-3","battery":{ ...属性 + info(18hex) + cells(hex)... }}
← {"act":"bat_in","msg_id":4,"suc":1}
平台登记/更新电池台账(⛔ 不建单 —— 订单由 on 建立,见 §5.7)
【开始充电 —— 取决于工作模式,二者只发生其一】
① 平台指令模式(默认)
← {"act":"on","msg_id":2,"id":"01-3"}
→ {"act":"on","msg_id":2,"id":"01-3","suc":1}
带 suc = 回执,平台不再回包
② 自动充电工作模式
→ {"act":"on","msg_id":5,"id":"01-3"}
不带 suc = 设备自行开始充电后的主动上报
← {"act":"on","msg_id":5,"suc":1}
平台按新请求处理并回包
【充电中】实时值随每 60 秒的 status_post 上报
【充满,设备主动上报】
→ {"act":"off","msg_id":6,"id":"01-3",
"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"}}
← {"act":"bat_out","msg_id":7,"suc":1}
平台结束订单
on/off都是双向的act,一律靠有无suc区分"回执"与"设备上报"(见 3.5)。
允许并行:同一连接内可以有多个未完成请求同时在飞,配对完全靠 msg_id,到达顺序不影响正确性。
t=0.0 → {"act":"status_post","msg_id":8,"rssi":85,"cabinet":{...},"boards":[...]}
t=0.1 ← {"act":"on","msg_id":3,"id":"01-2"}
t=0.2 ← {"act":"status_post","msg_id":8,"suc":1} ← 命中固件挂起表 → 完成 8
t=0.3 → {"act":"on","msg_id":3,"suc":1} ← 命中平台挂起表 → 完成 3
8. 异常与边界
8.1 会被平台静默丢弃、且不给任何回应的情况(固件会一直等不到应答)
| 情况 | 说明 |
|---|---|
上行缺 act |
平台无法解析报文类型 |
auth_str / login 缺 dev_id |
平台无法把这条 TCP 连接与设备绑定 |
未登录就发业务报文(首条不是 auth_str / login) |
除 auth_str / login 外,其余 act 必须先 login 成功(见 2.2)。未登录连接尚未与 dev_id 建立可信绑定,平台丢弃且无法回包;且连接建立后 30 秒内未 login 成功会被主动断开 |
发 auth_str 的 IMEI 未在平台登记 |
该设备未建档(合法设备必然已录入 IMEI)。平台不发安全码、不写缓存、不回包(见 5.1) |
| 平台内部处理异常(数据库 / 缓存故障等,概率极低) | 平台侧只记 ERROR,该条报文可能一条应答都收不到。固件按 60 秒超时处理(见 3.6),不要重发同一业务动作;下一条周期 status_post 照常发(用新的 msg_id)即可 |
固件必须保证每条上行都带
act、首条先完成auth_str/login(并带上dev_id)。出现上述情况时,固件侧表现为"发了没反应",超时后才丢弃——不要误判为网络问题。 ⚠️ 反过来,平台在未登录连接上也会"静默不投递"下行报文(见 2.2 / 4.2)—— 设备侧表现同样是"什么都没有收到",这是设计行为、不是断线。
8.2 平台会明确返回异常的常见情况
| 情况 | 平台应答 |
|---|---|
未知 act |
{"act":"...","suc":0,"msg_id":N,"err":"unknown act"} |
login 失败(不区分原因) |
{"act":"login","suc":0,"msg_id":N,"err":"invalid signature"} |
仓体 id 非法 |
{"act":"bat_in","suc":0,"msg_id":N,"err":"invalid id"} |
完整
err取值与含义见 3.3(含auth_str already issued/rate limit exceeded/invalid signature)。
8.3 其它约定
- 登录门控(双向):
login成功前,平台不会下发任何报文(on/off/open/pow_on/pow_off/reset及任何其它应答全部拦截)——设备收不到任何字节,唯一例外是auth_str/login自己的应答(平台侧拦截,见 2.2 / 4.2)。 - ⚠️ 不要依赖平台应答里出现的"未知字段":平台内部可能附带以
_开头的服务端字段(如_dev_id、_close),这些字段只用于平台内部路由/控制,不会下发到设备。固件必须忽略未知字段,不要因多字段而解析失败;也不要在应答里依赖id—— 应答不带id(id只出现在指令与设备上行报文里,表示动作目标,见 3.7)。 - 单条报文超过 **65536 字节(64 KB)**会导致连接被断开,固件需具备重连能力。
- 固件侧建议:断线后按 10s / 30s / 60s / 300s 递增间隔重连;重连后必须重新
login成功(收到suc=1且带spec)才能发送业务报文。
9. 固件自测清单
基础连通
- 首次开机:连接后首条报文是
auth_str,拿到安全码并持久保存后才发login - 重启/断线重连:连接后首条报文直接是
login(不再申请安全码) login失败后能识别平台主动断开(读取到 EOF),并重连后按"先auth_str"的顺序处理login持续失败时能检查本地时钟(平台只回invalid signature,不区分原因)- 平台管理员重置安全码后,设备无需人工干预即可自愈登录
- 建连后立即发
auth_str或login(不得先发业务报文;30 秒内未 login 成功会被平台断开) - 未登录时发其它
act不会得到任何应答;同时平台也不会向该连接下发任何报文(auth_str/login自己的应答除外)—— 固件不应依赖被丢弃的报文,也不应据此重试 - 同一时刻只维持一条 TCP 连接:同一
dev_id的新连接会让平台接管并断开旧连接(旧连接收不到任何通知报文) - 每条报文以
\n结尾,单行 ≤ 65536 字节(64 KB) - 每条上行都带
act;auth_str/login带dev_id(其余报文不带dev_id) msg_id在连接内由固件侧独立自增,不复用尚未完成的号
签名与登录
auth_str返回 8 位[A-Za-z0-9]- 用 6.2 节向量验签,期望值逐字符相等(
3957b959/269c249d) sign取 8 位(hex[56..64])timestamp为秒级,与平台时差在 ±60 秒内login成功收到spec、work_mode;spec恒为 4 位数字、不会是"0000"(平台按该柜拓扑生成,拓扑未配置时按柜型兜底)- 以收到的
spec决定上报结构:前 2 位 = 上报几块仓控板、后 2 位 = 每板上报几路通道(如"0606"→ 6 块板 × 每板 6 通道) spec与本机实际读到的板数 / 通道数不符时:先按spec的结构上报,并记日志反馈平台(不得自造新的act,见 2.3)- 按
work_mode行事:"0"等平台on指令;"1"检测到电池即自行充电并主动上报on
应答处理
- 应答的
msg_id与请求一致 - 收到应答后按
msg_id且act一致配对,不再回包 - 能按"有无
suc"区分应答与指令;收到不带suc的报文时执行并回执 - 回执带
act+msg_id+suc(异常时带err;不带dev_id) - 60 秒未应答则丢弃该请求,不重复发送业务动作
- 超时后迟到的应答被丢弃、不补配;已作废的
msg_id不被立即复用
状态上报
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 秒/次,且每次为全量(所有仓控板 × 全部通道,无增量、无分片)
- 空仓也上报:byte0 =
00(无电池),byte1 照实报电源状态(如82),数值全0
业务时序
bat_in→ 平台登记电池台账(不建单)→ 收到suc=1on(开始充电)→ 平台建单(work_mode="0"平台下发on;"1"设备上报on)- 收到
on指令并执行后回执 - 充满上报
off bat_out上报- 拔出电池时先
off再bat_out;若未充满强制拔出检测不到,平台会在该通道连续 2 次(约 2 分钟)报无电池后提前异常结单(forced,见 §5.6 状态兜底) - 断线重连后能重新
login并继续上报
10. 约定定稿
以下约定已定稿,固件按此实现:
| # | 事项 | 定稿值 |
|---|---|---|
| 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 平台均忽略 |
| 4 | 上报节奏与内容 | status_post 固定每 60 秒一次,每次全量上报(所有仓控板 × 全部通道;不区分空闲/充电,不做增量、不分片)。任意上行间隔必须 < 300 秒(平台 300 秒无上行即判离线) |
| 5 | 柜门与充电的关系 | 本产品为工厂用集中充电柜,柜门开 / 关是独立动作,与充电逻辑互不联动:开门不触发充电;充电启停也不依赖柜门状态。柜门实际状态由 status_post 的 cabinet.status1 bit4 上报(0=关闭 / 1=打开) |
| 6 | 柜门"开 / 关"的实现方式 | 开:平台下发 open(解锁),门由人拉开——门不会自动打开;关:人手动关闭。本协议不提供"关门"指令,任何柜型均如此。开 / 关状态都经 status_post 回传,最大延迟 60 秒,平台侧不得当作实时量 |
| 7 | 代码类字段的映射责任 | brand_code / model_code / material_code:4G 层按字符串原值透传,不做任何转换;「代码 → 名称」的映射在平台侧完成。映射表由平台维护(可热更新,不硬编码进固件);4G 层只须保证原值不丢、不改写、不补零、不转义 |
| 8 | 充电启动的两种工作模式 | ① 平台指令模式(默认):平台下发 on → 设备执行后回执(带 suc)。② 自动充电工作模式:设备检测到电池后自行开始充电 → 设备主动上报 on(不带 suc) → 平台按新请求处理并回包。两种形态一律靠有无 suc 区分(见 3.5),与 off 规则完全相同。当前模式由 login 成功应答的 work_mode 下发("0"=平台指令模式 / "1"=自动充电工作模式),平台侧可配置、后台可改,每次 login 以最新配置为准 |
| 9 | login 应答下发柜子规格与工作模式 |
spec(固定 4 位字符串、恒为数字、不会是 "0000")= 前 2 位仓控板数 + 后 2 位每板通道数;当前 36 仓柜型为 "0606"(6 块仓控板 × 每板 6 仓)。平台在每次 login 时按该柜已配置的仓控板 / 通道拓扑现算(不是"设备型号的持久存储"):拓扑已配置 → 按拓扑统计;拓扑未配置 / 不完整 → 按柜型兜底(该柜仓控板数 × 每板 6 通道)。固件必须以收到的 spec 决定上报几块板 / 每板几路通道(前 2 位 = 板数、后 2 位 = 每板通道数);若与实际不符,先按 spec 上报并记日志反馈平台,不得自造新 act。旧的 cell(仓控板数量)不再下发给设备(平台仅在生成 spec 兜底时内部使用),work_mode("0"=平台指令模式 / "1"=自动充电工作模式)平台侧可配置、后台可改,每次 login 以最新配置为准 |
| 10 | 连接登录门控(上行 + 下行,同一条规则) | 只有 auth_str / login 可以在未登录状态发送;其余所有 act(含 status_post、bat_in、on/off、回执等)必须先 login 成功,否则平台上行一律丢弃、且不回任何应答。⛔ 下行方向同一规则: login 成功前,平台不会向该连接投递任何报文 —— on / off / open / pow_on / pow_off / reset 等指令与其它应答全部不下发,唯一例外是 auth_str / login 自己的应答(否则首次配网会死锁)。因此未登录期间设备收不到任何字节。未登录的 TCP 连接不予保持:连接建立后 30 秒内未 login 成功,平台主动断开(固件应建连后立即 auth_str/login) |
| 11 | 停电上报是两条独立指令 | 没有 pow_fail 这条指令,也没有 info 字段。停电上报分两条独立 act:pow_fail_dc = 直流停电 —— 柜内接触器分闸(含远程 pow_off)导致仓控板下电,柜控板仍有电、可正常上报;pow_fail_ac = 交流停电 —— 柜控板自身断电(市电断),靠板载电容余电抢发最后一条,与接触器无关。两条均无附加字段,事件上报:平台收到即记录并置为该柜的停电状态,前端显示(不与其他业务逻辑联动);供电恢复后消除该状态 |
| 12 | 通道数据用 hex 透传,不拆字段 | status_post.channels[] 的每个元素 = 恰好 18 个 hex 字符(硬件「充电动态数据」9 字节原样拼接,4G 层不做任何转换);bat_in 里电池信息用同格式的 info,电芯数据用 cells(整段拼接 hex,长度 = cell_count × 4 + 16)。字节布局:byte0 电池状态 / byte1 电源状态 / byte2 SOC(1%) / byte3–4 电压(0.01V) / byte5–6 电流(0.01A) / byte7–8 电芯最高温度(1K),多字节一律大端。 通道号由数组下标决定(第 1 个元素 = 通道 1),数组顺序不得打乱;空仓 = byte0 00(无电池)+ byte1 照实报电源状态 + 数值全 0(例 "008200000000000000")。电池状态与电源状态相互独立,两回事,前端分开显示 |
| 13 | 数值口径:hex 由平台换算、JSON 由设备换算 | 统一规则(见 3.1,全文唯一口径): ① hex 字节流( channels[] 元素、info、cells)= 原始硬件单位(SOC 1% / 电压 0.01V / 电流 0.01A / 温度 1K),平台换算(×10/×10/×10/×100),固件原样拼接、不做任何换算;② JSON 数值字段( cabinet.temp / cabinet.meter / battery.rated_voltage / battery.rated_capacity)= 平台单位,设备换算后上报:temp 送 K×100(29800 = 298.00K,管理端按 temp/100 − 273.15 显示 ℃)、meter 送 Wh(125500 = 125.5 kWh)、rated_voltage 送 mV、rated_capacity 送 mAh。平台对 JSON 数值原样存库、不再换算 |
| 14 | 订单结束的兜底(平台侧行为,固件无需实现) | ① 订单超时(主要):订单"进行中"超过 10 小时未结束 → 平台自动结束(reason='timeout')。② 「无电池」状态兜底(已实现并生效):该通道已有活跃订单、而 status_post 的 batt_status bit7 = 0(无电池)连续 2 次(status_post 每 60 秒一次 → 约 2 分钟;平台对账每 5 分钟一次,故最迟约 5–7 分钟)→ 平台判定"电池已被取走",提前结束该订单:原因 forced(异常),充电记录状态记为异常。⚠️ 风险(待确认第 16 项):若"板离线时照报全 0 的板",该板所有通道 bit7 均为 0 → 平台会把整板离线误判为电池被拔走,把该板上所有活跃订单提前异常结单;若"省略该板"则不触发(退回 10 小时超时兜底)。请在答复第 16 项时一并说明离线时的上报方式(详见 §5.6) |
尚未定稿(7 项):energy 数据源(第 13 项)、代码表取值内容(第 14 项)、电芯数据字节数(第 15 项)、仓控板离线如何上报(第 16 项)、直流停电是否需要 pow_fail_dc(第 17 项)、强制拔出能否检测(第 18 项)、停电恢复后如何兜底(第 19 项)。
完整清单、现状与影响范围见文首「⚠️ 待确认事项」 —— 此处不再重复列一遍,避免两处漂移。
其余协议规则见文末「附:本文档采用的约定」。如需变更任一条,须双方书面确认后再改固件。
附:本文档采用的约定(如有异议请提出)
sign取hex[56..64],8 位小写十六进制suc:1=正常,0=异常,suc=0必须带err- 应答必带
act,值等于被应答报文的act msg_id在单条 TCP 连接内由各方向独立自增,应答原样回带;两个方向的编号允许重叠,不需要约定号段- 允许并行交互;配对按
msg_id+act双条件;每个交互 60 秒超时,超时作废、迟到应答丢弃、不重试 - 报文性质按有无
suc判定(带suc= 应答,不带 = 指令);收到应答不再回包,双向都遵守,链条才是单向的
附 A:全指令带注释示例(JSONC,仅供阅读用)
本节覆盖协议内全部指令,每条报文逐字段带
//注释。 ⚠️ 带注释的报文不是合法 JSON、不能直接解析;可直接复制自测的干净示例见第 5 节(已通过 JSON 校验)。→柜控板→平台;←平台→柜控板。
指令总览
| # | act |
方向 | 小节 |
|---|---|---|---|
| 1 | auth_str |
上行 | A.1.1 |
| 2 | login |
上行 | A.1.2(成功)/ A.1.3(失败) |
| 3 | status_post |
上行 | A.2.1 |
| 4 | pow_fail_dc / pow_fail_ac |
上行(停电,两条独立指令) | A.2.2 / A.2.3 |
| 5 | bat_in |
上行 | A.3.1 |
| 6 | bat_out |
上行 | A.3.2 |
| 7 | on |
上行(回执,或自动充电模式下自发上报) | A.4.1 |
| 8 | off |
上行(充满自动停) | A.4.2 |
| 9 | on |
下行(平台发起充电) | A.5.1 |
| 10 | off |
下行(平台停止充电) | A.5.2 |
| 11 | open |
下行(柜门解锁) | A.5.3 |
| 12 | pow_on |
下行(接触器合闸) | A.5.4 |
| 13 | pow_off |
下行(接触器分闸) | A.5.5 |
| 14 | reset |
下行(重启整柜 / 单仓控板) | A.5.6 |
| 15 | 下行指令的执行回执(第 9–14 条,共 6 条;见 A.6) | 上行 | A.6 |
本协议共 13 个不同
act(auth_str/login/status_post/pow_fail_dc/pow_fail_ac/bat_in/bat_out/on/off/open/pow_on/pow_off/reset);其中on/off是双向的(上行见 A.4、下行见 A.5)。
A.1 鉴权
A.1.1 auth_str —— 首次获取安全码
→ {"act":"auth_str","msg_id":1,"dev_id":"864720562557021"}
// act : 动作类型,双向必带;缺失时报文被丢弃且【无任何应答】
// msg_id : 本连接内的交互号,发送方自行分配(柜控板从 1 起自增)
// dev_id : 柜控板 IMEI,15 位十进制 ——【仅 auth_str / login 带】,
// 平台用它把这条 TCP 连接与设备绑定;之后报文一律不再带(见 2.2)
// 时机 : 仅"本地无安全码"时(首次开机 / 换板 / 被重置后)
← {"act":"auth_str","msg_id":1,"suc":1,"auth_str":"Ab3xY9Qz"}
// act : 与请求一致 —— 应答必带 act
// msg_id : 与请求完全一致 —— 配对键
// suc : 1=正常 / 0=异常;只有应答带此字段
// auth_str : 8 位安全码 [A-Za-z0-9],仅此一次返回,须存入非易失存储
// 未重置时重复申领 → 平台拒绝(见 A.7):
// ← {"act":"auth_str","msg_id":1,"suc":0,"err":"auth_str already issued"}
A.1.2 login —— 登录验签(成功)
→ {"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 位
// timestamp : 秒级 Unix 时间戳(不是毫秒),与平台时差须在 ±60 秒内
// iccid : 可选;上报后平台自动比对,SIM 卡变更会被记录
← {"act":"login","msg_id":2,"suc":1,"spec":"0606","work_mode":"0"}
// spec : 柜子规格,固定 4 位(前 2 位仓控板数 + 后 2 位每板通道数)
// "0606" = 6 块仓控板 × 每板 6 仓;平台据此把连接置为"已登录"
// work_mode : 充电工作模式 "0"=平台指令模式 / "1"=自动充电工作模式
// 【登录门控的前提是双向的】:这条应答(带 spec + suc=1)是"连接已登录"的唯一标志;
// 在此之前平台【上行丢弃 + 下行不投递】,除 auth_str/login 自己的应答外
// 设备收不到任何字节(含 on/off/open/pow_on/pow_off/reset,见 §2.2 / §10 第 10 条)
A.1.3 login —— 失败(平台随即断开连接)
← {"act":"login","msg_id":2,"suc":0,"err":"invalid signature"}
// 本地安全码与平台不一致(平台重置过 / 设备侧码被改被清)
// → 固件动作:重连后先发 auth_str 申领新码,保存后再 login
← {"act":"login","msg_id":2,"suc":0,"err":"invalid signature"}
// ⛔ login 失败【只有这一种应答】——平台刻意不区分:
// 安全码不一致 / 时间戳超窗 / 平台无该设备记录,对外都是 invalid signature
// (避免外部据此试探系统状态,属刻意安全设计)
// → 固件动作:重连后先发 auth_str 申领新码 → 保存 → 重新 login
// 若 auth_str 返回 already issued(码没被重置)→ 停止重试,
// 并【检查本地时钟】是否准确(时间戳超窗也表现为 invalid signature)
// ⚠️ 平台发出该应答后会【主动断开 TCP】,固件读到 EOF 后重新建连
A.2 周期与事件上报
A.2.1 status_post —— 周期状态上报(每 60 秒全量)
→ {"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"]}]}
// rssi : 4G 信号强度,整数 0–99(不是 dBm,不可为负)
// cabinet : 柜体状态,柜级只带一次
// status1 / status2 → 柜体状态 2 字节(hex 2 位,位定义见 §3.8)
// temp → 柜体温度 K×100(29800 = 298.00K)——【JSON 字段,由设备换算】
// meter → 电表读数 Wh(125500 = 125.5 kWh)——【JSON 字段,由设备换算】
// boards[] : 所有仓控板(board 1–15),每板带全部通道
// ⚠️ 示例只列了 1 块板:实际必须按 spec 上报【全部】板与每板【全部】通道
// channels = **每通道 18 个 hex 字符**(9 字节原样透传)
// 通道号由**数组下标**决定(第 1 个元素 = 通道 1);多字节**大端**
// byte0 电池状态 / byte1 电源状态(位定义见 §3.8)
// byte2 SOC(1%) / byte3–4 电压(0.01V)
// byte5–6 电流(0.01A) / byte7–8 电芯最高温度(1K)
// ⚠️ hex 内是【原始硬件单位】,换算由平台做(见 §3.1)
// 空仓 = byte0 "00"(无电池)+ byte1 照实报电源状态 + 数值全 0
// 例:"008200000000000000" = 空仓、但电源有电且已打开
// 节奏:固定每 60 秒一次,每次全量(不区分空闲/充电、不做增量)
// 本报文同时是【保活报文】:平台 300 秒无任何上行即判离线
← {"act":"status_post","msg_id":3,"suc":1}
// suc : 1=已接收
A.2.2 pow_fail_dc —— 直流停电
→ {"act":"pow_fail_dc","msg_id":4}
// 含义 : 柜内【接触器分闸】(含平台远程 pow_off)→ 仓控板下电
// 特征 : 柜控板本身仍有电,可正常上报,不依赖电容
// 时机 : 检测到停电瞬间立即发出(事件报文,非周期)
// 平台 : 置 pow_fail_dc 标记 + 停电告警
// 字段 : 只需 act / msg_id,【无附加字段】(设备身份由连接确定)
← {"act":"pow_fail_dc","msg_id":4,"suc":1}
A.2.3 pow_fail_ac —— 交流停电
→ {"act":"pow_fail_ac","msg_id":5}
// 含义 : 【柜控板自身断电】(市电 380V 断)→ 靠【板载电容余电】抢发这最后一条
// 特征 : 与接触器无关;发完柜子即完全失电
// 平台 : 置 pow_fail_ac 标记 + 停电告警
// ⚠️ 发完这条之后,平台不会再收到该柜任何报文,
// 直到恢复供电并重连 login
← {"act":"pow_fail_ac","msg_id":5,"suc":1}
A.3 电池交互
A.3.1 bat_in —— 电池插入
→ {"act":"bat_in","msg_id":5,"id":"01-3",
"battery":{
"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"}}
// id : 两段【仓控板-通道】(仓控板补零 01–15、通道 1–8 不补零),段数必须为 2
// battery_id: 电池序列号(ASCII ≤30),电池唯一标识;**BMS 读不到时传空串 ""**(不影响充电)
// brand_code / model_code / material_code
// : 硬件代码,**字符串原值透传**,由平台解析成名称
// ("1"=星恒 / "1"=磷酸铁锂;全套代码表待硬件提供)
// cell_count: 电芯节数(示例为 15 串磷酸铁锂:15×3.6V = 54.0V,与 info 里的电压自洽)
// 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" 解析:
// byte0 80 = 有电池、尚未充电(位定义见 §3.8)
// byte1 82 = 有电源 + 电源打开(位定义见 §3.8)
// SOC 55 = 85% 电压 1518 = 54.00V
// 电流 0320 = 8.00A 温度 012a = 298K
// ⚠️ 以上都是 hex 内的【原始硬件单位】,换算由平台做(见 §3.1)
// ⚠️ 插入首帧时平台还没下发 on,故不应是"正在充电"
// cells : 电芯数据整段拼接 hex,长度 = cell_count × 4 + 16
// (示例 15 节 → 76 字符:15 节电压 + 最高 + 最低 + 最高温 + 最低温)
// 只随 bat_in 带一次,不进周期上报;段内同为【原始硬件值】,平台换算
// (SOC / 电压 / 电流 / 温度不再单独成字段:都在 info 的 18 字符 hex 里,见上)
// ⚠️ energy(累计充电量)数据源待硬件确认,确认后再补入本报文
← {"act":"bat_in","msg_id":5,"suc":1}
// 平台动作:登记/更新电池台账(本指令【不建单】—— 订单由 on 建立,见 §5.7)
// 异常:id 段数 ≠ 2 → ← {"act":"bat_in","msg_id":5,"suc":0,"err":"invalid id"}
A.3.2 bat_out —— 电池拔出
→ {"act":"bat_out","msg_id":6,"id":"01-3",
"battery":{"battery_id":"SN123456","info":"80005f15180000012a"}}
// MCU 约定:先发 off,再发 bat_out
// battery 至少带 battery_id,建议带末帧动态 info(18 字符 hex,规则同 §5.3;hex 内为原始硬件值,平台换算)
← {"act":"bat_out","msg_id":6,"suc":1}
// 平台动作:结束订单(reason='user_stop')、电池台账置空闲
A.4 设备上报 / 回执(on / off)
A.4.1 on —— 开始充电(两种形态)
// ── ① 平台指令模式:平台先下发 on(见 A.5.1),设备执行后回执 ──
← {"act":"on","msg_id":2,"id":"01-3"}
→ {"act":"on","msg_id":2,"id":"01-3","suc":1}
// 带 suc + msg_id 回带平台号 → 回执,平台不再回包
// ── ② 自动充电工作模式:设备检测到电池后自行开始充电,主动上报 ──
→ {"act":"on","msg_id":7,"id":"01-3"}
// 不带 suc + msg_id 用自己的计数 → 平台按【新请求】处理并回包
← {"act":"on","msg_id":7,"suc":1}
// 两种形态一律靠"有没有 suc"区分(见 3.5),与 off 的规则相同
A.4.2 off —— 充满自动停止(设备主动上报)
→ {"act":"off","msg_id":6,"id":"01-3",
"battery":{"battery_id":"SN123456","info":"81026415540000012a"}}
// info="81026415540000012a" → byte0 81 = 有电池 + 充满;byte1 02 = 电源打开;
// SOC 64 = 100%;电压 1554 = 54.60V(位定义见 §3.8)
// 不带 suc → 这是设备【新发起】的上报(不是对平台指令的回执)
// → 平台结束订单并回包
← {"act":"off","msg_id":6,"suc":1}
// ⚠️ 同名 off 有两种语义,靠"有没有 suc"区分(见 3.5):
// 下行不带 suc → 平台停止充电指令(A.5.2)
// 上行不带 suc → 设备自发的充满自动停(本条)
A.5 平台下发指令(下行,全 6 条)
下行报文不带
suc(见 3.5);固件收到后立即执行,并在 60 秒内回执(见 A.6)。 ⛔ 登录门控(双向同一规则):该连接未login成功时,本节全部 6 条指令都不会下发,设备收不到任何字节(auth_str/login自己的应答除外,见 §2.2 / §10 第 10 条)。
A.5.1 on —— 开始充电(仓体级)
← {"act":"on","msg_id":2,"id":"01-3"}
// msg_id : 由平台分配,固件回执时【必须原样回带】,不要自己改号
// id : 两段【仓控板-通道】
// 固件动作:对该仓体开始充电 → 回执(见 A.6)
A.5.2 off —— 停止充电(仓体级)
← {"act":"off","msg_id":3,"id":"01-3"}
// 固件动作:停止该仓体充电 → 回执(带 suc、msg_id 原样回带)
// 区分同名的两种 off 靠【方向】,不靠 suc:
// 下行 off = 平台指令(本条)→ 固件执行后回执,平台不回包
// 上行 off = 设备上报(A.4.2)→ 平台按新请求处理并回包
// 固件收到的一定是下行,按"指令"处理即可,无需自行判别
A.5.3 open —— 柜门解锁(柜级,整柜)
← {"act":"open","msg_id":1}
// id : 【柜级指令不带 id】(省略该字段)—— id 只表示【动作目标】、不含 IMEI,
// 设备身份由本 TCP 连接确定(见 §3.7);平台内部路由字段不下发到设备
// 含义 : 对【整柜唯一的柜门】执行【解锁】—— 门不会自动打开,需人拉开
// 协议没有"按仓体开门"的指令,也没有"关门"指令
// 固件动作:解开门锁 → 回执;失败时回执带 err(如 "door lock not engaged")
// 状态回传: 柜门开/关由 status_post 的 cabinet.status1 bit4 带回
// (0=关闭 / 1=打开);关门为人手动,最迟 60 秒内上报
// ⚠️ 本产品为工厂集中充电柜:柜门动作【独立】,与充电逻辑互不联动
// 开门不触发充电;充电的启停也不依赖柜门状态
A.5.4 pow_on —— 接触器合闸(柜级)
← {"act":"pow_on","msg_id":4}
// 固件动作:接触器【合闸】→ 回执
// 后果:整柜上电(含各仓控板)→ 仓体/电池数据恢复
// 状态回传:接触器状态见 status_post 的 cabinet.status1 bit6(0导通 / 1断开)
A.5.5 pow_off —— 接触器分闸(柜级)
← {"act":"pow_off","msg_id":5}
// 固件动作:接触器【分闸】→ 回执
// ⚠️ 后果:整柜下电,【仓控板随之下电】
// → 该柜所有仓体/电池数据不可用,且设备会因此上报
// pow_fail_dc(直流停电,见 A.2.2)
// → 恢复供电需再下发 pow_on
// 属【运维动作】(检修/停用/复位总电),不是充放电业务
A.5.6 reset —— 重启(柜级 / 单仓控板)
← {"act":"reset","msg_id":6}
// id 省略(柜级)→ 重启【整柜】
// ⚠️ 必须先【回执】再重启(参见 §5.8):回执只表示"下发成功",
// 不表示重启成功;重启成功的标志是设备随后【重新 login】
← {"act":"reset","msg_id":7,"id":"01"}
// id = 仓控板(一段,如 "01")→ 只重启【1 号仓控板】
// 固件动作:先回执再执行重启(否则重启后无法回包)
A.6 设备对下行指令的执行回执
回执必带
act、msg_id原样回带;⚠️ 固件收到的、不带suc的下行报文就是指令(判据见 3.5;本节讲的是固件发出的回执,回执必须带suc)。 ⚠️ 回执发出即结束,不要等待"回执的应答"——平台不再回包。
A.6.1 成功回执(以 open 为例)
← {"act":"open","msg_id":1}
→ {"act":"open","msg_id":1,"suc":1}
// act : 与收到的指令一致(回执必带)
// msg_id : 与收到的指令完全一致 —— 必须原样回带
// id : 建议回带,与指令一致(可选 —— 平台配对只靠 msg_id + act,见 §5.8)
// suc : 1=执行正常
// 注意 : 【不带 dev_id】—— 回执走同一条连接,平台已知道是哪台设备
A.6.2 失败回执
→ {"act":"open","msg_id":1,"suc":0,"err":"door lock not engaged"}
// suc=0 时 err【必带】;平台据此在管理端显示"执行失败"
A.7 平台异常应答(suc=0 时 err 必带)
← {"act":"auth_str","msg_id":1,"suc":0,"err":"auth_str already issued"}
← {"act":"auth_str","msg_id":1,"suc":0,"err":"rate limit exceeded"}
← {"act":"login","msg_id":2,"suc":0,"err":"invalid signature"}
// ⛔ login 失败【只有这一种】,平台刻意不区分失败原因
← {"act":"bat_in","msg_id":5,"suc":0,"err":"invalid id"}
← {"act":"bat_out","msg_id":6,"suc":0,"err":"invalid id"}
← {"act":"heartbeat","msg_id":7,"suc":0,"err":"unknown act"}
// "unknown act" = act 不在本协议清单内。固件【不得】自定义新指令
// (本协议无独立心跳包,保活由 status_post 承担,见 2.3)
A.8 超时(60 秒,双向对称)
← {"act":"open","msg_id":4}
// t=0 :平台登记挂起项(msg_id=4, act=open)
// t=60s :仍未收到回执 → 平台作废该挂起项,不重传,记 WARN
// t=65s :迟到的回执到达 → 该 msg_id 已作废 → 【丢弃】,不补配
→ {"act":"status_post","msg_id":9,"rssi":85}
// t=0 :固件登记挂起项
// t=60s :仍未收到应答 → 固件作废该交互,【不重试】该业务动作
// (下一条周期 status_post 照常发,用新的 msg_id)
附 B:变更记录
| 版本 | 日期 | 变更内容 |
|---|---|---|
| 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.0 | 2026-09-14 | 首版:基于平台 TCP 服务实现逐项核对生成 |