pCCU 锂电池页新增 BCU 断路器控制(0x10XX81FF 经 pCanBridge 下行 CAN 总线)

- CanBms: bcuCtrlCanId(0x10XX81FF) + buildBcuRelayCtrlData(02 00 总正 总负 FF*4)
- SystemData: BcuCtrlCmd 下行队列(Web线程入队->Iterate/MOOS线程出队Notify) + 下发统计
- CCU: /api/bcu_ctrl(addr/action 或 pos/neg, 校验地址01h~36h) + Iterate 经 MOOS CAN_TX_0x* 下发
- pCanBridge 变双向透传: 订阅 CAN_TX_0x*,CanEndpoint::sendFrame(13字节CANET帧,TCP写,互斥保护)
- 网页: 每BCU节点断路器闭合/断开按钮(confirm确认) + 下发统计卡片
- 测试: pccuTest 控制帧编码用例(183项);集成测试新增假CANET服务器验证完整下行链路(闭合/断开/非法地址)
This commit is contained in:
zjk
2026-09-01 15:23:30 +08:00
parent 0639b5904d
commit 90caacf9a2
15 changed files with 517 additions and 22 deletions
+20
View File
@@ -520,6 +520,25 @@ static void testRcuRegistry() {
CHECK(gotName == "pm_status");
}
//--------------------------------------------------------------------------
// 13. BCU 断路器控制帧(0x10XX81FF 下行)
static void testBcuCtrlFrame() {
// CAN ID:0x10 XX 81 FF,XX=节点地址
CHECK(bcuCtrlCanId(1) == 0x100181FFu);
CHECK(bcuCtrlCanId(54) == 0x103681FFu);
// 数据域:02 00 总正 总负 FF*4
uint8_t d[8];
buildBcuRelayCtrlData(1, 1, d);
CHECK(d[0] == 0x02 && d[1] == 0x00);
CHECK(d[2] == 0x01 && d[3] == 0x01);
CHECK(d[4] == 0xFF && d[5] == 0xFF && d[6] == 0xFF && d[7] == 0xFF);
buildBcuRelayCtrlData(0, 0, d);
CHECK(d[2] == 0x00 && d[3] == 0x00);
buildBcuRelayCtrlData(1, 0, d);
CHECK(d[2] == 0x01 && d[3] == 0x00);
}
//--------------------------------------------------------------------------
int main() {
testFrameLengths();
@@ -535,6 +554,7 @@ int main() {
testDisRoundTrip();
testRegistryLengthDispatch();
testRcuRegistry();
testBcuCtrlFrame();
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
+112 -3
View File
@@ -33,6 +33,7 @@ import subprocess
import os
import signal
import sys
import json
import urllib.request
# ---- 配置 ----
@@ -42,10 +43,12 @@ 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")
# 帧头
@@ -184,6 +187,72 @@ def dis_lv_cmd_frame():
return dis_frame(0x0004, bytes(p))
#------------------ BCU 断路器控制下行测试 ------------------
class CanetServer(threading.Thread):
"""最小 CANET TCP Server 模拟:接受 pCanBridge 连接,捕获下行 13 字节帧"""
def __init__(self, port):
super().__init__(daemon=True)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(("127.0.0.1", port))
self.sock.listen(2)
self.frames = []
self.lock = threading.Lock()
self.stop_flag = False
def run(self):
self.sock.settimeout(1.0)
conn = None
while not self.stop_flag:
try:
if conn is None:
conn, _ = self.sock.accept()
conn.settimeout(0.5)
buf = b""
while not self.stop_flag:
try:
d = conn.recv(4096)
except socket.timeout:
break
if not d:
conn.close()
conn = None
break
buf += d
while len(buf) >= 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)
@@ -204,11 +273,13 @@ def recv_loop(sock, label, results, msg_ids, timeout):
def main():
# 清理
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log"):
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log",
"/tmp/pccu_it_can.db", "/tmp/pccu_it_can.db-wal", "/tmp/pccu_it_can.db-shm"):
if os.path.exists(f):
os.remove(f)
procs = []
canet = CanetServer(CANET_PORT)
try:
# 1. 启动 MOOSDB
moosdb = subprocess.Popen([MOOSDB_EXE, "--moos", "--port", "9000"],
@@ -216,11 +287,15 @@ def main():
procs.append(moosdb)
time.sleep(1.0)
# 2. 启动 pCCU
# 2. 启动 pCCU + pCanBridge(pCanBridge 连接模拟 USBCAN,用于下行测试)
pccu = subprocess.Popen([PCCU_EXE, MISSION],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
procs.append(pccu)
time.sleep(2.0)
canet.start()
pcan = subprocess.Popen([CANBRIDGE_EXE, MISSION],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
procs.append(pcan)
time.sleep(2.5)
if pccu.poll() is not None:
print("FAIL: pCCU exited early, code", pccu.returncode)
return 1
@@ -349,6 +424,39 @@ def main():
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)
@@ -363,6 +471,7 @@ def main():
return 0 if ok else 1
finally:
canet.stop_flag = True
for p in reversed(procs):
try:
p.terminate()
+12
View File
@@ -30,4 +30,16 @@ ProcessConfig = pCCU
web_port = 18081 // 避开 pPowerMangerHost(18080)
web_enable = true
}
ProcessConfig = pCanBridge
{
AppTick = 4
CommsTick = 4
// 测试用 CANET TCP Server(本脚本模拟,捕获 pCanBridge 下行帧)
can_host = 127.0.0.1
can_port = 16006
can_channel_name = CAN0
dbpath = /tmp/pccu_it_can.db
}