pCCU 新增真实CCU配电桥接(rcu链路原帧透传+网页显示)并适配板卡端口拓扑

- protocol/DisProtocol: 配电协议8条消息(指令0x0001~4/反馈0x0005~8, Sum8),复用pmSysvariable结构体+static_assert核对
- ChecksumPolicy 新增 Sum8;Frame 支持 1 字节校验和
- MessageRegistry 支持 (id+帧总长) 二维查找:配电指令与PM操控指令共用id按帧长区分(帧头length字段不可信)
- LinkManager: 按实际帧长分发 + sendRaw原帧透传 + 非本协议帧头落库节流告警;UdpEndpoint sendTo 加锁
- comm/RcuLinkManager: 真实CCU链路;core/DisBridge: 配电桥接器(反馈转pPowerManger+解码显示,指令转真实CCU,0x0004仅显示不转发)
- 网页新增"真实CCU"页签全字段展示;SnapshotBuilder realCcu 节 + rcu 链路描述
- h100.moos: 端口腾挪(真实CCU固定上报5001归pCCU,pPowerManger iport=5002),rcu_remote=192.168.0.140:7000
- 测试: pccuTest 新增配电/Sum8/长度分发/RCU注册表用例(176项);集成测试新增桥接字节一致+不误转发用例
This commit is contained in:
zjk
2026-09-01 15:06:11 +08:00
parent 5b8f0b1421
commit 0639b5904d
28 changed files with 1609 additions and 53 deletions
+142 -11
View File
@@ -6,6 +6,10 @@
测试脚本(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 单元测试覆盖
@@ -15,8 +19,11 @@
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
4. SQLite 记录了收/发原始帧
5. Web 页面可访问
4. 配电反馈(0x0005~0x0008)原帧转发到 16003(字节一致)
5. 配电指令(0x0001~0x0004 短帧)原帧转发到 16005(字节一致);
PM 操控指令(45B, 同id)不转发到真实CCU(按帧长区分)
6. SQLite 记录了收/发原始帧(含 link='rcu')
7. Web 页面可访问
"""
import socket
import struct
@@ -33,6 +40,8 @@ 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 接收转发的配电指令
WEB_PORT = 18081
DB_PATH = "/tmp/pccu_it_test.db"
MOOSDB_EXE = "/usr/local/bin/MOOSDB"
@@ -53,6 +62,13 @@ def frame(msg_id, payload, checksum=True):
return body
def dis_frame(msg_id, payload):
"""配电协议帧:0x40 0x40 + id(2B) + length(2B) + payload + Sum8 校验"""
total = 6 + len(payload) + 1
body = START + struct.pack("<HH", msg_id, total) + payload
return body + bytes([sum(body) & 0xFF])
def fc_status_frame():
# payload 144B,仅填关键字段,其余为0
p = bytearray(144)
@@ -98,6 +114,76 @@ def pm_paramset_frame():
return frame(0x0002, bytes(p))
#------------------ 配电协议帧(真实CCU桥接) ------------------
def dis_hv_fb_frame():
"""0x0005 高压母线配电反馈(disHighVolBusState 53B:19字节+17×u16)"""
p = bytearray(53)
p[0] = 0x55 # 燃料电池断路器 闭合
p[1] = 0xAA # 动力锂电池断路器 断开
struct.pack_into("<H", p, 19, 0x0FA0) # 动力汇流排电压
struct.pack_into("<H", p, 21, 123) # DC/DC5模块电流
struct.pack_into("<H", p, 25, 0x0F90) # A汇流排电压
return dis_frame(0x0005, bytes(p))
def dis_hva_fb_frame():
"""0x0006 高压母线A配电反馈(disHighAVolBusState 25B:9字节+8×u16)"""
p = bytearray(25)
p[0] = 0x55 # 艏部FT装置1/2/3断路器 闭合
struct.pack_into("<H", p, 9, 0x0F88) # 汇流排B电压
struct.pack_into("<H", p, 11, 321) # 汇流排B电流
return dis_frame(0x0006, bytes(p))
def dis_hvb_fb_frame():
"""0x0007 低压主母线配电反馈(disLowMainBusState 34B:10字节+12×u16)"""
p = bytearray(34)
p[0] = 0x55 # 锂电池组(仪表)断路器 闭合
struct.pack_into("<H", p, 10, 0x0F70) # 仪表母排电压
struct.pack_into("<H", p, 12, 654) # 艏部低压配电箱电流
return dis_frame(0x0007, bytes(p))
def dis_lv_fb_frame():
"""0x0008 仪表汇流排配电反馈(disLowBusState 29B:11字节+9×u16)"""
p = bytearray(29)
p[0] = 0x55 # UNIT1断路器 闭合
struct.pack_into("<H", p, 11, 0x0F60) # 仪表母排电压
struct.pack_into("<H", p, 13, 111) # UNIT1电流
return dis_frame(0x0008, bytes(p))
def dis_hv_cmd_frame():
"""0x0001 高压母线配电指令(disHighVolBusCmd 14B,pPowerManger 下发)"""
p = bytearray(14)
p[0] = 0x55 # 燃料电池断路器 闭合
p[1] = 0xAA # 动力锂电池断路器 断开
p[12] = 1 # DC/DC5模块启动
return dis_frame(0x0001, bytes(p))
def dis_hva_cmd_frame():
"""0x0002 高压母线A配电指令(disHighAVolBusCmd 6B)"""
p = bytearray(6)
p[0] = 0x55
return dis_frame(0x0002, bytes(p))
def dis_hvb_cmd_frame():
"""0x0003 低压主母线配电指令(disLowMainBusCmd 7B)"""
p = bytearray(7)
p[3] = 0x55 # DC-C1直流配电板断路器
return dis_frame(0x0003, bytes(p))
def dis_lv_cmd_frame():
"""0x0004 仪表汇流排配电指令(disLowBusCmd 8B)"""
p = bytearray(8)
p[0] = 0x55 # UNIT1断路器
return dis_frame(0x0004, bytes(p))
def recv_loop(sock, label, results, msg_ids, timeout):
"""持续接收指定 msg_id 的帧,记录到 results"""
sock.settimeout(timeout)
@@ -139,18 +225,25 @@ def main():
print("FAIL: pCCU exited early, code", pccu.returncode)
return 1
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003)
# 3. 启动接收监听(FC 控制转发 16001,PM 状态/配电反馈 16003,
# 转发真实CCU 的配电指令 16005)
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))
rcu_cmd_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rcu_cmd_recv.bind(("127.0.0.1", RCU_CMD_RECV_PORT))
fc_ctl_results, pm_status_results = [], []
fc_ctl_results, pm_status_results, rcu_cmd_results = [], [], []
t1 = threading.Thread(target=recv_loop,
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 12))
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 14))
t2 = threading.Thread(target=recv_loop,
args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12))
t1.start(); t2.start()
args=(pm_status_recv, "pm-status", pm_status_results,
{0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008}, 14))
t3 = threading.Thread(target=recv_loop,
args=(rcu_cmd_recv, "rcu-cmd", rcu_cmd_results,
{0x0001, 0x0002, 0x0003, 0x0004}, 14))
t1.start(); t2.start(); t3.start()
# 4. 发送 FC 状态(周期模拟)
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -164,7 +257,21 @@ def main():
time.sleep(0.5)
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT))
t1.join(); t2.join()
# 6. 发送配电反馈(真实CCU -> 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
@@ -208,16 +315,40 @@ def main():
else:
ok = False
# 校验 4:数据库有记录(FC/PM 链路收发)
# 校验 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("<H", f, 2)[0]
print(f"[配电反馈 0x{mid:04X} 转发] {'OK' if got else 'MISSING'} (字节一致)")
ok = ok and bool(got)
# 校验 5:配电指令原帧转发到真实CCU(字节一致)
for f in dis_cmd_frames:
got = [d for d in rcu_cmd_results if d == f]
mid = struct.unpack_from("<H", f, 2)[0]
print(f"[配电指令 0x{mid:04X} 转发] {'OK' if got else 'MISSING'} (字节一致)")
ok = ok and bool(got)
# 校验 6:PM 操控指令(0x0001, 45B)不得转发到真实CCU(按帧长区分)
pm_ctl_leak = [d for d in rcu_cmd_results if len(d) == 45]
print(f"[PM操控指令不桥接] 收到 {len(pm_ctl_leak)} 帧 (期望 0)")
ok = ok and not pm_ctl_leak
# 校验 7:数据库有记录(FC/PM 链路收发 + rcu 链路桥接)
time.sleep(1.0)
db_count = -1
db_count, db_rcu = -1, -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]
db_rcu = conn.execute(
"SELECT COUNT(*) FROM comm_log WHERE link='rcu'").fetchone()[0]
conn.close()
print(f"[数据库] comm_log 记录数 = {db_count} (期望>=6)")
print(f"[数据库] comm_log 记录数 = {db_count} (期望>=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条配电指令(发)
web_ok = False
try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)