Files
H100PowerManger/test/pccu/pccu_integration_test.py
T
zjk 88bed850ee
build-test-deploy / ci (push) Failing after 1h9m57s
pCCU 锂电池通信切换为 CAN 总线(BMS 协议,经 pCanBridge 透传)
协议层:
- 新增 protocol/CanBms.{h,cpp}:按 docs/BMS_协议字段定义.xlsx 解码
  0x10010000(总电压/SOC/故障码/状态位)、0x10010005(最大电流限制/
  总电流/最高单体电压)、0x10010006(总电压校验)。
  缩放用除法保证与十进制字面量严格一致;总电流按 2 字节 s16 实现
  (xlsx 标注 3 字节与其自身示例/实车帧矛盾,已在代码注释说明)。
- 删除 UDP 锂电池协议:protocol/BatProtocol.{h,cpp}、
  comm/BatLinkManager.h(动力/仪表双链路、自检/接触器/功率指令)。

pCCU 主体:
- CCU 订阅 pCanBridge 发布的 CAN_0x* 二进制消息并解码 BMS 报文;
  SystemData 双电池槽位改为单一 BmsStatusValue + CAN 帧计数;
  PM 电池启停/功率指令改为告警忽略(BMS CAN 协议未定义控制报文)。
- 修复两个本地联调发现的阻断性 bug:
  1) MOOS 通配订阅必须用三参数 Register("CAN_0x*","*",0),
     单参数形式不做通配匹配;
  2) 变量名前缀匹配偏移(compare 少比 1 字符 / sscanf +5 指向 'x'),
     改为 rfind 前缀判断 + +6 偏移。
- PowerCoordinator/CoordFsm 剥离电池接触器/切换时序(依赖已删除的
  UDP 指令),保留 FC 联动与协调策略占位(CoordSwitchingState 改为
  通用的 CoordBusyState)。

网页:
- 动力/仪表锂电池两个标签页合并为"锂电池BMS(CAN)":总电压/总电流/
  SOC/最大电流限制/最高单体电压/校验帧/故障码高亮/在线状态,
  附报文来源说明;链路状态页改为 CAN 链路行(pCanBridge)。

配置与测试:
- pCCU.moos / missions/h100.moos 移除电池 UDP 配置;
- pccuTest:删除旧电池编解码用例,新增 testCanBmsDecode
  (向量取自实车抓包:533.8V/20.1%/49/3.752V/-25.0A)103/103 通过;
- 集成测试改为纯 FC/PM 流程,本地实测 PASSED(含 Web 页面校验)。
2026-08-31 21:42:26 +08:00

249 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""pCCU 集成测试:验证真实 UDP 链路上的完整数据流。
拓扑:
测试脚本(FC模拟) --16000--> pCCU(fc_local) [FC状态 0x0002]
测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002]
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003]
锂电池已切换为 CAN 总线(BMS 协议,经 pCanBridge 透传 CAN_0x* 消息),
不在本脚本 UDP 拓扑内:BMS 解码由 pccuTest 单元测试覆盖
(testCanBmsDecode,向量取自实车抓包)。
校验:
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
4. SQLite 记录了收/发原始帧
5. Web 页面可访问
"""
import socket
import struct
import threading
import time
import subprocess
import os
import signal
import sys
import urllib.request
# ---- 配置 ----
FC_STATUS_PORT = 16000
FC_CTRL_RECV_PORT = 16001
PM_CTRL_PORT = 16002
PM_STATUS_RECV_PORT = 16003
WEB_PORT = 18081
DB_PATH = "/tmp/pccu_it_test.db"
MOOSDB_EXE = "/usr/local/bin/MOOSDB"
PCCU_EXE = "/home/zjk/project/H100PowerManger/bin/pCCU"
MISSION = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pccu_it.moos")
# 帧头
START = bytes([0x40, 0x40])
def frame(msg_id, payload, checksum=True):
total = 6 + len(payload) + (4 if checksum else 0)
hdr = START + struct.pack("<HH", msg_id, total)
body = hdr + payload
if checksum:
cks = sum(body) & 0xFFFFFFFF
return body + struct.pack("<I", cks)
return body
def fc_status_frame():
# payload 144B,仅填关键字段,其余为0
p = bytearray(144)
p[0] = 2 # 运行模式 自动
p[1] = 6 # 运行状态 运行
struct.pack_into("<H", p, 2, 0x0102) # 一级故障码
struct.pack_into("<H", p, 10, 1234) # 累积发电时间
p[12] = 0 # 故障等级
struct.pack_into("<H", p, 14, 2000) # 系统发电功率 0.01kW
p[16] = 80 # 储氢
p[17] = 70 # 液氧
struct.pack_into("<H", p, 24, 350) # 钯膜温度
struct.pack_into("<H", p, 68, 2500) # 舱室温度1
struct.pack_into("<H", p, 140, 4321) # 剩余发电量 MWh (字节147,148)
p[142] = 5 # 心跳
p[143] = 0x01 # 应急指令
return frame(0x0002, bytes(p), checksum=False)
def pm_control_frame():
p = bytearray(35)
struct.pack_into("<H", p, 0, 2026)
p[2], p[3], p[4], p[5], p[6], p[7] = 8, 26, 10, 30, 0, 0
p[8] = 2 # 模式 自动
p[9] = 2 # 操控 启动
p[10] = 120 # 输出功率 0.1kW
struct.pack_into("<h", p, 11, 123) # 纵倾姿态数据1 12.3°
struct.pack_into("<h", p, 13, -45) # 横倾姿态数据1 -4.5°
p[15] = 0x03 # 应急允许
struct.pack_into("<H", p, 16, 150) # 潜深
p[21] = 0x10 # 仪表锂电启动
p[22] = 0x20 # 动力锂电关闭
struct.pack_into("<H", p, 23, 300) # 动力电池功率
p[25] = 7 # 心跳
p[26] = 0xAA # 主机状态
return frame(0x0001, bytes(p))
def pm_paramset_frame():
p = bytearray(18)
p[0], p[1], p[2] = 75, 85, 93
p[5], p[6], p[7] = 75, 85, 93
return frame(0x0002, bytes(p))
def recv_loop(sock, label, results, msg_ids, timeout):
"""持续接收指定 msg_id 的帧,记录到 results"""
sock.settimeout(timeout)
end = time.time() + timeout
while time.time() < end:
try:
data, addr = sock.recvfrom(2048)
except socket.timeout:
break
except OSError:
break
if len(data) < 6:
continue
mid = struct.unpack_from("<H", data, 2)[0]
if mid in msg_ids:
results.append(data)
def main():
# 清理
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log"):
if os.path.exists(f):
os.remove(f)
procs = []
try:
# 1. 启动 MOOSDB
moosdb = subprocess.Popen([MOOSDB_EXE, "--moos", "--port", "9000"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
procs.append(moosdb)
time.sleep(1.0)
# 2. 启动 pCCU
pccu = subprocess.Popen([PCCU_EXE, MISSION],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
procs.append(pccu)
time.sleep(2.0)
if pccu.poll() is not None:
print("FAIL: pCCU exited early, code", pccu.returncode)
return 1
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003)
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))
fc_ctl_results, pm_status_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()
# 4. 发送 FC 状态(周期模拟)
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for _ in range(3):
fc_send.sendto(fc_status_frame(), ("127.0.0.1", FC_STATUS_PORT))
time.sleep(0.5)
# 5. 发送 PM 操控指令 + 参数设定
pm_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
pm_send.sendto(pm_control_frame(), ("127.0.0.1", PM_CTRL_PORT))
time.sleep(0.5)
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT))
t1.join(); t2.join()
ok = True
# 校验 1:PM 状态报文(248B)周期发送
pm_status = [d for d in pm_status_results if len(d) == 248]
print(f"[PM 状态报文] 收到 {len(pm_status)} 帧 (期望>=1, 每帧248B)")
if len(pm_status) >= 1:
d = pm_status[0]
total = struct.unpack_from("<H", d, 4)[0]
mode = d[6]; status = d[7]
print(f" -> length={total}, mode={mode}, status={status}, size={len(d)}")
ok = ok and (total == 248) and (mode == 2) and (status == 6)
else:
ok = False
# 校验 2:PM 操控转发为 FC 控制(24B,0827协议:姿态数据1转发,数据2字节预留恒0)
fc_ctl = [d for d in fc_ctl_results if len(d) == 24]
print(f"[FC 控制转发] 收到 {len(fc_ctl)} 帧 (期望>=1, 每帧24B)")
if fc_ctl:
d = fc_ctl[0]
cmd = d[7]; power = d[8]; hb = d[23]
pitch = struct.unpack_from("<h", d, 9)[0] # 字节10,11
roll = struct.unpack_from("<h", d, 13)[0] # 字节14,15
resv1 = struct.unpack_from("<H", d, 11)[0] # 字节12,13 预留
resv2 = struct.unpack_from("<H", d, 15)[0] # 字节16,17 预留
print(f" -> cmd={cmd}, outputPower={power}, pitch={pitch}, roll={roll}, "
f"resv1={resv1}, resv2={resv2}, heartbeat={hb}")
ok = ok and (cmd == 2) and (power == 120) and (hb == 7)
ok = ok and (pitch == 123) and (roll == -45)
ok = ok and (resv1 == 0) and (resv2 == 0)
else:
ok = False
# 校验 3:PM 参数反馈(12B)
fb = [d for d in pm_status_results if len(d) == 12]
print(f"[PM 参数反馈] 收到 {len(fb)} 帧 (期望>=1, 每帧12B)")
if fb:
flag = fb[0][6]
print(f" -> flag=0x{flag:02X}")
ok = ok and (flag == 0x10)
else:
ok = False
# 校验 4:数据库有记录(FC/PM 链路收发)
time.sleep(1.0)
db_count = -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]
conn.close()
print(f"[数据库] comm_log 记录数 = {db_count} (期望>=6)")
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
web_ok = False
try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)
html = resp.read().decode()
web_ok = ("pCCU" in html)
print(f"[Web] 页面可访问, 长度={len(html)}")
except Exception as e:
print(f"[Web] 访问失败: {e}")
ok = ok and web_ok
print("\n==== 集成测试 " + ("PASSED" if ok else "FAILED") + " ====")
return 0 if ok else 1
finally:
for p in reversed(procs):
try:
p.terminate()
except Exception:
pass
time.sleep(0.5)
for p in procs:
try:
p.kill()
except Exception:
pass
if __name__ == "__main__":
sys.exit(main())