Files
H100PowerManger/test/pccu/pccu_integration_test.py
T
zjk 54c32a180a 新增复合管控器(pCCU) + 惯导数据解析 + 设备控制页增强 + 0824协议文档归档
- 新增 pCCU 复合管控器程序(src/pCCU):FC/PM 双链路(UDP)、协议编解码器
  (FcProtocol/PmProtocol/Frame/Message/MessageRegistry)、快照与落库(DbStore)、
  内置 WebServer;挂入 src/CMakeLists.txt 与板卡 mission(h100.moos)
- 惯导/深度数据:新增 uDevice_insData 消息解析,接入 UpperCommManager,
  pPowerMangerHost 注册监视,UpmsgTest 覆盖
- pPowerMangerHost 设备控制页增强(web/index.html)
- 部署/运维脚本改为 4 个 systemd 服务(moosdb/pPowerManger/pPowerMangerHost/pCCU):
  deploy/clean-data/fetch-data 同步适配
- 协议文档:归档 0824 控制主机与复合管控器通信协议 xlsx 及关联 docx,移除旧 md
- 测试:新增 pccu 单元/集成测试与 disSysTest Python 配电仿真
- 清理:移除未使用的 CCU 状态调试打印 coutMsg;.gitignore 补充编译产物/临时文件忽略
2026-08-26 22:43:10 +08:00

237 lines
8.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]
校验:
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 20B)到 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
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
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 控制(20B)
fc_ctl = [d for d in fc_ctl_results if len(d) == 20]
print(f"[FC 控制转发] 收到 {len(fc_ctl)} 帧 (期望>=1, 每帧20B)")
if fc_ctl:
d = fc_ctl[0]
cmd = d[7]; power = d[8]; hb = d[19]
print(f" -> cmd={cmd}, outputPower={power}, heartbeat={hb}")
ok = ok and (cmd == 2) and (power == 120) and (hb == 7)
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())