#!/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] 测试脚本(真实CCU模拟) --16004--> pCCU(rcu_local) [配电反馈 0x0005~0x0008] pCCU --16003--> 测试脚本 [配电反馈原帧转发 pPowerManger] 测试脚本(PM模拟) --16002--> pCCU(pm_local) [配电指令 0x0001~0x0004 短帧] pCCU --16005--> 测试脚本 [配电指令原帧转发真实CCU] 锂电池已切换为 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. 配电反馈(0x0005~0x0008)原帧转发到 16003(字节一致) 5. 配电指令(0x0001~0x0004 短帧)原帧转发到 16005(字节一致); PM 操控指令(45B, 同id)不转发到真实CCU(按帧长区分) 6. SQLite 记录了收/发原始帧(含 link='rcu') 7. Web 页面可访问 """ import socket import struct import threading import time import subprocess import os import signal import sys import json import urllib.request # ---- 配置 ---- FC_STATUS_PORT = 16000 FC_CTRL_RECV_PORT = 16001 PM_CTRL_PORT = 16002 PM_STATUS_RECV_PORT = 16003 RCU_FB_PORT = 16004 # pCCU rcu 链路本地端口(收配电反馈) RCU_CMD_RECV_PORT = 16005 # 模拟真实CCU 接收转发的配电指令 CANET_PORT = 16006 # 模拟 USBCAN-8E-U TCP Server(捕获下行 CAN 帧) WEB_PORT = 18081 DB_PATH = "/tmp/pccu_it_test.db" MOOSDB_EXE = "/usr/local/bin/MOOSDB" PCCU_EXE = "/home/zjk/project/H100PowerManger/bin/pCCU" CANBRIDGE_EXE = "/home/zjk/project/H100PowerManger/bin/pCanBridge" 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("= 13: with self.lock: self.frames.append(buf[:13]) buf = buf[13:] except socket.timeout: continue except OSError: break def snapshot(self): with self.lock: return list(self.frames) def bcu_ctrl_expect(addr, pos, neg): """期望的下行 13 字节 CANET 帧:扩展帧 DLC8 + 0x10XX81FF + 02 00 pos neg FF*4""" return bytes([0x80 | 8, (0x10000000 | (addr << 16) | 0x81FF) >> 24 & 0xFF, (0x10000000 | (addr << 16) | 0x81FF) >> 16 & 0xFF, (0x10000000 | (addr << 16) | 0x81FF) >> 8 & 0xFF, (0x10000000 | (addr << 16) | 0x81FF) & 0xFF, 0x02, 0x00, 0x01 if pos else 0x00, 0x01 if neg else 0x00, 0xFF, 0xFF, 0xFF, 0xFF]) def api_get(path): try: resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}{path}", timeout=3) return json.loads(resp.read().decode()) except Exception as e: return {"ok": False, "error": str(e)} 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(" pCCU -> pPowerManger,应原帧转发到 16003) rcu_fb_frames = [dis_hv_fb_frame(), dis_hva_fb_frame(), dis_hvb_fb_frame(), dis_lv_fb_frame()] for f in rcu_fb_frames: pm_send.sendto(f, ("127.0.0.1", RCU_FB_PORT)) time.sleep(0.2) # 7. 发送配电指令(pPowerManger -> pCCU -> 真实CCU,应原帧转发到 16005) dis_cmd_frames = [dis_hv_cmd_frame(), dis_hva_cmd_frame(), dis_hvb_cmd_frame(), dis_lv_cmd_frame()] for f in dis_cmd_frames: pm_send.sendto(f, ("127.0.0.1", PM_CTRL_PORT)) time.sleep(0.2) t1.join(); t2.join(); t3.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(" 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:配电反馈原帧转发到 pPowerManger(字节一致) for i, f in enumerate(rcu_fb_frames): got = [d for d in pm_status_results if d == f] mid = struct.unpack_from("=6), " f"rcu 链路记录数 = {db_rcu} (期望>=8)") ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1) ok = ok and (db_rcu >= 8) # 4条配电反馈(收) + 4条配电指令(发) # 校验 8:BCU 断路器控制下行(网页API -> pCCU -> MOOS -> pCanBridge -> 模拟USBCAN) # 闭合(addr=5, 总正+总负=1) j = api_get("/api/bcu_ctrl?addr=5&pos=1&neg=1") print(f"[断路器闭合 API] {j}") ok = ok and j.get("ok") is True # action 语法(addr=6, action=1 等价 pos=neg=1) j = api_get("/api/bcu_ctrl?addr=6&action=1") print(f"[断路器闭合 API(action)] {j}") ok = ok and j.get("ok") is True # 非法地址 j = api_get("/api/bcu_ctrl?addr=99&action=1") print(f"[非法地址 API] {j}") ok = ok and j.get("ok") is False # 断开(addr=5, 总正+总负=0) j = api_get("/api/bcu_ctrl?addr=5&pos=0&neg=0") print(f"[断路器断开 API] {j}") ok = ok and j.get("ok") is True time.sleep(3.0) frames = canet.snapshot() print(f"[下行帧捕获] {len(frames)} 帧") exp_close5 = bcu_ctrl_expect(5, 1, 1) exp_close6 = bcu_ctrl_expect(6, 1, 1) exp_open5 = bcu_ctrl_expect(5, 0, 0) hit5 = any(f == exp_close5 for f in frames) hit6 = any(f == exp_close6 for f in frames) hit5o = any(f == exp_open5 for f in frames) leak99 = any(f[1] == 0x63 and f[2] == 0x00 for f in frames) # addr=99 未下发 print(f"[断路器闭合下行(addr=5)] {'OK' if hit5 else 'MISSING'}") print(f"[断路器闭合下行(addr=6)] {'OK' if hit6 else 'MISSING'}") print(f"[断路器断开下行(addr=5)] {'OK' if hit5o else 'MISSING'}") print(f"[非法地址未下发(addr=99)] {'OK' if not leak99 else 'LEAK'}") ok = ok and hit5 and hit6 and hit5o and not leak99 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: canet.stop_flag = True 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())