pCCU新增锂电池通信/协调操作 + 电源协调状态机骨架(惰性)

锂电池通信(docs/锂电池协议20230324.docx):
- 协议层 BatProtocol:自检0x0000(2B)/设备控制0x0001(17B)/电池组状态0x0003(80B)/
  电池包报警0x0004(68B),uint32字节和校验、小端
- 链路层 BatLinkManager:动力/仪表锂电池独立UDP链路,本机监听与远端IP端口
  均由.moos配置(dyn_bat_*/ins_bat_*/bat_work_condition)
- SystemData/SnapshotBuilder/网页:电池数据按动力/仪表归类展示(运行状态/接触器
  电气量/电池包电压温度/报警),收发帧落库SQLite,网页新增电池标签页

操作->指令下发(PowerCoordinator):
- 操作接口:自检/上下电(母线接触器55H接通77H断开,含预充)/功率设定(0~500kW)/
  电池切换(低压->高压6步、高压->低压5步,按协议时序)
- 步骤引擎:每电池独立步骤队列,下发->状态反馈确认->超时兜底;周期1s心跳控制帧
- PM操控指令insBatCmd/dynBatCmd/dynBatPower自动映射为电池操作

电源协调状态机骨架(CoordFsm, TinyFSM):
- 事件Tick/PmControl/StepDone + 状态Normal/Switching/Fault,react均为TODO占位
- 当前未接线(无start/dispatch调用,运行行为与接入前一致),CoordFsm.hpp头部附接线指南

测试与部署:
- pccuTest新增电池协议编解码/帧长/链路分发用例(175项通过);集成测试覆盖电池
  心跳、启停映射(0x10->0x55/0x20->0x77)、状态落库;同步pccu_it.moos与ws_check
- h100.moos与pCCU.moos增加锂电池链路配置;归档锂电池协议文档
This commit is contained in:
zjk
2026-08-30 14:40:27 +08:00
parent 2590282aed
commit 7e4d08808a
25 changed files with 2691 additions and 23 deletions
+85 -8
View File
@@ -4,15 +4,21 @@
拓扑:
测试脚本(FC模拟) --16000--> pCCU(fc_local) [FC状态 0x0002]
测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002]
测试脚本(电池模拟) --17001/17002--> pCCU [电池组状态 0x0003]
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003]
pCCU --17003/17004--> 测试脚本 [电池控制指令 0x0001 / 自检指令 0x0000]
校验:
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
4. SQLite 记录了收/发原始帧
5. Web 页面可访问
4. PM 操控中锂电池启停指令映射为电池控制指令(0x0001, 27B):
动力 0x20 关闭 -> 母线接触器 0x77;仪表 0x10 启动 -> 母线接触器 0x55
5. pCCU 周期(1s)向动力/仪表锂电池下发心跳控制指令
6. 电池组状态(0x0003)收到后落库 SQLite(link=bat_dyn/bat_ins)
7. SQLite 记录了收/发原始帧
8. Web 页面可访问
"""
import socket
import struct
@@ -29,6 +35,10 @@ FC_STATUS_PORT = 16000
FC_CTRL_RECV_PORT = 16001
PM_CTRL_PORT = 16002
PM_STATUS_RECV_PORT = 16003
DYN_BAT_PORT = 17001 # pCCU 监听动力电池状态
INS_BAT_PORT = 17002 # pCCU 监听仪表电池状态
DYN_BAT_RECV_PORT = 17003 # 测试监听(pCCU 发往"动力电池"的目标端口)
INS_BAT_RECV_PORT = 17004 # 测试监听(pCCU 发往"仪表电池"的目标端口)
WEB_PORT = 18081
DB_PATH = "/tmp/pccu_it_test.db"
MOOSDB_EXE = "/usr/local/bin/MOOSDB"
@@ -94,6 +104,26 @@ def pm_paramset_frame():
return frame(0x0002, bytes(p))
def bat_status_frame():
"""电池组状态 0x0003:payload 80B,带 uint32 校验和"""
p = bytearray(80)
struct.pack_into("<H", p, 0, 2026)
p[2], p[3], p[4], p[5], p[6], p[7] = 8, 30, 10, 30, 0, 0
p[8] = 0x10 # 工况 工房调试
p[9] = 0x22 # 自检完成,状态正常
p[10] = 0x22 # 满足功率要求
p[11] = 0x11 # 母线接触器闭合完成
p[12] = 0x22 # 二极管闭合完成(高4位) + 正极闭合完成(低4位)
struct.pack_into("<H", p, 14, 855) # SOC 85.5%
struct.pack_into("<H", p, 16, 1234) # 电量 123.4 kWh
struct.pack_into("<H", p, 18, 5000) # 允许最高放电功率 500 kW
struct.pack_into("<H", p, 22, 1500) # 当前接入功率 150 kW
struct.pack_into("<H", p, 36, 5400) # 负载端电压 54.00V
struct.pack_into("<h", p, 40, -200) # 放电电流 -10A(充电)
p[79] = 33 # 心跳
return frame(0x0003, bytes(p))
def recv_loop(sock, label, results, msg_ids, timeout):
"""持续接收指定 msg_id 的帧,记录到 results"""
sock.settimeout(timeout)
@@ -135,18 +165,27 @@ def main():
print("FAIL: pCCU exited early, code", pccu.returncode)
return 1
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003)
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003,电池控制 17003/17004)
fc_ctl_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fc_ctl_recv.bind(("127.0.0.1", FC_CTRL_RECV_PORT))
pm_status_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
pm_status_recv.bind(("127.0.0.1", PM_STATUS_RECV_PORT))
dyn_bat_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dyn_bat_recv.bind(("127.0.0.1", DYN_BAT_RECV_PORT))
ins_bat_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ins_bat_recv.bind(("127.0.0.1", INS_BAT_RECV_PORT))
fc_ctl_results, pm_status_results = [], []
dyn_bat_results, ins_bat_results = [], []
t1 = threading.Thread(target=recv_loop,
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 12))
t2 = threading.Thread(target=recv_loop,
args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12))
t1.start(); t2.start()
t3 = threading.Thread(target=recv_loop,
args=(dyn_bat_recv, "bat-dyn", dyn_bat_results, {0x0000, 0x0001}, 12))
t4 = threading.Thread(target=recv_loop,
args=(ins_bat_recv, "bat-ins", ins_bat_results, {0x0000, 0x0001}, 12))
t1.start(); t2.start(); t3.start(); t4.start()
# 4. 发送 FC 状态(周期模拟)
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -160,7 +199,14 @@ def main():
time.sleep(0.5)
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT))
t1.join(); t2.join()
# 6. 模拟锂电池上发电池组状态(动力/仪表)
bat_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for _ in range(3):
bat_send.sendto(bat_status_frame(), ("127.0.0.1", DYN_BAT_PORT))
bat_send.sendto(bat_status_frame(), ("127.0.0.1", INS_BAT_PORT))
time.sleep(0.5)
t1.join(); t2.join(); t3.join(); t4.join()
ok = True
@@ -204,18 +250,49 @@ def main():
else:
ok = False
# 校验 4:数据库有记录
# 校验 4:锂电池控制指令(27B)。
# PM 操控映射:动力 0x20 关闭 -> 母线接触器 0x77;仪表 0x10 启动 -> 母线接触器 0x55
dyn_ctl = [d for d in dyn_bat_results if len(d) == 27 and d[2] == 0x01]
ins_ctl = [d for d in ins_bat_results if len(d) == 27 and d[2] == 0x01]
print(f"[动力电池控制指令] 收到 {len(dyn_ctl)} 帧 (期望>=5, 每帧27B, 1s心跳)")
if dyn_ctl:
bus = dyn_ctl[-1][6 + 9] # 最后一帧:PM 指令生效后的期望状态
print(f" -> 母线接触器=0x{bus:02X} (期望0x77 关闭)")
ok = ok and (bus == 0x77)
else:
ok = False
print(f"[仪表电池控制指令] 收到 {len(ins_ctl)} 帧 (期望>=5, 每帧27B, 1s心跳)")
if ins_ctl:
bus = ins_ctl[-1][6 + 9]
print(f" -> 母线接触器=0x{bus:02X} (期望0x55 接通)")
ok = ok and (bus == 0x55)
else:
ok = False
# 校验 5:数据库有记录(含锂电池链路收发)
time.sleep(1.0)
db_count = -1
bat_dyn_rx = -1
bat_tx = -1
if os.path.exists(DB_PATH):
import sqlite3
conn = sqlite3.connect(DB_PATH)
db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0]
# 电池组状态接收(link=bat_dyn, 方向=收, msg_id=3)
bat_dyn_rx = conn.execute(
"SELECT COUNT(*) FROM comm_log WHERE link='bat_dyn' AND direction=0 AND msg_id=3"
).fetchone()[0]
# 电池控制指令发送(direction=1, msg_id=1, bat 链路)
bat_tx = conn.execute(
"SELECT COUNT(*) FROM comm_log WHERE link IN ('bat_dyn','bat_ins') AND direction=1 AND msg_id=1"
).fetchone()[0]
conn.close()
print(f"[数据库] comm_log 记录数 = {db_count}")
print(f"[数据库] comm_log 记录数 = {db_count} (期望>=6)")
print(f"[数据库] 动力电池状态接收记录 = {bat_dyn_rx} (期望>=1), 电池控制发送记录 = {bat_tx} (期望>=2)")
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
ok = ok and (bat_dyn_rx >= 1) and (bat_tx >= 2)
# 校验 5:Web 页面
# 校验 6:Web 页面
web_ok = False
try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)