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