From 0639b5904d151dce70a086f23a2cf1487be2ef7f Mon Sep 17 00:00:00 2001 From: zjk <1553836110@qq.com> Date: Tue, 1 Sep 2026 15:06:11 +0800 Subject: [PATCH] =?UTF-8?q?pCCU=20=E6=96=B0=E5=A2=9E=E7=9C=9F=E5=AE=9ECCU?= =?UTF-8?q?=E9=85=8D=E7=94=B5=E6=A1=A5=E6=8E=A5=EF=BC=88rcu=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=E5=8E=9F=E5=B8=A7=E9=80=8F=E4=BC=A0+=E7=BD=91?= =?UTF-8?q?=E9=A1=B5=E6=98=BE=E7=A4=BA=EF=BC=89=E5=B9=B6=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=9D=BF=E5=8D=A1=E7=AB=AF=E5=8F=A3=E6=8B=93=E6=89=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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项);集成测试新增桥接字节一致+不误转发用例 --- missions/h100.moos | 25 ++- src/pCCU/CCU.cpp | 83 +++++++- src/pCCU/CCU.h | 14 ++ src/pCCU/CCU_Info.cpp | 10 +- src/pCCU/CMakeLists.txt | 4 +- src/pCCU/comm/LinkManager.cpp | 33 ++- src/pCCU/comm/LinkManager.h | 9 +- src/pCCU/comm/PmLinkManager.h | 8 + src/pCCU/comm/RcuLinkManager.h | 51 +++++ src/pCCU/comm/UdpEndpoint.cpp | 1 + src/pCCU/comm/UdpEndpoint.h | 4 + src/pCCU/core/DisBridge.cpp | 173 ++++++++++++++++ src/pCCU/core/DisBridge.h | 55 +++++ src/pCCU/core/SnapshotBuilder.cpp | 284 +++++++++++++++++++++++++- src/pCCU/core/SnapshotBuilder.h | 8 +- src/pCCU/core/SystemData.h | 52 +++++ src/pCCU/pCCU.moos | 15 ++ src/pCCU/protocol/ChecksumPolicy.h | 7 + src/pCCU/protocol/DisProtocol.cpp | 155 ++++++++++++++ src/pCCU/protocol/DisProtocol.h | 115 +++++++++++ src/pCCU/protocol/Frame.cpp | 5 +- src/pCCU/protocol/MessageRegistry.cpp | 19 +- src/pCCU/protocol/MessageRegistry.h | 21 +- src/pCCU/web/pages/index.h | 217 +++++++++++++++++++- test/CMakeLists.txt | 1 + test/pccu/pccuTest.cpp | 134 ++++++++++++ test/pccu/pccu_integration_test.py | 153 +++++++++++++- test/pccu/pccu_it.moos | 6 + 28 files changed, 1609 insertions(+), 53 deletions(-) create mode 100644 src/pCCU/comm/RcuLinkManager.h create mode 100644 src/pCCU/core/DisBridge.cpp create mode 100644 src/pCCU/core/DisBridge.h create mode 100644 src/pCCU/protocol/DisProtocol.cpp create mode 100644 src/pCCU/protocol/DisProtocol.h diff --git a/missions/h100.moos b/missions/h100.moos index 2d22ddf..26875a2 100644 --- a/missions/h100.moos +++ b/missions/h100.moos @@ -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), diff --git a/src/pCCU/CCU.cpp b/src/pCCU/CCU.cpp index 39498cc..3fb1214 100644 --- a/src/pCCU/CCU.cpp +++ b/src/pCCU/CCU.cpp @@ -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&, 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& 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&, 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& frame) { + handleRcuMessage(msg, frame); + }); + m_rcuLink->setOnRawFrame([](int, const std::vector&, 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& frame) { void CCU::handlePmMessage(Message* msg, const std::vector& 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& frame) { } } +//--------------------------------------------------------- +// handleRcuMessage:处理真实 CCU 链路收到的消息(配电桥接) +// +// 配电反馈(0x0005~0x0008)与真实CCU状态报文(0x0004)由 DisBridge +// 处理(原帧转发 pPowerManger / 仅解码显示);其余(如配电指令 +// 回显)仅记录,不转发。 + +void CCU::handleRcuMessage(Message* msg, const std::vector& 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"; diff --git a/src/pCCU/CCU.h b/src/pCCU/CCU.h index 3a29f14..a6a3cd6 100644 --- a/src/pCCU/CCU.h +++ b/src/pCCU/CCU.h @@ -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& frame); void handlePmMessage(Message* msg, const std::vector& frame); + void handleRcuMessage(Message* msg, const std::vector& 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; diff --git a/src/pCCU/CCU_Info.cpp b/src/pCCU/CCU_Info.cpp index b29807f..825e9fc 100644 --- a/src/pCCU/CCU_Info.cpp +++ b/src/pCCU/CCU_Info.cpp @@ -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); } diff --git a/src/pCCU/CMakeLists.txt b/src/pCCU/CMakeLists.txt index 69002cc..db40c3f 100644 --- a/src/pCCU/CMakeLists.txt +++ b/src/pCCU/CMakeLists.txt @@ -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 diff --git a/src/pCCU/comm/LinkManager.cpp b/src/pCCU/comm/LinkManager.cpp index 3d8f486..aa844e0 100644 --- a/src/pCCU/comm/LinkManager.cpp +++ b/src/pCCU/comm/LinkManager.cpp @@ -54,17 +54,46 @@ bool LinkManager::sendMessageTo(uint16_t id, const void* value, return true; } +bool LinkManager::sendRaw(const std::vector& 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& 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()); diff --git a/src/pCCU/comm/LinkManager.h b/src/pCCU/comm/LinkManager.h index da23b6f..91cbd5b 100644 --- a/src/pCCU/comm/LinkManager.h +++ b/src/pCCU/comm/LinkManager.h @@ -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& 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& frame); + // 处理一帧收到的原始数据(供接收线程调用,也可测试时手动喂入) bool handleIncoming(const std::vector& 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; diff --git a/src/pCCU/comm/PmLinkManager.h b/src/pCCU/comm/PmLinkManager.h index 231227f..c7f0288 100644 --- a/src/pCCU/comm/PmLinkManager.h +++ b/src/pCCU/comm/PmLinkManager.h @@ -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) { diff --git a/src/pCCU/comm/RcuLinkManager.h b/src/pCCU/comm/RcuLinkManager.h new file mode 100644 index 0000000..ee8ab11 --- /dev/null +++ b/src/pCCU/comm/RcuLinkManager.h @@ -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 diff --git a/src/pCCU/comm/UdpEndpoint.cpp b/src/pCCU/comm/UdpEndpoint.cpp index 20d0ddb..7fe5254 100644 --- a/src/pCCU/comm/UdpEndpoint.cpp +++ b/src/pCCU/comm/UdpEndpoint.cpp @@ -36,6 +36,7 @@ bool UdpEndpoint::init(long localPort) { bool UdpEndpoint::sendTo(const std::string& host, long port, const std::vector& data) { if (!m_socket || data.empty()) return false; try { + std::lock_guard lock(m_sendMutex); m_socket->iSendMessageTo(const_cast(data.data()), static_cast(data.size()), port, host); diff --git a/src/pCCU/comm/UdpEndpoint.h b/src/pCCU/comm/UdpEndpoint.h index 259cb99..6c1d373 100644 --- a/src/pCCU/comm/UdpEndpoint.h +++ b/src/pCCU/comm/UdpEndpoint.h @@ -4,6 +4,7 @@ #define UNIX #include "MOOS/libMOOS/Comms/XPCUdpSocket.h" #include +#include #include #include @@ -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 diff --git a/src/pCCU/core/DisBridge.cpp b/src/pCCU/core/DisBridge.cpp new file mode 100644 index 0000000..b02d2cc --- /dev/null +++ b/src/pCCU/core/DisBridge.cpp @@ -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& frame) { + if (!isDisCmdMessage(msg)) return false; + forwardCmdToRcu(msg, frame); + return true; +} + +void DisBridge::forwardCmdToRcu(Message* msg, const std::vector& 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& 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& 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& 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 diff --git a/src/pCCU/core/DisBridge.h b/src/pCCU/core/DisBridge.h new file mode 100644 index 0000000..b5e831e --- /dev/null +++ b/src/pCCU/core/DisBridge.h @@ -0,0 +1,55 @@ +#ifndef PCCU_DIS_BRIDGE_H +#define PCCU_DIS_BRIDGE_H + +#include +#include +#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& frame); + + // RCU 链路收帧入口:命中配电反馈则原帧转发 pPowerManger 并解码 + // 显示;真实CCU 状态报文 0x0004 仅解码显示。返回是否已处理 + bool onRcuFrame(Message* msg, const std::vector& frame); + +private: + // PM 链路收到配电指令:原帧转发 + 解码缓存 + void forwardCmdToRcu(Message* msg, const std::vector& frame); + // RCU 链路收到配电反馈:原帧转发 + 解码缓存 + void forwardFbToPm(Message* msg, const std::vector& frame); + // RCU 链路收到真实CCU 状态报文:仅解码缓存(显示) + void storeRealCcuStatus(const std::vector& frame); + + LinkManager* m_pmLink = nullptr; + LinkManager* m_rcuLink = nullptr; + SystemData* m_sys = nullptr; +}; + +} // namespace ccu + +#endif // PCCU_DIS_BRIDGE_H diff --git a/src/pCCU/core/SnapshotBuilder.cpp b/src/pCCU/core/SnapshotBuilder.cpp index 6d05031..f69bd65 100644 --- a/src/pCCU/core/SnapshotBuilder.cpp +++ b/src/pCCU/core/SnapshotBuilder.cpp @@ -5,6 +5,7 @@ #include "json/json.h" #include #include +#include namespace ccu { @@ -18,10 +19,12 @@ void put(JsonVal& j, const char* k, uint32_t v){ j[k] = JsonVal(static_cast(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>& 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(st.fbCount)); + fwd["fwdFbOk"] = JsonVal(static_cast(st.fwdFbOk)); + fwd["fwdFbErr"] = JsonVal(static_cast(st.fwdFbErr)); + fwd["cmdCount"] = JsonVal(static_cast(st.cmdCount)); + fwd["fwdCmdOk"] = JsonVal(static_cast(st.fwdCmdOk)); + fwd["fwdCmdErr"] = JsonVal(static_cast(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(m_sys->pmStatusCount())); root["bmsStatusCount"] = JsonVal(static_cast(m_sys->bmsStatusCount())); root["canFrameCount"] = JsonVal(static_cast(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 条) diff --git a/src/pCCU/core/SnapshotBuilder.h b/src/pCCU/core/SnapshotBuilder.h index e343060..4588a6d 100644 --- a/src/pCCU/core/SnapshotBuilder.h +++ b/src/pCCU/core/SnapshotBuilder.h @@ -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; }; diff --git a/src/pCCU/core/SystemData.h b/src/pCCU/core/SystemData.h index 5bbdee5..ddced80 100644 --- a/src/pCCU/core/SystemData.h +++ b/src/pCCU/core/SystemData.h @@ -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 lock(m_mutex); + m_realCcu = v; + } + RealCcuState realCcu() const { + std::lock_guard 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; diff --git a/src/pCCU/pCCU.moos b/src/pCCU/pCCU.moos index 97884ad..a4cc6be 100644 --- a/src/pCCU/pCCU.moos +++ b/src/pCCU/pCCU.moos @@ -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* 消息透传,无需额外配置。 diff --git a/src/pCCU/protocol/ChecksumPolicy.h b/src/pCCU/protocol/ChecksumPolicy.h index 70f42be..a76dfbe 100644 --- a/src/pCCU/protocol/ChecksumPolicy.h +++ b/src/pCCU/protocol/ChecksumPolicy.h @@ -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(frame[offset]); + return (expect & 0xFF) == actual; + } if (m_type == ChecksumType::Sum32) { actual = static_cast(frame[offset]) | (static_cast(frame[offset + 1]) << 8) | diff --git a/src/pCCU/protocol/DisProtocol.cpp b/src/pCCU/protocol/DisProtocol.cpp new file mode 100644 index 0000000..365a7a4 --- /dev/null +++ b/src/pCCU/protocol/DisProtocol.cpp @@ -0,0 +1,155 @@ +#include "DisProtocol.h" +#include "Frame.h" +#include "MessageRegistry.h" +#include + +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 encodePacked(uint16_t id, const void* obj, size_t size, + const ChecksumPolicy& policy) { + std::vector p(size, 0); + std::memcpy(p.data(), obj, size); + return Frame::build(id, p, policy); +} + +bool decodePacked(const std::vector& 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 DisHighVolBusCmdMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disHighVolBusCmd), m_checksum); +} +bool DisHighVolBusCmdMessage::decode(const std::vector& frame, void* obj) const { + return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusCmd), obj); +} + +std::vector DisHighAVolBusCmdMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disHighAVolBusCmd), m_checksum); +} +bool DisHighAVolBusCmdMessage::decode(const std::vector& frame, void* obj) const { + return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusCmd), obj); +} + +std::vector DisHighBVolBusCmdMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disLowMainBusCmd), m_checksum); +} +bool DisHighBVolBusCmdMessage::decode(const std::vector& frame, void* obj) const { + return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusCmd), obj); +} + +std::vector DisLowBusCmdMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disLowBusCmd), m_checksum); +} +bool DisLowBusCmdMessage::decode(const std::vector& frame, void* obj) const { + return decodePacked(frame, id(), m_checksum, sizeof(disLowBusCmd), obj); +} + +//-------------------------------------------------------------------------- +// 反馈消息编解码 +//-------------------------------------------------------------------------- + +std::vector DisHighVolBusFbMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disHighVolBusState), m_checksum); +} +bool DisHighVolBusFbMessage::decode(const std::vector& frame, void* obj) const { + return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusState), obj); +} + +std::vector DisHighAVolBusFbMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disHighAVolBusState), m_checksum); +} +bool DisHighAVolBusFbMessage::decode(const std::vector& frame, void* obj) const { + return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusState), obj); +} + +std::vector DisHighBVolBusFbMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disLowMainBusState), m_checksum); +} +bool DisHighBVolBusFbMessage::decode(const std::vector& frame, void* obj) const { + return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusState), obj); +} + +std::vector DisLowBusFbMessage::encode(const void* obj) const { + return encodePacked(id(), obj, sizeof(disLowBusState), m_checksum); +} +bool DisLowBusFbMessage::decode(const std::vector& 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(msg) != nullptr || + dynamic_cast(msg) != nullptr || + dynamic_cast(msg) != nullptr || + dynamic_cast(msg) != nullptr; +} + +bool isDisFbMessage(const Message* msg) { + if (!msg) return false; + return dynamic_cast(msg) != nullptr || + dynamic_cast(msg) != nullptr || + dynamic_cast(msg) != nullptr || + dynamic_cast(msg) != nullptr; +} + +//-------------------------------------------------------------------------- +// 注册 +//-------------------------------------------------------------------------- + +void registerDisCmdMessages(MessageRegistry& reg) { + reg.registerMessage(std::unique_ptr(new DisHighVolBusCmdMessage())); + reg.registerMessage(std::unique_ptr(new DisHighAVolBusCmdMessage())); + reg.registerMessage(std::unique_ptr(new DisHighBVolBusCmdMessage())); + reg.registerMessage(std::unique_ptr(new DisLowBusCmdMessage())); +} + +void registerDisFbMessages(MessageRegistry& reg) { + reg.registerMessage(std::unique_ptr(new DisHighVolBusFbMessage())); + reg.registerMessage(std::unique_ptr(new DisHighAVolBusFbMessage())); + reg.registerMessage(std::unique_ptr(new DisHighBVolBusFbMessage())); + reg.registerMessage(std::unique_ptr(new DisLowBusFbMessage())); +} + +} // namespace ccu diff --git a/src/pCCU/protocol/DisProtocol.h b/src/pCCU/protocol/DisProtocol.h new file mode 100644 index 0000000..bae8799 --- /dev/null +++ b/src/pCCU/protocol/DisProtocol.h @@ -0,0 +1,115 @@ +#ifndef PCCU_DIS_PROTOCOL_H +#define PCCU_DIS_PROTOCOL_H + +#include +#include +#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 encode(const void* obj) const override; \ + bool decode(const std::vector& frame, void* obj) const override; \ + std::vector encode(const dataT& v) const { return encode(&v); } \ + bool decode(const std::vector& frame, dataT& v) const { \ + return decode(frame, static_cast(&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 diff --git a/src/pCCU/protocol/Frame.cpp b/src/pCCU/protocol/Frame.cpp index 8ddbfb9..1711f84 100644 --- a/src/pCCU/protocol/Frame.cpp +++ b/src/pCCU/protocol/Frame.cpp @@ -41,8 +41,9 @@ std::vector Frame::build( std::vector 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(sum)); + if (csSize == 4) FieldCodec::putU32(frame, off, sum); + else if (csSize == 1) frame[off] = static_cast(sum & 0xFF); + else FieldCodec::putU16(frame, off, static_cast(sum)); } return frame; } diff --git a/src/pCCU/protocol/MessageRegistry.cpp b/src/pCCU/protocol/MessageRegistry.cpp index fbe79ae..ab88234 100644 --- a/src/pCCU/protocol/MessageRegistry.cpp +++ b/src/pCCU/protocol/MessageRegistry.cpp @@ -5,8 +5,12 @@ namespace ccu { bool MessageRegistry::registerMessage(std::unique_ptr 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 MessageRegistry::all() const { return out; } -} // namespace ccu \ No newline at end of file +} // namespace ccu diff --git a/src/pCCU/protocol/MessageRegistry.h b/src/pCCU/protocol/MessageRegistry.h index 6dbf13e..f9da471 100644 --- a/src/pCCU/protocol/MessageRegistry.h +++ b/src/pCCU/protocol/MessageRegistry.h @@ -2,6 +2,7 @@ #define PCCU_MESSAGE_REGISTRY_H #include +#include #include #include #include @@ -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 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 all() const; private: - std::map> m_msgs; + std::multimap> m_msgs; }; } // namespace ccu -#endif // PCCU_MESSAGE_REGISTRY_H \ No newline at end of file +#endif // PCCU_MESSAGE_REGISTRY_H diff --git a/src/pCCU/web/pages/index.h b/src/pCCU/web/pages/index.h index 78ecef2..d4f6866 100644 --- a/src/pCCU/web/pages/index.h +++ b/src/pCCU/web/pages/index.h @@ -86,6 +86,7 @@ th{color:var(--dim);font-weight:500;}
系统 +
@@ -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('PM 链路', '收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err); + if(l.rcu){ + h+=row('RCU 链路 (真实CCU)', + '收:'+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 '闭合'; + if(v===0xAA) return '断开'; + if(v===0x5A) return '故障脱扣'; + return hex(v); +} +// 双值信号(0x55 正常 / 0xAA 异常) +function sigText(v){ + if(v===0x55) return '正常'; + if(v===0xAA) return '异常'; + return hex(v); +} +function insStatText(v){ return v===0x55 ? '正常' + : (v===0xAA ? '绝缘低' : hex(v)); } +// age:距最后收到该类帧的秒数;valid:是否收到过(未收到显示"从未收到") +function ageCell(age, valid){ + if(!valid || typeof age !== 'number' || isNaN(age)) + return '从未收到'; + const online = age >= 0 && age < 5; + return (online ? '' : '') + ageText(age) + + (online ? ' (在线)' : ' (超时)') + ''; +} +function fwdLine(okN, errN){ + return okN + ' 成功' + (errN ? ' '+errN+' 失败' : ''); +} + +// 母线卡片通用: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 '

真实CCU

数据状态等待数据...
'; + let h='
RCU 真实CCU 配电桥接 '+ + '原帧透传
'; + + // 桥接概览卡片 + { + 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+='
'; + 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)+'', pmFb: pageHeader(0x0003,'PM参数反馈','CCU→PM')+'
'+pmFbCard(snap.pmFb)+'
', links: '
'+linkCard(snap)+'
', + realCcu: realCcuPage(snap.realCcu), raw: rawCard(snap.logs) }; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a3064b2..f318581 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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 diff --git a/test/pccu/pccuTest.cpp b/test/pccu/pccuTest.cpp index c50f31b..74eb684 100644 --- a/test/pccu/pccuTest.cpp +++ b/test/pccu/pccuTest.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 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(&hvOut))); + CHECK(hvOut.fuelCellCircuitBreaker == DIS_BREAKER_ON); + CHECK(hvOut.powerLithiumBatteryCircuitBreaker == DIS_BREAKER_OFF); + CHECK(hvOut.dcdc5Module == 1 && hvOut.coolingSystem == 0); + // 篡改数据域 -> Sum8 失败 + std::vector bad = hf; bad[8] ^= 0xFF; + CHECK(!hvMsg.decode(bad, static_cast(&hvOut))); + + // 指令:低压主母线 / 仪表汇流排 + disLowMainBusCmd hvb; hvb.dcC1DCDistributionPanelCircuitBreaker = DIS_BREAKER_ON; + DisHighBVolBusCmdMessage hvbMsg; + std::vector bf = hvbMsg.encode(hvb); + CHECK(bf.size() == 14); + disLowMainBusCmd hvbOut; + CHECK(hvbMsg.decode(bf, static_cast(&hvbOut))); + CHECK(hvbOut.dcC1DCDistributionPanelCircuitBreaker == DIS_BREAKER_ON); + + disLowBusCmd lv; lv.unit2CircuitBreaker = DIS_BREAKER_ON; + DisLowBusCmdMessage lvMsg; + std::vector lf = lvMsg.encode(lv); + CHECK(lf.size() == 15); + disLowBusCmd lvOut; + CHECK(lvMsg.decode(lf, static_cast(&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 af = hvaMsg.encode(hva); + CHECK(af.size() == 32); + disHighAVolBusState hvaOut; + CHECK(hvaMsg.decode(af, static_cast(&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&) { + 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 f = hvMsg.encode(hv); + std::string gotName; + rcu.setOnMessage([&](Message* m, const std::vector&) { + 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; diff --git a/test/pccu/pccu_integration_test.py b/test/pccu/pccu_integration_test.py index d3aac41..4005648 100644 --- a/test/pccu/pccu_integration_test.py +++ b/test/pccu/pccu_integration_test.py @@ -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(" 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("=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) diff --git a/test/pccu/pccu_it.moos b/test/pccu/pccu_it.moos index 5d39377..0ded712 100644 --- a/test/pccu/pccu_it.moos +++ b/test/pccu/pccu_it.moos @@ -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 单元测试覆盖)