协议(唯一权威文档:docs/柜控板固件对接文档.md) - 应答必带 act(与请求一致)、msg_id 与请求一致;判"应答 vs 指令"只看有没有 suc - dev_id 只在 auth_str/login 携带;其余报文用内部字段 _dev_id 路由(下发前由 device-server 摘除) - id 只表示动作目标(柜级省略 / 一段=单板 / 两段=仓体级),不再含 IMEI - msg_id 每连接从 1 起自增;60 秒交互超时;status_post 每通道 18 字符 hex 全量上报 - 未登录连接 30 秒内未 login 即断开;未登录只放行 auth_str/login 平台侧实现 - 上行解析按新结构重写(cabinet/boards/battery),删除废弃的 status/parsed_status - 订单:唯一入口 = on;bat_in 只登记电池台账;4 条结束路径统一走 finalize_order(含充电记录落库) - 新增设备日志保留策略(90 天,分批删除);未登记 IMEI 静默丢弃;停电标记按 status1 bit0 恢复清除 - 待确认第 13 项兜底:平台侧按 电压 × 电流 × 时间 积分计算订单电量(workers/energy.rs) - 待确认第 18 项兜底:status_post bit7 连续 2 次无电池 → 判定电池被拔走并提前结单 修复(均为部署后才暴露的问题) - 队列消费禁用 BRPOP:无限阻塞会因代理掐连接永久卡死(零错误日志);有限超时也只是 "超时才醒",下行指令延迟 15~45 秒 → 改为 RPOP 非阻塞轮询,延迟降到 0.1~0.2 秒 - sync_iccid 把可空列 iccid 按 Option<String> 解码 → 整条 login 失败(ICCID 未上报的新柜子永远登录不上) - 迁移读 information_schema.COLUMN_COMMENT 需 CAST(... AS CHAR)(MySQL 8 驱动报 VARBINARY,否则迁移 panic) - 部署注意事项:.env 不支持行内注释;pscp 上传需 chmod +x;运行中二进制需 .new + mv 原子替换 部署 - 生产迁到 API 9000 / TCP 9001(systemd 单元补 EnvironmentFile、nginx 同步指向) - 新增 deploy/deploy-prod.ps1(本地一键:构建→校验→备份→原子替换→重启→验收→无副作用探测) - 废弃 deploy-prod.sh / deploy-test.sh(原脚本会在 target/release 找产物、装重复 systemd 单元、 并用本地 .env 覆盖线上 .env) 质量 - 单测:api-server 39 个 / device-server 13 个(含协议示例逐值断言、跨模块 Redis key 契约) - 两服务 cargo check 零错误零告警;web tsc --noEmit 通过 - test/tcp_e2e_smoke.py 端到端联调 13/13 通过
98 KiB
柜控板固件对接文档(TCP 接口)
| 项目 | 内容 |
|---|---|
| 文档版本 | v1.1(2026-09-14) |
| 交付对象 | 柜控板固件开发工程师 |
| 接口定义方 | 平台侧(本接口由平台方定义,固件按本文实现) |
| 协议性质 | 已冻结:报文格式、字段名、act 取值、签名算法一经确认不再变更 |
本文档只描述固件需要实现的内容。平台内部实现(存储、队列、业务逻辑)不在本文范围内。
⚠️ 待确认事项(请硬件侧确认;编号与第 10 节一致)
除下表 7 项外,本文其余内容均已定稿。 确认前固件可按"现状"(即本文当前写法)实现; 我们拿到答复后会同步更新本文并通知。这 7 项都不阻塞 4G 层开工。
| # | 事项 | 现状(本文当前写法) | 需硬件确认 | 影响范围 |
|---|---|---|---|---|
| 13 | energy(单订单累计充电量,Wh) |
本协议暂未包含 energy 字段 |
硬件是否有该数据源?若无,订单电量由平台侧按 电压 × 电流 × 时间 积分计算 |
§5.5 / §5.6 / §5.7 是否补该字段 |
关于第 13 项的兜底已落地:平台侧已按"积分计算"实现该兜底 —— 每条
status_post的通道电压/电流按 60 秒积分累加(协议固定 60 秒一次上报), 订单结束时写入订单电量并生成充电记录。硬件侧无需为此改协议; 若硬件后续确认能提供energy字段,平台改为直接采用硬件值即可(不影响报文结构以外的任何东西)。 | 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.5cells的解析与缓冲分配 | | 16 | 仓控板离线(485 芯片坏 / 线松 / 板子掉电)如何上报? | 本文暂未定义:4G 层读不到该板数据时该怎么报还没有约定(省略这块板?照报一块数值全0的板?还是另有在线/离线标志?)。硬件字典的「柜体状态1/2」只覆盖柜级(断电/烟/水/倾倒/门/风扇/接触器/喷淋/声光/门超时),没有"仓控板通信状态" | 硬件侧能提供什么形式的"仓控板在线/离线"信息?例如:
① 每块仓控板一个在线位(如放在boards[]里)
② 柜体数据里加一个「仓控板离线位图」(15 块板各一位)
③ 柜控板给出"应在线 N 块 / 实际失联 M 块"
④ 或明确回答:硬件无法检测
并请说明:板离线时 4G 层是省略该板、还是照报全 0 的板? | §5.3status_post需新增字段;平台「仓控板故障」告警完全依赖它 | | 17 | 直流停电是否还需要pow_fail_dc这条指令? | 现在直流停电有两条路径:① 事件指令pow_fail_dc(接触器分闸时抢发,见 §5.4.1)②status_post的cabinet.status1bit0「断电」(每 60 秒一个状态位)。而直流停电时柜控板本身仍有电,状态位理论上足以表达 | 直流停电能不能只用status1bit0 表达、取消pow_fail_dc这条指令?
请确认:接触器分闸时,status1bit0 是否会置 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_postbit7 归零或 10 小时订单超时兜底 | §5.6;订单能否正常结束 | | 19 | 停电恢复后如何兜底? | 停电期间设备完全失电,期间发生的电池拔出/插入等事件无法上报;恢复后设备重启,平台侧会留下不一致(例如挂着未结束的订单、台账里的电池已不在柜内) | 停电恢复后硬件侧如何处理?例如:
① 设备恢复后补报停电期间发生的事件?
② 还是以当前实际状态为准——重连后发一次全量status_post,由平台自行对账?
③ 或硬件不做特殊处理(平台侧靠 10 小时订单超时兜底) | §5.5 / §5.6 订单;掉电期间的状态对账机制 |
关于第 14 项:「代码 → 名称」的映射责任已定稿 —— 4G 层按字符串原值透传(不补零、不转义、不改写),映射在平台侧完成(见第 10 节第 7 条)。 所以第 14 项只缺"取值内容",不改变
brand_code/model_code/material_code的字段格式,不阻塞 4G 层实现。
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。平台无法判断设备本地是否持有安全码,也不需要判断。 - 安全码必须持久保存、掉电不丢。
- 平台在收到
auth_str/login时把 TCP 连接与dev_id绑定,并且只认这条连接上出现的第一个dev_id。一条 TCP 连接只服务一个dev_id。
⛔ 登录门控:除 auth_str / login 外,其余指令必须先 login 成功
| 连接状态 | 允许的 act |
其余 act 的处理 |
|---|---|---|
未登录(连接刚建立,尚未 login 成功) |
只有 auth_str、login |
平台一律丢弃,且不回任何应答 |
已登录(收到 suc=1 且带 spec 的 login 应答后) |
全部 act |
正常处理 |
- 未登录连接尚未与
dev_id建立可信绑定,平台无法路由应答,所以被丢弃的报文不会收到任何回应(固件表现为"发了没反应")。 - ⚠️ 未登录的 TCP 连接平台不予保持:连接建立后 30 秒内未完成
login成功,平台会主动断开。固件应在建连后立即发auth_str/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":N,"suc":0,"err":"auth_str already issued"}
此时停止重试,改为按重连间隔周期性重试 login,等待平台管理员重置。
- 重新申领有频率限制:同一 IMEI 5 次 / 5 分钟,超限返回
{"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即保活报文:平台对每条上行都会回{"act":"status_post","msg_id":N,"suc":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 通道 |
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)。
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 等) |
修正 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 秒内完成执行并回执,否则平台判超时,该次操作在管理端显示为"已下发·超时未确认"。
超时 ≠ 未执行
- 固件侧:超时只说明"没等到应答",平台可能已收到并处理(例如
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 的回执(不回包) |
回带原 id |
视形态 |
| 8 | off |
两种形态:① 不带 suc = 充满自动停止(设备新请求,需应答)② 带 suc = 对下行 off 的回执(不回包) |
回带原 id |
视形态 |
| 9 | open/pow_on/pow_off/reset |
下行指令的执行回执 | 回带原 id |
❌ 回执不回包(见 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成功前,平台不会下发open/pow_on/pow_off/reset(平台侧直接拦截,固件收不到任何报文)。
⚠️ 还有一道「设备在线」前置:平台侧要求该 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 仓)。由平台按该柜配置下发 |
work_mode |
string | 充电工作模式(平台侧可配置,仅这两种、不再扩展):"0" = 平台指令模式(默认)——充电由平台下发 on 指令发起,设备执行后回执"1" = 自动充电工作模式——设备检测到电池后自行开始充电,并主动上报 on(不带 suc) |
固件必须按 spec / work_mode 行事
-
spec:平台对该柜的规格定义(即柜子型号),如"0606"= 6 块仓控板 × 每板 6 仓。 它与仓控板实际能否通信无关 —— 某块板扫不到(485 芯片坏 / 线松)属于硬件故障,不由本字段表达。 -
work_mode:"0"→ 等平台on指令再开始充电;收到后执行并回执"1"→ 检测到电池即自行开始充电,并主动上报on(不带suc);平台仍可能下发on,此时按回执处理- 每次
login都以本次应答为准(平台改了配置,重连后即生效)
-
timestamp为秒级(不是毫秒),与平台时差须在 ±60 秒内。 -
iccid可选;上报后平台自动比对,SIM 卡更换会被记录。 -
平台只有在收到
suc=1且含spec的应答后,才把该连接视为已登录(敏感指令下发的前提)。 -
平台对
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。
顶层字段
| 字段 | 类型 | 说明 |
|---|---|---|
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 |
整数 | 柜体温度,硬件 1K ×100 → K×100(29800 = 298.00K) |
meter |
整数 | 电表读数,硬件 0.1kWh ×100 → Wh(125500 = 125.5 kWh) |
channels —— 每通道 18 个 hex 字符(9 字节),原样透传
一个通道 = 一个仓(可能空着、也可能插着电池)。不拆字段、直接拼接 hex——4G 层把硬件那 9 个字节按顺序拼成 18 个 hex 字符即可,无需做任何转换。
| 项 | 规则 |
|---|---|
| 形式 | 字符串数组,每个元素 = 恰好 18 个 hex 字符 |
| 大小写 | 平台解析不区分大小写(0e10 与 0E10 等价);建议固件统一小写 |
| 空仓 | byte0 = 00(无电池);byte1 照实报电源状态(电源与是否有电池无关);byte2–8 数值全 0。例:空仓但电源正常 → "008200000000000000" |
| ⚠️ 两个状态相互独立 | 电池状态(byte0)与电源状态(byte1)是两回事,平台前端也分两个指示分开显示——不要因为"没电池"就把 电源状态 写成 0 |
| 顺序 | 数组下标 = 通道号:第 1 个元素 = 通道 1,第 2 个 = 通道 2 …… |
| 长度 | 数组元素个数 = 该板通道数(当前 36 仓柜型为 6);顺序不得打乱 |
| 端序 | 多字节字段一律大端(高字节在前) |
每 18 个字符的字节布局(与硬件「充电动态数据」9 字节逐字节一致)
| 字节位(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
cabinet.status1/cabinet.status2,以及通道 hex 里的 byte0 / byte1(电池状态、电源状态)——这 4 个字节的位定义与解析示例统一见 3.8 状态字节(hex)解析表。
⚠️ 仓控板离线如何上报 —— 待硬件侧确认(待确认事项第 16 项) 若某块仓控板的 485 通信中断(芯片坏 / 线松 / 板子掉电),4G 层将读不到该板的任何数据。 这种情况下 4G 层应如何上报,本文暂未定义 —— 例如:省略这块板?照报一块数值全
0的板?还是另有在线/离线标志? 平台能否把"某块板离线"与"该板通道都空着"区分开,完全取决于硬件侧怎么报 —— 以硬件答复为准,本文不做任何预设。 在硬件答复前,平台侧「仓控板故障」这类告警不做实现。
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 |
整数 | 额定电压,硬件 0.1V ×100 → mV(48000 = 48.0V) |
rated_capacity |
整数 | 额定容量,硬件 0.1AH ×100 → mAh(24000 = 24.0Ah) |
动态数据(硬件「充电动态数据」9 字节)
| 字段 | 类型 | 说明 |
|---|---|---|
info |
string(恰好 18 个 hex 字符) | 该通道的 9 字节电池信息(硬件「充电动态数据」)。布局与解析规则完全同 5.3 的 channels 元素:byte0 电池状态 / byte1 电源状态 / byte2 SOC / byte3–4 电压 / byte5–6 电流 / byte7–8 电芯最高温度,多字节大端 |
info与status_post.channels[]里的元素是同一种 18 字符 hex,只是这里只针对"刚插入的这个通道"。
电芯数据(硬件「电芯数据」表)—— 同样拼接 hex,不拆字段
| 字段 | 类型 | 说明 |
|---|---|---|
cells |
string | 电芯数据整段拼接的 hex 字符串,长度 = cell_count × 4 + 16 个字符 |
cells 的段布局(按硬件「电芯数据」表顺序,多字节一律大端)
| 段 | 长度(hex 字符) | 含义 | 硬件单位 |
|---|---|---|---|
| 电芯电压 × N | N × 4(N = cell_count) |
各电芯电压,依次排列 | 2 字节/节 |
| 最高电压 | 4 | 所有电芯中的最高电压 | 2 字节 |
| 最低电压 | 4 | 最低电压 | 2 字节 |
| 最高温度 | 4 | 电芯最高温度 | 1K |
| 最低温度 | 4 | 电芯最低温度 | 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 = 3600mV 0e15 = 3605mV 0e0e = 3598mV … 0e17 0e0b 298K 296K
⚠️ 待确认第 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)。- 平台结束该仓体活跃订单(
reason='user_stop'),并把电池台账置空闲。
平台兜底:订单不会永远挂着
设备可能来不及/无法上报结束事件——例如停电或离线期间,人把电池取走了(设备根本没机会发 bat_out)。为此平台侧有兜底:
| 兜底 | 规则 |
|---|---|
| 订单超时兜底(主要) | 充电订单处于"进行中"超过 10 小时未结束 → 平台自动结束该订单(reason='timeout')。依据:充电不可能超过 10 小时(再久电池会出问题) |
| 状态兜底(可选,待确认 #16) | 若该通道有活跃订单、而 status_post 报该通道 batt_status bit7 = 0(无电池) → 说明电池已被取走,可提前结束。⚠️ 是否可用取决于 #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)
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}
平台创建充电订单
【开始充电 —— 取决于工作模式,二者只发生其一】
① 平台指令模式(默认)
← {"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) |
固件必须保证每条上行都带
act、首条先完成auth_str/login(并带上dev_id)。出现上述情况时,固件侧表现为"发了没反应",超时后才丢弃——不要误判为网络问题。
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 其它约定
- 平台下发的
open/pow_on/pow_off/reset,登录成功前不会下发(平台侧拦截)。 - ⚠️ 不要依赖平台应答里出现的"未知字段":平台内部可能附带以
_开头的服务端字段(如_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不会得到任何应答(固件不应依赖它,也不应据此重试) - 每条报文以
\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与实际柜型一致(如 6 板 × 6 仓 →"0606")- 按
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按 3.8 位定义构造(hex 2 位小写)cabinet.status1/status2与通道 hex 的 byte0 / byte1 按 3.8 位定义构造- 每个通道 hex 恰好 18 个字符、数组顺序 = 通道号 1→8、多字节大端
- 上报间隔 固定 60 秒/次,且每次为全量(所有仓控板 × 全部通道,无增量、无分片)
- 空仓也上报:byte0 =
00(无电池),byte1 照实报电源状态(如82),数值全0
业务时序
bat_in→ 平台登记电池台账(不建单)→ 收到suc=1on(开始充电)→ 平台建单(work_mode="0"平台下发on;"1"设备上报on)- 收到
on指令并执行后回执 - 充满上报
off bat_out上报- 断线重连后能重新
login并继续上报
10. 约定定稿
以下约定已定稿,固件按此实现:
| # | 事项 | 定稿值 |
|---|---|---|
| 1 | 接入地址 | 测试 118.31.119.206:3001;正式 118.31.119.206:9001。TCP 由平台 TCP 服务直接监听,不经域名/nginx |
| 2 | status_post 的板号与通道 |
boards[].board 取 1–15;channels[].channel 取 1–8(当前 36 仓柜型为 6 板 × 每板 6 通道,8/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 位字符串)= 前 2 位仓控板数 + 后 2 位每板通道数;当前 36 仓柜型为 "0606"(6 块仓控板 × 每板 6 仓)。由平台按该柜配置下发并作为柜子型号存储。旧的 cell(仓控板数量)已由 spec 取代,不再下发 |
| 10 | 连接登录门控 | 只有 auth_str / login 可以在未登录状态发送;其余所有 act(含 status_post、bat_in、on/off、回执等)必须先 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→8;空仓 = byte0 00(无电池)+ byte1 照实报电源状态 + 数值全 0(例 "008200000000000000")。电池状态与电源状态相互独立,两回事,前端分开显示 |
尚未定稿(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"=自动充电工作模式
// 敏感下行指令(open / pow_on / pow_off / reset)以此为前提
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)
// meter → 电表读数 Wh(125500 = 125.5 kWh)
// boards[] : 所有仓控板(board 1–15),每板带全部通道
// channels = **每通道 18 个 hex 字符**(9 字节原样透传)
// 数组顺序 = 通道号 1→8;多字节**大端**
// byte0 电池状态 / byte1 电源状态(位定义见 §3.8)
// byte2 SOC(1%) / byte3–4 电压(0.01V)
// byte5–6 电流(0.01A) / byte7–8 电芯最高温度(1K)
// 空仓 = 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,硬件 0.1V ×100)
// rated_capacity : mAh(24000 = 24.0Ah,硬件 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
// ⚠️ 插入首帧时平台还没下发 on,故不应是"正在充电"
// cells : 电芯数据整段拼接 hex,长度 = cell_count × 4 + 16
// (示例 15 节 → 76 字符:15 节电压 + 最高 + 最低 + 最高温 + 最低温)
// 只随 bat_in 带一次,不进周期上报
// soc/voltage/current/temp : 换算同 A.2.1
// cells : 电芯数据,**只随 bat_in 带一次**;项数须与 cell_count 相同
// ⚠️ 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,建议带末帧动态(换算同 A.2.1)
← {"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)。open/pow_on/pow_off/reset要求该连接已login成功,否则平台不投递。
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 : 柜级指令只写 IMEI(15 位)
// 含义 : 对【整柜唯一的柜门】执行【解锁】—— 门不会自动打开,需人拉开
// 协议没有"按仓体开门"的指令,也没有"关门"指令
// 固件动作:解开门锁 → 回执;失败时回执带 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)。 ⚠️ 回执发出即结束,不要等待"回执的应答"——平台不再回包。
A.6.1 成功回执(以 open 为例)
← {"act":"open","msg_id":1}
→ {"act":"open","msg_id":1,"suc":1}
// act : 与收到的指令一致(回执必带)
// msg_id : 与收到的指令完全一致 —— 必须原样回带
// id : 建议回带,与指令一致
// 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, ...}
// t=0 :固件登记挂起项
// t=60s :仍未收到应答 → 固件作废该交互,【不重试】该业务动作
// (下一条周期 status_post 照常发,用新的 msg_id)
附 B:变更记录
| 版本 | 日期 | 变更内容 |
|---|---|---|
| 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 单板 / 三段=仓体级) ⑯ 补充 pow_on / pow_off 业务语义:远程通断柜内总电(接触器)——pow_off 分闸后仓控板随之下电(仓体/电池数据不可用,并因此产生 pow_fail info:"0" 直流停电),属运维动作;pow_fail 与 cabinet.status1 bit0 的关系(事件 vs 状态)在 §5.4 说明 ⑰ pow_fail 的 info 区分定稿(判定依据 = "谁掉电"):"0"=直流停电(接触器分闸→仓控板下电,柜控板仍有电)/ "1"=交流停电(柜控板自身断电,靠板载电容余电抢发最后一条,与接触器无关);该条由「待确认」转入「约定定稿」第 11 条,待确认事项由 4 项缩减为 3 项 ⑱ 停电上报更正为两条独立指令:取消 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 项:仓控板离线(485 芯片坏 / 线松 / 掉电)如何上报 —— 4G 层读不到该板数据时该怎么报尚未约定(省略该板?照报全 0 的板?另有在线标志?),以硬件答复为准、本文不预设结论(§5.3 同步加警示) ⑳ login 失败统一为单一应答:取消 invalid timestamp / auth_str not found,只返回 invalid signature(不区分失败原因,避免外部试探系统状态——安全设计);同时修正 spec 的定位为「平台对柜子的规格定义(型号)」,与仓控板实际能否通信无关;work_mode 明确仅两种、不再扩展 ㉑ status_post 字段改名:batteries[] → channels[]、内层 group → channel(那是仓板的通道、可能空着,不是纯电池信息;同时与平台侧 channel_id 对齐);id 第三段说明改为「通道」;全文「电池组」按语境改为「通道」(硬件表名「电池组属性」保留) ㉒ 通道状态改为 hex 透传(大幅瘦身):status_post.channels[] 由"每通道 6 个命名字段"改为 每通道 18 个 hex 字符(9 字节原样拼接,4G 层零转换),数组下标 = 通道号;bat_in/bat_out/off 的动态数据同步改为 info(同一种 18 字符 hex);定字节布局与大端规则,§3.8/§5.3/§5.5/§7/§9/§10/附A 全面同步。体积:6 板 × 6 通道由 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.3device-server 转发时按连接注入设备身份(内部改动、固件无感) ㉛ id 也去掉 IMEI 前缀:既然设备身份来自连接,id 只表示动作目标 —— 不带=柜级、一段=单板(仅 reset,如 "01")、两段=仓体级(如 "01-3");仓体级段数校验由 ≠3 改为 ≠2;§3.7 / §4.2 / §5.5 / 附 A 全部示例与注释同步。平台侧 device-server 下发时需按目标连接补回 IMEI 前缀(内部改动) ㉜ 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 发 auth_str 时平台静默丢弃(不发码/不写缓存/不回包,见 §5.1 与 §8.1 表)—— 合法设备必然已建档,正常固件不会遇到;② 电池属性按原值入库(brand_code/model_code/material_code/cell_count/rated_voltage/rated_capacity 均入电池台账,映射出的中文名留待代码表确认后回填,与 §10 定稿第 7 条一致) |
| v1.0 | 2026-09-14 | 首版:基于平台 TCP 服务实现逐项核对生成 |