Files
H100PowerManger/test/pccu/pccu_integration_test.py
T
zjk 2590282aed
build-test-deploy / ci (push) Successful in 1h13m13s
0827协议修订:姿态数据2改预留 + 状态帧新增剩余发电量 + CCU地址可配置
FC协议(0827 docx):
- 控制指令域:字节12,13/16,17由纵倾/横倾姿态数据2改为预留(编码恒0、解码忽略)
- 状态反馈域:字节147,148由预留12改为剩余发电量(MWh),贯通JSON/网页显示
- PM→CCU链路:姿态字段4xuchar改为2xshort(数据1),结构体大小与线上布局不变
- 同步更新:CCU转发映射、SnapshotBuilder、pCCU网页、fuelcell网页、SQLite列描述、单测/模拟器
- 修正集成测试遗留断言(20B→24B、心跳d[19]→d[23])并增强预留字节校验

CCU地址可配置:
- pPowerManger 支持 moos 配置项 ccuhost/ccuport(默认127.0.0.1:7000)
- build-board.sh 增加 --ccu-host/--ccu-port 参数

文档:FC协议文档更新为0827版,清理过期协议docx
2026-08-28 00:09:17 +08:00

247 lines
9.1 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]
校验:
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:数据库有记录
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}")
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
# 校验 5:Web 页面
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())