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 // CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000
ccuhost = 127.0.0.1 ccuhost = 127.0.0.1
ccuport = 7000 ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001 // 本地输入端口(接收 CCU 数据);默认 5001。
// iport = 5001 // 5001 让给 pCCU 的 rcu 链路接收真实CCU(192.168.0.140) 固定上报的
// 配电反馈,故本进程接收端口改为 5002(pCCU 转发的反馈与状态报文发往此处)
iport = 5002
} }
ProcessConfig = pPowerMangerHost ProcessConfig = pPowerMangerHost
@@ -48,11 +50,24 @@ ProcessConfig = pCCU
fc_remote_port = 7000 fc_remote_port = 7000
// PM 链路(监听 pPowerManger 指令;向 pPowerManger 周期发送 PM 状态) // PM 链路(监听 pPowerManger 指令;向 pPowerManger 周期发送 PM 状态)
// pPowerManger 与 pCCU 同机部署,pPowerManger 绑定 0.0.0.0:5001, // pPowerManger 与 pCCU 同机部署;pPowerManger 接收端口为 5002(iport),
// 故 PM 状态发送目标用回环地址即可送达。 // 故 PM 状态发送目标用 127.0.0.1:5002。
pm_local_port = 7000 pm_local_port = 7000
pm_remote_ip = 127.0.0.1 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), // CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
+76 -7
View File
@@ -17,8 +17,10 @@ CCU::CCU() {}
CCU::~CCU() { CCU::~CCU() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; } if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
if (m_coord) { delete m_coord; m_coord = 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_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = 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_db) { m_db->close(); delete m_db; m_db = nullptr; }
if (m_snap) { delete m_snap; m_snap = nullptr; } if (m_snap) { delete m_snap; m_snap = nullptr; }
if (m_sysData) { delete m_sysData; m_sysData = 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_local_port") m_pmLocalPort = atol(value.c_str());
else if (param == "pm_remote_ip") m_pmHost = value; else if (param == "pm_remote_ip") m_pmHost = value;
else if (param == "pm_remote_port") m_pmPort = atol(value.c_str()); 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 == "dbpath") m_dbPath = value;
else if (param == "logpath") m_logPath = value; else if (param == "logpath") m_logPath = value;
else if (param == "web_port") m_webPort = atoi(value.c_str()); else if (param == "web_port") m_webPort = atoi(value.c_str());
@@ -81,6 +87,9 @@ bool CCU::OnStartUp() {
// 系统数据快照 // 系统数据快照
m_sysData = new SystemData(); m_sysData = new SystemData();
// 配电桥接器(须在链路 start 前完成依赖注入,避免接收线程空指针)
m_bridge = new DisBridge();
// FC 链路 // FC 链路
m_fcLink = new FcLinkManager(m_fcLocalPort, m_fcHost, m_fcPort); m_fcLink = new FcLinkManager(m_fcLocalPort, m_fcHost, m_fcPort);
m_fcLink->setLogSink(m_db); m_fcLink->setLogSink(m_db);
@@ -90,19 +99,40 @@ bool CCU::OnStartUp() {
m_fcLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) { m_fcLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) {
// 原始帧已通过 LogSink 落库;如需 Web 实时原始报文可在此扩展 // 原始帧已通过 LogSink 落库;如需 Web 实时原始报文可在此扩展
}); });
if (!m_fcLink->start())
LOG_F(ERROR, "FC link start failed");
// PM 链路 // PM 链路
m_pmLink = new PmLinkManager(m_pmLocalPort, m_pmHost, m_pmPort); m_pmLink = new PmLinkManager(m_pmLocalPort, m_pmHost, m_pmPort);
m_pmLink->setLogSink(m_db); m_pmLink->setLogSink(m_db);
m_pmLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) { 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); handlePmMessage(msg, frame);
}); });
m_pmLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) { 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()) if (!m_pmLink->start())
LOG_F(ERROR, "PM link start failed"); 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* 消息透传, // 锂电池:CAN 总线(BMS 协议),数据经 pCanBridge 以 CAN_0x* 消息透传,
// 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。 // 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。
@@ -112,7 +142,7 @@ bool CCU::OnStartUp() {
m_coord->setup(m_sysData, m_fcLink, m_pmLink); 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) { 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, " if (m_rcuLink) {
"battery=BMS/CAN via pCanBridge(CAN_0x*)", LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
m_fcLocalPort, m_fcHost.c_str(), m_fcPort, "rcu_link local=%ld -> %s:%ld (配电桥接), "
m_pmLocalPort, m_pmHost.c_str(), m_pmPort); "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; return true;
} }
@@ -155,6 +195,7 @@ void CCU::registerVariables() {
// 运行状态变量 // 运行状态变量
Register("CCU_FC_LINK_STATE", 0); Register("CCU_FC_LINK_STATE", 0);
Register("CCU_PM_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) { void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return; if (!msg) return;
// 配电指令已在入口由 DisBridge 处理(见 OnStartUp 的 onMessage 接线),
// 到达此处的均为 PM 协议消息(Sum32 校验)
switch (msg->id()) { switch (msg->id()) {
case 0x0001: { // PM 操控指令 -> 转发给 FC case 0x0001: { // PM 操控指令 -> 转发给 FC
PmControlValue c; 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 状态报文发送 // sendPmStatus:整合最新 FC 状态,编码 PM 状态报文发送
@@ -476,6 +532,19 @@ bool CCU::buildReport() {
<< " tx:" << m_pmLink->txCount() << " tx:" << m_pmLink->txCount()
<< " err:" << m_pmLink->errorCount() << "\n"; << " 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) { if (m_sysData) {
m_msgs << "锂电池: BMS/CAN (经 pCanBridge CAN_0x* 订阅)\n"; m_msgs << "锂电池: BMS/CAN (经 pCanBridge CAN_0x* 订阅)\n";
m_msgs << "CAN 帧接收计数:" << m_sysData->canFrameCount() << "\n"; m_msgs << "CAN 帧接收计数:" << m_sysData->canFrameCount() << "\n";
+14
View File
@@ -5,9 +5,11 @@
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h" #include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "comm/FcLinkManager.h" #include "comm/FcLinkManager.h"
#include "comm/PmLinkManager.h" #include "comm/PmLinkManager.h"
#include "comm/RcuLinkManager.h"
#include "core/SystemData.h" #include "core/SystemData.h"
#include "core/SnapshotBuilder.h" #include "core/SnapshotBuilder.h"
#include "core/PowerCoordinator.h" #include "core/PowerCoordinator.h"
#include "core/DisBridge.h"
#include "store/DbStore.h" #include "store/DbStore.h"
#include "web/WebServer.h" #include "web/WebServer.h"
@@ -20,6 +22,9 @@ namespace ccu {
// 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger // 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger
// 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池 // 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池
// 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003) // 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003)
// 收 PM 配电指令(0x0001~0x0004,Sum8短帧) -> DisBridge 原帧转发真实CCU(rcu 链路)
// 收真实CCU 配电反馈(0x0005~0x0008) -> DisBridge 原帧转发 pPowerManger + 解码显示
// 收真实CCU 状态报文(0x0004) -> 仅解码显示,不转发(避免状态冲突)
// 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文 // 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文
// (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照 // (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照
// (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用) // (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用)
@@ -43,6 +48,7 @@ private:
// 链路收帧处理 // 链路收帧处理
void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame); void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame);
void handlePmMessage(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 发布) // MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布)
void handleCanMessage(CMOOSMsg& msg); void handleCanMessage(CMOOSMsg& msg);
@@ -63,6 +69,12 @@ private:
std::string m_pmHost = "192.168.0.140"; std::string m_pmHost = "192.168.0.140";
long m_pmPort = 5001; 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_dbPath = "pccu_data.db";
std::string m_logPath = "pCCU.log"; std::string m_logPath = "pCCU.log";
int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开 int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开
@@ -78,6 +90,8 @@ private:
// 组件 // 组件
FcLinkManager* m_fcLink = nullptr; FcLinkManager* m_fcLink = nullptr;
PmLinkManager* m_pmLink = nullptr; PmLinkManager* m_pmLink = nullptr;
RcuLinkManager* m_rcuLink = nullptr;
DisBridge* m_bridge = nullptr;
SystemData* m_sysData = nullptr; SystemData* m_sysData = nullptr;
DbStore* m_db = nullptr; DbStore* m_db = nullptr;
SnapshotBuilder* m_snap = 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(" It receives FC status, integrates & forwards to pPowerManger,");
blk(" maps pPowerManger commands to FC operations, receives CAN "); blk(" maps pPowerManger commands to FC operations, receives CAN ");
blk(" bus BMS battery frames via pCanBridge (CAN_0x* MOOS msgs), "); 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(" "); blk(" ");
} }
@@ -68,6 +70,10 @@ void showExampleConfigAndExit() {
blk(" pm_local_port = 7000 // PM 指令接收端口 "); blk(" pm_local_port = 7000 // PM 指令接收端口 ");
blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP "); blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP ");
blk(" pm_remote_port = 5001 // 控制主机接收端口 "); 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(" // 锂电池 BMS 走 CAN 总线(经 pCanBridge 的 CAN_0x* 透传) ");
blk(" dbpath = pccu_data.db // 数据库路径 "); blk(" dbpath = pccu_data.db // 数据库路径 ");
blk(" logpath = pCCU.log // 日志路径 "); blk(" logpath = pCCU.log // 日志路径 ");
@@ -91,11 +97,13 @@ void showInterfaceAndExit() {
blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(锂电池 BMS) "); blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(锂电池 BMS) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 "); blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 "); blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" "); blk(" ");
blk("PUBLICATIONS: "); blk("PUBLICATIONS: ");
blk("------------------------------------ "); blk("------------------------------------ ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 "); blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 "); blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" "); blk(" ");
exit(0); exit(0);
} }
+3 -1
View File
@@ -1,6 +1,6 @@
#-------------------------------------------------------- #--------------------------------------------------------
# The CMakeLists.txt for: pCCU # The CMakeLists.txt for: pCCU
# 复合管控器网关:FC <-> pPowerManger # 复合管控器网关:FC <-> pPowerManger,配电桥接 <-> 真实CCU
#-------------------------------------------------------- #--------------------------------------------------------
if (${WIN32}) if (${WIN32})
@@ -24,6 +24,7 @@ SET(CCU_PROTOCOL_SRC
protocol/MessageRegistry.cpp protocol/MessageRegistry.cpp
protocol/FcProtocol.cpp protocol/FcProtocol.cpp
protocol/PmProtocol.cpp protocol/PmProtocol.cpp
protocol/DisProtocol.cpp
protocol/CanBms.cpp protocol/CanBms.cpp
) )
@@ -36,6 +37,7 @@ SET(CCU_CORE_SRC
core/SnapshotBuilder.cpp core/SnapshotBuilder.cpp
core/PowerCoordinator.cpp core/PowerCoordinator.cpp
core/CoordFsm.cpp core/CoordFsm.cpp
core/DisBridge.cpp
) )
SET(CCU_STORE_SRC SET(CCU_STORE_SRC
+31 -2
View File
@@ -54,17 +54,46 @@ bool LinkManager::sendMessageTo(uint16_t id, const void* value,
return true; 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) { bool LinkManager::handleIncoming(const std::vector<uint8_t>& frame) {
if (frame.size() < FRAME_HEADER_LEN) return false; if (frame.size() < FRAME_HEADER_LEN) return false;
uint16_t id = 0; uint16_t id = 0;
if (!Frame::parseId(frame, id)) { if (!Frame::parseId(frame, id)) {
LOG_F(WARNING, "[%s] bad frame header", m_linkName.c_str()); // 非本协议帧(如真实CCU 混发的旧协议 0x20 0x20 状态帧、无效帧):
// 原样落库便于排查,告警按 10s 节流避免刷屏
++m_errorCount; ++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; 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) { if (!msg) {
LOG_F(WARNING, "[%s] unknown msg id 0x%04X, len=%zu", LOG_F(WARNING, "[%s] unknown msg id 0x%04X, len=%zu",
m_linkName.c_str(), id, frame.size()); m_linkName.c_str(), id, frame.size());
+8 -1
View File
@@ -48,7 +48,9 @@ public:
MessageRegistry& registry() { return m_registry; } MessageRegistry& registry() { return m_registry; }
const MessageRegistry& registry() const { 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) { void setOnMessage(std::function<void(Message* msg, const std::vector<uint8_t>& frame)> cb) {
m_onMessage = std::move(cb); m_onMessage = std::move(cb);
} }
@@ -68,6 +70,10 @@ public:
bool sendMessageTo(uint16_t id, const void* value, bool sendMessageTo(uint16_t id, const void* value,
const std::string& host, long port); const std::string& host, long port);
// 原样发送一帧到默认远端(桥接透传用,不做编码);
// 消息 id 从帧头解析,收发计数与落库口径与 sendMessage 一致
bool sendRaw(const std::vector<uint8_t>& frame);
// 处理一帧收到的原始数据(供接收线程调用,也可测试时手动喂入) // 处理一帧收到的原始数据(供接收线程调用,也可测试时手动喂入)
bool handleIncoming(const std::vector<uint8_t>& frame); bool handleIncoming(const std::vector<uint8_t>& frame);
@@ -102,6 +108,7 @@ private:
ILinkLogSink* m_logSink = nullptr; ILinkLogSink* m_logSink = nullptr;
double m_lastRxTime = 0; double m_lastRxTime = 0;
double m_lastBadHdrLog = 0; // 非本协议帧头告警节流
unsigned long m_rxCount = 0; unsigned long m_rxCount = 0;
unsigned long m_txCount = 0; unsigned long m_txCount = 0;
unsigned long m_errorCount = 0; unsigned long m_errorCount = 0;
+8
View File
@@ -3,6 +3,7 @@
#include "LinkManager.h" #include "LinkManager.h"
#include "../protocol/PmProtocol.h" #include "../protocol/PmProtocol.h"
#include "../protocol/DisProtocol.h"
namespace ccu { namespace ccu {
@@ -11,6 +12,12 @@ namespace ccu {
// //
// - 本地监听:控制主机指令端口(默认 7000) // - 本地监听:控制主机指令端口(默认 7000)
// - 发送目标:控制主机(默认 192.168.0.140:5001) // - 发送目标:控制主机(默认 192.168.0.140:5001)
//
// 注册的消息:
// - PM 协议 0x0001~0x0004(Sum32)
// - 配电指令 0x0001~0x0004(Sum8,13~21B 短帧):pPowerManger 下发的
// 配电指令与其操控/参数指令共用 id,仅帧长不同,由注册表按
// (id+帧长) 区分,DisBridge 据此原桥接转发真实CCU
// 端口/地址均可由 .moos 配置覆盖。 // 端口/地址均可由 .moos 配置覆盖。
//============================================================================ //============================================================================
@@ -20,6 +27,7 @@ public:
const std::string& pmHost, long pmPort) const std::string& pmHost, long pmPort)
: LinkManager("pm", localPort), m_pmHost(pmHost), m_pmPort(pmPort) { : LinkManager("pm", localPort), m_pmHost(pmHost), m_pmPort(pmPort) {
registerPmMessages(registry()); registerPmMessages(registry());
registerDisCmdMessages(registry());
} }
void setPmAddress(const std::string& host, long port) { 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) { bool UdpEndpoint::sendTo(const std::string& host, long port, const std::vector<uint8_t>& data) {
if (!m_socket || data.empty()) return false; if (!m_socket || data.empty()) return false;
try { try {
std::lock_guard<std::mutex> lock(m_sendMutex);
m_socket->iSendMessageTo(const_cast<uint8_t*>(data.data()), m_socket->iSendMessageTo(const_cast<uint8_t*>(data.data()),
static_cast<int>(data.size()), static_cast<int>(data.size()),
port, host); port, host);
+4
View File
@@ -4,6 +4,7 @@
#define UNIX #define UNIX
#include "MOOS/libMOOS/Comms/XPCUdpSocket.h" #include "MOOS/libMOOS/Comms/XPCUdpSocket.h"
#include <cstdint> #include <cstdint>
#include <mutex>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -17,6 +18,8 @@ namespace ccu {
// - 向指定远端 (host:port) 发送数据报 // - 向指定远端 (host:port) 发送数据报
// - 阻塞接收数据报 // - 阻塞接收数据报
// 不关心协议内容,仅负责字节的收发。 // 不关心协议内容,仅负责字节的收发。
// 发送可能来自多个线程(MOOS 主线程 + 各链路接收线程的桥接转发),
// sendTo 内部加锁串行化。
//============================================================================ //============================================================================
class UdpEndpoint { class UdpEndpoint {
@@ -39,6 +42,7 @@ public:
private: private:
XPCUdpSocket* m_socket = nullptr; XPCUdpSocket* m_socket = nullptr;
long m_localPort = 0; long m_localPort = 0;
std::mutex m_sendMutex; // 串行化并发 sendTo
}; };
} // namespace ccu } // 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 "json/json.h"
#include <sstream> #include <sstream>
#include <cstdio> #include <cstdio>
#include <utility>
namespace ccu { 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)); } void putI(JsonVal& j, const char* k, int16_t v) { j[k] = JsonVal(static_cast<int>(v)); }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。 // 消息描述:根据链路 + 消息 ID + 帧长 + 方向(0=收 1=发),给出中文名与
// 方向是相对 pCCU 而言:收=对方→CCU,发=CCU→对方。 // "来源→去向"。方向是相对 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") { if (link == "fc") {
switch (id) { switch (id) {
case 0x0001: return "FC控制指令"; case 0x0001: return "FC控制指令";
@@ -30,10 +33,30 @@ std::string describeMessageName(const std::string& link, uint16_t id) {
} }
} else if (link == "pm") { } else if (link == "pm") {
switch (id) { switch (id) {
case 0x0001: return "PM操控指令"; case 0x0001:
case 0x0002: return "PM参数设定"; return (length > 0 && length <= 21) ? "PM配电指令(高压母线)" : "PM操控指令";
case 0x0003: return "PM参数反馈"; case 0x0002:
case 0x0004: return "PM状态报文"; 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; 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 describeDirection(const std::string& link, int direction) {
std::string peer = "PM"; std::string peer = "PM";
if (link == "fc") peer = "FC"; if (link == "fc") peer = "FC";
else if (link == "rcu") peer = "真实CCU";
return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU"); return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU");
} }
@@ -57,7 +81,7 @@ JsonVal commLogToJson(const CommLogRow& r) {
e["msgId"] = r.msgId; e["msgId"] = r.msgId;
e["checksumOk"] = r.checksumOk; e["checksumOk"] = r.checksumOk;
e["hex"] = r.hex; 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); e["dirText"] = describeDirection(r.link, r.direction);
return e; return e;
} }
@@ -391,11 +415,247 @@ JsonVal linkJson(const LinkManager* lm) {
return j; 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 } // namespace
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
DbStore* db) LinkManager* rcu, DbStore* db)
: m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {} : m_sys(sys), m_fc(fc), m_pm(pm), m_rcu(rcu), m_db(db) {}
std::string SnapshotBuilder::build() const { std::string SnapshotBuilder::build() const {
JsonVal root(Json::objectValue); JsonVal root(Json::objectValue);
@@ -417,6 +677,8 @@ std::string SnapshotBuilder::build() const {
root["pmStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmStatusCount())); root["pmStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmStatusCount()));
root["bmsStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->bmsStatusCount())); root["bmsStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->bmsStatusCount()));
root["canFrameCount"] = JsonVal(static_cast<Json::UInt>(m_sys->canFrameCount())); root["canFrameCount"] = JsonVal(static_cast<Json::UInt>(m_sys->canFrameCount()));
// 真实CCU 配电桥接数据(RCU 链路)
root["realCcu"] = realCcuJson(m_sys->realCcu(), MOOSTime(false));
} else { } else {
root["fc"] = JsonVal(Json::objectValue); root["fc"] = JsonVal(Json::objectValue);
root["pmCmd"] = JsonVal(Json::objectValue); root["pmCmd"] = JsonVal(Json::objectValue);
@@ -425,11 +687,13 @@ std::string SnapshotBuilder::build() const {
root["pmFb"] = JsonVal(Json::objectValue); root["pmFb"] = JsonVal(Json::objectValue);
root["pmStatus"] = JsonVal(Json::objectValue); root["pmStatus"] = JsonVal(Json::objectValue);
root["bms"] = JsonVal(Json::arrayValue); root["bms"] = JsonVal(Json::arrayValue);
root["realCcu"] = JsonVal(Json::objectValue);
} }
JsonVal links(Json::objectValue); JsonVal links(Json::objectValue);
links["fc"] = linkJson(m_fc); links["fc"] = linkJson(m_fc);
links["pm"] = linkJson(m_pm); links["pm"] = linkJson(m_pm);
if (m_rcu) links["rcu"] = linkJson(m_rcu);
root["links"] = links; root["links"] = links;
// 最近原始帧(默认 20 条) // 最近原始帧(默认 20 条)
+5 -3
View File
@@ -12,13 +12,14 @@ class DbStore;
//============================================================================ //============================================================================
// SnapshotBuilder:构建网页推送的 JSON 快照。 // SnapshotBuilder:构建网页推送的 JSON 快照。
// //
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN)、两条 UDP 链路统计、 // 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN + 真实CCU 配电桥接)、
// 最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。 // 三条 UDP 链路统计、最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
//============================================================================ //============================================================================
class SnapshotBuilder { class SnapshotBuilder {
public: public:
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db); SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
LinkManager* rcu, DbStore* db);
// 生成完整快照 JSON // 生成完整快照 JSON
std::string build() const; std::string build() const;
@@ -30,6 +31,7 @@ private:
SystemData* m_sys; SystemData* m_sys;
LinkManager* m_fc; LinkManager* m_fc;
LinkManager* m_pm; LinkManager* m_pm;
LinkManager* m_rcu;
DbStore* m_db; DbStore* m_db;
}; };
+52
View File
@@ -6,9 +6,48 @@
#include "../protocol/FcProtocol.h" #include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h" #include "../protocol/PmProtocol.h"
#include "../protocol/CanBms.h" #include "../protocol/CanBms.h"
#include "../protocol/DisProtocol.h"
namespace ccu { 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:跨线程共享的最新状态快照。 // SystemData:跨线程共享的最新状态快照。
// //
@@ -138,6 +177,18 @@ public:
return m_canFrameCount; 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: private:
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
FcStatusValue m_fcStatus; FcStatusValue m_fcStatus;
@@ -147,6 +198,7 @@ private:
PmStatusValue m_pmStatus; PmStatusValue m_pmStatus;
PmParamSetFbValue m_pmParamFb; PmParamSetFbValue m_pmParamFb;
BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1 BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1
RealCcuState m_realCcu;
unsigned long m_fcStatusCount = 0; unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0; unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0; unsigned long m_fcControlCount = 0;
+15
View File
@@ -4,7 +4,13 @@
// 链路拓扑: // 链路拓扑:
// 燃料电池控制器(FC) 192.168.1.162:7000(UDP) // 燃料电池控制器(FC) 192.168.1.162:7000(UDP)
// 控制主机(pPowerManger) 192.168.0.140:5001(UDP) // 控制主机(pPowerManger) 192.168.0.140:5001(UDP)
// 真实CCU设备(rcu) 192.168.0.200:7000(UDP,配电桥接,占位地址需按实际修改)
// 锂电池 BMS CAN 总线(经 pCanBridge 订阅 CAN_0x* 透传) // 锂电池 BMS CAN 总线(经 pCanBridge 订阅 CAN_0x* 透传)
//
// 配电桥接(rcu_enable = true 时启用):
// pPowerManger 下发的配电指令(0x0001~0x0004) 经 pCCU 原帧转发真实CCU;
// 真实CCU 上报的配电反馈(0x0005~0x0008) 经 pCCU 原帧转发 pPowerManger,
// 同时解码显示在网页"真实CCU"页签;真实CCU 状态报文(0x0004)仅显示不转发。
//============================================================================ //============================================================================
ProcessConfig = pCCU ProcessConfig = pCCU
@@ -26,6 +32,15 @@ ProcessConfig = pCCU
pm_remote_ip = 192.168.0.140 pm_remote_ip = 192.168.0.140
pm_remote_port = 5001 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), // CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
// 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。 // 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。
+7
View File
@@ -12,6 +12,7 @@ namespace ccu {
// //
// 协议联调阶段校验和的实现可能调整,因此对每条消息独立配置策略: // 协议联调阶段校验和的实现可能调整,因此对每条消息独立配置策略:
// - None : 无校验和字段(如 FC 状态帧文档未列校验行) // - None : 无校验和字段(如 FC 状态帧文档未列校验行)
// - Sum8 : uint8 字节和(配电协议,取字节和低 8 位)
// - Sum32 : uint32 字节和,从域起始符到校验和之前所有数据的字节和 // - Sum32 : uint32 字节和,从域起始符到校验和之前所有数据的字节和
// - Sum16 : uint16 字节和(预留,备用) // - Sum16 : uint16 字节和(预留,备用)
// 新增策略只需在此类扩展,不影响其它代码。 // 新增策略只需在此类扩展,不影响其它代码。
@@ -19,6 +20,7 @@ namespace ccu {
enum class ChecksumType { enum class ChecksumType {
None, // 无校验 None, // 无校验
Sum8, // uint8 字节和(配电协议)
Sum32, // uint32 字节和 Sum32, // uint32 字节和
Sum16, // uint16 字节和 Sum16, // uint16 字节和
}; };
@@ -32,6 +34,7 @@ public:
// 校验和字段占用的字节数;None 返回 0 // 校验和字段占用的字节数;None 返回 0
size_t size() const { size_t size() const {
switch (m_type) { switch (m_type) {
case ChecksumType::Sum8: return 1;
case ChecksumType::Sum32: return 4; case ChecksumType::Sum32: return 4;
case ChecksumType::Sum16: return 2; case ChecksumType::Sum16: return 2;
case ChecksumType::None: return 0; case ChecksumType::None: return 0;
@@ -60,6 +63,10 @@ public:
uint32_t actual = 0; uint32_t actual = 0;
size_t offset = frame.size() - cs; 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) { if (m_type == ChecksumType::Sum32) {
actual = static_cast<uint32_t>(frame[offset]) | actual = static_cast<uint32_t>(frame[offset]) |
(static_cast<uint32_t>(frame[offset + 1]) << 8) | (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); std::vector<uint8_t> body(frame.begin(), frame.end() - csSize);
uint32_t sum = checksum.compute(body); uint32_t sum = checksum.compute(body);
size_t off = frame.size() - csSize; size_t off = frame.size() - csSize;
if (csSize == 4) FieldCodec::putU32(frame, off, sum); if (csSize == 4) FieldCodec::putU32(frame, off, sum);
else FieldCodec::putU16(frame, off, static_cast<uint16_t>(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; return frame;
} }
+15 -2
View File
@@ -5,8 +5,12 @@ namespace ccu {
bool MessageRegistry::registerMessage(std::unique_ptr<Message> msg) { bool MessageRegistry::registerMessage(std::unique_ptr<Message> msg) {
if (!msg) return false; if (!msg) return false;
uint16_t id = msg->id(); uint16_t id = msg->id();
if (m_msgs.count(id) != 0) return false; // id 冲突 // 同 id 且同总长的重复注册视为冲突;同 id 不同总长为合法
m_msgs[id] = std::move(msg); //(配电指令与 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; return true;
} }
@@ -15,6 +19,15 @@ Message* MessageRegistry::find(uint16_t id) const {
return (it != m_msgs.end()) ? it->second.get() : nullptr; 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 { size_t MessageRegistry::size() const {
return m_msgs.size(); return m_msgs.size();
} }
+14 -5
View File
@@ -2,6 +2,7 @@
#define PCCU_MESSAGE_REGISTRY_H #define PCCU_MESSAGE_REGISTRY_H
#include <cstdint> #include <cstdint>
#include <cstddef>
#include <map> #include <map>
#include <memory> #include <memory>
#include <vector> #include <vector>
@@ -12,29 +13,37 @@ namespace ccu {
//============================================================================ //============================================================================
// MessageRegistry:消息注册表与收发分发中心。 // MessageRegistry:消息注册表与收发分发中心。
// //
// 每条链路(FC 链路 / PM 链路)持有各自的注册表实例。 // 每条链路(FC 链路 / PM 链路 / RCU 链路)持有各自的注册表实例。
// 由于 FC 协议与 PM 协议的域标识符存在重叠(0x0001/0x0002), // 由于 FC 协议与 PM 协议的域标识符存在重叠(0x0001/0x0002),
// 分属不同注册表可完全隔离,互不干扰。 // 分属不同注册表可完全隔离,互不干扰。
// //
// 同 id 多消息:配电指令 0x0001~0x0004 与 PM 协议操控/参数指令共用 id,
// 仅帧总长不同,因此注册表允许同 id 多条消息,并支持按总长精确查找:
// find(id, totalLength) 精确匹配(收帧分发用,未命中回退 find(id))
// find(id) 返回该 id 的第一条(兼容旧接口)
//
// 协议新增/调整消息时只需 register() 对应 Message 实现,上层无需改动。 // 协议新增/调整消息时只需 register() 对应 Message 实现,上层无需改动。
//============================================================================ //============================================================================
class MessageRegistry { class MessageRegistry {
public: public:
// 注册一条消息;返回是否成功(id 冲突时返回 false) // 注册一条消息;返回是否成功(同 id 且同总长的重复注册返回 false)
bool registerMessage(std::unique_ptr<Message> msg); bool registerMessage(std::unique_ptr<Message> msg);
// 按 id 查找消息;未注册返回 nullptr // 按 id 查找消息(同 id 多条时返回第一条);未注册返回 nullptr
Message* find(uint16_t id) const; Message* find(uint16_t id) const;
// 按 id + 帧总长精确查找;未命中时回退为 find(id)
Message* find(uint16_t id, size_t totalLength) const;
// 已注册消息数量 // 已注册消息数量
size_t size() const; size_t size() const;
// 所有已注册消息(按 id 升序) // 所有已注册消息(按 id 升序,同 id 按注册顺序)
std::vector<Message*> all() const; std::vector<Message*> all() const;
private: private:
std::map<uint16_t, std::unique_ptr<Message>> m_msgs; std::multimap<uint16_t, std::unique_ptr<Message>> m_msgs;
}; };
} // namespace ccu } // namespace ccu
+216 -1
View File
@@ -86,6 +86,7 @@ th{color:var(--dim);font-weight:500;}
</div> </div>
<div class="tab-group"><span class="tg-label">系统</span> <div class="tab-group"><span class="tg-label">系统</span>
<button class="tab" data-tab="links">链路状态</button> <button class="tab" data-tab="links">链路状态</button>
<button class="tab" data-tab="realCcu">真实CCU</button>
<button class="tab" data-tab="raw">原始报文</button> <button class="tab" data-tab="raw">原始报文</button>
</div> </div>
</div> </div>
@@ -95,7 +96,7 @@ th{color:var(--dim);font-weight:500;}
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
const pagesEl = $('pages'); const pagesEl = $('pages');
const statusEl = $('status'); 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'; let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); } function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
@@ -154,6 +155,7 @@ function bmsOnline(n){
} }
// 最后更新时间文本(a:距最后收到报文的秒数) // 最后更新时间文本(a:距最后收到报文的秒数)
function ageText(a){ function ageText(a){
if(typeof a !== 'number' || isNaN(a)) return '—';
if(a < 90) return a.toFixed(1)+' 秒前'; if(a < 90) return a.toFixed(1)+' 秒前';
if(a < 3600) return Math.floor(a/60)+' 分 '+Math.floor(a%60)+' 秒前'; if(a < 3600) return Math.floor(a/60)+' 分 '+Math.floor(a%60)+' 秒前';
return Math.floor(a/3600)+' 时 '+Math.floor((a%3600)/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); '收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err);
h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>', h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>',
'收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err); '收:'+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 节点在线即视为链路在线 // CAN/BMS 行:任一 BCU 节点在线即视为链路在线
const bmsNodes = Array.isArray(s.bms) ? s.bms : []; const bmsNodes = Array.isArray(s.bms) ? s.bms : [];
const bmsAnyOnline = bmsNodes.some(function(n){ return bmsOnline(n); }); const bmsAnyOnline = bmsNodes.some(function(n){ return bmsOnline(n); });
@@ -490,6 +496,214 @@ function pmStatusCard(d){
return card('整合后状态', h); 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){ function pageHeader(id, name, dir){
const idHex = '0x'+id.toString(16).toUpperCase().padStart(4,'0'); 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>', pmStatusCard(snap.pmStatus)+batteryCard(snap.pmStatus)+emergCard(snap.pmStatus)+'</div>',
pmFb: pageHeader(0x0003,'PM参数反馈','CCU→PM')+'<div class="grid">'+pmFbCard(snap.pmFb)+'</div>', pmFb: pageHeader(0x0003,'PM参数反馈','CCU→PM')+'<div class="grid">'+pmFbCard(snap.pmFb)+'</div>',
links: '<div class="grid">'+linkCard(snap)+'</div>', links: '<div class="grid">'+linkCard(snap)+'</div>',
realCcu: realCcuPage(snap.realCcu),
raw: rawCard(snap.logs) raw: rawCard(snap.logs)
}; };
+1
View File
@@ -137,6 +137,7 @@ add_executable(pccuTest
${CCU_DIR}/protocol/MessageRegistry.cpp ${CCU_DIR}/protocol/MessageRegistry.cpp
${CCU_DIR}/protocol/FcProtocol.cpp ${CCU_DIR}/protocol/FcProtocol.cpp
${CCU_DIR}/protocol/PmProtocol.cpp ${CCU_DIR}/protocol/PmProtocol.cpp
${CCU_DIR}/protocol/DisProtocol.cpp
${CCU_DIR}/protocol/CanBms.cpp ${CCU_DIR}/protocol/CanBms.cpp
${CCU_DIR}/comm/UdpEndpoint.cpp ${CCU_DIR}/comm/UdpEndpoint.cpp
${CCU_DIR}/comm/LinkManager.cpp ${CCU_DIR}/comm/LinkManager.cpp
+134
View File
@@ -15,10 +15,12 @@
#include "../../src/pCCU/protocol/MessageRegistry.h" #include "../../src/pCCU/protocol/MessageRegistry.h"
#include "../../src/pCCU/protocol/FcProtocol.h" #include "../../src/pCCU/protocol/FcProtocol.h"
#include "../../src/pCCU/protocol/PmProtocol.h" #include "../../src/pCCU/protocol/PmProtocol.h"
#include "../../src/pCCU/protocol/DisProtocol.h"
#include "../../src/pCCU/protocol/CanBms.h" #include "../../src/pCCU/protocol/CanBms.h"
#include "../../src/pCCU/comm/LinkManager.h" #include "../../src/pCCU/comm/LinkManager.h"
#include "../../src/pCCU/comm/FcLinkManager.h" #include "../../src/pCCU/comm/FcLinkManager.h"
#include "../../src/pCCU/comm/PmLinkManager.h" #include "../../src/pCCU/comm/PmLinkManager.h"
#include "../../src/pCCU/comm/RcuLinkManager.h"
#include "../../src/pCCU/store/DbStore.h" #include "../../src/pCCU/store/DbStore.h"
using namespace ccu; using namespace ccu;
@@ -390,6 +392,134 @@ static void testCanBmsDecode() {
!isCanBcuId(0x100181FF) && !isCanBcuId(0x10370000)); !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() { int main() {
testFrameLengths(); testFrameLengths();
@@ -401,6 +531,10 @@ int main() {
testLinkDispatch(); testLinkDispatch();
testDbStore(); testDbStore();
testCanBmsDecode(); testCanBmsDecode();
testDisFrameLengths();
testDisRoundTrip();
testRegistryLengthDispatch();
testRcuRegistry();
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail); std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1; return g_fail == 0 ? 0 : 1;
+142 -11
View File
@@ -6,6 +6,10 @@
测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002] 测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002]
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发] pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003] 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* 消息), 锂电池已切换为 CAN 总线(BMS 协议,经 pCanBridge 透传 CAN_0x* 消息),
不在本脚本 UDP 拓扑内:BMS 解码由 pccuTest 单元测试覆盖 不在本脚本 UDP 拓扑内:BMS 解码由 pccuTest 单元测试覆盖
@@ -15,8 +19,11 @@
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003 1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001 2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003 3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
4. SQLite 记录了收/发原始帧 4. 配电反馈(0x0005~0x0008)原帧转发到 16003(字节一致)
5. Web 页面可访问 5. 配电指令(0x0001~0x0004 短帧)原帧转发到 16005(字节一致);
PM 操控指令(45B, 同id)不转发到真实CCU(按帧长区分)
6. SQLite 记录了收/发原始帧(含 link='rcu')
7. Web 页面可访问
""" """
import socket import socket
import struct import struct
@@ -33,6 +40,8 @@ FC_STATUS_PORT = 16000
FC_CTRL_RECV_PORT = 16001 FC_CTRL_RECV_PORT = 16001
PM_CTRL_PORT = 16002 PM_CTRL_PORT = 16002
PM_STATUS_RECV_PORT = 16003 PM_STATUS_RECV_PORT = 16003
RCU_FB_PORT = 16004 # pCCU rcu 链路本地端口(收配电反馈)
RCU_CMD_RECV_PORT = 16005 # 模拟真实CCU 接收转发的配电指令
WEB_PORT = 18081 WEB_PORT = 18081
DB_PATH = "/tmp/pccu_it_test.db" DB_PATH = "/tmp/pccu_it_test.db"
MOOSDB_EXE = "/usr/local/bin/MOOSDB" MOOSDB_EXE = "/usr/local/bin/MOOSDB"
@@ -53,6 +62,13 @@ def frame(msg_id, payload, checksum=True):
return body 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(): def fc_status_frame():
# payload 144B,仅填关键字段,其余为0 # payload 144B,仅填关键字段,其余为0
p = bytearray(144) p = bytearray(144)
@@ -98,6 +114,76 @@ def pm_paramset_frame():
return frame(0x0002, bytes(p)) 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): def recv_loop(sock, label, results, msg_ids, timeout):
"""持续接收指定 msg_id 的帧,记录到 results""" """持续接收指定 msg_id 的帧,记录到 results"""
sock.settimeout(timeout) sock.settimeout(timeout)
@@ -139,18 +225,25 @@ def main():
print("FAIL: pCCU exited early, code", pccu.returncode) print("FAIL: pCCU exited early, code", pccu.returncode)
return 1 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 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fc_ctl_recv.bind(("127.0.0.1", FC_CTRL_RECV_PORT)) 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 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
pm_status_recv.bind(("127.0.0.1", PM_STATUS_RECV_PORT)) 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, 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, t2 = threading.Thread(target=recv_loop,
args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12)) args=(pm_status_recv, "pm-status", pm_status_results,
t1.start(); t2.start() {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 状态(周期模拟) # 4. 发送 FC 状态(周期模拟)
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -164,7 +257,21 @@ def main():
time.sleep(0.5) time.sleep(0.5)
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT)) 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 ok = True
@@ -208,16 +315,40 @@ def main():
else: else:
ok = False 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) time.sleep(1.0)
db_count = -1 db_count, db_rcu = -1, -1
if os.path.exists(DB_PATH): if os.path.exists(DB_PATH):
import sqlite3 import sqlite3
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0] 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() 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_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
ok = ok and (db_rcu >= 8) # 4条配电反馈(收) + 4条配电指令(发)
web_ok = False web_ok = False
try: try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3) 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_ip = 127.0.0.1
pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文) 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 协议,经 pCanBridge 的 CAN_0x* 消息透传
// (集成测试不覆盖 CAN 链路,BMS 解码由 pccuTest 单元测试覆盖) // (集成测试不覆盖 CAN 链路,BMS 解码由 pccuTest 单元测试覆盖)