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
+20 -5
View File
@@ -20,8 +20,10 @@ ProcessConfig = pPowerManger
// CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000
ccuhost = 127.0.0.1
ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
// 本地输入端口(接收 CCU 数据);默认 5001。
// 5001 让给 pCCU 的 rcu 链路接收真实CCU(192.168.0.140) 固定上报的
// 配电反馈,故本进程接收端口改为 5002(pCCU 转发的反馈与状态报文发往此处)
iport = 5002
}
ProcessConfig = pPowerMangerHost
@@ -48,11 +50,24 @@ ProcessConfig = pCCU
fc_remote_port = 7000
// PM 链路(监听 pPowerManger 指令;向 pPowerManger 周期发送 PM 状态)
// pPowerManger 与 pCCU 同机部署,pPowerManger 绑定 0.0.0.0:5001,
// 故 PM 状态发送目标用回环地址即可送达。
// pPowerManger 与 pCCU 同机部署;pPowerManger 接收端口为 5002(iport),
// 故 PM 状态发送目标用 127.0.0.1:5002。
pm_local_port = 7000
pm_remote_ip = 127.0.0.1
pm_remote_port = 5001
pm_remote_port = 5002
//======== 真实CCU 桥接链路(配电协议) ========
// 真实CCU(192.168.0.140) 设备侧固定上报到 192.168.0.223:5001
// (与 pPowerManger 默认 CCU 配置一致:CCUPORT=7000 为其监听口,
// 上报目标为控制主机 5001),设备侧不可修改。
// 故 pCCU 的 rcu 链路直接绑定 5001 接收其反馈:
// 配电反馈 0x0005~0x0008:原帧转发 pPowerManger(127.0.0.1:5002) + 网页显示
// 配电指令 0x0001~0x0004:转发目标为真实CCU 监听口 192.168.0.140:7000
// 其余帧(旧协议 0x20 0x20 状态帧、无效帧)落库忽略
rcu_enable = true
rcu_local_port = 5001
rcu_remote_ip = 192.168.0.140
rcu_remote_port = 7000
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
+76 -7
View File
@@ -17,8 +17,10 @@ CCU::CCU() {}
CCU::~CCU() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
if (m_coord) { delete m_coord; m_coord = nullptr; }
if (m_bridge) { delete m_bridge; m_bridge = nullptr; }
if (m_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; }
if (m_rcuLink) { m_rcuLink->stop(); delete m_rcuLink; m_rcuLink = nullptr; }
if (m_db) { m_db->close(); delete m_db; m_db = nullptr; }
if (m_snap) { delete m_snap; m_snap = nullptr; }
if (m_sysData) { delete m_sysData; m_sysData = nullptr; }
@@ -49,6 +51,10 @@ bool CCU::OnStartUp() {
else if (param == "pm_local_port") m_pmLocalPort = atol(value.c_str());
else if (param == "pm_remote_ip") m_pmHost = value;
else if (param == "pm_remote_port") m_pmPort = atol(value.c_str());
else if (param == "rcu_enable") m_rcuEnable = (tolower(value) == "true" || value == "1");
else if (param == "rcu_local_port") m_rcuLocalPort = atol(value.c_str());
else if (param == "rcu_remote_ip") m_rcuHost = value;
else if (param == "rcu_remote_port") m_rcuPort = atol(value.c_str());
else if (param == "dbpath") m_dbPath = value;
else if (param == "logpath") m_logPath = value;
else if (param == "web_port") m_webPort = atoi(value.c_str());
@@ -81,6 +87,9 @@ bool CCU::OnStartUp() {
// 系统数据快照
m_sysData = new SystemData();
// 配电桥接器(须在链路 start 前完成依赖注入,避免接收线程空指针)
m_bridge = new DisBridge();
// FC 链路
m_fcLink = new FcLinkManager(m_fcLocalPort, m_fcHost, m_fcPort);
m_fcLink->setLogSink(m_db);
@@ -90,19 +99,40 @@ bool CCU::OnStartUp() {
m_fcLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) {
// 原始帧已通过 LogSink 落库;如需 Web 实时原始报文可在此扩展
});
if (!m_fcLink->start())
LOG_F(ERROR, "FC link start failed");
// PM 链路
m_pmLink = new PmLinkManager(m_pmLocalPort, m_pmHost, m_pmPort);
m_pmLink->setLogSink(m_db);
m_pmLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
// 配电指令(0x0001~0x0004,Sum8 短帧)优先桥接转发真实CCU;
// 注册表按帧总长区分配电指令与 PM 操控/参数指令(共用 id)
if (m_bridge && m_bridge->onPmFrame(msg, frame)) return;
handlePmMessage(msg, frame);
});
m_pmLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
});
// 真实 CCU 链路(配电桥接,rcu_enable 开启时创建)
if (m_rcuEnable) {
m_rcuLink = new RcuLinkManager(m_rcuLocalPort, m_rcuHost, m_rcuPort);
m_rcuLink->setLogSink(m_db);
m_rcuLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
handleRcuMessage(msg, frame);
});
m_rcuLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
});
}
// 注入桥接依赖(m_rcuLink 可为空:未启用桥接时相关入口自动失效)
m_bridge->setup(m_pmLink, m_rcuLink, m_sysData);
// 启动链路
if (!m_fcLink->start())
LOG_F(ERROR, "FC link start failed");
if (!m_pmLink->start())
LOG_F(ERROR, "PM link start failed");
if (m_rcuLink && !m_rcuLink->start())
LOG_F(ERROR, "RCU link start failed on port %ld", m_rcuLocalPort);
// 锂电池:CAN 总线(BMS 协议),数据经 pCanBridge 以 CAN_0x* 消息透传,
// 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。
@@ -112,7 +142,7 @@ bool CCU::OnStartUp() {
m_coord->setup(m_sysData, m_fcLink, m_pmLink);
// 快照构建器(链路就绪后创建)
m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_db);
m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_rcuLink, m_db);
// 网页
if (m_webEnable) {
@@ -128,10 +158,20 @@ bool CCU::OnStartUp() {
}
}
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort);
if (m_rcuLink) {
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"rcu_link local=%ld -> %s:%ld (配电桥接), "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort,
m_rcuLocalPort, m_rcuHost.c_str(), m_rcuPort);
} else {
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"rcu_link disabled, "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort);
}
return true;
}
@@ -155,6 +195,7 @@ void CCU::registerVariables() {
// 运行状态变量
Register("CCU_FC_LINK_STATE", 0);
Register("CCU_PM_LINK_STATE", 0);
Register("CCU_RCU_LINK_STATE", 0);
}
//---------------------------------------------------------
@@ -281,6 +322,8 @@ void CCU::handleFcMessage(Message* msg, const std::vector<uint8_t>& frame) {
void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return;
// 配电指令已在入口由 DisBridge 处理(见 OnStartUp 的 onMessage 接线),
// 到达此处的均为 PM 协议消息(Sum32 校验)
switch (msg->id()) {
case 0x0001: { // PM 操控指令 -> 转发给 FC
PmControlValue c;
@@ -340,6 +383,19 @@ void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
}
}
//---------------------------------------------------------
// handleRcuMessage:处理真实 CCU 链路收到的消息(配电桥接)
//
// 配电反馈(0x0005~0x0008)与真实CCU状态报文(0x0004)由 DisBridge
// 处理(原帧转发 pPowerManger / 仅解码显示);其余(如配电指令
// 回显)仅记录,不转发。
void CCU::handleRcuMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return;
if (m_bridge && m_bridge->onRcuFrame(msg, frame)) return;
LOG_F(WARNING, "[RCU] unhandled msg id 0x%04X, len=%zu", msg->id(), frame.size());
}
//---------------------------------------------------------
// sendPmStatus:整合最新 FC 状态,编码 PM 状态报文发送
@@ -476,6 +532,19 @@ bool CCU::buildReport() {
<< " tx:" << m_pmLink->txCount()
<< " err:" << m_pmLink->errorCount() << "\n";
}
if (m_rcuLink) {
m_msgs << "RCU 链路 local:" << m_rcuLink->localPort()
<< " -> " << m_rcuLink->rcuHost() << ":" << m_rcuLink->rcuPort()
<< " rx:" << m_rcuLink->rxCount()
<< " tx:" << m_rcuLink->txCount()
<< " err:" << m_rcuLink->errorCount() << "\n";
if (m_sysData) {
const RealCcuState& st = m_sysData->realCcu();
m_msgs << "配电桥接: 反馈转发 " << st.fwdFbOk << " 成功 / " << st.fwdFbErr
<< " 失败, 指令转发 " << st.fwdCmdOk << " 成功 / " << st.fwdCmdErr
<< " 失败\n";
}
}
if (m_sysData) {
m_msgs << "锂电池: BMS/CAN (经 pCanBridge CAN_0x* 订阅)\n";
m_msgs << "CAN 帧接收计数:" << m_sysData->canFrameCount() << "\n";
+14
View File
@@ -5,9 +5,11 @@
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "comm/FcLinkManager.h"
#include "comm/PmLinkManager.h"
#include "comm/RcuLinkManager.h"
#include "core/SystemData.h"
#include "core/SnapshotBuilder.h"
#include "core/PowerCoordinator.h"
#include "core/DisBridge.h"
#include "store/DbStore.h"
#include "web/WebServer.h"
@@ -20,6 +22,9 @@ namespace ccu {
// 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger
// 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池
// 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003)
// 收 PM 配电指令(0x0001~0x0004,Sum8短帧) -> DisBridge 原帧转发真实CCU(rcu 链路)
// 收真实CCU 配电反馈(0x0005~0x0008) -> DisBridge 原帧转发 pPowerManger + 解码显示
// 收真实CCU 状态报文(0x0004) -> 仅解码显示,不转发(避免状态冲突)
// 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文
// (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照
// (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用)
@@ -43,6 +48,7 @@ private:
// 链路收帧处理
void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame);
void handlePmMessage(Message* msg, const std::vector<uint8_t>& frame);
void handleRcuMessage(Message* msg, const std::vector<uint8_t>& frame);
// MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布)
void handleCanMessage(CMOOSMsg& msg);
@@ -63,6 +69,12 @@ private:
std::string m_pmHost = "192.168.0.140";
long m_pmPort = 5001;
// 真实 CCU 桥接链路(配电协议透传)
bool m_rcuEnable = false; // 默认关闭,.moos 中开启
long m_rcuLocalPort = 7100;
std::string m_rcuHost = "192.168.0.200"; // 占位地址,.moos 中改为实际值
long m_rcuPort = 7000;
std::string m_dbPath = "pccu_data.db";
std::string m_logPath = "pCCU.log";
int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开
@@ -78,6 +90,8 @@ private:
// 组件
FcLinkManager* m_fcLink = nullptr;
PmLinkManager* m_pmLink = nullptr;
RcuLinkManager* m_rcuLink = nullptr;
DisBridge* m_bridge = nullptr;
SystemData* m_sysData = nullptr;
DbStore* m_db = nullptr;
SnapshotBuilder* m_snap = nullptr;
+9 -1
View File
@@ -19,7 +19,9 @@ void showSynopsis() {
blk(" It receives FC status, integrates & forwards to pPowerManger,");
blk(" maps pPowerManger commands to FC operations, receives CAN ");
blk(" bus BMS battery frames via pCanBridge (CAN_0x* MOOS msgs), ");
blk(" stores data in SQLite and provides a web monitoring page. ");
blk(" bridges power-distribution (配电) frames between pPowerManger");
blk(" and a real CCU device (rcu link), stores data in SQLite and ");
blk(" provides a web monitoring page. ");
blk(" ");
}
@@ -68,6 +70,10 @@ void showExampleConfigAndExit() {
blk(" pm_local_port = 7000 // PM 指令接收端口 ");
blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP ");
blk(" pm_remote_port = 5001 // 控制主机接收端口 ");
blk(" rcu_enable = true // 是否启用真实CCU配电桥接 ");
blk(" rcu_local_port = 7100 // 真实CCU 数据接收端口 ");
blk(" rcu_remote_ip = 192.168.0.200 // 真实CCU IP(占位,按实际修改) ");
blk(" rcu_remote_port= 7000 // 真实CCU 接收端口 ");
blk(" // 锂电池 BMS 走 CAN 总线(经 pCanBridge 的 CAN_0x* 透传) ");
blk(" dbpath = pccu_data.db // 数据库路径 ");
blk(" logpath = pCCU.log // 日志路径 ");
@@ -91,11 +97,13 @@ void showInterfaceAndExit() {
blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(锂电池 BMS) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" ");
exit(0);
}
+3 -1
View File
@@ -1,6 +1,6 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pCCU
# 复合管控器网关:FC <-> pPowerManger
# 复合管控器网关:FC <-> pPowerManger,配电桥接 <-> 真实CCU
#--------------------------------------------------------
if (${WIN32})
@@ -24,6 +24,7 @@ SET(CCU_PROTOCOL_SRC
protocol/MessageRegistry.cpp
protocol/FcProtocol.cpp
protocol/PmProtocol.cpp
protocol/DisProtocol.cpp
protocol/CanBms.cpp
)
@@ -36,6 +37,7 @@ SET(CCU_CORE_SRC
core/SnapshotBuilder.cpp
core/PowerCoordinator.cpp
core/CoordFsm.cpp
core/DisBridge.cpp
)
SET(CCU_STORE_SRC
+31 -2
View File
@@ -54,17 +54,46 @@ bool LinkManager::sendMessageTo(uint16_t id, const void* value,
return true;
}
bool LinkManager::sendRaw(const std::vector<uint8_t>& frame) {
if (frame.empty()) return false;
const std::string& host = defaultRemoteHost();
long port = defaultRemotePort();
if (host.empty() || port <= 0) return false;
if (!m_endpoint.sendTo(host, port, frame)) {
LOG_F(ERROR, "[%s] send raw frame (%zu B) to %s:%ld failed",
m_linkName.c_str(), frame.size(), host.c_str(), port);
return false;
}
++m_txCount;
uint16_t id = 0;
Frame::parseId(frame, id);
if (m_logSink) m_logSink->onRawFrame(1, id, m_linkName, frame, true);
if (m_onRawFrame) m_onRawFrame(1, frame, true);
return true;
}
bool LinkManager::handleIncoming(const std::vector<uint8_t>& frame) {
if (frame.size() < FRAME_HEADER_LEN) return false;
uint16_t id = 0;
if (!Frame::parseId(frame, id)) {
LOG_F(WARNING, "[%s] bad frame header", m_linkName.c_str());
// 非本协议帧(如真实CCU 混发的旧协议 0x20 0x20 状态帧、无效帧):
// 原样落库便于排查,告警按 10s 节流避免刷屏
++m_errorCount;
if (m_logSink) m_logSink->onRawFrame(0, 0, m_linkName, frame, false);
double now = MOOSTime(false);
if (now - m_lastBadHdrLog >= 10.0) {
m_lastBadHdrLog = now;
LOG_F(WARNING, "[%s] 非本协议帧头(%02X %02X, %zu B),已忽略(10s节流)",
m_linkName.c_str(), frame[0], frame[1], frame.size());
}
return false;
}
Message* msg = m_registry.find(id);
// 优先按 id + 实际帧总长精确匹配(配电指令 0x0001~0x0004 与 PM 指令
// 共用 id,仅帧长不同;帧头 length 字段不可信,须用实际收到的字节数),
// 未命中回退该 id 的第一条(保持旧行为:decode 失败报错)
Message* msg = m_registry.find(id, frame.size());
if (!msg) {
LOG_F(WARNING, "[%s] unknown msg id 0x%04X, len=%zu",
m_linkName.c_str(), id, frame.size());
+8 -1
View File
@@ -48,7 +48,9 @@ public:
MessageRegistry& registry() { return m_registry; }
const MessageRegistry& registry() const { return m_registry; }
// 收帧回调:msg 为注册表中命中的消息对象(可调用 decode 解码),frame 为原始完整帧
// 收帧回调:msg 为注册表中命中的消息对象(可调用 decode 解码),frame 为原始完整帧。
// 注意:同 id 不同长度的消息(如配电指令与 PM 指令)按帧总长精确区分,
// 回调携带的 msg 即精确命中的对象;长度未命中时回退该 id 的第一条。
void setOnMessage(std::function<void(Message* msg, const std::vector<uint8_t>& frame)> cb) {
m_onMessage = std::move(cb);
}
@@ -68,6 +70,10 @@ public:
bool sendMessageTo(uint16_t id, const void* value,
const std::string& host, long port);
// 原样发送一帧到默认远端(桥接透传用,不做编码);
// 消息 id 从帧头解析,收发计数与落库口径与 sendMessage 一致
bool sendRaw(const std::vector<uint8_t>& frame);
// 处理一帧收到的原始数据(供接收线程调用,也可测试时手动喂入)
bool handleIncoming(const std::vector<uint8_t>& frame);
@@ -102,6 +108,7 @@ private:
ILinkLogSink* m_logSink = nullptr;
double m_lastRxTime = 0;
double m_lastBadHdrLog = 0; // 非本协议帧头告警节流
unsigned long m_rxCount = 0;
unsigned long m_txCount = 0;
unsigned long m_errorCount = 0;
+8
View File
@@ -3,6 +3,7 @@
#include "LinkManager.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
@@ -11,6 +12,12 @@ namespace ccu {
//
// - 本地监听:控制主机指令端口(默认 7000)
// - 发送目标:控制主机(默认 192.168.0.140:5001)
//
// 注册的消息:
// - PM 协议 0x0001~0x0004(Sum32)
// - 配电指令 0x0001~0x0004(Sum8,13~21B 短帧):pPowerManger 下发的
// 配电指令与其操控/参数指令共用 id,仅帧长不同,由注册表按
// (id+帧长) 区分,DisBridge 据此原桥接转发真实CCU
// 端口/地址均可由 .moos 配置覆盖。
//============================================================================
@@ -20,6 +27,7 @@ public:
const std::string& pmHost, long pmPort)
: LinkManager("pm", localPort), m_pmHost(pmHost), m_pmPort(pmPort) {
registerPmMessages(registry());
registerDisCmdMessages(registry());
}
void setPmAddress(const std::string& host, long port) {
+51
View File
@@ -0,0 +1,51 @@
#ifndef PCCU_RCU_LINK_MANAGER_H
#define PCCU_RCU_LINK_MANAGER_H
#include "LinkManager.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
//============================================================================
// RcuLinkManager:面向真实 CCU 设备的链路(配电桥接)。
//
// - 本地监听:rcu_local_port(默认 7100),接收真实CCU 上报的数据
// - 发送目标:真实CCU(默认占位 192.168.0.200:7000,.moos 可配)
//
// 注册的消息:
// - 配电反馈 0x0005~0x0008(Sum8):真实CCU 上报的配电状态,
// 由 DisBridge 原帧转发给 pPowerManger 并解码显示
// - 配电指令 0x0001~0x0004(Sum8):真实CCU 的指令回显仅识别,不转发
// - PM 消息 0x0001~0x0004(Sum32):真实CCU 状态报文 0x0004 仅解码
// 显示,不转发(避免与 pCCU 自身模拟的状态报文冲突)
//============================================================================
class RcuLinkManager : public LinkManager {
public:
RcuLinkManager(long localPort,
const std::string& rcuHost, long rcuPort)
: LinkManager("rcu", localPort), m_rcuHost(rcuHost), m_rcuPort(rcuPort) {
registerDisFbMessages(registry());
registerDisCmdMessages(registry());
registerPmMessages(registry());
}
void setRcuAddress(const std::string& host, long port) {
m_rcuHost = host; m_rcuPort = port;
}
const std::string& rcuHost() const { return m_rcuHost; }
long rcuPort() const { return m_rcuPort; }
protected:
std::string defaultRemoteHost() const override { return m_rcuHost; }
long defaultRemotePort() const override { return m_rcuPort; }
private:
std::string m_rcuHost;
long m_rcuPort;
};
} // namespace ccu
#endif // PCCU_RCU_LINK_MANAGER_H
+1
View File
@@ -36,6 +36,7 @@ bool UdpEndpoint::init(long localPort) {
bool UdpEndpoint::sendTo(const std::string& host, long port, const std::vector<uint8_t>& data) {
if (!m_socket || data.empty()) return false;
try {
std::lock_guard<std::mutex> lock(m_sendMutex);
m_socket->iSendMessageTo(const_cast<uint8_t*>(data.data()),
static_cast<int>(data.size()),
port, host);
+4
View File
@@ -4,6 +4,7 @@
#define UNIX
#include "MOOS/libMOOS/Comms/XPCUdpSocket.h"
#include <cstdint>
#include <mutex>
#include <string>
#include <vector>
@@ -17,6 +18,8 @@ namespace ccu {
// - 向指定远端 (host:port) 发送数据报
// - 阻塞接收数据报
// 不关心协议内容,仅负责字节的收发。
// 发送可能来自多个线程(MOOS 主线程 + 各链路接收线程的桥接转发),
// sendTo 内部加锁串行化。
//============================================================================
class UdpEndpoint {
@@ -39,6 +42,7 @@ public:
private:
XPCUdpSocket* m_socket = nullptr;
long m_localPort = 0;
std::mutex m_sendMutex; // 串行化并发 sendTo
};
} // namespace ccu
+173
View File
@@ -0,0 +1,173 @@
#include "DisBridge.h"
#include "../comm/LinkManager.h"
#include "../protocol/DisProtocol.h"
#include "SystemData.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "../pPowerManger/logc/loguru.hpp"
namespace ccu {
void DisBridge::setup(LinkManager* pmLink, LinkManager* rcuLink, SystemData* sys) {
m_pmLink = pmLink;
m_rcuLink = rcuLink;
m_sys = sys;
}
//--------------------------------------------------------------------------
// PM 链路入口:pPowerManger -> 真实CCU(配电指令)
//--------------------------------------------------------------------------
bool DisBridge::onPmFrame(Message* msg, const std::vector<uint8_t>& frame) {
if (!isDisCmdMessage(msg)) return false;
forwardCmdToRcu(msg, frame);
return true;
}
void DisBridge::forwardCmdToRcu(Message* msg, const std::vector<uint8_t>& frame) {
if (!m_rcuLink || !m_sys) return;
// 原帧透传给真实CCU(帧字节不变,Sum8 校验和保持有效)
bool ok = m_rcuLink->sendRaw(frame);
// 取副本 -> 更新 -> 写回(跨线程共享快照的既有模式)
RealCcuState st = m_sys->realCcu();
st.cmdCount++;
if (ok) st.fwdCmdOk++; else st.fwdCmdErr++;
// 解码缓存最近指令(仅用于展示;解码失败不影响透传结果)
if (msg) {
bool decoded = false;
switch (msg->id()) {
case DIS_HV_CMD_ID: {
DisHighVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvCmd);
break;
}
case DIS_HVA_CMD_ID: {
DisHighAVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvaCmd);
break;
}
case DIS_HVB_CMD_ID: {
DisHighBVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvbCmd);
break;
}
case DIS_LV_CMD_ID: {
DisLowBusCmdMessage m;
decoded = m.decode(frame, st.lastLvCmd);
break;
}
default:
break;
}
if (decoded) {
st.cmdValid = true;
st.lastCmdRx = MOOSTime(false);
} else {
LOG_F(WARNING, "[桥接] 配电指令 0x%04X 解码失败(透传不受影响)", msg->id());
}
}
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 配电指令 0x%04X (%zu B) -> 真实CCU %s", msg ? msg->id() : 0,
frame.size(), ok ? "成功" : "失败");
}
//--------------------------------------------------------------------------
// RCU 链路入口:真实CCU -> pPowerManger(配电反馈)+ 状态报文显示
//--------------------------------------------------------------------------
bool DisBridge::onRcuFrame(Message* msg, const std::vector<uint8_t>& frame) {
if (isDisFbMessage(msg)) {
forwardFbToPm(msg, frame);
return true;
}
if (!isDisCmdMessage(msg) && msg && msg->id() == 0x0004) {
// 真实CCU 状态报文:仅解码显示,不转发(避免与 pCCU 模拟状态冲突)
storeRealCcuStatus(frame);
return true;
}
return false; // 配电指令回显等:不处理,交上层记录
}
void DisBridge::forwardFbToPm(Message* msg, const std::vector<uint8_t>& frame) {
if (!m_pmLink || !m_sys) return;
// 原帧透传给 pPowerManger
bool ok = m_pmLink->sendRaw(frame);
RealCcuState st = m_sys->realCcu();
st.fbCount++;
if (ok) st.fwdFbOk++; else st.fwdFbErr++;
// 解码缓存四条母线最新状态
if (msg) {
bool decoded = false;
switch (msg->id()) {
case DIS_HV_FB_ID: {
DisHighVolBusFbMessage m;
decoded = m.decode(frame, st.hvBus);
if (decoded) st.hvLastRx = MOOSTime(false);
break;
}
case DIS_HVA_FB_ID: {
DisHighAVolBusFbMessage m;
decoded = m.decode(frame, st.hvaBus);
if (decoded) st.hvaLastRx = MOOSTime(false);
break;
}
case DIS_HVB_FB_ID: {
DisHighBVolBusFbMessage m;
decoded = m.decode(frame, st.hvbBus);
if (decoded) st.hvbLastRx = MOOSTime(false);
break;
}
case DIS_LV_FB_ID: {
DisLowBusFbMessage m;
decoded = m.decode(frame, st.lvBus);
if (decoded) st.lvLastRx = MOOSTime(false);
break;
}
default:
break;
}
if (decoded) {
switch (msg->id()) {
case DIS_HV_FB_ID: st.hvValid = true; break;
case DIS_HVA_FB_ID: st.hvaValid = true; break;
case DIS_HVB_FB_ID: st.hvbValid = true; break;
case DIS_LV_FB_ID: st.lvValid = true; break;
default: break;
}
} else {
LOG_F(WARNING, "[桥接] 配电反馈 0x%04X 解码失败(透传不受影响)", msg->id());
}
}
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 配电反馈 0x%04X (%zu B) -> pPowerManger %s", msg ? msg->id() : 0,
frame.size(), ok ? "成功" : "失败");
}
void DisBridge::storeRealCcuStatus(const std::vector<uint8_t>& frame) {
if (!m_sys) return;
PmStatusMessage m;
PmStatusValue v;
if (!m.decode(frame, v)) {
LOG_F(WARNING, "[桥接] 真实CCU 状态报文 0x0004 解码失败");
return;
}
RealCcuState st = m_sys->realCcu();
st.ccuStatus = v;
st.ccuStatusValid = true;
st.ccuStatusLastRx = MOOSTime(false);
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 真实CCU 状态报文: mode=%d status=%d heartbeat=%d (仅显示)",
v.fc_mode, v.fc_status, v.heartbeat);
}
} // namespace ccu
+55
View File
@@ -0,0 +1,55 @@
#ifndef PCCU_DIS_BRIDGE_H
#define PCCU_DIS_BRIDGE_H
#include <cstdint>
#include <vector>
#include "../protocol/Message.h"
namespace ccu {
class LinkManager;
class SystemData;
//============================================================================
// DisBridge:配电协议桥接器(pPowerManger <-> 真实CCU)。
//
// pCCU 居中桥接,原帧透传(不做解析重编码,校验和无需重算):
// pPowerManger --配电指令 0x0001~0x0004(Sum8,短帧)--> pCCU --原帧--> 真实CCU
// 真实CCU --配电反馈 0x0005~0x0008(Sum8)------> pCCU --原帧--> pPowerManger
//
// 同时把配电反馈(及真实CCU 状态报文 0x0004)解码写入 SystemData
// 供网页显示与统计;真实CCU 的 0x0004 仅显示不转发,避免与 pCCU
// 自身模拟的状态报文冲突。
//
// 调用约定:onPmFrame 在 PM 链路接收线程回调,onRcuFrame 在 RCU
// 链路接收线程回调;返回 true 表示该帧已被桥接处理,上层不再处理。
//============================================================================
class DisBridge {
public:
// 注入依赖(在 CCU::OnStartUp 中链路创建完成后调用)
void setup(LinkManager* pmLink, LinkManager* rcuLink, SystemData* sys);
// PM 链路收帧入口:命中配电指令则原帧转发真实CCU,返回是否已处理
bool onPmFrame(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收帧入口:命中配电反馈则原帧转发 pPowerManger 并解码
// 显示;真实CCU 状态报文 0x0004 仅解码显示。返回是否已处理
bool onRcuFrame(Message* msg, const std::vector<uint8_t>& frame);
private:
// PM 链路收到配电指令:原帧转发 + 解码缓存
void forwardCmdToRcu(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收到配电反馈:原帧转发 + 解码缓存
void forwardFbToPm(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收到真实CCU 状态报文:仅解码缓存(显示)
void storeRealCcuStatus(const std::vector<uint8_t>& frame);
LinkManager* m_pmLink = nullptr;
LinkManager* m_rcuLink = nullptr;
SystemData* m_sys = nullptr;
};
} // namespace ccu
#endif // PCCU_DIS_BRIDGE_H
+274 -10
View File
@@ -5,6 +5,7 @@
#include "json/json.h"
#include <sstream>
#include <cstdio>
#include <utility>
namespace ccu {
@@ -18,10 +19,12 @@ void put(JsonVal& j, const char* k, uint32_t v){ j[k] = JsonVal(static_cast<Json
void putI(JsonVal& j, const char* k, int16_t v) { j[k] = JsonVal(static_cast<int>(v)); }
//--------------------------------------------------------------------------
// 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。
// 方向是相对 pCCU 而言:收=对方→CCU,发=CCU→对方。
// 消息描述:根据链路 + 消息 ID + 帧长 + 方向(0=收 1=发),给出中文名与
// "来源→去向"。方向是相对 pCCU 而言:收=对方→CCU,发=CCU→对方。
// length 用于区分同 id 消息:配电指令 0x0001~0x0004 为 13~21B 短帧,
// 与 PM 操控/参数指令(28/45B)共用 id。
//--------------------------------------------------------------------------
std::string describeMessageName(const std::string& link, uint16_t id) {
std::string describeMessageName(const std::string& link, uint16_t id, int length) {
if (link == "fc") {
switch (id) {
case 0x0001: return "FC控制指令";
@@ -30,10 +33,30 @@ std::string describeMessageName(const std::string& link, uint16_t id) {
}
} else if (link == "pm") {
switch (id) {
case 0x0001: return "PM操控指令";
case 0x0002: return "PM参数设定";
case 0x0003: return "PM参数反馈";
case 0x0004: return "PM状态报文";
case 0x0001:
return (length > 0 && length <= 21) ? "PM配电指令(高压母线)" : "PM操控指令";
case 0x0002:
return (length > 0 && length <= 21) ? "PM配电指令(高压母线A)" : "PM参数设定";
case 0x0003:
return (length > 0 && length <= 21) ? "PM配电指令(低压主母线)" : "PM参数反馈";
case 0x0004:
return (length > 0 && length <= 21) ? "PM配电指令(仪表汇流排)" : "PM状态报文";
case 0x0005: return "PM配电反馈(高压母线)";
case 0x0006: return "PM配电反馈(高压母线A)";
case 0x0007: return "PM配电反馈(低压主母线)";
case 0x0008: return "PM配电反馈(仪表汇流排)";
default: break;
}
} else if (link == "rcu") {
switch (id) {
case 0x0001: return "配电指令(高压母线)";
case 0x0002: return "配电指令(高压母线A)";
case 0x0003: return "配电指令(低压主母线)";
case 0x0004: return "配电指令(仪表汇流排)";
case 0x0005: return "配电反馈(高压母线)";
case 0x0006: return "配电反馈(高压母线A)";
case 0x0007: return "配电反馈(低压主母线)";
case 0x0008: return "配电反馈(仪表汇流排)";
default: break;
}
}
@@ -45,6 +68,7 @@ std::string describeMessageName(const std::string& link, uint16_t id) {
std::string describeDirection(const std::string& link, int direction) {
std::string peer = "PM";
if (link == "fc") peer = "FC";
else if (link == "rcu") peer = "真实CCU";
return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU");
}
@@ -57,7 +81,7 @@ JsonVal commLogToJson(const CommLogRow& r) {
e["msgId"] = r.msgId;
e["checksumOk"] = r.checksumOk;
e["hex"] = r.hex;
e["name"] = describeMessageName(r.link, r.msgId);
e["name"] = describeMessageName(r.link, r.msgId, r.length);
e["dirText"] = describeDirection(r.link, r.direction);
return e;
}
@@ -391,11 +415,247 @@ JsonVal linkJson(const LinkManager* lm) {
return j;
}
//--------------------------------------------------------------------------
// 真实CCU 配电桥接 -> JSON(rcu 节)
// 断路器/开关量按原始值下发(0x55 闭合 / 0xAA 断开 / 0x5A 故障),
// 由前端渲染;age 为距最后收到该类帧的秒数(-1 表示从未收到)。
//--------------------------------------------------------------------------
JsonVal ageJson(bool valid, double lastRx, double now) {
JsonVal j(Json::objectValue);
j["valid"] = JsonVal(valid);
j["age"] = JsonVal(valid ? (now - lastRx) : -1.0);
return j;
}
// 断路器组( 名称 -> 原始值 )
JsonVal disBreakersJson(const std::vector<std::pair<const char*, uint8_t>>& items) {
JsonVal j(Json::objectValue);
for (const auto& it : items) put(j, it.first, it.second);
return j;
}
JsonVal hvBusJson(const disHighVolBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"fuelCell", v.fuelCellCircuitBreaker},
{"powerLithiumBattery", v.powerLithiumBatteryCircuitBreaker},
{"propulsionMotor", v.propulsionMotorCircuitBreaker},
{"lithiumBatteryGroupInstrument", v.lithiumBatteryGroupInstrumentCircuitBreaker},
{"dcDc5Module", v.dcDc5ModuleCircuitBreaker},
{"bowHighVoltageDistributionBox", v.bowHighVoltageDistributionBoxCircuitBreaker},
{"sternFTDevice45", v.sternFTDevice45CircuitBreaker},
{"fbReserved", v.fbReservedCircuitBreaker},
{"sternRudderSwitch1", v.sternRudderSwitch1CircuitBreaker},
{"sternRudderSwitch2", v.sternRudderSwitch2CircuitBreaker},
{"tyzReserved", v.tyzReservedCircuitBreaker},
{"reserved", v.reservedCircuitBreaker},
});
put(j, "dcDcFaultWord1", v.dcDcFaultWord1);
put(j, "dcDcFaultWord2", v.dcDcFaultWord2);
put(j, "powerBusInsulationStatus", v.powerBusInsulationStatus);
put(j, "aBusInsulationStatus", v.aBusInsulationStatus);
put(j, "meterPowerLossSignal", v.meterPowerLossSignal);
put(j, "emergencyPowerLossSignal", v.emergencyPowerLossSignal);
put(j, "dcDcTemperature", v.dcDcTemperature);
put(j, "powerBusVoltage", v.powerBusVoltage);
put(j, "dcDc5ModuleCurrent", v.dcDc5ModuleCurrent);
put(j, "powerBusCurrent", v.powerBusCurrent);
put(j, "busbarAVoltage", v.busbarAVoltage);
put(j, "propulsionMotorControlBoxCurrent", v.propulsionMotorControlBoxCurrent);
put(j, "busbarACurrent", v.busbarACurrent);
put(j, "lithiumBatteryGroupMeterCurrent", v.lithiumBatteryGroupMeterCurrent);
put(j, "bowHighVoltageDistributionBoxCurrent", v.bowHighVoltageDistributionBoxCurrent);
put(j, "sternFTDevice45Current", v.sternFTDevice45Current);
put(j, "tyzReservedSwitchCurrent", v.tyzReservedSwitchCurrent);
put(j, "sternRudderSwitch1Current", v.sternRudderSwitch1Current);
put(j, "reservedCurrent1", v.reservedCurrent1);
put(j, "sternRudderSwitch2Current", v.sternRudderSwitch2Current);
put(j, "reservedCurrent2", v.reservedCurrent2);
put(j, "fbReservedSwitchCurrent", v.fbReservedSwitchCurrent);
put(j, "reservedCurrent3", v.reservedCurrent3);
put(j, "coolWaterPressure", v.coolWaterPressure);
return j;
}
JsonVal hvaBusJson(const disHighAVolBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"bowFTDevice123", v.bowFTDevice123CircuitBreaker},
{"actuator", v.actuatorCircuitBreaker},
{"mastSteeringGearControlBox", v.mastSteeringGearControlBoxCircuitBreaker},
{"xcz", v.xczCircuitBreaker},
{"bowRudderControlBox", v.bowRudderControlBoxCircuitBreaker},
{"openWaterCoverStartCylinder", v.openWaterCoverStartCylinderCircuitBreaker},
});
put(j, "instrumentPowerFailureSignal", v.instrumentPowerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "waterIngressionAlarm", v.waterIngressionAlarm);
put(j, "busbarBVoltage", v.busbarBVoltage);
put(j, "busbarBCurrent", v.busbarBCurrent);
put(j, "bowFTDevice123Current", v.bowFTDevice123Current);
put(j, "actuatorCurrent", v.actuatorCurrent);
put(j, "mastSteeringGearControlBoxCurrent", v.mastSteeringGearControlBoxCurrent);
put(j, "xczCurrent", v.xczCurrent);
put(j, "bowRudderControlBoxCurrent", v.bowRudderControlBoxCurrent);
put(j, "openWaterCoverStartCylinderCurrent", v.openWaterCoverStartCylinderCurrent);
return j;
}
JsonVal hvbBusJson(const disLowMainBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"lithiumBatteryGroupInstrument", v.lithiumBatteryGroupInstrumentCircuitBreaker},
{"bowLowVoltageDistributionBox", v.bowLowVoltageDistributionBoxCircuitBreaker},
{"unit4InstrumentDC48V", v.unit4InstrumentDC48VCircuitBreaker},
{"dcC1DCDistributionPanel", v.dcC1DCDistributionPanelCircuitBreaker},
{"reserved1", v.reservedCircuitBreaker1},
{"reserved2", v.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup2", v.emergencyLithiumBatteryGroup2CircuitBreaker},
});
put(j, "instrumentPowerFailureSignal", v.instrumentPowerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "instrumentBusbarInsulationLow", v.instrumentBusbarInsulationLow);
put(j, "instrumentBusbarVoltage", v.instrumentBusbarVoltage);
put(j, "bowLowVoltageDistributionBoxCurrent", v.bowLowVoltageDistributionBoxCurrent);
put(j, "dcC1InstrumentDC48VCurrent", v.dcC1InstrumentDC48VCurrent);
put(j, "reservedCurrent1", v.reservedCurrent1);
put(j, "emergencyLithiumBatteryGroup2Current", v.emergencyLithiumBatteryGroup2Current);
put(j, "lithiumBatteryGroupInstrumentCurrent", v.lithiumBatteryGroupInstrumentCurrent);
put(j, "unit4InstrumentDC48VCurrent", v.unit4InstrumentDC48VCurrent);
put(j, "reservedCurrent2", v.reservedCurrent2);
put(j, "emergency2BusbarVoltage", v.emergency2BusbarVoltage);
put(j, "compositeEnergyEmergencyDC48VCurrent", v.compositeEnergyManagementSystemEmergencyDC48VCurrent);
put(j, "fuelCellSecurityEmergencyDC48VCurrent", v.fuelCellSecuritySystemEmergencyDC48VCurrent);
put(j, "plcControlPowerCurrent", v.plcControlPowerCurrent);
return j;
}
JsonVal lvBusJson(const disLowBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"unit1", v.unit1CircuitBreaker},
{"unit2", v.unit2CircuitBreaker},
{"unit3", v.unit3CircuitBreaker},
{"unit5", v.unit5CircuitBreaker},
{"bowPZDevice", v.bowPZDeviceCircuitBreaker},
{"reserved1", v.reservedCircuitBreaker1},
{"reserved2", v.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup1", v.emergencyLithiumBatteryGroup1CircuitBreaker},
});
put(j, "powerFailureSignal", v.powerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "waterIngressionAlarm", v.waterIngressionAlarm);
put(j, "instrumentBusbarVoltage", v.instrumentBusbarVoltage);
put(j, "instrumentBusbarCurrent", v.instrumentBusbarCurrent);
put(j, "unit1Current", v.unit1Current);
put(j, "unit2Current", v.unit2Current);
put(j, "unit3Current", v.unit3Current);
put(j, "unit5Current", v.unit5Current);
put(j, "bowPZDeviceCurrent", v.bowPZDeviceCurrent);
put(j, "emergencyLithiumBatteryGroup1Current", v.emergencyLithiumBatteryGroup1Current);
put(j, "emergency1BusbarVoltage", v.emergency1BusbarVoltage);
return j;
}
JsonVal realCcuJson(const RealCcuState& st, double now) {
JsonVal j(Json::objectValue);
// 桥接统计
JsonVal fwd(Json::objectValue);
fwd["fbCount"] = JsonVal(static_cast<Json::UInt>(st.fbCount));
fwd["fwdFbOk"] = JsonVal(static_cast<Json::UInt>(st.fwdFbOk));
fwd["fwdFbErr"] = JsonVal(static_cast<Json::UInt>(st.fwdFbErr));
fwd["cmdCount"] = JsonVal(static_cast<Json::UInt>(st.cmdCount));
fwd["fwdCmdOk"] = JsonVal(static_cast<Json::UInt>(st.fwdCmdOk));
fwd["fwdCmdErr"] = JsonVal(static_cast<Json::UInt>(st.fwdCmdErr));
j["forward"] = fwd;
// 四条母线最新反馈(全字段)+ 新鲜度
j["hvAge"] = ageJson(st.hvValid, st.hvLastRx, now);
j["hvaAge"] = ageJson(st.hvaValid, st.hvaLastRx, now);
j["hvbAge"] = ageJson(st.hvbValid, st.hvbLastRx, now);
j["lvAge"] = ageJson(st.lvValid, st.lvLastRx, now);
j["hv"] = st.hvValid ? hvBusJson(st.hvBus) : JsonVal(Json::objectValue);
j["hva"] = st.hvaValid ? hvaBusJson(st.hvaBus) : JsonVal(Json::objectValue);
j["hvb"] = st.hvbValid ? hvbBusJson(st.hvbBus) : JsonVal(Json::objectValue);
j["lv"] = st.lvValid ? lvBusJson(st.lvBus) : JsonVal(Json::objectValue);
// 最近配电指令(pPowerManger -> 真实CCU)
JsonVal lastCmd(Json::objectValue);
lastCmd["valid"] = JsonVal(st.cmdValid);
lastCmd["age"] = JsonVal(st.cmdValid ? (now - st.lastCmdRx) : -1.0);
JsonVal cmds(Json::objectValue);
cmds["hv"] = disBreakersJson({
{"fuelCell", st.lastHvCmd.fuelCellCircuitBreaker},
{"powerLithiumBattery", st.lastHvCmd.powerLithiumBatteryCircuitBreaker},
{"propulsionMotor", st.lastHvCmd.propulsionMotorCircuitBreaker},
{"lithiumBatteryGroupInstrument", st.lastHvCmd.lithiumBatteryGroupInstrumentCircuitBreaker},
{"dcDc5Module", st.lastHvCmd.dcDc5ModuleCircuitBreaker},
{"bowHighVoltageDistributionBox", st.lastHvCmd.bowHighVoltageDistributionBoxCircuitBreaker},
{"sternFTDevice45", st.lastHvCmd.sternFTDevice45CircuitBreaker},
{"fbReserved", st.lastHvCmd.fbReservedCircuitBreaker},
{"sternRudderSwitch1", st.lastHvCmd.sternRudderSwitch1CircuitBreaker},
{"sternRudderSwitch2", st.lastHvCmd.sternRudderSwitch2CircuitBreaker},
{"tyzReserved", st.lastHvCmd.tyzReservedCircuitBreaker},
{"reserved", st.lastHvCmd.reservedCircuitBreaker},
{"dcdc5ModuleStart", st.lastHvCmd.dcdc5Module},
{"coolingSystemStart", st.lastHvCmd.coolingSystem},
});
cmds["hva"] = disBreakersJson({
{"bowFTDevice123", st.lastHvaCmd.bowFTDevice123CircuitBreaker},
{"actuator", st.lastHvaCmd.actuatorCircuitBreaker},
{"mastSteeringGearControlBox", st.lastHvaCmd.mastSteeringGearControlBoxCircuitBreaker},
{"xcz", st.lastHvaCmd.xczCircuitBreaker},
{"bowRudderControlBox", st.lastHvaCmd.bowRudderControlBoxCircuitBreaker},
{"openWaterCoverStartCylinder", st.lastHvaCmd.openWaterCoverStartCylinderCircuitBreaker},
});
cmds["hvb"] = disBreakersJson({
{"lithiumBatteryGroupInstrument", st.lastHvbCmd.lithiumBatteryGroupInstrumentCircuitBreaker},
{"bowLowVoltageDistributionBox", st.lastHvbCmd.bowLowVoltageDistributionBoxCircuitBreaker},
{"unit4InstrumentDC48V", st.lastHvbCmd.unit4InstrumentDC48VCircuitBreaker},
{"dcC1DCDistributionPanel", st.lastHvbCmd.dcC1DCDistributionPanelCircuitBreaker},
{"reserved1", st.lastHvbCmd.reservedCircuitBreaker1},
{"reserved2", st.lastHvbCmd.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup2", st.lastHvbCmd.emergencyLithiumBatteryGroup2CircuitBreaker},
});
cmds["lv"] = disBreakersJson({
{"unit1", st.lastLvCmd.unit1CircuitBreaker},
{"unit2", st.lastLvCmd.unit2CircuitBreaker},
{"unit3", st.lastLvCmd.unit3CircuitBreaker},
{"unit5", st.lastLvCmd.unit5CircuitBreaker},
{"bowPZDevice", st.lastLvCmd.bowPZDeviceCircuitBreaker},
{"reserved1", st.lastLvCmd.reservedCircuitBreaker1},
{"reserved2", st.lastLvCmd.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup1", st.lastLvCmd.emergencyLithiumBatteryGroup1CircuitBreaker},
});
lastCmd["cmds"] = cmds;
j["lastCmd"] = lastCmd;
// 真实CCU 状态报文 0x0004(仅显示)
JsonVal ccu(Json::objectValue);
ccu["valid"] = JsonVal(st.ccuStatusValid);
ccu["age"] = JsonVal(st.ccuStatusValid ? (now - st.ccuStatusLastRx) : -1.0);
if (st.ccuStatusValid) {
put(ccu, "fc_mode", st.ccuStatus.fc_mode);
put(ccu, "fc_status", st.ccuStatus.fc_status);
put(ccu, "fc_fault_level", st.ccuStatus.fc_fault_level);
put(ccu, "generation_power", st.ccuStatus.generation_power);
put(ccu, "dyn_soc", st.ccuStatus.dyn_soc);
put(ccu, "ins_soc", st.ccuStatus.ins_soc);
put(ccu, "heartbeat", st.ccuStatus.heartbeat);
put(ccu, "emergency_cmd", st.ccuStatus.emergency_cmd);
}
j["ccuStatus"] = ccu;
return j;
}
} // namespace
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
DbStore* db)
: m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {}
LinkManager* rcu, DbStore* db)
: m_sys(sys), m_fc(fc), m_pm(pm), m_rcu(rcu), m_db(db) {}
std::string SnapshotBuilder::build() const {
JsonVal root(Json::objectValue);
@@ -417,6 +677,8 @@ std::string SnapshotBuilder::build() const {
root["pmStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmStatusCount()));
root["bmsStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->bmsStatusCount()));
root["canFrameCount"] = JsonVal(static_cast<Json::UInt>(m_sys->canFrameCount()));
// 真实CCU 配电桥接数据(RCU 链路)
root["realCcu"] = realCcuJson(m_sys->realCcu(), MOOSTime(false));
} else {
root["fc"] = JsonVal(Json::objectValue);
root["pmCmd"] = JsonVal(Json::objectValue);
@@ -425,11 +687,13 @@ std::string SnapshotBuilder::build() const {
root["pmFb"] = JsonVal(Json::objectValue);
root["pmStatus"] = JsonVal(Json::objectValue);
root["bms"] = JsonVal(Json::arrayValue);
root["realCcu"] = JsonVal(Json::objectValue);
}
JsonVal links(Json::objectValue);
links["fc"] = linkJson(m_fc);
links["pm"] = linkJson(m_pm);
if (m_rcu) links["rcu"] = linkJson(m_rcu);
root["links"] = links;
// 最近原始帧(默认 20 条)
+5 -3
View File
@@ -12,13 +12,14 @@ class DbStore;
//============================================================================
// SnapshotBuilder:构建网页推送的 JSON 快照。
//
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN)、两条 UDP 链路统计、
// 最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN + 真实CCU 配电桥接)、
// 三条 UDP 链路统计、最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
//============================================================================
class SnapshotBuilder {
public:
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db);
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
LinkManager* rcu, DbStore* db);
// 生成完整快照 JSON
std::string build() const;
@@ -30,6 +31,7 @@ private:
SystemData* m_sys;
LinkManager* m_fc;
LinkManager* m_pm;
LinkManager* m_rcu;
DbStore* m_db;
};
+52
View File
@@ -6,9 +6,48 @@
#include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/CanBms.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
//============================================================================
// RealCcuState:真实 CCU 设备的桥接数据缓存(配电协议)。
//
// 数据来源:RCU 链路(真实CCU -> pCCU),由 DisBridge 在转发的同时
// 解码写入;网页快照读取展示。各 lastRx 时间由写入方更新(MOOSTime),
// 供在线判定与新鲜度展示。
//============================================================================
struct RealCcuState {
// 四条配电母线最新反馈(valid 表示收到过对应反馈帧)
disHighVolBusState hvBus{}; bool hvValid = false;
disHighAVolBusState hvaBus{}; bool hvaValid = false;
disLowMainBusState hvbBus{}; bool hvbValid = false;
disLowBusState lvBus{}; bool lvValid = false;
double hvLastRx = 0, hvaLastRx = 0, hvbLastRx = 0, lvLastRx = 0;
// 最近收到的配电指令(pPowerManger 下发、已转发真实CCU,用于展示)
disHighVolBusCmd lastHvCmd{};
disHighAVolBusCmd lastHvaCmd{};
disLowMainBusCmd lastHvbCmd{};
disLowBusCmd lastLvCmd{};
bool cmdValid = false;
double lastCmdRx = 0;
// 真实CCU 状态报文 0x0004(仅解码显示,不转发)
PmStatusValue ccuStatus{};
bool ccuStatusValid = false;
double ccuStatusLastRx = 0;
// 桥接统计
unsigned long fbCount = 0; // 收到配电反馈帧数
unsigned long fwdFbOk = 0; // 反馈转发 pPowerManger 成功帧数
unsigned long fwdFbErr = 0; // 反馈转发失败帧数
unsigned long cmdCount = 0; // 收到配电指令帧数
unsigned long fwdCmdOk = 0; // 指令转发真实CCU 成功帧数
unsigned long fwdCmdErr = 0; // 指令转发失败帧数
};
//============================================================================
// SystemData:跨线程共享的最新状态快照。
//
@@ -138,6 +177,18 @@ public:
return m_canFrameCount;
}
//-------- 真实 CCU 桥接数据(配电协议,RCU 链路) --------
// 整体更新真实CCU 快照(DisBridge 在接收线程先取副本->修改->写回)
void updateRealCcu(const RealCcuState& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_realCcu = v;
}
RealCcuState realCcu() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_realCcu;
}
private:
mutable std::mutex m_mutex;
FcStatusValue m_fcStatus;
@@ -147,6 +198,7 @@ private:
PmStatusValue m_pmStatus;
PmParamSetFbValue m_pmParamFb;
BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1
RealCcuState m_realCcu;
unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0;
+15
View File
@@ -4,7 +4,13 @@
// 链路拓扑:
// 燃料电池控制器(FC) 192.168.1.162:7000(UDP)
// 控制主机(pPowerManger) 192.168.0.140:5001(UDP)
// 真实CCU设备(rcu) 192.168.0.200:7000(UDP,配电桥接,占位地址需按实际修改)
// 锂电池 BMS CAN 总线(经 pCanBridge 订阅 CAN_0x* 透传)
//
// 配电桥接(rcu_enable = true 时启用):
// pPowerManger 下发的配电指令(0x0001~0x0004) 经 pCCU 原帧转发真实CCU;
// 真实CCU 上报的配电反馈(0x0005~0x0008) 经 pCCU 原帧转发 pPowerManger,
// 同时解码显示在网页"真实CCU"页签;真实CCU 状态报文(0x0004)仅显示不转发。
//============================================================================
ProcessConfig = pCCU
@@ -26,6 +32,15 @@ ProcessConfig = pCCU
pm_remote_ip = 192.168.0.140
pm_remote_port = 5001
//======== 真实CCU 桥接链路(配电协议) ========
// 是否启用桥接(关闭时 pCCU 行为与纯模拟版一致)
rcu_enable = true
// 本机接收真实CCU 数据的端口
rcu_local_port = 7100
// 真实CCU 地址(转发配电指令目标;占位地址,按实际设备修改)
rcu_remote_ip = 192.168.0.200
rcu_remote_port = 7000
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
// 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。
+7
View File
@@ -12,6 +12,7 @@ namespace ccu {
//
// 协议联调阶段校验和的实现可能调整,因此对每条消息独立配置策略:
// - None : 无校验和字段(如 FC 状态帧文档未列校验行)
// - Sum8 : uint8 字节和(配电协议,取字节和低 8 位)
// - Sum32 : uint32 字节和,从域起始符到校验和之前所有数据的字节和
// - Sum16 : uint16 字节和(预留,备用)
// 新增策略只需在此类扩展,不影响其它代码。
@@ -19,6 +20,7 @@ namespace ccu {
enum class ChecksumType {
None, // 无校验
Sum8, // uint8 字节和(配电协议)
Sum32, // uint32 字节和
Sum16, // uint16 字节和
};
@@ -32,6 +34,7 @@ public:
// 校验和字段占用的字节数;None 返回 0
size_t size() const {
switch (m_type) {
case ChecksumType::Sum8: return 1;
case ChecksumType::Sum32: return 4;
case ChecksumType::Sum16: return 2;
case ChecksumType::None: return 0;
@@ -60,6 +63,10 @@ public:
uint32_t actual = 0;
size_t offset = frame.size() - cs;
if (m_type == ChecksumType::Sum8) {
actual = static_cast<uint32_t>(frame[offset]);
return (expect & 0xFF) == actual;
}
if (m_type == ChecksumType::Sum32) {
actual = static_cast<uint32_t>(frame[offset]) |
(static_cast<uint32_t>(frame[offset + 1]) << 8) |
+155
View File
@@ -0,0 +1,155 @@
#include "DisProtocol.h"
#include "Frame.h"
#include "MessageRegistry.h"
#include <cstring>
namespace ccu {
//============================================================================
// 结构体尺寸核对:pPowerManger 按同结构裸解析收帧,布局必须与文档一致;
// 若 pmSysvariable.h 结构调整导致尺寸变化,此处编译期报错提醒同步。
//============================================================================
static_assert(sizeof(disHighVolBusCmd) == 14, "disHighVolBusCmd 尺寸与协议不符");
static_assert(sizeof(disHighAVolBusCmd) == 6, "disHighAVolBusCmd 尺寸与协议不符");
static_assert(sizeof(disLowMainBusCmd) == 7, "disLowMainBusCmd 尺寸与协议不符");
static_assert(sizeof(disLowBusCmd) == 8, "disLowBusCmd 尺寸与协议不符");
static_assert(sizeof(disHighVolBusState) == 53, "disHighVolBusState 尺寸与协议不符");
static_assert(sizeof(disHighAVolBusState) == 25, "disHighAVolBusState 尺寸与协议不符");
static_assert(sizeof(disLowMainBusState) == 34, "disLowMainBusState 尺寸与协议不符");
static_assert(sizeof(disLowBusState) == 29, "disLowBusState 尺寸与协议不符");
//============================================================================
// 通用编解码:配电结构体为 #pragma pack(1) 的 POD,与线上字节布局一致
//(pPowerManger 亦按同结构裸解析),因此直接按字节拷贝。
//============================================================================
namespace {
std::vector<uint8_t> encodePacked(uint16_t id, const void* obj, size_t size,
const ChecksumPolicy& policy) {
std::vector<uint8_t> p(size, 0);
std::memcpy(p.data(), obj, size);
return Frame::build(id, p, policy);
}
bool decodePacked(const std::vector<uint8_t>& frame, uint16_t id,
const ChecksumPolicy& policy, size_t size, void* obj) {
// 注意:不依赖帧头 length 字段(pPowerManger 发送的配电指令该字段
// 与总长不符),validateFrame 只要实际帧长足够即放行,校验和兜底。
if (!Message::validateFrame(frame, id, policy,
FRAME_HEADER_LEN + size + policy.size()))
return false;
std::memcpy(obj, frame.data() + FRAME_HEADER_LEN, size);
return true;
}
} // namespace
//--------------------------------------------------------------------------
// 指令消息编解码
//--------------------------------------------------------------------------
std::vector<uint8_t> DisHighVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighVolBusCmd), m_checksum);
}
bool DisHighVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusCmd), obj);
}
std::vector<uint8_t> DisHighAVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighAVolBusCmd), m_checksum);
}
bool DisHighAVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusCmd), obj);
}
std::vector<uint8_t> DisHighBVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowMainBusCmd), m_checksum);
}
bool DisHighBVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusCmd), obj);
}
std::vector<uint8_t> DisLowBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowBusCmd), m_checksum);
}
bool DisLowBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowBusCmd), obj);
}
//--------------------------------------------------------------------------
// 反馈消息编解码
//--------------------------------------------------------------------------
std::vector<uint8_t> DisHighVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighVolBusState), m_checksum);
}
bool DisHighVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusState), obj);
}
std::vector<uint8_t> DisHighAVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighAVolBusState), m_checksum);
}
bool DisHighAVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusState), obj);
}
std::vector<uint8_t> DisHighBVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowMainBusState), m_checksum);
}
bool DisHighBVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusState), obj);
}
std::vector<uint8_t> DisLowBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowBusState), m_checksum);
}
bool DisLowBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowBusState), obj);
}
//--------------------------------------------------------------------------
// 消息类型判断(供桥接器/上层分发使用)
//--------------------------------------------------------------------------
bool isDisMessage(const Message* msg) {
return isDisCmdMessage(msg) || isDisFbMessage(msg);
}
bool isDisCmdMessage(const Message* msg) {
if (!msg) return false;
return dynamic_cast<const DisHighVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighAVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighBVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisLowBusCmdMessage*>(msg) != nullptr;
}
bool isDisFbMessage(const Message* msg) {
if (!msg) return false;
return dynamic_cast<const DisHighVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighAVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighBVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisLowBusFbMessage*>(msg) != nullptr;
}
//--------------------------------------------------------------------------
// 注册
//--------------------------------------------------------------------------
void registerDisCmdMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new DisHighVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighAVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighBVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisLowBusCmdMessage()));
}
void registerDisFbMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new DisHighVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighAVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighBVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisLowBusFbMessage()));
}
} // namespace ccu
+115
View File
@@ -0,0 +1,115 @@
#ifndef PCCU_DIS_PROTOCOL_H
#define PCCU_DIS_PROTOCOL_H
#include <cstdint>
#include <vector>
#include "Message.h"
#include "pmSysvariable.h"
namespace ccu {
//============================================================================
// 配电协议(配电控制器 <-> 复合管控器,经 CCU 在 PM 链路与真实 CCU 链路间桥接)
// 依据:docs/配电控制器与复合管控器的通信协议20260824.docx
// 数据结构复用 src/pPowerManger/pmSysvariable.h(pPowerManger 按同结构
// 裸解析收帧,复用可保证两侧 sizeof/字段布局完全一致)。
//
// 帧格式:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + uint8 校验和
// 校验和 = 从域起始符到校验和之前所有数据的字节和低 8 位(Sum8)。
//
// 注意(重要):
// 配电指令 0x0001~0x0004 与 PM 协议操控/参数指令共用 id,仅能靠
// 帧总长区分(配电指令 13~21B,PM 指令 28/45B)。且 pPowerManger 发送
// 配电指令时帧头 length 字段并非总长(见 udpComm.cpp TODO),
// 分发必须用实际帧长(见 MessageRegistry::find(id, totalLength))。
//
// 八条消息(均 Sum8 校验):
// 指令(pPowerManger -> CCU -> 真实CCU) payload 总长
// 0x0001 高压母线配电指令 disHighVolBusCmd 14B 21B
// 0x0002 高压母线A配电指令 disHighAVolBusCmd 6B 13B
// 0x0003 低压主母线配电指令 disLowMainBusCmd 7B 14B
// 0x0004 仪表汇流排配电指令 disLowBusCmd 8B 15B
// 反馈(真实CCU -> CCU -> pPowerManger)
// 0x0005 高压母线配电反馈 disHighVolBusState 53B 60B
// 0x0006 高压母线A配电反馈 disHighAVolBusState 25B 32B
// 0x0007 低压主母线配电反馈 disLowMainBusState 34B 41B
// 0x0008 仪表汇流排配电反馈 disLowBusState 29B 36B
// (payload/总长由 static_assert 在编译期核对,见 DisProtocol.cpp)
//============================================================================
enum : uint16_t {
DIS_HV_CMD_ID = 0x0001, // 高压母线配电指令
DIS_HVA_CMD_ID = 0x0002, // 高压母线A配电指令
DIS_HVB_CMD_ID = 0x0003, // 低压主母线配电指令
DIS_LV_CMD_ID = 0x0004, // 仪表汇流排配电指令
DIS_HV_FB_ID = 0x0005, // 高压母线配电反馈
DIS_HVA_FB_ID = 0x0006, // 高压母线A配电反馈
DIS_HVB_FB_ID = 0x0007, // 低压主母线配电反馈
DIS_LV_FB_ID = 0x0008, // 仪表汇流排配电反馈
};
// 断路器/开关类指令与状态值
enum : uint8_t {
DIS_BREAKER_ON = 0x55, // 闭合
DIS_BREAKER_OFF = 0xAA, // 断开
DIS_BREAKER_FLT = 0x5A, // 故障脱扣
};
// 判断是否配电协议消息(指令或反馈),桥接器据此识别
bool isDisMessage(const Message* msg);
bool isDisCmdMessage(const Message* msg);
bool isDisFbMessage(const Message* msg);
//--------------------------------------------------------------------------
// 指令消息(pPowerManger 下发,pCCU 原帧转发给真实CCU)
//--------------------------------------------------------------------------
#define PCCU_DIS_MSG_CLASS(clsName, msgId, msgName, dataT) \
class clsName : public Message { \
public: \
uint16_t id() const override { return msgId; } \
const char* name() const override { return msgName; } \
const ChecksumPolicy& checksum() const override { return m_checksum; } \
size_t payloadLength() const override { return sizeof(dataT); } \
std::vector<uint8_t> encode(const void* obj) const override; \
bool decode(const std::vector<uint8_t>& frame, void* obj) const override; \
std::vector<uint8_t> encode(const dataT& v) const { return encode(&v); } \
bool decode(const std::vector<uint8_t>& frame, dataT& v) const { \
return decode(frame, static_cast<void*>(&v)); \
} \
private: \
ChecksumPolicy m_checksum{ChecksumType::Sum8}; \
}
// 高压母线配电指令(0x0001)
PCCU_DIS_MSG_CLASS(DisHighVolBusCmdMessage, DIS_HV_CMD_ID, "dis_hv_cmd", disHighVolBusCmd);
// 高压母线A配电指令(0x0002)
PCCU_DIS_MSG_CLASS(DisHighAVolBusCmdMessage, DIS_HVA_CMD_ID, "dis_hva_cmd", disHighAVolBusCmd);
// 低压主母线配电指令(0x0003)
PCCU_DIS_MSG_CLASS(DisHighBVolBusCmdMessage, DIS_HVB_CMD_ID, "dis_hvb_cmd", disLowMainBusCmd);
// 仪表汇流排配电指令(0x0004)
PCCU_DIS_MSG_CLASS(DisLowBusCmdMessage, DIS_LV_CMD_ID, "dis_lv_cmd", disLowBusCmd);
//--------------------------------------------------------------------------
// 反馈消息(真实CCU 上报,pCCU 原帧转发给 pPowerManger 并解码显示)
//--------------------------------------------------------------------------
// 高压母线配电反馈(0x0005)
PCCU_DIS_MSG_CLASS(DisHighVolBusFbMessage, DIS_HV_FB_ID, "dis_hv_fb", disHighVolBusState);
// 高压母线A配电反馈(0x0006)
PCCU_DIS_MSG_CLASS(DisHighAVolBusFbMessage, DIS_HVA_FB_ID, "dis_hva_fb", disHighAVolBusState);
// 低压主母线配电反馈(0x0007)
PCCU_DIS_MSG_CLASS(DisHighBVolBusFbMessage, DIS_HVB_FB_ID, "dis_hvb_fb", disLowMainBusState);
// 仪表汇流排配电反馈(0x0008)
PCCU_DIS_MSG_CLASS(DisLowBusFbMessage, DIS_LV_FB_ID, "dis_lv_fb", disLowBusState);
#undef PCCU_DIS_MSG_CLASS
// 注册配电指令消息(PM 链路用:识别 pPowerManger 下发的配电指令)
void registerDisCmdMessages(class MessageRegistry& reg);
// 注册配电反馈消息(RCU 链路用:识别真实CCU 上报的配电反馈)
void registerDisFbMessages(class MessageRegistry& reg);
} // namespace ccu
#endif // PCCU_DIS_PROTOCOL_H
+3 -2
View File
@@ -41,8 +41,9 @@ std::vector<uint8_t> Frame::build(
std::vector<uint8_t> body(frame.begin(), frame.end() - csSize);
uint32_t sum = checksum.compute(body);
size_t off = frame.size() - csSize;
if (csSize == 4) FieldCodec::putU32(frame, off, sum);
else FieldCodec::putU16(frame, off, static_cast<uint16_t>(sum));
if (csSize == 4) FieldCodec::putU32(frame, off, sum);
else if (csSize == 1) frame[off] = static_cast<uint8_t>(sum & 0xFF);
else FieldCodec::putU16(frame, off, static_cast<uint16_t>(sum));
}
return frame;
}
+15 -2
View File
@@ -5,8 +5,12 @@ namespace ccu {
bool MessageRegistry::registerMessage(std::unique_ptr<Message> msg) {
if (!msg) return false;
uint16_t id = msg->id();
if (m_msgs.count(id) != 0) return false; // id 冲突
m_msgs[id] = std::move(msg);
// 同 id 且同总长的重复注册视为冲突;同 id 不同总长为合法
//(配电指令与 PM 指令共用 id,仅帧长不同)
for (auto it = m_msgs.lower_bound(id); it != m_msgs.upper_bound(id); ++it) {
if (it->second->totalLength() == msg->totalLength()) return false;
}
m_msgs.insert(std::make_pair(id, std::move(msg)));
return true;
}
@@ -15,6 +19,15 @@ Message* MessageRegistry::find(uint16_t id) const {
return (it != m_msgs.end()) ? it->second.get() : nullptr;
}
Message* MessageRegistry::find(uint16_t id, size_t totalLength) const {
Message* first = nullptr;
for (auto it = m_msgs.lower_bound(id); it != m_msgs.upper_bound(id); ++it) {
if (it->second->totalLength() == totalLength) return it->second.get();
if (!first) first = it->second.get();
}
return first; // 总长未命中时回退该 id 的第一条
}
size_t MessageRegistry::size() const {
return m_msgs.size();
}
+14 -5
View File
@@ -2,6 +2,7 @@
#define PCCU_MESSAGE_REGISTRY_H
#include <cstdint>
#include <cstddef>
#include <map>
#include <memory>
#include <vector>
@@ -12,29 +13,37 @@ namespace ccu {
//============================================================================
// MessageRegistry:消息注册表与收发分发中心。
//
// 每条链路(FC 链路 / PM 链路)持有各自的注册表实例。
// 每条链路(FC 链路 / PM 链路 / RCU 链路)持有各自的注册表实例。
// 由于 FC 协议与 PM 协议的域标识符存在重叠(0x0001/0x0002),
// 分属不同注册表可完全隔离,互不干扰。
//
// 同 id 多消息:配电指令 0x0001~0x0004 与 PM 协议操控/参数指令共用 id,
// 仅帧总长不同,因此注册表允许同 id 多条消息,并支持按总长精确查找:
// find(id, totalLength) 精确匹配(收帧分发用,未命中回退 find(id))
// find(id) 返回该 id 的第一条(兼容旧接口)
//
// 协议新增/调整消息时只需 register() 对应 Message 实现,上层无需改动。
//============================================================================
class MessageRegistry {
public:
// 注册一条消息;返回是否成功(id 冲突时返回 false)
// 注册一条消息;返回是否成功(同 id 且同总长的重复注册返回 false)
bool registerMessage(std::unique_ptr<Message> msg);
// 按 id 查找消息;未注册返回 nullptr
// 按 id 查找消息(同 id 多条时返回第一条);未注册返回 nullptr
Message* find(uint16_t id) const;
// 按 id + 帧总长精确查找;未命中时回退为 find(id)
Message* find(uint16_t id, size_t totalLength) const;
// 已注册消息数量
size_t size() const;
// 所有已注册消息(按 id 升序)
// 所有已注册消息(按 id 升序,同 id 按注册顺序)
std::vector<Message*> all() const;
private:
std::map<uint16_t, std::unique_ptr<Message>> m_msgs;
std::multimap<uint16_t, std::unique_ptr<Message>> m_msgs;
};
} // namespace ccu
+216 -1
View File
@@ -86,6 +86,7 @@ th{color:var(--dim);font-weight:500;}
</div>
<div class="tab-group"><span class="tg-label">系统</span>
<button class="tab" data-tab="links">链路状态</button>
<button class="tab" data-tab="realCcu">真实CCU</button>
<button class="tab" data-tab="raw">原始报文</button>
</div>
</div>
@@ -95,7 +96,7 @@ th{color:var(--dim);font-weight:500;}
const $ = id => document.getElementById(id);
const pagesEl = $('pages');
const statusEl = $('status');
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','raw'];
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','realCcu','raw'];
let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
@@ -154,6 +155,7 @@ function bmsOnline(n){
}
// 最后更新时间文本(a:距最后收到报文的秒数)
function ageText(a){
if(typeof a !== 'number' || isNaN(a)) return '—';
if(a < 90) return a.toFixed(1)+' 秒前';
if(a < 3600) return Math.floor(a/60)+' 分 '+Math.floor(a%60)+' 秒前';
return Math.floor(a/3600)+' 时 '+Math.floor((a%3600)/60)+' 分前';
@@ -323,6 +325,10 @@ function linkCard(s){
'收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err);
h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>',
'收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err);
if(l.rcu){
h+=row('<span><span class="status-dot '+dot(online(l.rcu.lastRx))+'"></span>RCU 链路 (真实CCU)</span>',
'收:'+l.rcu.rx+' 发:'+l.rcu.tx+' 误:'+l.rcu.err);
}
// CAN/BMS 行:任一 BCU 节点在线即视为链路在线
const bmsNodes = Array.isArray(s.bms) ? s.bms : [];
const bmsAnyOnline = bmsNodes.some(function(n){ return bmsOnline(n); });
@@ -490,6 +496,214 @@ function pmStatusCard(d){
return card('整合后状态', h);
}
//------------------ 真实 CCU(配电桥接) 卡片 ------------------
// 数据来源:RCU 链路。配电反馈(0x0005~0x0008)由 pCCU 原帧转发
// pPowerManger 并在此全字段展示;配电指令(0x0001~0x0004)由 pPowerManger
// 经 pCCU 原帧转发真实CCU。断路器/开关量:0x55 闭合 0xAA 断开 0x5A 故障
function brkText(v){
if(v===0x55) return '<span class="fc-ok">闭合</span>';
if(v===0xAA) return '断开';
if(v===0x5A) return '<span class="fc-bad">故障脱扣</span>';
return hex(v);
}
// 双值信号(0x55 正常 / 0xAA 异常)
function sigText(v){
if(v===0x55) return '<span class="fc-ok">正常</span>';
if(v===0xAA) return '<span class="fc-bad">异常</span>';
return hex(v);
}
function insStatText(v){ return v===0x55 ? '<span class="fc-ok">正常</span>'
: (v===0xAA ? '<span class="fc-bad">绝缘低</span>' : hex(v)); }
// age:距最后收到该类帧的秒数;valid:是否收到过(未收到显示"从未收到")
function ageCell(age, valid){
if(!valid || typeof age !== 'number' || isNaN(age))
return '<span style="color:var(--dim)">从未收到</span>';
const online = age >= 0 && age < 5;
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">') + ageText(age)
+ (online ? ' (在线)' : ' (超时)') + '</span>';
}
function fwdLine(okN, errN){
return okN + ' 成功' + (errN ? ' <span class="fc-bad">'+errN+' 失败</span>' : '');
}
// 母线卡片通用:breakers 子表 + 信号/电气量行(无数据时只显示概要行)
function busCard(title, bus, age, rows){
let h='';
const b = bus && bus.breakers ? bus.breakers : {};
const keys = Object.keys(bNamesDis).filter(function(k){ return b[k] !== undefined; });
if(!keys.length){
h+=sub('断路器状态');
h+=row('状态','暂无数据');
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
h+=sub('断路器状态');
for(const k of keys) h+=row(bNamesDis[k], brkText(b[k]));
for(const r of rows) h+=row(r[0], r[1]);
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
// 母线断路器中文名表(busCard 共用)
const bNamesDis = {
fuelCell:'燃料电池断路器', powerLithiumBattery:'动力锂电池断路器',
propulsionMotor:'推进电机断路器', lithiumBatteryGroupInstrument:'锂电池组(仪表)断路器',
dcDc5Module:'DC/DC5模块断路器', bowHighVoltageDistributionBox:'艏部高压配电箱断路器',
sternFTDevice45:'艉部FT装置4/5断路器', fbReserved:'FB预留断路器',
sternRudderSwitch1:'艉舵开关1断路器', sternRudderSwitch2:'艉舵开关2断路器',
tyzReserved:'TYZ预留断路器', reserved:'预留断路器',
bowFTDevice123:'艏部FT装置1/2/3断路器', actuator:'启闭机构断路器',
mastSteeringGearControlBox:'桅杆舵机控制箱断路器', xcz:'XCZ断路器',
bowRudderControlBox:'艏舵控制箱断路器', openWaterCoverStartCylinder:'敞水盖启动电缸断路器',
bowLowVoltageDistributionBox:'艏部低压配电箱断路器', unit4InstrumentDC48V:'UNIT4仪表DC48V断路器',
dcC1DCDistributionPanel:'DC-C1直流配电板断路器', reserved1:'预留断路器1', reserved2:'预留断路器2',
emergencyLithiumBatteryGroup2:'应急锂电池组2断路器',
unit1:'UNIT1断路器', unit2:'UNIT2断路器', unit3:'UNIT3断路器', unit5:'UNIT5断路器',
bowPZDevice:'艏部PZ装置断路器', emergencyLithiumBatteryGroup1:'应急锂电池组1断路器',
dcdc5ModuleStart:'DC/DC5模块启动', coolingSystemStart:'冷却系统启动'
};
function realCcuPage(d){
if(!d) return '<div class="card"><h2>真实CCU</h2><div class="row"><span class="k">数据状态</span><span class="v">等待数据...</span></div></div>';
let h='<div class="page-hdr"><span class="raw-id">RCU</span> <span class="raw-name">真实CCU 配电桥接</span> '+
'<span class="badge ok">原帧透传</span></div><div class="grid">';
// 桥接概览卡片
{
let c='';
const fwd = d.forward || {};
c+=row('配电反馈收帧', fwd.fbCount || 0);
c+=row('反馈转发PM', fwdLine(fwd.fwdFbOk || 0, fwd.fwdFbErr || 0));
c+=row('配电指令收帧', fwd.cmdCount || 0);
c+=row('指令转发真实CCU', fwdLine(fwd.fwdCmdOk || 0, fwd.fwdCmdErr || 0));
const ccu = d.ccuStatus || {};
c+=sub('真实CCU 状态报文(仅显示,不转发)');
c+=row('在线', ageCell(ccu.age, ccu.valid));
if(ccu.valid){
c+=row('运行模式', modeText(ccu.fc_mode));
c+=row('运行状态', statusText(ccu.fc_status));
c+=row('系统故障等级', faultLvlBadge(ccu.fc_fault_level));
c+=row('系统发电功率', (ccu.generation_power*0.01).toFixed(2)+' kW');
c+=row('仪表/动力SOC', ccu.ins_soc+'% / '+ccu.dyn_soc+'%');
c+=row('通信心跳', ccu.heartbeat);
}
h+=card('桥接概览', c);
}
// 高压母线(0x0005 反馈 / 0x0001 指令)
{
const hv = d.hv || {};
const rows = [
['DC/DC5故障字低', hex(hv.dcDcFaultWord1)],
['DC/DC5故障字高', hex(hv.dcDcFaultWord2)],
['动力母排绝缘状态', insStatText(hv.powerBusInsulationStatus)],
['A汇流排绝缘状态', insStatText(hv.aBusInsulationStatus)],
['仪表电源状态', sigText(hv.meterPowerLossSignal)],
['应急电源状态', sigText(hv.emergencyPowerLossSignal)],
['DC/DC温度', hv.dcDcTemperature],
['动力汇流排电压', hv.powerBusVoltage],
['DC/DC5模块电流', hv.dcDc5ModuleCurrent],
['动力汇流排电流', hv.powerBusCurrent],
['A汇流排电压', hv.busbarAVoltage],
['推进电机控制箱电流', hv.propulsionMotorControlBoxCurrent],
['A汇流排电流', hv.busbarACurrent],
['锂电池组(仪表)电流', hv.lithiumBatteryGroupMeterCurrent],
['艏部高压配电箱电流', hv.bowHighVoltageDistributionBoxCurrent],
['艉部FT装置4/5电流', hv.sternFTDevice45Current],
['TYZ预留开关电流', hv.tyzReservedSwitchCurrent],
['艉舵开关1电流', hv.sternRudderSwitch1Current],
['预留电流1', hv.reservedCurrent1],
['艉舵开关2电流', hv.sternRudderSwitch2Current],
['预留电流2', hv.reservedCurrent2],
['FB预留电流', hv.fbReservedSwitchCurrent],
['预留电流3', hv.reservedCurrent3],
['冷却水压力', hv.coolWaterPressure],
];
h+=busCard('高压母线 (0x0005 反馈 / 0x0001 指令)', hv, d.hvAge, rows);
}
// 高压母线A(0x0006 反馈 / 0x0002 指令)
{
const b = d.hva || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['汇流排B电压', b.busbarBVoltage],
['汇流排B电流', b.busbarBCurrent],
['艏部FT装置1/2/3电流', b.bowFTDevice123Current],
['启闭机构电流', b.actuatorCurrent],
['桅杆舵机控制箱电流', b.mastSteeringGearControlBoxCurrent],
['XCZ电流', b.xczCurrent],
['艏舵控制箱电流', b.bowRudderControlBoxCurrent],
['敞水盖启动电缸电流', b.openWaterCoverStartCylinderCurrent],
];
h+=busCard('高压母线A (0x0006 反馈 / 0x0002 指令)', b, d.hvaAge, rows);
}
// 低压主母线(0x0007 反馈 / 0x0003 指令)
{
const b = d.hvb || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['仪表汇流排绝缘', insStatText(b.instrumentBusbarInsulationLow)],
['仪表母排电压', b.instrumentBusbarVoltage],
['艏部低压配电箱电流', b.bowLowVoltageDistributionBoxCurrent],
['DC-C1仪表DC48V电流', b.dcC1InstrumentDC48VCurrent],
['预留电流1', b.reservedCurrent1],
['应急锂电池组2电流', b.emergencyLithiumBatteryGroup2Current],
['锂电池组(仪表)电流', b.lithiumBatteryGroupInstrumentCurrent],
['UNIT4仪表DC48V电流', b.unit4InstrumentDC48VCurrent],
['预留电流2', b.reservedCurrent2],
['应急2汇流排电压', b.emergency2BusbarVoltage],
['复合能源应急DC48V电流', b.compositeEnergyEmergencyDC48VCurrent],
['燃料电池安全应急DC48V电流', b.fuelCellSecurityEmergencyDC48VCurrent],
['PLC控制电源电流', b.plcControlPowerCurrent],
];
h+=busCard('低压主母线 (0x0007 反馈 / 0x0003 指令)', b, d.hvbAge, rows);
}
// 仪表汇流排(0x0008 反馈 / 0x0004 指令)
{
const b = d.lv || {};
const rows = [
['仪表电源失电信号', sigText(b.powerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['仪表母排电压', b.instrumentBusbarVoltage],
['仪表母排电流', b.instrumentBusbarCurrent],
['UNIT1电流', b.unit1Current],
['UNIT2电流', b.unit2Current],
['UNIT3电流', b.unit3Current],
['UNIT5电流', b.unit5Current],
['艏部PZ装置电流', b.bowPZDeviceCurrent],
['应急锂电池组1电流', b.emergencyLithiumBatteryGroup1Current],
['应急1汇流排电压', b.emergency1BusbarVoltage],
];
h+=busCard('仪表汇流排 (0x0008 反馈 / 0x0004 指令)', b, d.lvAge, rows);
}
// 最近下发指令卡片
{
const lc = d.lastCmd || {};
let c='';
c+=row('最后指令时间', ageCell(lc.age, lc.valid));
const cmds = lc.cmds || {};
const groups = [['hv','高压母线'],['hva','高压母线A'],['hvb','低压主母线'],['lv','仪表汇流排']];
for(const g of groups){
const gb = cmds[g[0]] || {};
const keys = Object.keys(gb);
if(!keys.length) continue;
c+=sub(g[1]+' (0x'+({hv:'0001',hva:'0002',hvb:'0003',lv:'0004'})[g[0]]+')');
for(const k of keys) c+=row(bNamesDis[k]||k, brkText(gb[k]));
}
h+=card('最近下发的配电指令 (PM→真实CCU)', c);
}
h+='</div>';
return h;
}
//------------------ 标签页头部 ------------------
function pageHeader(id, name, dir){
const idHex = '0x'+id.toString(16).toUpperCase().padStart(4,'0');
@@ -559,6 +773,7 @@ function render(snap){
pmStatusCard(snap.pmStatus)+batteryCard(snap.pmStatus)+emergCard(snap.pmStatus)+'</div>',
pmFb: pageHeader(0x0003,'PM参数反馈','CCU→PM')+'<div class="grid">'+pmFbCard(snap.pmFb)+'</div>',
links: '<div class="grid">'+linkCard(snap)+'</div>',
realCcu: realCcuPage(snap.realCcu),
raw: rawCard(snap.logs)
};
+1
View File
@@ -137,6 +137,7 @@ add_executable(pccuTest
${CCU_DIR}/protocol/MessageRegistry.cpp
${CCU_DIR}/protocol/FcProtocol.cpp
${CCU_DIR}/protocol/PmProtocol.cpp
${CCU_DIR}/protocol/DisProtocol.cpp
${CCU_DIR}/protocol/CanBms.cpp
${CCU_DIR}/comm/UdpEndpoint.cpp
${CCU_DIR}/comm/LinkManager.cpp
+134
View File
@@ -15,10 +15,12 @@
#include "../../src/pCCU/protocol/MessageRegistry.h"
#include "../../src/pCCU/protocol/FcProtocol.h"
#include "../../src/pCCU/protocol/PmProtocol.h"
#include "../../src/pCCU/protocol/DisProtocol.h"
#include "../../src/pCCU/protocol/CanBms.h"
#include "../../src/pCCU/comm/LinkManager.h"
#include "../../src/pCCU/comm/FcLinkManager.h"
#include "../../src/pCCU/comm/PmLinkManager.h"
#include "../../src/pCCU/comm/RcuLinkManager.h"
#include "../../src/pCCU/store/DbStore.h"
using namespace ccu;
@@ -390,6 +392,134 @@ static void testCanBmsDecode() {
!isCanBcuId(0x100181FF) && !isCanBcuId(0x10370000));
}
//--------------------------------------------------------------------------
// 9. 配电协议帧长度与 Sum8 校验
static void testDisFrameLengths() {
DisHighVolBusCmdMessage hvCmd; CHECK(hvCmd.totalLength() == 21); // 6+14+1
DisHighAVolBusCmdMessage hvaCmd; CHECK(hvaCmd.totalLength() == 13); // 6+6+1
DisHighBVolBusCmdMessage hvbCmd; CHECK(hvbCmd.totalLength() == 14); // 6+7+1
DisLowBusCmdMessage lvCmd; CHECK(lvCmd.totalLength() == 15); // 6+8+1
DisHighVolBusFbMessage hvFb; CHECK(hvFb.totalLength() == 60); // 6+53+1
DisHighAVolBusFbMessage hvaFb; CHECK(hvaFb.totalLength() == 32); // 6+25+1
DisHighBVolBusFbMessage hvbFb; CHECK(hvbFb.totalLength() == 41); // 6+34+1
DisLowBusFbMessage lvFb; CHECK(lvFb.totalLength() == 36); // 6+29+1
}
//--------------------------------------------------------------------------
// 10. 配电指令/反馈编解码往返(Sum8 校验,篡改检测)
static void testDisRoundTrip() {
// 指令:高压母线(含启动类字节)
disHighVolBusCmd hv;
hv.fuelCellCircuitBreaker = DIS_BREAKER_ON;
hv.powerLithiumBatteryCircuitBreaker = DIS_BREAKER_OFF;
hv.dcdc5Module = 1; hv.coolingSystem = 0;
DisHighVolBusCmdMessage hvMsg;
std::vector<uint8_t> hf = hvMsg.encode(hv);
CHECK(hf.size() == 21);
// Sum8 校验和位于帧尾 1 字节
unsigned sum = 0;
for (size_t i = 0; i + 1 < hf.size(); ++i) sum += hf[i];
CHECK(hf.back() == (sum & 0xFF));
disHighVolBusCmd hvOut;
CHECK(hvMsg.decode(hf, static_cast<void*>(&hvOut)));
CHECK(hvOut.fuelCellCircuitBreaker == DIS_BREAKER_ON);
CHECK(hvOut.powerLithiumBatteryCircuitBreaker == DIS_BREAKER_OFF);
CHECK(hvOut.dcdc5Module == 1 && hvOut.coolingSystem == 0);
// 篡改数据域 -> Sum8 失败
std::vector<uint8_t> bad = hf; bad[8] ^= 0xFF;
CHECK(!hvMsg.decode(bad, static_cast<void*>(&hvOut)));
// 指令:低压主母线 / 仪表汇流排
disLowMainBusCmd hvb; hvb.dcC1DCDistributionPanelCircuitBreaker = DIS_BREAKER_ON;
DisHighBVolBusCmdMessage hvbMsg;
std::vector<uint8_t> bf = hvbMsg.encode(hvb);
CHECK(bf.size() == 14);
disLowMainBusCmd hvbOut;
CHECK(hvbMsg.decode(bf, static_cast<void*>(&hvbOut)));
CHECK(hvbOut.dcC1DCDistributionPanelCircuitBreaker == DIS_BREAKER_ON);
disLowBusCmd lv; lv.unit2CircuitBreaker = DIS_BREAKER_ON;
DisLowBusCmdMessage lvMsg;
std::vector<uint8_t> lf = lvMsg.encode(lv);
CHECK(lf.size() == 15);
disLowBusCmd lvOut;
CHECK(lvMsg.decode(lf, static_cast<void*>(&lvOut)));
CHECK(lvOut.unit2CircuitBreaker == DIS_BREAKER_ON);
// 反馈:高压母线A(断路器+u16 电气量抽查)
disHighAVolBusState hva;
hva.bowFTDevice123CircuitBreaker = DIS_BREAKER_ON;
hva.busbarBVoltage = 0x1234; hva.bowFTDevice123Current = 567;
DisHighAVolBusFbMessage hvaMsg;
std::vector<uint8_t> af = hvaMsg.encode(hva);
CHECK(af.size() == 32);
disHighAVolBusState hvaOut;
CHECK(hvaMsg.decode(af, static_cast<void*>(&hvaOut)));
CHECK(hvaOut.bowFTDevice123CircuitBreaker == DIS_BREAKER_ON);
CHECK(hvaOut.busbarBVoltage == 0x1234);
CHECK(hvaOut.bowFTDevice123Current == 567);
CHECK(isDisCmdMessage(&hvMsg) && isDisFbMessage(&hvaMsg));
CHECK(!isDisMessage(nullptr));
}
//--------------------------------------------------------------------------
// 11. 注册表 (id+帧长) 二维分发:配电指令与 PM 指令共用 id 不串扰
static void testRegistryLengthDispatch() {
MessageRegistry reg;
registerPmMessages(reg); // 0x0001(45B) 0x0002(28B) 0x0003(12B) 0x0004(248B)
registerDisCmdMessages(reg); // 0x0001(21B) 0x0002(13B) 0x0003(14B) 0x0004(15B)
CHECK(reg.size() == 8);
// 精确长度命中
CHECK(reg.find(0x0001, 45)->name() == std::string("pm_control"));
CHECK(reg.find(0x0001, 21)->name() == std::string("dis_hv_cmd"));
CHECK(reg.find(0x0002, 28)->name() == std::string("pm_param_set"));
CHECK(reg.find(0x0002, 13)->name() == std::string("dis_hva_cmd"));
CHECK(reg.find(0x0004, 248)->name() == std::string("pm_status"));
CHECK(reg.find(0x0004, 15)->name() == std::string("dis_lv_cmd"));
// LinkManager 收帧分发:配电指令短帧 -> dis 消息;PM 长帧 -> pm 消息
LinkManager lm("test_pm", 0);
registerPmMessages(lm.registry());
registerDisCmdMessages(lm.registry());
std::string gotName;
lm.setOnMessage([&](Message* m, const std::vector<uint8_t>&) {
gotName = m ? m->name() : "";
});
disHighVolBusCmd hv; hv.fuelCellCircuitBreaker = DIS_BREAKER_ON;
DisHighVolBusCmdMessage hvMsg;
CHECK(lm.handleIncoming(hvMsg.encode(hv)));
CHECK(gotName == "dis_hv_cmd");
PmControlValue pc; pc.mode = 2;
PmControlMessage pcMsg;
CHECK(lm.handleIncoming(pcMsg.encode(pc)));
CHECK(gotName == "pm_control");
}
//--------------------------------------------------------------------------
// 12. RCU 链路注册表:配电反馈 + 真实CCU 状态报文可识别
static void testRcuRegistry() {
RcuLinkManager rcu(0, "127.0.0.1", 1);
// 反馈命中
disHighVolBusState hv; hv.powerBusVoltage = 0x0FA0;
DisHighVolBusFbMessage hvMsg;
std::vector<uint8_t> f = hvMsg.encode(hv);
std::string gotName;
rcu.setOnMessage([&](Message* m, const std::vector<uint8_t>&) {
gotName = m ? m->name() : "";
});
CHECK(rcu.handleIncoming(f));
CHECK(gotName == "dis_hv_fb");
// 真实CCU 状态报文(PM 状态消息复用)
PmStatusValue st; st.heartbeat = 9;
PmStatusMessage stMsg;
CHECK(rcu.handleIncoming(stMsg.encode(st)));
CHECK(gotName == "pm_status");
}
//--------------------------------------------------------------------------
int main() {
testFrameLengths();
@@ -401,6 +531,10 @@ int main() {
testLinkDispatch();
testDbStore();
testCanBmsDecode();
testDisFrameLengths();
testDisRoundTrip();
testRegistryLengthDispatch();
testRcuRegistry();
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
+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)
+6
View File
@@ -16,6 +16,12 @@ ProcessConfig = pCCU
pm_remote_ip = 127.0.0.1
pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文)
// 真实CCU 桥接链路(配电协议透传)
rcu_enable = true
rcu_local_port = 16004 // 监听真实CCU 配电反馈(测试发送此端口)
rcu_remote_ip = 127.0.0.1
rcu_remote_port = 16005 // 真实CCU(测试监听此端口收转发的配电指令)
// 锂电池:CAN 总线 BMS 协议,经 pCanBridge 的 CAN_0x* 消息透传
// (集成测试不覆盖 CAN 链路,BMS 解码由 pccuTest 单元测试覆盖)