#!/usr/bin/env python3 """pCCU 集成测试:验证真实 UDP 链路上的完整数据流。 拓扑: 测试脚本(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. 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 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 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" 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("=1, 每帧248B)") if len(pm_status) >= 1: d = pm_status[0] total = struct.unpack_from(" 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(" 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:锂电池控制指令(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} (期望>=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) # 校验 6: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())