充电柜矩阵系统 (Power Matrix System / PMS)
产品名:安知充(anzhizhichong)
项目简介
36仓体直流电瓶车充电柜,仓数不固定:1柜控板+N仓控板,每仓控板管6仓,最小6仓。
技术栈
- 后端:Rust + Axum
- 前端:React + Arco Design Pro
- 数据库:MySQL(阿里云RDS)
- 缓存:Redis(阿里云Redis)
- 设备通讯:TCP长连接(柜控板4G模块,Air780E Cat.1)
项目结构
charging-cabinet/
├── tasks/ # 任务文件(opencode执行)
├── docs/ # 开发文档、协议文档
├── software/
│ ├── server/ # Rust后端
│ └── web/ # React前端
├── hardware/ # 硬件资料(原理图/PCB等)
└── test/ # 测试用例/报告
部署架构
| 环境 | 域名 | 说明 |
|---|---|---|
| 正式环境 | app.anzhizhichong.com | 后台管理 + API服务 |
| 测试环境 | test.anzhizhichong.com | 测试环境 |
服务器:CentOS Stream 9
内部服务:
- Gitea: https://app.anzhizhichong.com/doc/
- 文档Wiki: https://app.anzhizhichong.com/doc/anzhizhichong/charging-cabinet/wiki/
服务架构
┌─────────────────────────────────────────────────────────────┐
│ API服务 (api-server) — 主服务 │
│ - HTTP API(后台管理 + H5)端口: 3000 │
│ - 前端轮询刷新(每5秒) │
│ - 业务逻辑处理(验证/存DB/告警) │
│ - 节点管理(设备服务注册/心跳) │
│ - 后台协程处理Redis队列 │
└─────────────────────────────────────────────────────────────┘
↑ Redis LIST ↑ Redis LIST
│ device:report:{imei} │ device:cmd:{imei}
│ (设备上报) │ (平台指令)
│ │
┌─────────────────────────────────────────────────────────────┐
│ 设备服务 (device-server) — 轻量化 │
│ - TCP监听: 3002(设备长连接) │
│ - 只处理通信和指令解析 │
│ - 不存DB、不验证签名、不处理业务 │
│ - 注册到API服务(启动/心跳/注销) │
│ - Redis连接注册: device:{imei} → {node_id, ip, port} │
└─────────────────────────────────────────────────────────────┘
核心设计原则:
- TCP服务轻量化 — 只处理TCP通信和指令解析,不处理业务逻辑
- Redis做缓冲 — 服务间通信用Redis LIST,不用HTTP直接调用
- 全异步处理 — 所有指令异步处理,msg_id匹配响应
- 协程处理任务 — API服务用tokio::spawn处理Redis队列消息
通信流程:
- 设备上报:设备 → TCP → 设备服务 → Redis LIST → API服务协程处理
- 平台指令:API服务 → Redis LIST → 设备服务 → TCP → 设备
- 指令响应:设备 → TCP → 设备服务 → Redis LIST → API服务匹配msg_id
节点管理:
- 设备服务启动时注册到API服务
- 每60秒心跳,180秒无心跳视为离线
- 支持多节点扩展(当前单节点)
TCP通讯协议
通信规则
- TCP长连接,无需PING包
- 空闲1分钟上报,充电15秒上报
- 超时5000ms
- JSON去空格回车省流量,LF处理粘包
dev_id:IMEI(10进制),sub_device_id:485 IDmsg_id:自增会话ID- 全局通道号:
dev_id-sub_device_id-通道号(如123456789012345-01-6)
签名机制
- 设备首次连接调
auth_str接口获取8位安全码(仅一次) sign = SHA256(dev_id + auth_str + timestamp).substring(56, 63)- 时间戳秒级,平台校验±60秒
指令清单
| 动作 | 方向 | id格式 | 说明 |
|---|---|---|---|
login |
设备→平台 | 无 | 登录(含sign+timestamp) |
auth_str |
设备→平台 | 无 | 获取安全码(仅首次) |
status_post |
设备→平台 | 无 | 定时上报(rssi+status字典) |
pow_fail |
设备→平台 | 无 | 停电上报 |
bat_in |
设备→平台 | IMEI-仓控板-通道 |
电池插入(含BMS数据,字段待定) |
bat_out |
设备→平台 | IMEI-仓控板-通道 |
电池拔出 |
on |
双向 | IMEI-仓控板-通道 |
开始充电(平台下发/设备上报) |
off |
双向 | IMEI-仓控板-通道 |
停止供电(平台手动/设备自动) |
open |
平台→设备 | IMEI |
开门 |
status_get |
平台→设备 | IMEI-仓控板 |
查仓板状态 |
reset |
平台→设备 | IMEI 或 IMEI-仓控板 |
重启整柜或仓控板 |
status_post格式
{
"act": "status_post",
"msg_id": 42,
"dev_id": "123456789012345",
"rssi": 85,
"status": {
"123456789012345-01": "000100020003000400050006",
"123456789012345-02": "000100020003000400050006"
}
}
- key:
IMEI-仓控板ID - value: 6个通道状态拼接(每通道2字节=4个HEX字符)
- 平台从key解析仓控板数量
指令详情
login(设备→平台)
设备登录,验证签名。
// 请求
{"act":"login","msg_id":1,"dev_id":"123456789012345","sign":"abc12345","timestamp":1719800000,"h_ver":"1.0.0","s_ver":"1.0.0"}
// 响应
{"suc":1,"msg_id":1,"cell":6}
字段说明:
sign— 签名(SHA256(dev_id+auth_str+timestamp)后8位)timestamp— 秒级时间戳(平台校验±60秒)h_ver— 硬件版本s_ver— 软件版本cell— 仓控板数量
auth_str(设备→平台)
获取8位安全码,仅首次连接调用。
// 请求
{"act":"auth_str","msg_id":2,"dev_id":"123456789012345","ccid":"89860000000000000000"}
// 响应
{"suc":1,"msg_id":2,"data":{"auth_str":"abcd1234"}}
字段说明:
ccid— SIM卡ICCID号auth_str— 8位随机安全码(仅返回一次)
status_post(设备→平台)
定时上报,空闲60秒/充电15秒。
// 请求
{"act":"status_post","msg_id":10,"dev_id":"123456789012345","rssi":85,"status":{"123456789012345-01":"000100020003000400050006","123456789012345-02":"000100020003000400050006"}}
// 响应
{"suc":1,"msg_id":10}
status字段说明:
- key:
IMEI-仓控板ID - value: 6个通道状态拼接(每通道2字节=4个HEX字符)
- 平台从key解析仓控板数量
pow_fail(设备→平台)
停电上报。
// 请求
{"act":"pow_fail","msg_id":15,"dev_id":"123456789012345"}
// 响应
{"suc":1,"msg_id":15}
bat_in(设备→平台)
电池插入事件,立即上报。
// 请求
{"act":"bat_in","msg_id":20,"id":"123456789012345-01-6","bms":{"soc":45,"voltage":52.3}}
// 响应
{"suc":1,"msg_id":20}
平台处理:
- 记录电池信息
- 验证电池合规性
- 自动下发
on指令
BMS数据字段待定。
bat_out(设备→平台)
电池拔出事件,立即上报。
// 请求
{"act":"bat_out","msg_id":21,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":21}
on(双向)
平台→设备: 开始充电
// 请求
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":30}
设备→平台: 充电开始确认(自动充电模式)
// 上报
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
off(双向)
平台→设备: 手动停止充电
// 请求
{"act":"off","msg_id":31,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":31}
设备→平台: 充满自动停止
// 上报
{"act":"off","msg_id":31,"id":"123456789012345-01-6"}
open(平台→设备)
开门,只需IMEI。
// 请求
{"act":"open","msg_id":40,"id":"123456789012345"}
// 响应
{"suc":1,"msg_id":40}
错误码63 = 门锁故障。
status_get(平台→设备)
查询仓板状态。
// 请求
{"act":"status_get","msg_id":50,"id":"123456789012345-01"}
// 响应
{"suc":1,"msg_id":50,"data":{"status":"..."}}
reset(平台→设备)
重启整柜或仓控板。
// 重启整柜
{"act":"reset","msg_id":60,"id":"123456789012345"}
// 重启仓控板
{"act":"reset","msg_id":61,"id":"123456789012345-01"}
// 响应
{"suc":1,"msg_id":60}
充电结束原因(平台判断)
| 平台收到 | 判断原因 |
|---|---|
off (设备→平台) |
normal — 正常充满 |
off (平台→设备) |
user_stop — 用户手动停止 |
bat_out |
normal — 电池拔出 |
bat_in (通道已有电池) |
forced — 新电池插入,旧电池强制结束 |
| 超时未响应 | timeout — 通信超时 |
开发规范
- 任务需求写到
tasks/目录的md文件 - 通过Agent子代理包裹opencode执行,异步通知
- 质量约束模板固定追加到每个任务文件
- 完成代码后执行对应栈静态校验(Rust→cargo check,TS→tsc --noEmit),零报错零警告
Description
Languages
Rust
62%
TypeScript
30.5%
Vue
3.5%
Python
2.1%
HTML
1%
Other
0.9%