From 88bed850eea790ddb72e910e0a5581e81dc25875 Mon Sep 17 00:00:00 2001 From: zjk <1553836110@qq.com> Date: Mon, 31 Aug 2026 21:42:26 +0800 Subject: [PATCH] =?UTF-8?q?pCCU=20=E9=94=82=E7=94=B5=E6=B1=A0=E9=80=9A?= =?UTF-8?q?=E4=BF=A1=E5=88=87=E6=8D=A2=E4=B8=BA=20CAN=20=E6=80=BB=E7=BA=BF?= =?UTF-8?q?=EF=BC=88BMS=20=E5=8D=8F=E8=AE=AE=EF=BC=8C=E7=BB=8F=20pCanBridg?= =?UTF-8?q?e=20=E9=80=8F=E4=BC=A0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 协议层: - 新增 protocol/CanBms.{h,cpp}:按 docs/BMS_协议字段定义.xlsx 解码 0x10010000(总电压/SOC/故障码/状态位)、0x10010005(最大电流限制/ 总电流/最高单体电压)、0x10010006(总电压校验)。 缩放用除法保证与十进制字面量严格一致;总电流按 2 字节 s16 实现 (xlsx 标注 3 字节与其自身示例/实车帧矛盾,已在代码注释说明)。 - 删除 UDP 锂电池协议:protocol/BatProtocol.{h,cpp}、 comm/BatLinkManager.h(动力/仪表双链路、自检/接触器/功率指令)。 pCCU 主体: - CCU 订阅 pCanBridge 发布的 CAN_0x* 二进制消息并解码 BMS 报文; SystemData 双电池槽位改为单一 BmsStatusValue + CAN 帧计数; PM 电池启停/功率指令改为告警忽略(BMS CAN 协议未定义控制报文)。 - 修复两个本地联调发现的阻断性 bug: 1) MOOS 通配订阅必须用三参数 Register("CAN_0x*","*",0), 单参数形式不做通配匹配; 2) 变量名前缀匹配偏移(compare 少比 1 字符 / sscanf +5 指向 'x'), 改为 rfind 前缀判断 + +6 偏移。 - PowerCoordinator/CoordFsm 剥离电池接触器/切换时序(依赖已删除的 UDP 指令),保留 FC 联动与协调策略占位(CoordSwitchingState 改为 通用的 CoordBusyState)。 网页: - 动力/仪表锂电池两个标签页合并为"锂电池BMS(CAN)":总电压/总电流/ SOC/最大电流限制/最高单体电压/校验帧/故障码高亮/在线状态, 附报文来源说明;链路状态页改为 CAN 链路行(pCanBridge)。 配置与测试: - pCCU.moos / missions/h100.moos 移除电池 UDP 配置; - pccuTest:删除旧电池编解码用例,新增 testCanBmsDecode (向量取自实车抓包:533.8V/20.1%/49/3.752V/-25.0A)103/103 通过; - 集成测试改为纯 FC/PM 流程,本地实测 PASSED(含 Web 页面校验)。 --- missions/h100.moos | 13 +- src/pCCU/CCU.cpp | 179 +++++------- src/pCCU/CCU.h | 29 +- src/pCCU/CCU_Info.cpp | 24 +- src/pCCU/CMakeLists.txt | 2 +- src/pCCU/comm/BatLinkManager.h | 47 --- src/pCCU/core/CoordFsm.cpp | 69 ++--- src/pCCU/core/CoordFsm.hpp | 69 ++--- src/pCCU/core/PowerCoordinator.cpp | 363 +---------------------- src/pCCU/core/PowerCoordinator.h | 113 +------- src/pCCU/core/SnapshotBuilder.cpp | 122 ++------ src/pCCU/core/SnapshotBuilder.h | 9 +- src/pCCU/core/SystemData.h | 44 ++- src/pCCU/pCCU.moos | 24 +- src/pCCU/protocol/BatProtocol.cpp | 446 ----------------------------- src/pCCU/protocol/BatProtocol.h | 334 --------------------- src/pCCU/protocol/CanBms.cpp | 57 ++++ src/pCCU/protocol/CanBms.h | 63 ++++ src/pCCU/web/pages/index.h | 147 +++------- test/CMakeLists.txt | 2 +- test/pccu/pccuTest.cpp | 279 +++--------------- test/pccu/pccu_integration_test.py | 95 +----- test/pccu/pccu_it.moos | 9 +- 23 files changed, 424 insertions(+), 2115 deletions(-) delete mode 100644 src/pCCU/comm/BatLinkManager.h delete mode 100644 src/pCCU/protocol/BatProtocol.cpp delete mode 100644 src/pCCU/protocol/BatProtocol.h create mode 100644 src/pCCU/protocol/CanBms.cpp create mode 100644 src/pCCU/protocol/CanBms.h diff --git a/missions/h100.moos b/missions/h100.moos index 553ee0a..b836fe1 100644 --- a/missions/h100.moos +++ b/missions/h100.moos @@ -52,16 +52,9 @@ ProcessConfig = pCCU pm_remote_ip = 127.0.0.1 pm_remote_port = 5001 - // 锂电池链路(协议:docs/锂电池协议20230324.docx) - // 动力/仪表锂电池各自独立端口;本机监听端口需与电池侧配置的目的端口一致 - dyn_bat_local_port = 7001 - dyn_bat_remote_ip = 192.168.100.137 - dyn_bat_remote_port = 7000 - ins_bat_local_port = 7002 - ins_bat_remote_ip = 192.168.100.136 - ins_bat_remote_port = 7000 - // 电池工况设定:10工房调试 / 30试验实航 / 50科研模式 - bat_work_condition = 10 + //======== 锂电池 ======== + // CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx), + // 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。 dbpath = /root/work/h100/data/pccu_data.db logpath = /root/work/h100/data/pCCU.log diff --git a/src/pCCU/CCU.cpp b/src/pCCU/CCU.cpp index c4306c1..1e53b57 100644 --- a/src/pCCU/CCU.cpp +++ b/src/pCCU/CCU.cpp @@ -2,7 +2,9 @@ #include "MBUtils.h" #include "../pPowerManger/logc/loguru.hpp" #include "protocol/Frame.h" +#include "protocol/CanBms.h" #include +#include using namespace ccu; using namespace std; @@ -17,9 +19,7 @@ CCU::~CCU() { if (m_coord) { delete m_coord; m_coord = 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_dynBatLink) { m_dynBatLink->stop(); delete m_dynBatLink; m_dynBatLink = nullptr; } - if (m_insBatLink) { m_insBatLink->stop(); delete m_insBatLink; m_insBatLink = nullptr; } - if (m_db) { delete m_db; m_db = nullptr; } + if (m_db) { m_db->close(); delete m_db; m_db = nullptr; } if (m_snap) { delete m_snap; m_snap = nullptr; } if (m_sysData) { delete m_sysData; m_sysData = nullptr; } } @@ -49,13 +49,6 @@ 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 == "dyn_bat_local_port") m_dynBatLocalPort = atol(value.c_str()); - else if (param == "dyn_bat_remote_ip") m_dynBatHost = value; - else if (param == "dyn_bat_remote_port") m_dynBatPort = atol(value.c_str()); - else if (param == "ins_bat_local_port") m_insBatLocalPort = atol(value.c_str()); - else if (param == "ins_bat_remote_ip") m_insBatHost = value; - else if (param == "ins_bat_remote_port") m_insBatPort = atol(value.c_str()); - else if (param == "bat_work_condition") m_batWorkCondition = 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()); @@ -106,38 +99,20 @@ bool CCU::OnStartUp() { m_pmLink->setOnMessage([this](Message* msg, const std::vector& frame) { handlePmMessage(msg, frame); }); - m_pmLink->setOnRawFrame([](int dir, const std::vector&, bool) { + m_pmLink->setOnRawFrame([](int, const std::vector&, bool) { }); if (!m_pmLink->start()) LOG_F(ERROR, "PM link start failed"); - // 锂电池链路(动力 / 仪表) - m_dynBatLink = new BatLinkManager(BatRole::Dyn, m_dynBatLocalPort, m_dynBatHost, m_dynBatPort); - m_dynBatLink->setLogSink(m_db); - m_dynBatLink->setOnMessage([this](Message* msg, const std::vector& frame) { - handleBatMessage(BatRole::Dyn, msg, frame); - }); - m_dynBatLink->setOnRawFrame([](int, const std::vector&, bool) {}); - if (!m_dynBatLink->start()) - LOG_F(ERROR, "Dyn battery link start failed"); + // 锂电池:CAN 总线(BMS 协议),数据经 pCanBridge 以 CAN_0x* 消息透传, + // 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。 - m_insBatLink = new BatLinkManager(BatRole::Ins, m_insBatLocalPort, m_insBatHost, m_insBatPort); - m_insBatLink->setLogSink(m_db); - m_insBatLink->setOnMessage([this](Message* msg, const std::vector& frame) { - handleBatMessage(BatRole::Ins, msg, frame); - }); - m_insBatLink->setOnRawFrame([](int, const std::vector&, bool) {}); - if (!m_insBatLink->start()) - LOG_F(ERROR, "Ins battery link start failed"); - - // 电源协调器(操作->指令下发 + 锂电池/燃料电池协调占位) + // 电源协调器(FC 联动 + 协调算法占位) m_coord = new PowerCoordinator(); - m_coord->setup(m_sysData, m_fcLink, m_pmLink, m_dynBatLink, m_insBatLink); - m_coord->setWorkCondition(static_cast(m_batWorkCondition)); + m_coord->setup(m_sysData, m_fcLink, m_pmLink); // 快照构建器(链路就绪后创建) - m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, - m_dynBatLink, m_insBatLink, m_db); + m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_db); // 网页 if (m_webEnable) { @@ -154,11 +129,9 @@ bool CCU::OnStartUp() { } LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, " - "bat_dyn local=%ld -> %s:%ld, bat_ins 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_dynBatLocalPort, m_dynBatHost.c_str(), m_dynBatPort, - m_insBatLocalPort, m_insBatHost.c_str(), m_insBatPort); + m_pmLocalPort, m_pmHost.c_str(), m_pmPort); return true; } @@ -175,11 +148,13 @@ bool CCU::OnConnectToServer() { void CCU::registerVariables() { AppCastingMOOSApp::RegisterVariables(); - // pCCU 数据交换主要走 UDP;MOOSDB 仅登记运行状态变量 + // 订阅 pCanBridge 透传的全部 CAN 帧(变量名 CAN_0x%08X,二进制数据域)。 + // 注意:MOOS 通配订阅必须用三参数重载(变量模式 + 来源模式), + // 单参数 Register("CAN_0x*") 不会做通配匹配。 + Register("CAN_0x*", "*", 0); + // 运行状态变量 Register("CCU_FC_LINK_STATE", 0); Register("CCU_PM_LINK_STATE", 0); - Register("CCU_DYN_BAT_LINK_STATE", 0); - Register("CCU_INS_BAT_LINK_STATE", 0); } //--------------------------------------------------------- @@ -190,11 +165,57 @@ bool CCU::OnNewMail(MOOSMSG_LIST &NewMail) { MOOSMSG_LIST::iterator p; for (p = NewMail.begin(); p != NewMail.end(); p++) { CMOOSMsg &msg = *p; - // 预留:未来如需通过 MOOSDB 下发指令可在此处理 + // pCanBridge 透传的 CAN 帧(变量名前缀 CAN_0x) + if (msg.m_sKey.rfind("CAN_0x", 0) == 0) { + handleCanMessage(msg); + continue; + } } return true; } +//--------------------------------------------------------- +// handleCanMessage:解码 pCanBridge 透传的 CAN 帧 +// +// 消息格式(pCanBridge 约定): +// m_sKey = "CAN_0x%08X"(CAN ID,扩展帧) +// m_sVal = 二进制数据域(dlc 字节) +// m_dfVal2 = 原始帧信息字节(bit7 FF / bit6 RTR / bit3~0 DLC) + +void CCU::handleCanMessage(CMOOSMsg& msg) { + if (!m_sysData) return; + + // 从变量名解析 CAN ID(跳过前缀 "CAN_0x" 共 6 字符) + uint32_t canId = 0; + if (std::sscanf(msg.m_sKey.c_str() + 6, "%x", &canId) != 1) return; + + m_sysData->incCanFrameCount(); + + // 远程帧无数据域,直接跳过 + if (msg.IsBinary() && msg.GetBinaryDataSize() > 0) { + unsigned int n = msg.GetBinaryDataSize(); + unsigned char* d = msg.GetBinaryData(); + if (!d) return; + + // 合并语义:取当前快照 -> 解码部分更新 -> 写回 + BmsStatusValue v = m_sysData->bmsStatus(); + if (decodeCanBmsFrame(canId, d, static_cast(n), v)) { + v.valid = true; + v.lastRxTime = MOOSTime(false); + m_sysData->updateBmsStatus(v); + + // 节流日志:1s 一条,便于联调观察 + double now = MOOSTime(); + if (now - m_lastBmsLog >= 1.0) { + m_lastBmsLog = now; + LOG_F(INFO, "[BMS/CAN] u=%.1fV i=%.1fA soc=%.1f%% fault=%d status=%d cellMax=%.3fV", + v.totalVoltage, v.totalCurrent, v.soc, + v.faultCode, v.statusBits, v.maxCellVoltage); + } + } + } +} + //--------------------------------------------------------- // Iterate:周期任务(AppTick 次/秒) @@ -209,7 +230,7 @@ bool CCU::Iterate() { m_lastStatusTx = now; } - // 电源协调器:操作步骤推进 + 锂电池心跳指令(1s) + 协调算法占位 + // 电源协调器:协调算法占位 if (m_coord) m_coord->tick(now); @@ -279,14 +300,11 @@ void CCU::handlePmMessage(Message* msg, const std::vector& frame) { m_fcLink->sendMessage(0x0001, &f); LOG_F(INFO, "[FC] forward control cmd=%d power=%d", f.cmd, f.outputPower); - // 锂电池启停/功率指令 -> 生成电池操作序列(协调器按步骤下发) - // 启停指令:00无效 / 10启动 / 20关闭 - if (m_coord) { - if (c.insBatCmd == 0x10) m_coord->submitPowerOn(BatRole::Ins); - else if (c.insBatCmd == 0x20) m_coord->submitPowerOff(BatRole::Ins); - if (c.dynBatCmd == 0x10) m_coord->submitPowerOn(BatRole::Dyn); - else if (c.dynBatCmd == 0x20) m_coord->submitPowerOff(BatRole::Dyn); - if (c.dynBatPower > 0) m_coord->submitSetPower(BatRole::Dyn, c.dynBatPower); + // 锂电池启停/功率指令:BMS CAN 协议未定义控制报文,暂不支持 + if (c.insBatCmd != 0 || c.dynBatCmd != 0 || c.dynBatPower != 0) { + LOG_F(WARNING, "[PM] battery cmd ignored (insBat=0x%02X dynBat=0x%02X dynPwr=%u): " + "BMS CAN 协议未定义控制报文", + c.insBatCmd, c.dynBatCmd, c.dynBatPower); } } else { LOG_F(ERROR, "[PM] control decode failed"); @@ -316,43 +334,6 @@ void CCU::handlePmMessage(Message* msg, const std::vector& frame) { } } -//--------------------------------------------------------- -// handleBatMessage:处理锂电池链路收到的消息(动力/仪表共用协议) - -void CCU::handleBatMessage(BatRole role, Message* msg, const std::vector& frame) { - if (!msg) return; - const char* tag = (role == BatRole::Dyn) ? "BAT_DYN" : "BAT_INS"; - switch (msg->id()) { - case 0x0003: { // 电池组状态反馈 - BatStatusValue v; - if (msg->decode(frame, static_cast(&v))) { - m_sysData->updateBatStatus(static_cast(role), v); - LOG_F(INFO, "[%s] status: wc=0x%02X selfCheck=0x%02X bus=0x%02X " - "soc=%.1f%% pwr_out=%.1fkW hb=%d", - tag, v.workCondition, v.selfCheckState, v.busContactorState, - v.soc * 0.1, v.currentAccessPower * 0.1, v.heartbeat); - } else { - LOG_F(ERROR, "[%s] status decode failed", tag); - } - break; - } - case 0x0004: { // 电池包报警信息 - BatPackAlarmValue v; - if (msg->decode(frame, static_cast(&v))) { - m_sysData->updateBatAlarm(static_cast(role), v); - LOG_F(INFO, "[%s] pack alarm: packNo=%d alarm1=0x%02X alarm2=0x%02X", - tag, v.packNo, v.alarmFlag[0], v.alarmFlag[1]); - } else { - LOG_F(ERROR, "[%s] pack alarm decode failed", tag); - } - break; - } - default: - LOG_F(WARNING, "[%s] unhandled msg id 0x%04X", tag, msg->id()); - break; - } -} - //--------------------------------------------------------- // sendPmStatus:整合最新 FC 状态,编码 PM 状态报文发送 @@ -489,29 +470,15 @@ bool CCU::buildReport() { << " tx:" << m_pmLink->txCount() << " err:" << m_pmLink->errorCount() << "\n"; } - if (m_dynBatLink) { - m_msgs << "动力锂电池链路 local:" << m_dynBatLink->localPort() - << " rx:" << m_dynBatLink->rxCount() - << " tx:" << m_dynBatLink->txCount() - << " err:" << m_dynBatLink->errorCount() << "\n"; - } - if (m_insBatLink) { - m_msgs << "仪表锂电池链路 local:" << m_insBatLink->localPort() - << " rx:" << m_insBatLink->rxCount() - << " tx:" << m_insBatLink->txCount() - << " err:" << m_insBatLink->errorCount() << "\n"; - } - if (m_coord) { - m_msgs << "协调器待执行步骤:" << m_coord->pendingSteps() << "\n"; - } if (m_sysData) { + m_msgs << "锂电池: BMS/CAN (经 pCanBridge CAN_0x* 订阅)\n"; + m_msgs << "CAN 帧接收计数:" << m_sysData->canFrameCount() << "\n"; + m_msgs << "BMS 报文接收计数:" << m_sysData->bmsStatusCount() << "\n"; m_msgs << "FC 状态接收次数:" << m_sysData->fcStatusCount() << "\n"; m_msgs << "PM 指令接收次数:" << m_sysData->pmControlCount() << "\n"; - m_msgs << "动力电池状态接收次数:" << m_sysData->batStatusCount(0) << "\n"; - m_msgs << "仪表电池状态接收次数:" << m_sysData->batStatusCount(1) << "\n"; } if (m_db) { m_msgs << "数据库记录数:" << m_db->count() << "\n"; } return true; -} \ No newline at end of file +} diff --git a/src/pCCU/CCU.h b/src/pCCU/CCU.h index a8e9026..f997eb6 100644 --- a/src/pCCU/CCU.h +++ b/src/pCCU/CCU.h @@ -5,7 +5,6 @@ #include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h" #include "comm/FcLinkManager.h" #include "comm/PmLinkManager.h" -#include "comm/BatLinkManager.h" #include "core/SystemData.h" #include "core/SnapshotBuilder.h" #include "core/PowerCoordinator.h" @@ -20,10 +19,10 @@ namespace ccu { // 数据流: // 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger // 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池 -// -> 锂电池启停/功率指令映射为电池操作序列(协调器) // 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003) -// 收 电池组状态(0x0003)/电池包报警(0x0004)(动力/仪表锂电池) -> SystemData 快照 -// 周期向锂电池下发设备控制指令(0x0001,1s 心跳);协调器按操作时序下发自检(0x0000)等 +// 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文 +// (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照 +// (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用) // 收发全部帧落库 SQLite;网页周期推送 JSON 快照。 //============================================================================ @@ -44,7 +43,9 @@ private: // 链路收帧处理 void handleFcMessage(Message* msg, const std::vector& frame); void handlePmMessage(Message* msg, const std::vector& frame); - void handleBatMessage(BatRole role, Message* msg, const std::vector& frame); + + // MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布) + void handleCanMessage(CMOOSMsg& msg); // 整合并发送 PM 状态报文 void sendPmStatus(); @@ -62,22 +63,14 @@ private: std::string m_pmHost = "192.168.0.140"; long m_pmPort = 5001; - // 锂电池链路(动力/仪表,ip与端口均可配置) - long m_dynBatLocalPort = 7001; - std::string m_dynBatHost = "192.168.100.137"; - long m_dynBatPort = 7000; - - long m_insBatLocalPort = 7002; - std::string m_insBatHost = "192.168.100.136"; - long m_insBatPort = 7000; - - long m_batWorkCondition = 10; // 工况设定 10工房/30实航/50科研 - std::string m_dbPath = "pccu_data.db"; std::string m_logPath = "pCCU.log"; int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开 bool m_webEnable = true; + // CAN BMS 日志节流(避免 60帧/s 刷爆日志) + double m_lastBmsLog = 0; + // 心跳 uint8_t m_pmHeartbeat = 0; double m_lastStatusTx = 0; @@ -85,8 +78,6 @@ private: // 组件 FcLinkManager* m_fcLink = nullptr; PmLinkManager* m_pmLink = nullptr; - BatLinkManager* m_dynBatLink = nullptr; - BatLinkManager* m_insBatLink = nullptr; SystemData* m_sysData = nullptr; DbStore* m_db = nullptr; SnapshotBuilder* m_snap = nullptr; @@ -96,4 +87,4 @@ private: } // namespace ccu -#endif // PCCU_CCU_H \ No newline at end of file +#endif // PCCU_CCU_H diff --git a/src/pCCU/CCU_Info.cpp b/src/pCCU/CCU_Info.cpp index 74664db..b29807f 100644 --- a/src/pCCU/CCU_Info.cpp +++ b/src/pCCU/CCU_Info.cpp @@ -15,11 +15,11 @@ void showSynopsis() { blk("SYNOPSIS: "); blk("------------------------------------ "); blk(" The pCCU application is the composite controller (CCU) "); - blk(" gateway between the fuel cell (FC) system / lithium battery "); - blk(" packs and pPowerManger. It receives FC & battery status, "); - blk(" integrates & forwards to pPowerManger, maps pPowerManger "); - blk(" commands to FC / battery operations, stores data in SQLite "); - blk(" and provides a web monitoring page. "); + blk(" gateway between the fuel cell (FC) system and pPowerManger. "); + 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(" "); } @@ -68,13 +68,7 @@ void showExampleConfigAndExit() { blk(" pm_local_port = 7000 // PM 指令接收端口 "); blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP "); blk(" pm_remote_port = 5001 // 控制主机接收端口 "); - blk(" dyn_bat_local_port = 7001 // 动力锂电池状态接收端口 "); - blk(" dyn_bat_remote_ip = 192.168.100.137 // 动力锂电池 IP "); - blk(" dyn_bat_remote_port = 7000 // 动力锂电池控制端口 "); - blk(" ins_bat_local_port = 7002 // 仪表锂电池状态接收端口 "); - blk(" ins_bat_remote_ip = 192.168.100.136 // 仪表锂电池 IP "); - blk(" ins_bat_remote_port = 7000 // 仪表锂电池控制端口 "); - blk(" bat_work_condition = 10 // 电池工况 10工房/30实航/50科研 "); + blk(" // 锂电池 BMS 走 CAN 总线(经 pCanBridge 的 CAN_0x* 透传) "); blk(" dbpath = pccu_data.db // 数据库路径 "); blk(" logpath = pCCU.log // 日志路径 "); blk(" web_port = 8080 // 网页端口(避开8090/18080) "); @@ -94,14 +88,14 @@ void showInterfaceAndExit() { blk(" "); blk("SUBSCRIPTIONS: "); blk("------------------------------------ "); - blk(" (数据交换主要通过 UDP 链路完成,MOOSDB 交互有限) "); + blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(锂电池 BMS) "); + blk(" CCU_FC_LINK_STATE = 运行状态字符串 "); + blk(" CCU_PM_LINK_STATE = 运行状态字符串 "); blk(" "); blk("PUBLICATIONS: "); blk("------------------------------------ "); blk(" CCU_FC_LINK_STATE = 运行状态字符串 "); blk(" CCU_PM_LINK_STATE = 运行状态字符串 "); - blk(" CCU_DYN_BAT_LINK_STATE = 运行状态字符串 "); - blk(" CCU_INS_BAT_LINK_STATE = 运行状态字符串 "); blk(" "); exit(0); } diff --git a/src/pCCU/CMakeLists.txt b/src/pCCU/CMakeLists.txt index e72a243..69002cc 100644 --- a/src/pCCU/CMakeLists.txt +++ b/src/pCCU/CMakeLists.txt @@ -24,7 +24,7 @@ SET(CCU_PROTOCOL_SRC protocol/MessageRegistry.cpp protocol/FcProtocol.cpp protocol/PmProtocol.cpp - protocol/BatProtocol.cpp + protocol/CanBms.cpp ) SET(CCU_COMM_SRC diff --git a/src/pCCU/comm/BatLinkManager.h b/src/pCCU/comm/BatLinkManager.h deleted file mode 100644 index 145c90f..0000000 --- a/src/pCCU/comm/BatLinkManager.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef PCCU_BAT_LINK_MANAGER_H -#define PCCU_BAT_LINK_MANAGER_H - -#include "LinkManager.h" -#include "../protocol/BatProtocol.h" - -namespace ccu { - -//============================================================================ -// BatLinkManager:面向锂电池组(动力/仪表,协议一致)的链路。 -// -// - 本地监听:接收电池状态反馈的端口(动力/仪表各自独立端口,可配置) -// - 发送目标:锂电池组(默认 动力 192.168.100.137:7000 / 仪表 192.168.100.136:7000) -// 地址端口均可由 .moos 配置覆盖。 -//============================================================================ - -class BatLinkManager : public LinkManager { -public: - BatLinkManager(BatRole role, long localPort, - const std::string& batHost, long batPort) - : LinkManager((role == BatRole::Dyn) ? "bat_dyn" : "bat_ins", localPort), - m_role(role), - m_batHost(batHost), m_batPort(batPort) { - registerBatMessages(registry()); - } - - BatRole role() const { return m_role; } - - void setBatAddress(const std::string& host, long port) { - m_batHost = host; m_batPort = port; - } - const std::string& batHost() const { return m_batHost; } - long batPort() const { return m_batPort; } - -protected: - std::string defaultRemoteHost() const override { return m_batHost; } - long defaultRemotePort() const override { return m_batPort; } - -private: - BatRole m_role; - std::string m_batHost; - long m_batPort; -}; - -} // namespace ccu - -#endif // PCCU_BAT_LINK_MANAGER_H diff --git a/src/pCCU/core/CoordFsm.cpp b/src/pCCU/core/CoordFsm.cpp index 69d2f3d..ab98a62 100644 --- a/src/pCCU/core/CoordFsm.cpp +++ b/src/pCCU/core/CoordFsm.cpp @@ -24,71 +24,53 @@ void CoordNormalState::react(TickEvent const &e) { // TODO(策略填写):周期决策入口(AppTick 频率,默认 4Hz)。 // // 读取最新状态: - // sys()->fcStatus() 燃料电池状态 - // sys()->batStatus((int)BatRole::Dyn) 动力锂电池状态 - // sys()->batStatus((int)BatRole::Ins) 仪表锂电池状态 - // sys()->batAlarm(role) 电池包报警信息 - // sys()->pmControl() 主机最新操控指令 + // sys()->fcStatus() 燃料电池状态 + // sys()->bmsStatus() 锂电池 BMS 状态(总电压/SOC/电流/故障码,CAN) + // sys()->pmControl() 主机最新操控指令 // - // 下发操作(提交后由协调器步骤引擎按序执行并等待确认): - // coord()->submitPowerOn(role) / submitPowerOff(role) - // coord()->submitSelfCheck(role) / submitSelfCheckReset(role) - // coord()->submitSetPower(role, kW) - // coord()->submitSwitchLowToHigh(low, high) / submitSwitchHighToLow(high, low) - // coord()->setWorkCondition(wc) + // 下发操作: // coord()->sendFcControl(FcControlValue) 直接控燃料电池 - // - // 防重复下发:提交前检查 coord()->busy() / pendingSteps() (void)e; } void CoordNormalState::react(PmControlEvent const &e) { // TODO(策略填写):响应主机操控指令(边沿触发,指令到达一次触发一次)。 - // 示例(按需解开并完善): - // if (e.cmd.dynBatCmd == 0x10) coord()->submitPowerOn(BatRole::Dyn); - // if (e.cmd.dynBatCmd == 0x20) coord()->submitPowerOff(BatRole::Dyn); - // if (e.cmd.dynBatPower > 0) coord()->submitSetPower(BatRole::Dyn, e.cmd.dynBatPower); - LOG_F(INFO, "[CoordFsm][Normal] PmControlEvent: mode=%d cmd=%d insBat=0x%02X dynBat=0x%02X dynPwr=%u", - e.cmd.mode, e.cmd.cmd, e.cmd.insBatCmd, e.cmd.dynBatCmd, e.cmd.dynBatPower); + LOG_F(INFO, "[CoordFsm][Normal] PmControlEvent: mode=%d cmd=%d power=%d", + e.cmd.mode, e.cmd.cmd, e.cmd.outputPower); } void CoordNormalState::react(StepDoneEvent const &e) { - // TODO(策略填写):单步操作完成/超时的处理。 + // TODO(策略填写):单步协调操作完成/超时的处理。 // e.confirmed==false 表示超时未确认; - // e.status.busContactorState / posDiodeState 可判 0x33/0x44 等故障值。 - // 切换流程示例(按需解开):步骤失败时进入故障态 - // if (!e.confirmed) transit(); - LOG_F(INFO, "[CoordFsm][Normal] StepDoneEvent: role=%d type=%d confirmed=%d", - static_cast(e.role), static_cast(e.type), e.confirmed); + // e.status.faultCode / statusBits 可判 BMS 故障。 + LOG_F(INFO, "[CoordFsm][Normal] StepDoneEvent: confirmed=%d soc=%.1f%% u=%.1fV", + e.confirmed, e.status.soc, e.status.totalVoltage); } //============================================================================ -// CoordSwitchingState 电池切换中 +// CoordBusyState 协调操作进行中 //============================================================================ -void CoordSwitchingState::entry() { - LOG_F(INFO, "[CoordFsm] 进入状态: CoordSwitchingState"); +void CoordBusyState::entry() { + LOG_F(INFO, "[CoordFsm] 进入状态: CoordBusyState"); } -void CoordSwitchingState::exit() { - LOG_F(INFO, "[CoordFsm] 离开状态: CoordSwitchingState"); +void CoordBusyState::exit() { + LOG_F(INFO, "[CoordFsm] 离开状态: CoordBusyState"); } -void CoordSwitchingState::react(TickEvent const &e) { - // TODO(策略填写):切换过程中的周期监视(如各步骤进度/超时统计、 - // 是否需要中止切换并回退)。状态可通过 sys()->batStatus(role) 获取。 +void CoordBusyState::react(TickEvent const &e) { + // TODO(策略填写):操作进行中的周期监视(进度/超时统计、是否需要中止)。 + // 状态可通过 sys()->bmsStatus() / sys()->fcStatus() 获取。 (void)e; } -void CoordSwitchingState::react(StepDoneEvent const &e) { - // TODO(策略填写):切换步骤完成推进。 - // 全部步骤完成后返回正常运行态(coord()->pendingSteps()==0 可作为判据): - // if (coord()->pendingSteps() == 0) transit(); - // 步骤失败(超时或接触器故障)可中止并进入故障态: +void CoordBusyState::react(StepDoneEvent const &e) { + // TODO(策略填写):操作步骤完成推进。 + // 全部步骤完成后返回正常运行态;步骤失败可中止并进入故障态: // if (!e.confirmed) transit(); - LOG_F(INFO, "[CoordFsm][Switching] StepDoneEvent: role=%d type=%d confirmed=%d pending=%zu", - static_cast(e.role), static_cast(e.type), e.confirmed, - coord() ? coord()->pendingSteps() : 0); + // else transit(); + LOG_F(INFO, "[CoordFsm][Busy] StepDoneEvent: confirmed=%d", e.confirmed); } //============================================================================ @@ -106,13 +88,12 @@ void CoordFaultState::exit() { void CoordFaultState::react(TickEvent const &e) { // TODO(策略填写):故障处理与恢复判断(周期检查故障是否消除, // 消除后 transit() 回正常运行态)。 - // 可用:sys()->fcStatus().fc_fault_level、sys()->batStatus(role).emergencyState / - // alarmFlag、sys()->batAlarm(role) 等。 + // 可用:sys()->fcStatus().fc_fault_level、sys()->bmsStatus().faultCode 等。 (void)e; } void CoordFaultState::react(StepDoneEvent const &e) { - // TODO(策略填写):故障态下操作步骤结果处理(如降级下电指令的确认)。 + // TODO(策略填写):故障态下操作步骤结果处理(如降级恢复的确认)。 (void)e; } diff --git a/src/pCCU/core/CoordFsm.hpp b/src/pCCU/core/CoordFsm.hpp index bd30a35..dbc287a 100644 --- a/src/pCCU/core/CoordFsm.hpp +++ b/src/pCCU/core/CoordFsm.hpp @@ -4,7 +4,7 @@ #include "../fsm/tinyfsm.hpp" #include "../protocol/FcProtocol.h" #include "../protocol/PmProtocol.h" -#include "../protocol/BatProtocol.h" +#include "../protocol/CanBms.h" #include "PowerCoordinator.h" namespace ccu { @@ -22,59 +22,31 @@ class SystemData; // 【设计约定】 // - 边沿事件走 dispatch:指令到达(PmControlEvent)、操作步骤完成(StepDoneEvent)、 // 周期节拍(TickEvent); -// - 电平状态不进事件:最新 FC/电池状态由状态机在 TickEvent 中直接读取 -// SystemData 快照,避免 1Hz 状态报文淹没事件队列; +// - 电平状态不进事件:最新 FC/BMS 状态由状态机在 TickEvent 中直接读取 +// SystemData 快照,避免周期状态报文淹没事件队列; // - TinyFSM 非线程安全:dispatch 必须收敛到单线程(MOOS 主循环线程), -// 链路接收线程产生的事件一律经 PowerCoordinator::pushEvent() 排队, -// 由 tick() 在主线程统一 drain 后逐个 dispatch(见接线指南第 1 步)。 +// 接收线程产生的事件一律先排队,由 tick() 在主线程统一 drain 后 +// 逐个 dispatch。 // // 【接线指南:将来启用时按以下步骤操作】 // -// 1) PowerCoordinator 增加事件桥(core/PowerCoordinator.h/.cpp): -// a. 新增成员: -// std::function m_tickHook = nullptr; // 节拍转发 -// std::mutex m_evtMutex; -// std::deque> m_events; // 事件队列(闭包) -// 新增方法: -// void setTickHook(std::function h) { m_tickHook = std::move(h); } -// void pushEvent(std::function fn) { // 线程安全;带上限防积压 -// std::lock_guard l(m_evtMutex); -// if (m_events.size() >= 512) { m_events.pop_front(); /*LOG告警*/ } -// m_events.push_back(std::move(fn)); -// } -// b. tick() 末尾 drain(单线程执行闭包;coordinationTick 占位改为转发): -// if (m_tickHook) m_tickHook(now); -// std::deque> evs; -// { std::lock_guard l(m_evtMutex); evs.swap(m_events); } -// for (auto& f : evs) f(); -// c. processSteps 步骤完成/超时处(pop_front 之前,仅接线后产生事件): -// if (m_tickHook) { -// BatOpStep info = s; // 拷贝步骤信息 -// BatStatusValue st = m_sys ? m_sys->batStatus(static_cast(s.role)) -// : BatStatusValue(); -// bool ok = confirmed; -// pushEvent([info, st, ok]() { -// CoordFsm::dispatch(StepDoneEvent(info.role, info.type, ok, st)); -// }); -// } -// (事件产生以 m_tickHook 非空为前提:未接线时零开销、零行为) +// 1) 事件桥(可在 PowerCoordinator 或独立 EventBus 中实现): +// a. 线程安全事件队列:pushEvent(std::function) +// b. tick() 末尾 drain 队列并逐个执行(单线程 dispatch) // -// 2) CCU::OnStartUp(m_coord->setup(...) 之后): +// 2) CCU::OnStartUp: // CoordFsm::init(m_coord, m_sysData); // CoordFsm::start(); -// m_coord->setTickHook([](double now) { -// CoordFsm::dispatch(TickEvent(now)); -// }); // // 3) CCU::handlePmMessage 的 0x0001 分支(decode 成功后): -// m_coord->pushEvent([c]() { CoordFsm::dispatch(PmControlEvent(c)); }); +// CoordFsm::dispatch(PmControlEvent(c)); // (c 为已解析的 PmControlValue,按值捕获) // // 4) 验证:编译运行后日志出现 "[CoordFsm] 进入状态: CoordNormalState" 即接线成功。 // // 【状态说明】 // CoordNormalState 正常运行(初始状态):周期功率分配/阈值判断、响应主机指令 -// CoordSwitchingState 电池切换中:等待 StepDoneEvent 推进,完成后返回 Normal +// CoordBusyState 协调操作进行中:等待 StepDoneEvent 推进,完成后返回 Normal // CoordFaultState 故障处理:告警抑制/降级运行/恢复判断 //============================================================================ @@ -94,16 +66,13 @@ struct PmControlEvent : tinyfsm::Event { explicit PmControlEvent(const PmControlValue& c) : cmd(c) {} }; -// 电池操作步骤完成/超时:由协调器步骤引擎产生 +// 协调操作步骤完成/超时:由协调动作引擎产生(预留)。 +// status 携带完成时刻的 BMS 状态快照(SOC/电压/电流/故障码,可判故障) struct StepDoneEvent : tinyfsm::Event { - BatRole role; // 目标电池 - BatOpType type; // 步骤类型 - bool confirmed; // true=状态反馈确认;false=超时未确认 - BatStatusValue status; // 完成时刻状态快照(含接触器状态/报警标识字,可判 0x33/0x44 等故障) - StepDoneEvent() - : role(BatRole::Dyn), type(BatOpType::ApplyControl), confirmed(false) {} - StepDoneEvent(BatRole r, BatOpType t, bool ok, const BatStatusValue& s) - : role(r), type(t), confirmed(ok), status(s) {} + bool confirmed; // true=确认完成;false=超时未确认 + BmsStatusValue status; // 完成时刻 BMS 状态快照 + StepDoneEvent() : confirmed(false) {} + StepDoneEvent(bool ok, const BmsStatusValue& s) : confirmed(ok), status(s) {} }; //---- 状态机 --------------------------------------------------------------- @@ -145,8 +114,8 @@ public: void react(StepDoneEvent const &e) override; }; -// 电池切换中(提交 submitSwitchLowToHigh/HighToLow 后进入) -class CoordSwitchingState : public CoordFsm { +// 协调操作进行中(提交多步协调动作后进入) +class CoordBusyState : public CoordFsm { public: void entry() override; void exit() override; diff --git a/src/pCCU/core/PowerCoordinator.cpp b/src/pCCU/core/PowerCoordinator.cpp index 90737fc..345a41f 100644 --- a/src/pCCU/core/PowerCoordinator.cpp +++ b/src/pCCU/core/PowerCoordinator.cpp @@ -2,384 +2,49 @@ #include "SystemData.h" #include "../comm/FcLinkManager.h" #include "../comm/PmLinkManager.h" -#include "../comm/BatLinkManager.h" -#include "loguru.hpp" -#include namespace ccu { -namespace { - -//---- 状态反馈判定辅助(0x0003 电池组状态) ---- - -// 电池自检完成(正常或异常均视为"自检动作结束",异常由上层算法处理) -bool selfCheckFinished(const BatStatusValue& s) { - return s.selfCheckState == BAT_SCST_OK || s.selfCheckState == BAT_SCST_FAULT; -} - -// 母线接触器闭合/断开完成 -bool busClosed(const BatStatusValue& s) { return s.busContactorState == BAT_BUSST_CLOSED; } -bool busOpened(const BatStatusValue& s) { return s.busContactorState == BAT_BUSST_OPENED; } - -// 正极接触器闭合/断开完成(低4位) -bool posClosed(const BatStatusValue& s) { return batPosStateOf(s.posDiodeState) == BAT_POSST_CLOSED; } -bool posOpened(const BatStatusValue& s) { return batPosStateOf(s.posDiodeState) == BAT_POSST_OPENED; } - -// 二极管回路闭合/断开完成(高4位) -bool diodeClosed(const BatStatusValue& s) { return batDiodeStateOf(s.posDiodeState) == BAT_DIODST_CLOSED; } -bool diodeOpened(const BatStatusValue& s) { return batDiodeStateOf(s.posDiodeState) == BAT_DIODST_OPENED; } - -// 自检步骤确认:优先确认"自检完成";若设备从未反馈(状态恒 0),由超时兜底 -std::function doneSelfCheck() { - return std::function(selfCheckFinished); -} - -} // namespace - -//--------------------------------------------------------- -// 构造 / 依赖注入 - PowerCoordinator::PowerCoordinator() {} void PowerCoordinator::setup(SystemData* sys, FcLinkManager* fc, - PmLinkManager* pm, - BatLinkManager* dynBat, - BatLinkManager* insBat) { + PmLinkManager* pm) { m_sys = sys; m_fc = fc; m_pm = pm; - m_bat[static_cast(BatRole::Dyn)] = dynBat; - m_bat[static_cast(BatRole::Ins)] = insBat; } -//--------------------------------------------------------- -// 操作接口 - -// 提交一个步骤:挂入其目标电池的队列(同一电池内的步骤按序执行)。 -// 复合操作(电池切换)的各步骤依次调用本函数即可保持时序。 -static void pushStep(std::deque& q, const BatOpStep& s) { - q.push_back(s); -} - -bool PowerCoordinator::submit(const BatOpStep& step) { - std::deque& q = m_steps[static_cast(step.role)]; - pushStep(q, step); - LOG_F(INFO, "[Coord] operation step queued: role=%s type=%d (pending=%zu)", - batRoleName(step.role), static_cast(step.type), q.size()); - return true; -} - -bool PowerCoordinator::submitSelfCheck(BatRole role) { - BatOpStep s; - s.role = role; - s.type = BatOpType::SelfCheck; - s.selfCheck = true; - s.done = doneSelfCheck(); - s.timeoutSec = 30.0; // 自检耗时较长 - return submit(s); -} - -bool PowerCoordinator::submitSelfCheckReset(BatRole role) { - BatOpStep s; - s.role = role; - s.type = BatOpType::SelfCheck; - s.selfCheckReset = true; - s.done = nullptr; // 复位指令无确认反馈,仅等待超时 - s.timeoutSec = 2.0; - return submit(s); -} - -bool PowerCoordinator::submitPowerOn(BatRole role) { - BatOpStep s; - s.role = role; - s.type = BatOpType::ApplyControl; - s.busContactor = BAT_CTR_CLOSE; // 接通(含预充过程) - s.done = busClosed; - s.timeoutSec = 30.0; - return submit(s); -} - -bool PowerCoordinator::submitPowerOff(BatRole role) { - BatOpStep s; - s.role = role; - s.type = BatOpType::ApplyControl; - s.busContactor = BAT_CTR_OPEN; // 断开 - s.done = busOpened; - s.timeoutSec = 15.0; - return submit(s); -} - -bool PowerCoordinator::submitSetPower(BatRole role, uint16_t kw) { - if (kw > 500) kw = 500; // 协议范围 0~500 kW - BatOpStep s; - s.role = role; - s.type = BatOpType::ApplyControl; - s.setPower = true; - s.powerKw = kw; - s.done = nullptr; // 以电池功率配置状态反馈为准,占位阶段直接下发 - s.timeoutSec = 1.0; - return submit(s); -} - -bool PowerCoordinator::submitSwitchLowToHigh(BatRole low, BatRole high) { - // 正常切换,低压切高压(协议文档时序): - // 1.发送高压电池自检命令 - // 2.低压电池二极管接触器闭合 - // 3.低压电池正极接触器断开 - // 4.高压电池母线接触器闭合 - // 5.高压电池二极管接触器闭合 - // 6.低压电池二极管接触器断开 - // 全部步骤挂入高压电池队列,保证跨电池步骤按序执行 - std::deque& q = m_steps[static_cast(high)]; - - BatOpStep s1; - s1.role = high; s1.type = BatOpType::SelfCheck; - s1.selfCheck = true; s1.done = doneSelfCheck(); s1.timeoutSec = 30.0; - q.push_back(s1); - - BatOpStep s2; - s2.role = low; s2.type = BatOpType::ApplyControl; - s2.diodeContactor = BAT_CTR_CLOSE; s2.done = diodeClosed; s2.timeoutSec = 15.0; - q.push_back(s2); - - BatOpStep s3; - s3.role = low; s3.type = BatOpType::ApplyControl; - s3.posContactor = BAT_CTR_OPEN; s3.done = posOpened; s3.timeoutSec = 15.0; - q.push_back(s3); - - BatOpStep s4; - s4.role = high; s4.type = BatOpType::ApplyControl; - s4.busContactor = BAT_CTR_CLOSE; s4.done = busClosed; s4.timeoutSec = 30.0; - q.push_back(s4); - - BatOpStep s5; - s5.role = high; s5.type = BatOpType::ApplyControl; - s5.diodeContactor = BAT_CTR_CLOSE; s5.done = diodeClosed; s5.timeoutSec = 15.0; - q.push_back(s5); - - BatOpStep s6; - s6.role = low; s6.type = BatOpType::ApplyControl; - s6.diodeContactor = BAT_CTR_OPEN; s6.done = diodeOpened; s6.timeoutSec = 15.0; - q.push_back(s6); - - LOG_F(INFO, "[Coord] switch low->high submitted (%s -> %s), 6 steps queued on %s", - batRoleName(low), batRoleName(high), batRoleName(high)); - return true; -} - -bool PowerCoordinator::submitSwitchHighToLow(BatRole high, BatRole low) { - // 异常切换,高压切低压(协议文档时序): - // 1.发送低压电池自检命令 - // 2.高压电池二极管接触器闭合 - // 3.低压电池二极管接触器闭合 - // 4.高压电池母线接触器断开 - // 5.低压电池正极接触器闭合 - // 全部步骤挂入低压电池队列,保证跨电池步骤按序执行 - std::deque& q = m_steps[static_cast(low)]; - - BatOpStep s1; - s1.role = low; s1.type = BatOpType::SelfCheck; - s1.selfCheck = true; s1.done = doneSelfCheck(); s1.timeoutSec = 30.0; - q.push_back(s1); - - BatOpStep s2; - s2.role = high; s2.type = BatOpType::ApplyControl; - s2.diodeContactor = BAT_CTR_CLOSE; s2.done = diodeClosed; s2.timeoutSec = 15.0; - q.push_back(s2); - - BatOpStep s3; - s3.role = low; s3.type = BatOpType::ApplyControl; - s3.diodeContactor = BAT_CTR_CLOSE; s3.done = diodeClosed; s3.timeoutSec = 15.0; - q.push_back(s3); - - BatOpStep s4; - s4.role = high; s4.type = BatOpType::ApplyControl; - s4.busContactor = BAT_CTR_OPEN; s4.done = busOpened; s4.timeoutSec = 15.0; - q.push_back(s4); - - BatOpStep s5; - s5.role = low; s5.type = BatOpType::ApplyControl; - s5.posContactor = BAT_CTR_CLOSE; s5.done = posClosed; s5.timeoutSec = 15.0; - q.push_back(s5); - - LOG_F(INFO, "[Coord] switch high->low submitted (%s -> %s), 5 steps queued on %s", - batRoleName(high), batRoleName(low), batRoleName(low)); - return true; -} - -void PowerCoordinator::setWorkCondition(uint8_t wc) { - desired(BatRole::Dyn).workCondition = wc; - desired(BatRole::Ins).workCondition = wc; - LOG_F(INFO, "[Coord] work condition set to 0x%02X for both batteries", wc); -} - -//--------------------------------------------------------- -// 周期驱动 - void PowerCoordinator::tick(double now) { - processSteps(now); - periodicControl(now); coordinationTick(now); } -size_t PowerCoordinator::pendingSteps() const { - return m_steps[0].size() + m_steps[1].size(); -} - -// 操作步骤执行:每条队列的当前步骤下发后等待状态反馈确认或超时。 -// 动力/仪表两条队列相互独立、并行推进。 -void PowerCoordinator::processSteps(double now) { - for (int i = 0; i < 2; ++i) { - std::deque& q = m_steps[i]; - if (q.empty()) { - m_stepActive[i] = false; - continue; - } - BatOpStep& s = q.front(); - - if (!m_stepActive[i]) { - // 首次下发 - m_stepStart[i] = now; - m_stepLastTx[i] = now; - applyStep(s, now); - m_stepActive[i] = true; - continue; - } - - // 自检指令周期重发(1s),直至确认完成 - if (s.type == BatOpType::SelfCheck && (now - m_stepLastTx[i]) >= 1.0) { - applyStep(s, now); - } - - bool confirmed = false; - if (s.done && m_sys) { - const BatStatusValue st = m_sys->batStatus(static_cast(s.role)); - confirmed = s.done(st); - } - bool timedOut = (now - m_stepStart[i]) >= s.timeoutSec; - - if (confirmed || timedOut) { - if (timedOut && !confirmed) { - LOG_F(WARNING, "[Coord] step timeout (role=%s type=%d), continue next step", - batRoleName(s.role), static_cast(s.type)); - } else { - LOG_F(INFO, "[Coord] step confirmed (role=%s type=%d)", - batRoleName(s.role), static_cast(s.type)); - } - q.pop_front(); - m_stepActive[i] = false; - } - } -} - -// 心跳:周期 1s 向两块锂电池下发设备控制指令(携带当前期望状态) -void PowerCoordinator::periodicControl(double now) { - for (int i = 0; i < 2; ++i) { - DesiredState& d = m_desired[i]; - if ((now - d.lastCtrlTx) >= 1.0) - sendControl(static_cast(i), now); - } -} - -//--------------------------------------------------------- -// 指令下发 - -void PowerCoordinator::applyStep(const BatOpStep& s, double now) { - const int idx = static_cast(s.role); - if (s.type == BatOpType::SelfCheck) { - sendSelfCheck(s.role, s.selfCheck, s.selfCheckReset); - m_stepLastTx[idx] = now; - return; - } - // ApplyControl:更新期望状态并立即下发一帧 - DesiredState& d = desired(s.role); - if (s.busContactor) d.busContactor = s.busContactor; - if (s.posContactor) d.posContactor = s.posContactor; - if (s.diodeContactor) d.diodeContactor = s.diodeContactor; - if (s.setPower) d.powerKw = s.powerKw; - sendControl(s.role, now); - m_stepLastTx[idx] = now; -} - -bool PowerCoordinator::sendSelfCheck(BatRole role, bool start, bool reset) { - BatLinkManager* l = batLink(role); - if (!l || !l->isRunning()) return false; - BatSelfCheckValue v; - v.selfCheck = start ? BAT_SC_START : BAT_SC_NONE; - v.selfCheckReset = reset ? BAT_SCR_RESET : BAT_SCR_NONE; - bool ok = l->sendMessage(0x0000, &v); - LOG_F(INFO, "[Coord] %s self-check cmd sent: start=%d reset=%d -> %s", - batRoleName(role), start, reset, ok ? "ok" : "fail"); - return ok; -} - -bool PowerCoordinator::sendControl(BatRole role, double now) { - BatLinkManager* l = batLink(role); - if (!l || !l->isRunning()) return false; - - DesiredState& d = desired(role); - - BatControlValue c; - // 系统时间(本地时间) - time_t t = ::time(nullptr); - struct tm lt; - localtime_r(&t, <); - c.year = static_cast(lt.tm_year + 1900); - c.month = static_cast(lt.tm_mon + 1); - c.day = static_cast(lt.tm_mday); - c.hour = static_cast(lt.tm_hour); - c.minute = static_cast(lt.tm_min); - c.second = static_cast(lt.tm_sec); - c.ms10 = 0; - - c.workCondition = d.workCondition; - c.busContactor = d.busContactor; - c.posContactor = d.posContactor; - c.diodeContactor = d.diodeContactor; - c.powerKw = d.powerKw; - c.reserved = 0; - c.heartbeat = d.heartbeat++; - - if (l->sendMessage(0x0001, &c)) { - d.lastCtrlTx = now; - return true; - } - return false; -} - bool PowerCoordinator::sendFcControl(const FcControlValue& fcCmd) { if (!m_fc || !m_fc->isRunning()) return false; return m_fc->sendMessage(0x0001, &fcCmd); } -BatLinkManager* PowerCoordinator::batLink(BatRole role) const { - return m_bat[static_cast(role)]; -} - //--------------------------------------------------------- // 协调算法占位 // // 【占位说明】 -// 本函数为锂电池与燃料电池协调策略的入口,由 CCU::Iterate 周期调用。 +// 本函数为锂电池(BMS/CAN)与燃料电池协调策略的入口, +// 由 CCU::Iterate 周期调用。 // // 可用输入(通过 m_sys 读取最新状态): -// - m_sys->fcStatus() : 燃料电池最新状态 -// - m_sys->batStatus((int)role) : 动力/仪表锂电池最新状态 -// - m_sys->batAlarm((int)role) : 动力/仪表电池包报警信息 -// - m_sys->pmControl() : 控制主机最新操控指令 +// - m_sys->fcStatus() : 燃料电池最新状态 +// - m_sys->bmsStatus() : 锂电池 BMS 最新状态(总电压/SOC/电流/故障码, +// CAN 经 pCanBridge 透传,protocol/CanBms.h) +// - m_sys->pmControl() : 控制主机最新操控指令 // -// 可用操作(提交后将按步骤下发指令): -// - submitPowerOn/Off(role) : 电池上下电(母线接触器) -// - submitSetPower(role, kW) : 电池功率设定 -// - submitSelfCheck(role) : 电池自检 -// - submitSwitchLowToHigh(low, high) : 正常切换(低压->高压) -// - submitSwitchHighToLow(high, low) : 异常切换(高压->低压) -// - setWorkCondition(wc) : 工况设定 -// - sendFcControl(FcControlValue) : 直接下发燃料电池控制指令 +// 可用操作: +// - sendFcControl(FcControlValue) : 直接下发燃料电池控制指令 // -// TODO(开发者完善):在此实现具体的功率分配、电池切换时机、 -// 燃料电池与锂电池联合调度等协调策略。当前为空实现。 +// 说明:BMS CAN 协议未定义远程控制报文(启停/接触器/功率设定), +// 电池侧控制暂不可用;协调策略围绕 FC 与 BMS 状态联动设计。 +// +// TODO(开发者完善):在此实现具体的功率分配、燃料电池与 +// 锂电池联合调度等协调策略。当前为空实现。 //--------------------------------------------------------- void PowerCoordinator::coordinationTick(double now) { (void)now; // 占位:暂不执行任何协调动作 diff --git a/src/pCCU/core/PowerCoordinator.h b/src/pCCU/core/PowerCoordinator.h index 5bfd6d4..c7296c6 100644 --- a/src/pCCU/core/PowerCoordinator.h +++ b/src/pCCU/core/PowerCoordinator.h @@ -2,10 +2,6 @@ #define PCCU_POWER_COORDINATOR_H #include -#include -#include -#include -#include "../protocol/BatProtocol.h" #include "../protocol/FcProtocol.h" namespace ccu { @@ -13,54 +9,21 @@ namespace ccu { class SystemData; class FcLinkManager; class PmLinkManager; -class BatLinkManager; //============================================================================ // PowerCoordinator:电源协调器。 // // 职责: -// 1. 操作 -> 指令:把"自检/上下电/电池切换/功率设定"等高级操作, -// 按协议文档要求的时序拆解为操作步骤队列,逐步下发锂电池指令, -// 并以电池状态反馈确认每一步的执行结果(超时兜底)。 -// 2. 心跳维持:周期(1s)向动力/仪表锂电池下发设备控制指令(0x0001), -// 携带当前期望状态(工况/接触器/功率)与自增心跳。 -// 3. 协调算法占位:coordinationTick() 为锂电池与燃料电池协调策略的 -// 占位入口,由后续完善(当前为空实现)。 +// 1. FC 控制指令下发(供协调算法联动使用)。 +// 2. 协调算法占位:coordinationTick() 为锂电池(BMS/CAN)与燃料电池 +// 协调策略的占位入口,由后续完善(当前为空实现)。 // -// 电池切换时序(docs/锂电池协议20230324.docx): -// 正常切换(低压->高压): -// 高压电池自检 -> 低压二极管闭合 -> 低压正极断开 -// -> 高压母线闭合 -> 高压二极管闭合 -> 低压二极管断开 -// 异常切换(高压->低压): -// 低压电池自检 -> 高压二极管闭合 -> 低压二极管闭合 -// -> 高压母线断开 -> 低压正极闭合 +// 说明:锂电池通信已切换为 CAN 总线(BMS 协议,经 pCanBridge 透传, +// 见 protocol/CanBms.h)。BMS 报文仅含状态字段(电压/电流/SOC/故障码), +// 未定义远程控制报文,因此原 UDP 电池指令链路(自检/接触器/功率设定) +// 已移除;FC 控制仍走 UDP。 //============================================================================ -// 操作步骤类型 -enum class BatOpType { - SelfCheck, // 下发自检指令(0x0000) - ApplyControl, // 修改接触器/功率期望值(随 0x0001 控制指令下发) -}; - -// 单个操作步骤 -struct BatOpStep { - BatRole role = BatRole::Dyn; - BatOpType type = BatOpType::ApplyControl; - // SelfCheck 参数 - bool selfCheck = false; // 触发自检(01H) - bool selfCheckReset = false; // 自检复位(55H) - // ApplyControl 参数(0 = 保持不变,仅下发非 0 值时更新期望状态) - uint8_t busContactor = BAT_CTR_NONE; // BAT_CTR_CLOSE / BAT_CTR_OPEN - uint8_t posContactor = BAT_CTR_NONE; - uint8_t diodeContactor = BAT_CTR_NONE; - bool setPower = false; // 是否更新功率期望 - uint16_t powerKw = 0; // 电池组功率 kW (0~500) - // 完成判定:返回 true 表示该步骤完成(以状态反馈 0x0003 为准); - // 为空时仅等待下发后超时结束 - std::function done; - double timeoutSec = 10.0; // 确认超时(超时也进入下一步并告警) -}; - class PowerCoordinator { public: PowerCoordinator(); @@ -68,78 +31,22 @@ public: // 注入依赖(均在 CCU::OnStartUp 中创建完成后调用) void setup(SystemData* sys, FcLinkManager* fc, - PmLinkManager* pm, - BatLinkManager* dynBat, - BatLinkManager* insBat); + PmLinkManager* pm); - //---------------- 操作接口(生成指令序列) ---------------- - // 单电池自检(随状态反馈确认自检完成) - bool submitSelfCheck(BatRole role); - // 自检复位(下发一次,无确认判定) - bool submitSelfCheckReset(BatRole role); - // 上电:母线接触器接通(55H,含预充过程) - bool submitPowerOn(BatRole role); - // 下电:母线接触器断开(77H) - bool submitPowerOff(BatRole role); - // 功率设定(kW,0~500) - bool submitSetPower(BatRole role, uint16_t kw); - // 正常切换:低压 -> 高压(协议文档时序) - bool submitSwitchLowToHigh(BatRole low, BatRole high); - // 异常切换:高压 -> 低压(协议文档时序) - bool submitSwitchHighToLow(BatRole high, BatRole low); - // 工况设定(BAT_WC_WORKSHOP/BAT_WC_SEA_TRIAL/BAT_WC_RESEARCH),随心跳帧下发 - void setWorkCondition(uint8_t wc); - - bool busy() const { return pendingSteps() > 0; } - size_t pendingSteps() const; // 两条队列中待执行步骤总数 - - //---------------- 周期驱动 ---------------- - // 由 CCU::Iterate 周期调用(now = MOOSTime()) + // 周期驱动:由 CCU::Iterate 周期调用(now = MOOSTime()) void tick(double now); // 直接向燃料电池下发控制指令(供协调算法联动使用) bool sendFcControl(const FcControlValue& fcCmd); //---------------- 协调算法占位 ---------------- - // TODO(算法完善):锂电池与燃料电池协调策略入口。 + // TODO(算法完善):锂电池(BMS/CAN)与燃料电池协调策略入口。 void coordinationTick(double now); private: - // 每电池期望状态(随 0x0001 周期下发) - struct DesiredState { - uint8_t workCondition = BAT_WC_NONE; - uint8_t busContactor = BAT_CTR_NONE; - uint8_t posContactor = BAT_CTR_NONE; - uint8_t diodeContactor = BAT_CTR_NONE; - uint16_t powerKw = 0; - uint8_t heartbeat = 0; - double lastCtrlTx = 0; // 上次控制帧发送时间 - }; - - bool submit(const BatOpStep& step); - void processSteps(double now); - void periodicControl(double now); - void applyStep(const BatOpStep& s, double now); - bool sendSelfCheck(BatRole role, bool start, bool reset); - bool sendControl(BatRole role, double now); - - BatLinkManager* batLink(BatRole role) const; - DesiredState& desired(BatRole role) { return m_desired[static_cast(role)]; } - SystemData* m_sys = nullptr; FcLinkManager* m_fc = nullptr; PmLinkManager* m_pm = nullptr; - BatLinkManager* m_bat[2] = {nullptr, nullptr}; // [Dyn, Ins] - - DesiredState m_desired[2]; - - // 每电池一条步骤队列:动力/仪表可并行执行各自的操作; - // 复合操作(如电池切换)的全部步骤按序挂入其首个步骤目标电池的队列, - // 步骤自身携带目标 role,从而保证跨电池时序 - std::deque m_steps[2]; - bool m_stepActive[2] = {false, false}; - double m_stepStart[2] = {0, 0}; - double m_stepLastTx[2] = {0, 0}; }; } // namespace ccu diff --git a/src/pCCU/core/SnapshotBuilder.cpp b/src/pCCU/core/SnapshotBuilder.cpp index 11eb55d..3fcc37c 100644 --- a/src/pCCU/core/SnapshotBuilder.cpp +++ b/src/pCCU/core/SnapshotBuilder.cpp @@ -1,5 +1,6 @@ #include "SnapshotBuilder.h" #include "../store/DbStore.h" +#include "../protocol/CanBms.h" #include "json/json.h" #include #include @@ -34,14 +35,6 @@ std::string describeMessageName(const std::string& link, uint16_t id) { case 0x0004: return "PM状态报文"; default: break; } - } else if (link == "bat_dyn" || link == "bat_ins") { - switch (id) { - case 0x0000: return "电池自检指令"; - case 0x0001: return "电池控制指令"; - case 0x0003: return "电池组状态"; - case 0x0004: return "电池包报警"; - default: break; - } } char buf[32]; std::snprintf(buf, sizeof(buf), "未知(0x%04X)", id); @@ -51,8 +44,6 @@ 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 == "bat_dyn") peer = "动力电池"; - else if (link == "bat_ins") peer = "仪表电池"; return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU"); } @@ -218,84 +209,19 @@ JsonVal pmFbJson(const PmParamSetFbValue& v) { return j; } -// 锂电池组状态(0x0003 收)-> JSON(关键字段) -JsonVal batStatusJson(const BatStatusValue& v) { +// 锂电池 BMS 状态(CAN,docs/BMS_协议字段定义.xlsx)-> JSON +JsonVal bmsStatusJson(const BmsStatusValue& v) { JsonVal j(Json::objectValue); - j["time"] = JsonVal(static_cast(v.year)); - put(j, "workCondition", v.workCondition); - put(j, "selfCheckState", v.selfCheckState); - put(j, "powerCfgState", v.powerCfgState); - put(j, "busContactorState", v.busContactorState); - put(j, "posDiodeState", v.posDiodeState); - put(j, "emergencyState", v.emergencyState); - put(j, "soc", v.soc); // 0.1% - put(j, "energy", v.energy); // 0.1kWh - put(j, "maxDischargePower", v.maxDischargePower); // 0.1kW - put(j, "maxChargePower", v.maxChargePower); // 0.1kW - put(j, "currentAccessPower", v.currentAccessPower); // 0.1kW - put(j, "packOnlineFlag", v.packOnlineFlag); - put(j, "packAccessFlag", v.packAccessFlag); - put(j, "relayPosCharge", v.relayPosCharge); - put(j, "relayPreNeg", v.relayPreNeg); - put(j, "relayOut2Pre", v.relayOut2Pre); - put(j, "relayChgOnlinePre", v.relayChgOnlinePre); - put(j, "voltageLoad", v.voltageLoad); // 0.01V - put(j, "voltagePack", v.voltagePack); // 0.01V - putI(j, "current", v.current); // 0.05A - put(j, "insulationPos", v.insulationPos); - put(j, "insulationNeg", v.insulationNeg); - put(j, "packVoltageMid", v.packVoltageMid); // 0.1V - put(j, "packVoltageMin", v.packVoltageMin); - put(j, "packVoltageMinNo", v.packVoltageMinNo); - put(j, "packVoltageMax", v.packVoltageMax); - put(j, "packVoltageMaxNo", v.packVoltageMaxNo); - put(j, "packDeltaMax", v.packDeltaMax); // 0.1mV - put(j, "packDeltaMaxNo", v.packDeltaMaxNo); - put(j, "packDeltaMaxVoltage", v.packDeltaMaxVoltage); - put(j, "cellTempAvg", v.cellTempAvg); // -40℃ - put(j, "cellTempMin", v.cellTempMin); - put(j, "cellTempMinNo", v.cellTempMinNo); - put(j, "cellTempMax", v.cellTempMax); - put(j, "cellTempMaxNo", v.cellTempMaxNo); - put(j, "alarmFlag1", v.alarmFlag[0]); - put(j, "alarmFlag2", v.alarmFlag[1]); - put(j, "alarmFlag3", v.alarmFlag[2]); - put(j, "alarmFlag4", v.alarmFlag[3]); - put(j, "alarmFlag5", v.alarmFlag[4]); - put(j, "alarmFlag6", v.alarmFlag[5]); - put(j, "alarmPosFlag", v.alarmPosFlag); - put(j, "chargeStatus", v.chargeStatus); - put(j, "heartbeat", v.heartbeat); - return j; -} - -// 锂电池包报警信息(0x0004 收)-> JSON -JsonVal batAlarmJson(const BatPackAlarmValue& v) { - JsonVal j(Json::objectValue); - put(j, "packNo", v.packNo); - put(j, "alarmFlag1", v.alarmFlag[0]); - put(j, "alarmFlag2", v.alarmFlag[1]); - put(j, "alarmFlag3", v.alarmFlag[2]); - put(j, "alarmFlag4", v.alarmFlag[3]); - put(j, "alarmFlag5", v.alarmFlag[4]); - put(j, "alarmFlag6", v.alarmFlag[5]); - put(j, "soc", v.soc); // 0.1% - put(j, "relayPosNeg", v.relayPosNeg); - put(j, "voltageLoad", v.voltageLoad); // 0.1V - put(j, "voltagePack", v.voltagePack); // 0.1V - putI(j, "current", v.current); // 0.05A - put(j, "insulationPos", v.insulationPos); - put(j, "insulationNeg", v.insulationNeg); - put(j, "tempMaxNo", v.tempMaxNo); - put(j, "tempMax1", v.tempMax1); - put(j, "tempMin1", v.tempMin1); - put(j, "tempAvg", v.tempAvg); - put(j, "balanceState", v.balanceState); - put(j, "voltMaxNo", v.voltMaxNo); - put(j, "voltMax1", v.voltMax1); // 0.1mV - put(j, "voltMinNo", v.voltMinNo); - put(j, "voltMin1", v.voltMin1); - put(j, "voltAvg", v.voltAvg); + j["totalVoltage"] = JsonVal(v.totalVoltage); // V + j["soc"] = JsonVal(v.soc); // % + j["faultCode"] = JsonVal(v.faultCode); // 故障码 + j["statusBits"] = JsonVal(v.statusBits); // 状态位 + j["maxCurrentLimit"] = JsonVal(v.maxCurrentLimit); // A + j["totalCurrent"] = JsonVal(v.totalCurrent); // A(放电为正) + j["maxCellVoltage"] = JsonVal(v.maxCellVoltage); // V + j["totalVoltageChk"] = JsonVal(v.totalVoltageChk); // V(校验) + j["valid"] = JsonVal(v.valid); + j["lastRx"] = JsonVal(v.lastRxTime); return j; } @@ -414,8 +340,8 @@ JsonVal linkJson(const LinkManager* lm) { } // namespace SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, - LinkManager* batDyn, LinkManager* batIns, DbStore* db) - : m_sys(sys), m_fc(fc), m_pm(pm), m_batDyn(batDyn), m_batIns(batIns), m_db(db) {} + DbStore* db) + : m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {} std::string SnapshotBuilder::build() const { JsonVal root(Json::objectValue); @@ -427,17 +353,14 @@ std::string SnapshotBuilder::build() const { root["pmParam"] = pmParamJson(m_sys->pmParamSet()); root["pmFb"] = pmFbJson(m_sys->pmParamFb()); root["pmStatus"] = pmStatusJson(m_sys->pmStatus()); - // 锂电池(0=动力 1=仪表) - root["batDyn"] = batStatusJson(m_sys->batStatus(0)); - root["batIns"] = batStatusJson(m_sys->batStatus(1)); - root["batDynAlarm"] = batAlarmJson(m_sys->batAlarm(0)); - root["batInsAlarm"] = batAlarmJson(m_sys->batAlarm(1)); + // 锂电池 BMS(CAN,经 pCanBridge 透传) + root["bms"] = bmsStatusJson(m_sys->bmsStatus()); root["fcStatusCount"] = JsonVal(static_cast(m_sys->fcStatusCount())); root["pmControlCount"] = JsonVal(static_cast(m_sys->pmControlCount())); root["fcControlCount"] = JsonVal(static_cast(m_sys->fcControlCount())); root["pmStatusCount"] = JsonVal(static_cast(m_sys->pmStatusCount())); - root["batDynStatusCount"] = JsonVal(static_cast(m_sys->batStatusCount(0))); - root["batInsStatusCount"] = JsonVal(static_cast(m_sys->batStatusCount(1))); + root["bmsStatusCount"] = JsonVal(static_cast(m_sys->bmsStatusCount())); + root["canFrameCount"] = JsonVal(static_cast(m_sys->canFrameCount())); } else { root["fc"] = JsonVal(Json::objectValue); root["pmCmd"] = JsonVal(Json::objectValue); @@ -445,17 +368,12 @@ std::string SnapshotBuilder::build() const { root["pmParam"] = JsonVal(Json::objectValue); root["pmFb"] = JsonVal(Json::objectValue); root["pmStatus"] = JsonVal(Json::objectValue); - root["batDyn"] = JsonVal(Json::objectValue); - root["batIns"] = JsonVal(Json::objectValue); - root["batDynAlarm"] = JsonVal(Json::objectValue); - root["batInsAlarm"] = JsonVal(Json::objectValue); + root["bms"] = JsonVal(Json::objectValue); } JsonVal links(Json::objectValue); links["fc"] = linkJson(m_fc); links["pm"] = linkJson(m_pm); - links["batDyn"] = linkJson(m_batDyn); - links["batIns"] = linkJson(m_batIns); root["links"] = links; // 最近原始帧(默认 20 条) diff --git a/src/pCCU/core/SnapshotBuilder.h b/src/pCCU/core/SnapshotBuilder.h index 208591a..e343060 100644 --- a/src/pCCU/core/SnapshotBuilder.h +++ b/src/pCCU/core/SnapshotBuilder.h @@ -12,14 +12,13 @@ class DbStore; //============================================================================ // SnapshotBuilder:构建网页推送的 JSON 快照。 // -// 组合 SystemData(最新状态)、两条链路统计、最近原始帧日志, -// 序列化为 JSON 字符串(jsoncpp)。串行化逻辑集中在此处,便于维护。 +// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN)、两条 UDP 链路统计、 +// 最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。 //============================================================================ class SnapshotBuilder { public: - SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, - LinkManager* batDyn, LinkManager* batIns, DbStore* db); + SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db); // 生成完整快照 JSON std::string build() const; @@ -31,8 +30,6 @@ private: SystemData* m_sys; LinkManager* m_fc; LinkManager* m_pm; - LinkManager* m_batDyn; // 动力锂电池链路 - LinkManager* m_batIns; // 仪表锂电池链路 DbStore* m_db; }; diff --git a/src/pCCU/core/SystemData.h b/src/pCCU/core/SystemData.h index 3d259d1..410df57 100644 --- a/src/pCCU/core/SystemData.h +++ b/src/pCCU/core/SystemData.h @@ -5,7 +5,7 @@ #include #include "../protocol/FcProtocol.h" #include "../protocol/PmProtocol.h" -#include "../protocol/BatProtocol.h" +#include "../protocol/CanBms.h" namespace ccu { @@ -13,7 +13,7 @@ namespace ccu { // SystemData:跨线程共享的最新状态快照。 // // - 接收线程(FC 链路)写 FcStatus 快照 -// - 接收线程(锂电池链路)写 动力/仪表 BatStatus 快照(role: 0=动力 1=仪表) +// - MOOS 主线程(OnNewMail)写 BMS 快照(CAN 经 pCanBridge 透传) // - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送 // - 网页线程读快照展示 // 通过互斥锁保护读写。 @@ -101,36 +101,31 @@ public: return m_pmParamFb; } - //-------- 锂电池(role: 0=动力 1=仪表,见 BatRole) -------- + //-------- 锂电池 BMS(CAN 总线,经 pCanBridge 透传) -------- - // 更新/获取最新电池组状态(0x0003) - void updateBatStatus(int role, const BatStatusValue& v) { + // 更新最新 BMS 状态(每帧部分更新,由调用方先取出快照再合并) + void updateBmsStatus(const BmsStatusValue& v) { std::lock_guard lock(m_mutex); - m_batStatus[role] = v; - m_batStatusCount[role]++; + m_bmsStatus = v; + m_bmsStatusCount++; } - BatStatusValue batStatus(int role) const { + BmsStatusValue bmsStatus() const { std::lock_guard lock(m_mutex); - return m_batStatus[role]; + return m_bmsStatus; } - unsigned long batStatusCount(int role) const { + unsigned long bmsStatusCount() const { std::lock_guard lock(m_mutex); - return m_batStatusCount[role]; + return m_bmsStatusCount; } - // 更新/获取最新电池包报警信息(0x0004) - void updateBatAlarm(int role, const BatPackAlarmValue& v) { + // CAN 链路收帧计数(含非 BMS 报文,用于链路状态展示) + void incCanFrameCount() { std::lock_guard lock(m_mutex); - m_batAlarm[role] = v; - m_batAlarmCount[role]++; + m_canFrameCount++; } - BatPackAlarmValue batAlarm(int role) const { + unsigned long canFrameCount() const { std::lock_guard lock(m_mutex); - return m_batAlarm[role]; - } - unsigned long batAlarmCount(int role) const { - std::lock_guard lock(m_mutex); - return m_batAlarmCount[role]; + return m_canFrameCount; } private: @@ -141,14 +136,13 @@ private: FcControlValue m_fcControl; PmStatusValue m_pmStatus; PmParamSetFbValue m_pmParamFb; - BatStatusValue m_batStatus[2]; - BatPackAlarmValue m_batAlarm[2]; + BmsStatusValue m_bmsStatus; unsigned long m_fcStatusCount = 0; unsigned long m_pmControlCount = 0; unsigned long m_fcControlCount = 0; unsigned long m_pmStatusCount = 0; - unsigned long m_batStatusCount[2] = {0, 0}; - unsigned long m_batAlarmCount[2] = {0, 0}; + unsigned long m_bmsStatusCount = 0; + unsigned long m_canFrameCount = 0; }; } // namespace ccu diff --git a/src/pCCU/pCCU.moos b/src/pCCU/pCCU.moos index a61aecb..97884ad 100644 --- a/src/pCCU/pCCU.moos +++ b/src/pCCU/pCCU.moos @@ -2,12 +2,9 @@ // pCCU 配置示例 // // 链路拓扑: -// 燃料电池控制器(FC) 192.168.1.162:7000 -// 控制主机(pPowerManger) 192.168.0.140:5001 -// 动力锂电池 192.168.100.137:7000 -// 仪表锂电池 192.168.100.136:7000 -// pCCU 本机监听:FC 状态端口 6000、PM 指令端口 7000、 -// 动力电池端口 7001、仪表电池端口 7002 +// 燃料电池控制器(FC) 192.168.1.162:7000(UDP) +// 控制主机(pPowerManger) 192.168.0.140:5001(UDP) +// 锂电池 BMS CAN 总线(经 pCanBridge 订阅 CAN_0x* 透传) //============================================================================ ProcessConfig = pCCU @@ -29,18 +26,9 @@ ProcessConfig = pCCU pm_remote_ip = 192.168.0.140 pm_remote_port = 5001 - //======== 锂电池链路(与动力/仪表锂电池组通信) ======== - // 协议:docs/锂电池协议20230324.docx - // 本机接收电池状态反馈的端口(需与电池侧配置的目的端口一致) - dyn_bat_local_port = 7001 - ins_bat_local_port = 7002 - // 锂电池组地址(发送自检/控制指令目标) - dyn_bat_remote_ip = 192.168.100.137 - dyn_bat_remote_port = 7000 - ins_bat_remote_ip = 192.168.100.136 - ins_bat_remote_port = 7000 - // 电池工况设定:10工房调试 / 30试验实航 / 50科研模式 - bat_work_condition = 10 + //======== 锂电池 ======== + // CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx), + // 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。 //======== 存储与日志 ======== // 数据库路径(SQLite) diff --git a/src/pCCU/protocol/BatProtocol.cpp b/src/pCCU/protocol/BatProtocol.cpp deleted file mode 100644 index cbc885b..0000000 --- a/src/pCCU/protocol/BatProtocol.cpp +++ /dev/null @@ -1,446 +0,0 @@ -#include "BatProtocol.h" -#include "FieldCodec.h" -#include "Frame.h" -#include "MessageRegistry.h" - -namespace ccu { - -void registerBatMessages(MessageRegistry& reg) { - reg.registerMessage(std::unique_ptr(new BatSelfCheckMessage())); - reg.registerMessage(std::unique_ptr(new BatControlMessage())); - reg.registerMessage(std::unique_ptr(new BatStatusMessage())); - reg.registerMessage(std::unique_ptr(new BatPackAlarmMessage())); -} - -//============================================================================ -// 0x0000 自检指令 -// 数据域 2 字节,偏移 0~1 -//============================================================================ - -namespace { -enum : size_t { - BO_SELFCHECK = 0, // 电池自检 00/01 - BO_SELFCHECK_RST = 1, // 自检复位指令 00/55 -}; -} - -std::vector BatSelfCheckMessage::encode(const void* obj) const { - const BatSelfCheckValue* v = static_cast(obj); - std::vector p(payloadLength(), 0); - FieldCodec::putU8(p, BO_SELFCHECK, v->selfCheck); - FieldCodec::putU8(p, BO_SELFCHECK_RST, v->selfCheckReset); - return Frame::build(id(), p, checksum()); -} - -bool BatSelfCheckMessage::decode(const std::vector& frame, void* obj) const { - if (!validateFrame(frame, id(), checksum(), totalLength())) return false; - BatSelfCheckValue* v = static_cast(obj); - size_t o = FRAME_HEADER_LEN; - v->selfCheck = FieldCodec::getU8(frame, o + BO_SELFCHECK); - v->selfCheckReset = FieldCodec::getU8(frame, o + BO_SELFCHECK_RST); - return true; -} - -//============================================================================ -// 0x0001 设备控制指令 -// 数据域 17 字节,偏移 0~16 -//============================================================================ - -namespace { -enum : size_t { - BC_YEAR = 0, // u16 - BC_MONTH = 2, - BC_DAY = 3, - BC_HOUR = 4, - BC_MINUTE = 5, - BC_SECOND = 6, - BC_MS10 = 7, - BC_WORKCOND = 8, // 工况设定 - BC_BUS_CTR = 9, // 母线接触器 - BC_POS_CTR = 10, // 正极接触器 - BC_DIODE_CTR = 11, // 二极管路接触器 - BC_POWER = 12, // u16 电池组功率 kW - BC_RESV = 14, // u16 预留 - BC_HEARTBEAT = 16, // 通信心跳 -}; -} - -std::vector BatControlMessage::encode(const void* obj) const { - const BatControlValue* v = static_cast(obj); - std::vector p(payloadLength(), 0); - - FieldCodec::putU16(p, BC_YEAR, v->year); - FieldCodec::putU8(p, BC_MONTH, v->month); - FieldCodec::putU8(p, BC_DAY, v->day); - FieldCodec::putU8(p, BC_HOUR, v->hour); - FieldCodec::putU8(p, BC_MINUTE, v->minute); - FieldCodec::putU8(p, BC_SECOND, v->second); - FieldCodec::putU8(p, BC_MS10, v->ms10); - FieldCodec::putU8(p, BC_WORKCOND, v->workCondition); - FieldCodec::putU8(p, BC_BUS_CTR, v->busContactor); - FieldCodec::putU8(p, BC_POS_CTR, v->posContactor); - FieldCodec::putU8(p, BC_DIODE_CTR, v->diodeContactor); - FieldCodec::putU16(p, BC_POWER, v->powerKw); - FieldCodec::putU16(p, BC_RESV, v->reserved); - FieldCodec::putU8(p, BC_HEARTBEAT, v->heartbeat); - - return Frame::build(id(), p, checksum()); -} - -bool BatControlMessage::decode(const std::vector& frame, void* obj) const { - if (!validateFrame(frame, id(), checksum(), totalLength())) return false; - BatControlValue* v = static_cast(obj); - size_t o = FRAME_HEADER_LEN; - - v->year = FieldCodec::getU16(frame, o + BC_YEAR); - v->month = FieldCodec::getU8(frame, o + BC_MONTH); - v->day = FieldCodec::getU8(frame, o + BC_DAY); - v->hour = FieldCodec::getU8(frame, o + BC_HOUR); - v->minute = FieldCodec::getU8(frame, o + BC_MINUTE); - v->second = FieldCodec::getU8(frame, o + BC_SECOND); - v->ms10 = FieldCodec::getU8(frame, o + BC_MS10); - v->workCondition = FieldCodec::getU8(frame, o + BC_WORKCOND); - v->busContactor = FieldCodec::getU8(frame, o + BC_BUS_CTR); - v->posContactor = FieldCodec::getU8(frame, o + BC_POS_CTR); - v->diodeContactor= FieldCodec::getU8(frame, o + BC_DIODE_CTR); - v->powerKw = FieldCodec::getU16(frame, o + BC_POWER); - v->reserved = FieldCodec::getU16(frame, o + BC_RESV); - v->heartbeat = FieldCodec::getU8(frame, o + BC_HEARTBEAT); - return true; -} - -//============================================================================ -// 0x0003 状态反馈——电池组状态 -// 数据域 80 字节,偏移 0~79 -//============================================================================ - -namespace { -enum : size_t { - BS_YEAR = 0, // u16 - BS_MONTH = 2, - BS_DAY = 3, - BS_HOUR = 4, - BS_MINUTE = 5, - BS_SECOND = 6, - BS_MS10 = 7, - BS_WORKCOND = 8, // 工况 - BS_SELFCHECK = 9, // 电池自检状态 - BS_POWERCFG = 10, // 电池功率配置状态 - BS_BUS_CTR = 11, // 电池母线接触器状态 - BS_POS_DIODE = 12, // 正极接触器与二极管回路状态 - BS_EMERGENCY = 13, // 电池紧急状态 - BS_SOC = 14, // u16 0.1% - BS_ENERGY = 16, // u16 0.1kWh - BS_MAX_DIS_PWR = 18, // u16 0.1kW 允许最高放电功率 - BS_MAX_CHG_PWR = 20, // u16 0.1kW 允许充电功率 - BS_CUR_ACC_PWR = 22, // u16 0.1kW 当前接入功率 - BS_ONLINE = 24, // u32 电池包在线信息 - BS_ACCESS = 28, // u32 电池包接入信息 - BS_RELAY_POS = 32, // 总控正极+充电继电器 - BS_RELAY_PRE = 33, // 总控预充+负极继电器 - BS_RELAY_OUT2 = 34, // 第二路输出+预充 - BS_RELAY_CHG = 35, // 在线充电+预充 - BS_V_LOAD = 36, // u16 0.01V - BS_V_PACK = 38, // u16 0.01V - BS_CURRENT = 40, // s16 0.05A - BS_INS_POS = 42, // u16 10kΩ - BS_INS_NEG = 44, // u16 10kΩ - BS_M_TEMP1 = 46, // u8 总控温度1(文档标注删除,保留偏移) - BS_M_TEMP2 = 47, // u8 总控温度2(文档标注删除,保留偏移) - BS_PK_V_MID = 48, // u16 0.1V - BS_PK_V_MIN = 50, // u16 0.1V - BS_PK_V_MIN_NO = 52, - BS_PK_V_MAX = 53, // u16 0.1V - BS_PK_V_MAX_NO = 55, - BS_PK_DELTA = 56, // u16 0.1mV - BS_PK_DELTA_NO = 58, - BS_PK_DELTA_V = 59, // u16 0.1mV - BS_T_AVG = 61, // u8 -40℃ - BS_T_MIN = 62, // u8 -40℃ - BS_T_MIN_NO = 63, - BS_T_MAX = 64, // u8 -40℃ - BS_T_MAX_NO = 65, - BS_ALARM1 = 66, // u8 * 6 报警标识字1~6 - BS_ALARM_POS = 72, // u32 报警位置标识字 - BS_CHARGE_ST = 76, // u8 充放电状态 - BS_RESV = 77, // u16 - BS_HEARTBEAT = 79, -}; -inline void getU8Arr(const std::vector& f, size_t off, uint8_t* dst, size_t n) { - for (size_t i = 0; i < n; ++i) dst[i] = f[off + i]; -} -inline void putU8Arr(std::vector& p, size_t off, const uint8_t* src, size_t n) { - for (size_t i = 0; i < n; ++i) p[off + i] = src[i]; -} -} - -std::vector BatStatusMessage::encode(const void* obj) const { - const BatStatusValue* v = static_cast(obj); - std::vector p(payloadLength(), 0); - - FieldCodec::putU16(p, BS_YEAR, v->year); - FieldCodec::putU8(p, BS_MONTH, v->month); - FieldCodec::putU8(p, BS_DAY, v->day); - FieldCodec::putU8(p, BS_HOUR, v->hour); - FieldCodec::putU8(p, BS_MINUTE, v->minute); - FieldCodec::putU8(p, BS_SECOND, v->second); - FieldCodec::putU8(p, BS_MS10, v->ms10); - FieldCodec::putU8(p, BS_WORKCOND, v->workCondition); - FieldCodec::putU8(p, BS_SELFCHECK, v->selfCheckState); - FieldCodec::putU8(p, BS_POWERCFG, v->powerCfgState); - FieldCodec::putU8(p, BS_BUS_CTR, v->busContactorState); - FieldCodec::putU8(p, BS_POS_DIODE, v->posDiodeState); - FieldCodec::putU8(p, BS_EMERGENCY, v->emergencyState); - FieldCodec::putU16(p, BS_SOC, v->soc); - FieldCodec::putU16(p, BS_ENERGY, v->energy); - FieldCodec::putU16(p, BS_MAX_DIS_PWR, v->maxDischargePower); - FieldCodec::putU16(p, BS_MAX_CHG_PWR, v->maxChargePower); - FieldCodec::putU16(p, BS_CUR_ACC_PWR, v->currentAccessPower); - FieldCodec::putU32(p, BS_ONLINE, v->packOnlineFlag); - FieldCodec::putU32(p, BS_ACCESS, v->packAccessFlag); - FieldCodec::putU8(p, BS_RELAY_POS, v->relayPosCharge); - FieldCodec::putU8(p, BS_RELAY_PRE, v->relayPreNeg); - FieldCodec::putU8(p, BS_RELAY_OUT2, v->relayOut2Pre); - FieldCodec::putU8(p, BS_RELAY_CHG, v->relayChgOnlinePre); - FieldCodec::putU16(p, BS_V_LOAD, v->voltageLoad); - FieldCodec::putU16(p, BS_V_PACK, v->voltagePack); - FieldCodec::putU16(p, BS_CURRENT, static_cast(v->current)); - FieldCodec::putU16(p, BS_INS_POS, v->insulationPos); - FieldCodec::putU16(p, BS_INS_NEG, v->insulationNeg); - FieldCodec::putU8(p, BS_M_TEMP1, v->masterTemp1); - FieldCodec::putU8(p, BS_M_TEMP2, v->masterTemp2); - FieldCodec::putU16(p, BS_PK_V_MID, v->packVoltageMid); - FieldCodec::putU16(p, BS_PK_V_MIN, v->packVoltageMin); - FieldCodec::putU8(p, BS_PK_V_MIN_NO, v->packVoltageMinNo); - FieldCodec::putU16(p, BS_PK_V_MAX, v->packVoltageMax); - FieldCodec::putU8(p, BS_PK_V_MAX_NO, v->packVoltageMaxNo); - FieldCodec::putU16(p, BS_PK_DELTA, v->packDeltaMax); - FieldCodec::putU8(p, BS_PK_DELTA_NO, v->packDeltaMaxNo); - FieldCodec::putU16(p, BS_PK_DELTA_V, v->packDeltaMaxVoltage); - FieldCodec::putU8(p, BS_T_AVG, v->cellTempAvg); - FieldCodec::putU8(p, BS_T_MIN, v->cellTempMin); - FieldCodec::putU8(p, BS_T_MIN_NO, v->cellTempMinNo); - FieldCodec::putU8(p, BS_T_MAX, v->cellTempMax); - FieldCodec::putU8(p, BS_T_MAX_NO, v->cellTempMaxNo); - putU8Arr(p, BS_ALARM1, v->alarmFlag, 6); - FieldCodec::putU32(p, BS_ALARM_POS, v->alarmPosFlag); - FieldCodec::putU8(p, BS_CHARGE_ST, v->chargeStatus); - FieldCodec::putU16(p, BS_RESV, v->reserved); - FieldCodec::putU8(p, BS_HEARTBEAT, v->heartbeat); - - return Frame::build(id(), p, checksum()); -} - -bool BatStatusMessage::decode(const std::vector& frame, void* obj) const { - if (!validateFrame(frame, id(), checksum(), totalLength())) return false; - BatStatusValue* v = static_cast(obj); - size_t o = FRAME_HEADER_LEN; - - v->year = FieldCodec::getU16(frame, o + BS_YEAR); - v->month = FieldCodec::getU8(frame, o + BS_MONTH); - v->day = FieldCodec::getU8(frame, o + BS_DAY); - v->hour = FieldCodec::getU8(frame, o + BS_HOUR); - v->minute = FieldCodec::getU8(frame, o + BS_MINUTE); - v->second = FieldCodec::getU8(frame, o + BS_SECOND); - v->ms10 = FieldCodec::getU8(frame, o + BS_MS10); - v->workCondition = FieldCodec::getU8(frame, o + BS_WORKCOND); - v->selfCheckState = FieldCodec::getU8(frame, o + BS_SELFCHECK); - v->powerCfgState = FieldCodec::getU8(frame, o + BS_POWERCFG); - v->busContactorState = FieldCodec::getU8(frame, o + BS_BUS_CTR); - v->posDiodeState = FieldCodec::getU8(frame, o + BS_POS_DIODE); - v->emergencyState = FieldCodec::getU8(frame, o + BS_EMERGENCY); - v->soc = FieldCodec::getU16(frame, o + BS_SOC); - v->energy = FieldCodec::getU16(frame, o + BS_ENERGY); - v->maxDischargePower = FieldCodec::getU16(frame, o + BS_MAX_DIS_PWR); - v->maxChargePower = FieldCodec::getU16(frame, o + BS_MAX_CHG_PWR); - v->currentAccessPower = FieldCodec::getU16(frame, o + BS_CUR_ACC_PWR); - v->packOnlineFlag = FieldCodec::getU32(frame, o + BS_ONLINE); - v->packAccessFlag = FieldCodec::getU32(frame, o + BS_ACCESS); - v->relayPosCharge = FieldCodec::getU8(frame, o + BS_RELAY_POS); - v->relayPreNeg = FieldCodec::getU8(frame, o + BS_RELAY_PRE); - v->relayOut2Pre = FieldCodec::getU8(frame, o + BS_RELAY_OUT2); - v->relayChgOnlinePre = FieldCodec::getU8(frame, o + BS_RELAY_CHG); - v->voltageLoad = FieldCodec::getU16(frame, o + BS_V_LOAD); - v->voltagePack = FieldCodec::getU16(frame, o + BS_V_PACK); - v->current = static_cast(FieldCodec::getU16(frame, o + BS_CURRENT)); - v->insulationPos = FieldCodec::getU16(frame, o + BS_INS_POS); - v->insulationNeg = FieldCodec::getU16(frame, o + BS_INS_NEG); - v->masterTemp1 = FieldCodec::getU8(frame, o + BS_M_TEMP1); - v->masterTemp2 = FieldCodec::getU8(frame, o + BS_M_TEMP2); - v->packVoltageMid = FieldCodec::getU16(frame, o + BS_PK_V_MID); - v->packVoltageMin = FieldCodec::getU16(frame, o + BS_PK_V_MIN); - v->packVoltageMinNo = FieldCodec::getU8(frame, o + BS_PK_V_MIN_NO); - v->packVoltageMax = FieldCodec::getU16(frame, o + BS_PK_V_MAX); - v->packVoltageMaxNo = FieldCodec::getU8(frame, o + BS_PK_V_MAX_NO); - v->packDeltaMax = FieldCodec::getU16(frame, o + BS_PK_DELTA); - v->packDeltaMaxNo = FieldCodec::getU8(frame, o + BS_PK_DELTA_NO); - v->packDeltaMaxVoltage= FieldCodec::getU16(frame, o + BS_PK_DELTA_V); - v->cellTempAvg = FieldCodec::getU8(frame, o + BS_T_AVG); - v->cellTempMin = FieldCodec::getU8(frame, o + BS_T_MIN); - v->cellTempMinNo = FieldCodec::getU8(frame, o + BS_T_MIN_NO); - v->cellTempMax = FieldCodec::getU8(frame, o + BS_T_MAX); - v->cellTempMaxNo = FieldCodec::getU8(frame, o + BS_T_MAX_NO); - getU8Arr(frame, o + BS_ALARM1, v->alarmFlag, 6); - v->alarmPosFlag = FieldCodec::getU32(frame, o + BS_ALARM_POS); - v->chargeStatus = FieldCodec::getU8(frame, o + BS_CHARGE_ST); - v->reserved = FieldCodec::getU16(frame, o + BS_RESV); - v->heartbeat = FieldCodec::getU8(frame, o + BS_HEARTBEAT); - return true; -} - -//============================================================================ -// 0x0004 状态反馈——电池包报警信息 -// 数据域 68 字节,偏移 0~67 -//============================================================================ - -namespace { -enum : size_t { - BA_YEAR = 0, // u16 - BA_MONTH = 2, - BA_DAY = 3, - BA_HOUR = 4, - BA_MINUTE = 5, - BA_SECOND = 6, - BA_MS10 = 7, - BA_PACKNO = 8, // 电池包号 1~24 - BA_ALARM1 = 9, // u8 * 6 报警标识字1~6 - BA_SOC = 15, // u16 0.1% - BA_RELAY = 17, // 正极+负极继电器状态 - BA_V_LOAD = 18, // u16 0.1V - BA_V_PACK = 20, // u16 0.1V - BA_CURRENT = 22, // s16 0.05A - BA_INS_POS = 24, // u16 10kΩ - BA_INS_NEG = 26, // u16 10kΩ - BA_T_MAX_NO = 28, - BA_T_MAX1 = 29, // u8 -40℃ - BA_T_MAX2 = 30, - BA_T_MAX3 = 31, - BA_T_MAX4 = 32, - BA_T_MIN4 = 33, - BA_T_MIN3 = 34, - BA_T_MIN2 = 35, - BA_T_MIN1 = 36, // u8 -40℃ - BA_T_MIN_NO = 37, - BA_T_AVG = 38, // u8 -40℃ - BA_BALANCE = 39, // 均衡状态 0/1 - BA_V_MAX_NO = 40, - BA_V_MAX1 = 41, // u16 0.1mV - BA_V_MAX2 = 43, - BA_V_MAX3 = 45, - BA_V_MAX4 = 47, - BA_V_MAX5 = 49, - BA_V_MAX6 = 51, - BA_V_MIN6 = 53, - BA_V_MIN5 = 55, - BA_V_MIN4 = 57, - BA_V_MIN3 = 59, - BA_V_MIN2 = 61, - BA_V_MIN1 = 63, // u16 0.1mV - BA_V_MIN_NO = 65, - BA_V_AVG = 66, // u16 -}; -} - -std::vector BatPackAlarmMessage::encode(const void* obj) const { - const BatPackAlarmValue* v = static_cast(obj); - std::vector p(payloadLength(), 0); - - FieldCodec::putU16(p, BA_YEAR, v->year); - FieldCodec::putU8(p, BA_MONTH, v->month); - FieldCodec::putU8(p, BA_DAY, v->day); - FieldCodec::putU8(p, BA_HOUR, v->hour); - FieldCodec::putU8(p, BA_MINUTE, v->minute); - FieldCodec::putU8(p, BA_SECOND, v->second); - FieldCodec::putU8(p, BA_MS10, v->ms10); - FieldCodec::putU8(p, BA_PACKNO, v->packNo); - putU8Arr(p, BA_ALARM1, v->alarmFlag, 6); - FieldCodec::putU16(p, BA_SOC, v->soc); - FieldCodec::putU8(p, BA_RELAY, v->relayPosNeg); - FieldCodec::putU16(p, BA_V_LOAD, v->voltageLoad); - FieldCodec::putU16(p, BA_V_PACK, v->voltagePack); - FieldCodec::putU16(p, BA_CURRENT, static_cast(v->current)); - FieldCodec::putU16(p, BA_INS_POS, v->insulationPos); - FieldCodec::putU16(p, BA_INS_NEG, v->insulationNeg); - FieldCodec::putU8(p, BA_T_MAX_NO, v->tempMaxNo); - FieldCodec::putU8(p, BA_T_MAX1, v->tempMax1); - FieldCodec::putU8(p, BA_T_MAX2, v->tempMax2); - FieldCodec::putU8(p, BA_T_MAX3, v->tempMax3); - FieldCodec::putU8(p, BA_T_MAX4, v->tempMax4); - FieldCodec::putU8(p, BA_T_MIN4, v->tempMin4); - FieldCodec::putU8(p, BA_T_MIN3, v->tempMin3); - FieldCodec::putU8(p, BA_T_MIN2, v->tempMin2); - FieldCodec::putU8(p, BA_T_MIN1, v->tempMin1); - FieldCodec::putU8(p, BA_T_MIN_NO, v->tempMinNo); - FieldCodec::putU8(p, BA_T_AVG, v->tempAvg); - FieldCodec::putU8(p, BA_BALANCE, v->balanceState); - FieldCodec::putU8(p, BA_V_MAX_NO, v->voltMaxNo); - FieldCodec::putU16(p, BA_V_MAX1, v->voltMax1); - FieldCodec::putU16(p, BA_V_MAX2, v->voltMax2); - FieldCodec::putU16(p, BA_V_MAX3, v->voltMax3); - FieldCodec::putU16(p, BA_V_MAX4, v->voltMax4); - FieldCodec::putU16(p, BA_V_MAX5, v->voltMax5); - FieldCodec::putU16(p, BA_V_MAX6, v->voltMax6); - FieldCodec::putU16(p, BA_V_MIN6, v->voltMin6); - FieldCodec::putU16(p, BA_V_MIN5, v->voltMin5); - FieldCodec::putU16(p, BA_V_MIN4, v->voltMin4); - FieldCodec::putU16(p, BA_V_MIN3, v->voltMin3); - FieldCodec::putU16(p, BA_V_MIN2, v->voltMin2); - FieldCodec::putU16(p, BA_V_MIN1, v->voltMin1); - FieldCodec::putU8(p, BA_V_MIN_NO, v->voltMinNo); - FieldCodec::putU16(p, BA_V_AVG, v->voltAvg); - - return Frame::build(id(), p, checksum()); -} - -bool BatPackAlarmMessage::decode(const std::vector& frame, void* obj) const { - if (!validateFrame(frame, id(), checksum(), totalLength())) return false; - BatPackAlarmValue* v = static_cast(obj); - size_t o = FRAME_HEADER_LEN; - - v->year = FieldCodec::getU16(frame, o + BA_YEAR); - v->month = FieldCodec::getU8(frame, o + BA_MONTH); - v->day = FieldCodec::getU8(frame, o + BA_DAY); - v->hour = FieldCodec::getU8(frame, o + BA_HOUR); - v->minute = FieldCodec::getU8(frame, o + BA_MINUTE); - v->second = FieldCodec::getU8(frame, o + BA_SECOND); - v->ms10 = FieldCodec::getU8(frame, o + BA_MS10); - v->packNo = FieldCodec::getU8(frame, o + BA_PACKNO); - getU8Arr(frame, o + BA_ALARM1, v->alarmFlag, 6); - v->soc = FieldCodec::getU16(frame, o + BA_SOC); - v->relayPosNeg = FieldCodec::getU8(frame, o + BA_RELAY); - v->voltageLoad = FieldCodec::getU16(frame, o + BA_V_LOAD); - v->voltagePack = FieldCodec::getU16(frame, o + BA_V_PACK); - v->current = static_cast(FieldCodec::getU16(frame, o + BA_CURRENT)); - v->insulationPos= FieldCodec::getU16(frame, o + BA_INS_POS); - v->insulationNeg= FieldCodec::getU16(frame, o + BA_INS_NEG); - v->tempMaxNo = FieldCodec::getU8(frame, o + BA_T_MAX_NO); - v->tempMax1 = FieldCodec::getU8(frame, o + BA_T_MAX1); - v->tempMax2 = FieldCodec::getU8(frame, o + BA_T_MAX2); - v->tempMax3 = FieldCodec::getU8(frame, o + BA_T_MAX3); - v->tempMax4 = FieldCodec::getU8(frame, o + BA_T_MAX4); - v->tempMin4 = FieldCodec::getU8(frame, o + BA_T_MIN4); - v->tempMin3 = FieldCodec::getU8(frame, o + BA_T_MIN3); - v->tempMin2 = FieldCodec::getU8(frame, o + BA_T_MIN2); - v->tempMin1 = FieldCodec::getU8(frame, o + BA_T_MIN1); - v->tempMinNo = FieldCodec::getU8(frame, o + BA_T_MIN_NO); - v->tempAvg = FieldCodec::getU8(frame, o + BA_T_AVG); - v->balanceState = FieldCodec::getU8(frame, o + BA_BALANCE); - v->voltMaxNo = FieldCodec::getU8(frame, o + BA_V_MAX_NO); - v->voltMax1 = FieldCodec::getU16(frame, o + BA_V_MAX1); - v->voltMax2 = FieldCodec::getU16(frame, o + BA_V_MAX2); - v->voltMax3 = FieldCodec::getU16(frame, o + BA_V_MAX3); - v->voltMax4 = FieldCodec::getU16(frame, o + BA_V_MAX4); - v->voltMax5 = FieldCodec::getU16(frame, o + BA_V_MAX5); - v->voltMax6 = FieldCodec::getU16(frame, o + BA_V_MAX6); - v->voltMin6 = FieldCodec::getU16(frame, o + BA_V_MIN6); - v->voltMin5 = FieldCodec::getU16(frame, o + BA_V_MIN5); - v->voltMin4 = FieldCodec::getU16(frame, o + BA_V_MIN4); - v->voltMin3 = FieldCodec::getU16(frame, o + BA_V_MIN3); - v->voltMin2 = FieldCodec::getU16(frame, o + BA_V_MIN2); - v->voltMin1 = FieldCodec::getU16(frame, o + BA_V_MIN1); - v->voltMinNo = FieldCodec::getU8(frame, o + BA_V_MIN_NO); - v->voltAvg = FieldCodec::getU16(frame, o + BA_V_AVG); - return true; -} - -} // namespace ccu diff --git a/src/pCCU/protocol/BatProtocol.h b/src/pCCU/protocol/BatProtocol.h deleted file mode 100644 index b7bf9ec..0000000 --- a/src/pCCU/protocol/BatProtocol.h +++ /dev/null @@ -1,334 +0,0 @@ -#ifndef PCCU_BAT_PROTOCOL_H -#define PCCU_BAT_PROTOCOL_H - -#include -#include -#include "Message.h" - -namespace ccu { - -//============================================================================ -// 锂电池协议(信息系统计算机 <-> 动力锂电池组 / 仪表锂电池组) -// 依据:docs/锂电池协议20230324.docx -// -// 网络拓扑(默认值,均可通过 .moos 配置覆盖): -// 信息系统计算机(pCCU) 本机监听端口可配置 -// 动力锂电池 192.168.100.137:7000 -// 仪表锂电池 192.168.100.136:7000 -// -// 帧格式统一:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + uint32 校验和 -// 校验和为从域起始符到校验和之前所有数据的字节和。 -// -// 四条消息(均带 uint32 校验和): -// 0x0000 自检指令 (信息系统->锂电池) payload 2B,总长 12B -// 0x0001 设备控制指令 (信息系统->锂电池) payload 17B,总长 27B -// 0x0003 状态反馈-电池组状态(锂电池->信息系统) payload 80B,总长 90B -// 0x0004 状态反馈-电池包报警(锂电池->信息系统) payload 68B,总长 78B -//============================================================================ - -// 锂电池角色:动力 / 仪表(两套设备协议一致,仅网络地址不同) -enum class BatRole : int { - Dyn = 0, // 动力锂电池 - Ins = 1, // 仪表锂电池 -}; -inline const char* batRoleName(BatRole r) { - return (r == BatRole::Dyn) ? "动力锂电池" : "仪表锂电池"; -} - -// 自检指令取值 -enum BatSelfCheckCmd : uint8_t { - BAT_SC_NONE = 0x00, // 无效 - BAT_SC_START = 0x01, // 自检 -}; -enum BatSelfCheckReset : uint8_t { - BAT_SCR_NONE = 0x00, // 无效 - BAT_SCR_RESET = 0x55, // 复位 -}; - -// 工况设定 -enum BatWorkCondition : uint8_t { - BAT_WC_NONE = 0x00, // 无效 - BAT_WC_WORKSHOP = 0x10, // 工房调试 - BAT_WC_SEA_TRIAL = 0x30, // 试验实航 - BAT_WC_RESEARCH = 0x50, // 科研模式 -}; - -// 接触器指令(母线/正极/二极管路通用) -enum BatContactorCmd : uint8_t { - BAT_CTR_NONE = 0x00, // 无效(表示"保持不变") - BAT_CTR_CLOSE = 0x55, // 接通 - BAT_CTR_OPEN = 0x77, // 断开 -}; - -// 电池自检状态(0x0003 状态反馈) -enum BatSelfCheckState : uint8_t { - BAT_SCST_INVALID = 0x00, // 无效 - BAT_SCST_RUNNING = 0x11, // 自检过程中 - BAT_SCST_OK = 0x22, // 自检完成,状态正常 - BAT_SCST_FAULT = 0x33, // 自检完成,状态异常 -}; - -// 母线接触器状态(0x0003 状态反馈) -enum BatBusContactorState : uint8_t { - BAT_BUSST_INVALID = 0x00, // 无效 - BAT_BUSST_RUNNING = 0x01, // 执行中 - BAT_BUSST_CLOSED = 0x11, // 母线接触器闭合完成 - BAT_BUSST_OPENED = 0x22, // 母线接触器断开完成 - BAT_BUSST_CLOSE_ERR = 0x33, // 闭合故障 - BAT_BUSST_OPEN_ERR = 0x44, // 断开故障 -}; - -//-------------------------------------------------------------------------- -// 0x0000 自检指令(信息系统 -> 锂电池) -// payload 2 字节 -//-------------------------------------------------------------------------- -struct BatSelfCheckValue { - uint8_t selfCheck = 0; // 00H无效 / 01H自检 - uint8_t selfCheckReset = 0; // 00H无效 / 55H复位 -}; - -//-------------------------------------------------------------------------- -// 0x0001 设备控制指令(信息系统 -> 锂电池) -// 控制指令随机下发;同时作为心跳帧周期 1s 下发 -// payload 17 字节 -//-------------------------------------------------------------------------- -struct BatControlValue { - // 系统时间 - uint16_t year = 0; - uint8_t month = 0; - uint8_t day = 0; - uint8_t hour = 0; - uint8_t minute = 0; - uint8_t second = 0; - uint8_t ms10 = 0; // 10毫秒 - // 指令 - uint8_t workCondition = 0; // 工况设定 00/10/30/50 - uint8_t busContactor = 0; // 母线接触器 00/55/77(上下电用,含预充过程) - uint8_t posContactor = 0; // 正极接触器 00/55/77(切换用) - uint8_t diodeContactor = 0; // 二极管路接触器 00/55/77 - uint16_t powerKw = 0; // 电池组功率 0~500 kW - uint16_t reserved = 0; // 预留 - uint8_t heartbeat = 0; // 通信心跳 0~255,每次+1 -}; - -//-------------------------------------------------------------------------- -// 0x0003 状态反馈——电池组状态(锂电池 -> 信息系统) -// 主动上发周期 1s;接到设备控制指令后回复 -// payload 80 字节 -//-------------------------------------------------------------------------- -struct BatStatusValue { - // 系统时间 - uint16_t year = 0; - uint8_t month = 0; - uint8_t day = 0; - uint8_t hour = 0; - uint8_t minute = 0; - uint8_t second = 0; - uint8_t ms10 = 0; - // 状态 - uint8_t workCondition = 0; // 工况 00/10/30/50 - uint8_t selfCheckState = 0; // 电池自检状态 00/11/22/33 - uint8_t powerCfgState = 0; // 电池功率配置状态 00/11/22/33/44 - uint8_t busContactorState = 0;// 电池母线接触器状态 00/01/11/22/33/44 - uint8_t posDiodeState = 0; // 正极接触器(低4位)与二极管回路(高4位)状态 - uint8_t emergencyState = 0; // 电池紧急状态 0x00/0x1A~0x1F/0x3A~0x3F/0x5A~0x5F - // 容量与功率 - uint16_t soc = 0; // SOC,0.1% - uint16_t energy = 0; // 电池电量,0.1kWh - uint16_t maxDischargePower = 0;// 电池允许最高放电功率,0.1kW - uint16_t maxChargePower = 0; // 电池允许充电功率,0.1kW - uint16_t currentAccessPower = 0; // 电池当前接入功率,0.1kW - uint32_t packOnlineFlag = 0; // 电池包在线信息 bit=1 在线 - uint32_t packAccessFlag = 0; // 电池包接入信息 bit=1 接入 - // 继电器状态 - uint8_t relayPosCharge = 0; // 总控正极(高4位)与充电(低4位)继电器状态 - uint8_t relayPreNeg = 0; // 总控预充(高4位)与负极(低4位)继电器状态 - uint8_t relayOut2Pre = 0; // 第二路输出(高4位)与其预充(低4位)继电器状态 - uint8_t relayChgOnlinePre = 0;// 在线充电(高4位)与其预充(低4位)继电器状态 - // 电气量 - uint16_t voltageLoad = 0; // 总控电池电压负载端,0.01V - uint16_t voltagePack = 0; // 总控电池电压电池端,0.01V - int16_t current = 0; // 总控电池放电电流,0.05A(放电为正/充电为负) - uint16_t insulationPos = 0; // 总控电池正端绝缘电阻,10kΩ - uint16_t insulationNeg = 0; // 总控电池负端绝缘电阻,10kΩ - uint8_t masterTemp1 = 0; // 总控温度1(文档标注删除,保留占位偏移) - uint8_t masterTemp2 = 0; // 总控温度2(文档标注删除,保留占位偏移) - // 电池包电压 - uint16_t packVoltageMid = 0; // 电池包中值电压,0.1V - uint16_t packVoltageMin = 0; // 电池包最低电压,0.1V - uint8_t packVoltageMinNo = 0; // 最低电压电池包号 - uint16_t packVoltageMax = 0; // 电池包最高电压,0.1V - uint8_t packVoltageMaxNo = 0; // 最高电压电池包号 - uint16_t packDeltaMax = 0; // 单个电池包最大压差,0.1mV - uint8_t packDeltaMaxNo = 0; // 最大压差电池包号 - uint16_t packDeltaMaxVoltage = 0; // 最大压差电池包最高电压,0.1mV - // 温度(偏移量 -40℃) - uint8_t cellTempAvg = 0; // 电池单体均值温度,-40℃ - uint8_t cellTempMin = 0; // 电池单体最低温度,-40℃ - uint8_t cellTempMinNo = 0; // 最低温度电池包号 - uint8_t cellTempMax = 0; // 电池单体最高温度,-40℃ - uint8_t cellTempMaxNo = 0; // 最高温度电池包号 - // 报警 - uint8_t alarmFlag[6] = {0}; // 电池报警标识字1~6 - uint32_t alarmPosFlag = 0; // 电池报警位置标识字 - // 其它 - uint8_t chargeStatus = 0; // 电池充放电状态 00无效/10充电中/20功率输出中 - uint16_t reserved = 0; // 预留 - uint8_t heartbeat = 0; // 通信心跳 0~255,每次+1 -}; - -//-------------------------------------------------------------------------- -// 0x0004 状态反馈——电池包报警信息(锂电池 -> 信息系统) -// 自检完成后和报警状态更改后主动上发,最多 1s 一次 -// payload 68 字节 -//-------------------------------------------------------------------------- -struct BatPackAlarmValue { - // 系统时间 - uint16_t year = 0; - uint8_t month = 0; - uint8_t day = 0; - uint8_t hour = 0; - uint8_t minute = 0; - uint8_t second = 0; - uint8_t ms10 = 0; - uint8_t packNo = 0; // 电池包号 1~24 - // 报警 - uint8_t alarmFlag[6] = {0}; // 电池报警标识字1~6 - // 状态 - uint16_t soc = 0; // 电池包SOC,0.1% - uint8_t relayPosNeg = 0; // 电池包正极(高4位)与负极(低4位)继电器状态 - // 电气量 - uint16_t voltageLoad = 0; // 电池包电压负载端,0.1V - uint16_t voltagePack = 0; // 电池包电压电池端,0.1V - int16_t current = 0; // 电池包放电电流,0.05A(放电为正/充电为负) - uint16_t insulationPos = 0; // 电池包正端绝缘电阻,10kΩ - uint16_t insulationNeg = 0; // 电池包负端绝缘电阻,10kΩ - // 温度(偏移量 -40℃) - uint8_t tempMaxNo = 0; // 电池单体最高温度号 - uint8_t tempMax1 = 0; // 电池单体最高温度1,-40℃ - uint8_t tempMax2 = 0; // 电池单体高温2 - uint8_t tempMax3 = 0; // 电池单体高温3 - uint8_t tempMax4 = 0; // 电池单体高温4 - uint8_t tempMin4 = 0; // 电池单体低温4 - uint8_t tempMin3 = 0; // 电池单体低温3 - uint8_t tempMin2 = 0; // 电池单体低温2 - uint8_t tempMin1 = 0; // 电池单体最低温度1,-40℃ - uint8_t tempMinNo = 0; // 电池单体最低温度号 - uint8_t tempAvg = 0; // 电池单体平均温度,-40℃ - // 均衡 - uint8_t balanceState = 0; // 电池单体均衡状态 0无/1有 - // 单体电压(0.1mV) - uint8_t voltMaxNo = 0; // 电池单体最高电压号 - uint16_t voltMax1 = 0; // 电池单体最高电压1 - uint16_t voltMax2 = 0; // 电池单体高电压2 - uint16_t voltMax3 = 0; // 电池单体高电压3 - uint16_t voltMax4 = 0; // 电池单体高电压4 - uint16_t voltMax5 = 0; // 电池单体高电压5 - uint16_t voltMax6 = 0; // 电池单体高电压6 - uint16_t voltMin6 = 0; // 电池单体低电压6 - uint16_t voltMin5 = 0; // 电池单体低电压5 - uint16_t voltMin4 = 0; // 电池单体低电压4 - uint16_t voltMin3 = 0; // 电池单体低电压3 - uint16_t voltMin2 = 0; // 电池单体低电压2 - uint16_t voltMin1 = 0; // 电池单体最低电压1 - uint8_t voltMinNo = 0; // 电池单体最低电压号 - uint16_t voltAvg = 0; // 电池单体平均电压 -}; - -// 状态反馈便捷解析:正极接触器部分(低4位) -inline uint8_t batPosStateOf(uint8_t posDiodeState) { return posDiodeState & 0x0F; } -// 状态反馈便捷解析:二极管回路部分(高4位) -inline uint8_t batDiodeStateOf(uint8_t posDiodeState) { return posDiodeState >> 4; } - -// 正极接触器状态值(低4位):X1执行中 X2闭合完成 X3断开完成 X4闭合故障 X5断开故障 -enum BatPosContactorState : uint8_t { - BAT_POSST_RUNNING = 0x1, - BAT_POSST_CLOSED = 0x2, - BAT_POSST_OPENED = 0x3, - BAT_POSST_CLOSE_ERR = 0x4, - BAT_POSST_OPEN_ERR = 0x5, -}; -// 二极管回路状态值(高4位):1X执行中 2X闭合完成 3X断开完成 4X闭合故障 5X断开故障 6X负载端无电压 -enum BatDiodeState : uint8_t { - BAT_DIODST_RUNNING = 0x1, - BAT_DIODST_CLOSED = 0x2, - BAT_DIODST_OPENED = 0x3, - BAT_DIODST_CLOSE_ERR = 0x4, - BAT_DIODST_OPEN_ERR = 0x5, - BAT_DIODST_NO_LOAD_V = 0x6, // 负载端无电压故障 -}; - -//-------------------------------------------------------------------------- -// 消息类 -//-------------------------------------------------------------------------- - -class BatSelfCheckMessage : public Message { -public: - uint16_t id() const override { return 0x0000; } - const char* name() const override { return "bat_self_check"; } - const ChecksumPolicy& checksum() const override { return m_checksum; } - size_t payloadLength() const override { return 2; } - std::vector encode(const void* obj) const override; - bool decode(const std::vector& frame, void* obj) const override; - std::vector encode(const BatSelfCheckValue& v) const { return encode(&v); } - bool decode(const std::vector& frame, BatSelfCheckValue& v) const { - return decode(frame, static_cast(&v)); - } -private: - ChecksumPolicy m_checksum{ChecksumType::Sum32}; -}; - -class BatControlMessage : public Message { -public: - uint16_t id() const override { return 0x0001; } - const char* name() const override { return "bat_control"; } - const ChecksumPolicy& checksum() const override { return m_checksum; } - size_t payloadLength() const override { return 17; } - std::vector encode(const void* obj) const override; - bool decode(const std::vector& frame, void* obj) const override; - std::vector encode(const BatControlValue& v) const { return encode(&v); } - bool decode(const std::vector& frame, BatControlValue& v) const { - return decode(frame, static_cast(&v)); - } -private: - ChecksumPolicy m_checksum{ChecksumType::Sum32}; -}; - -class BatStatusMessage : public Message { -public: - uint16_t id() const override { return 0x0003; } - const char* name() const override { return "bat_status"; } - const ChecksumPolicy& checksum() const override { return m_checksum; } - size_t payloadLength() const override { return 80; } - std::vector encode(const void* obj) const override; - bool decode(const std::vector& frame, void* obj) const override; - std::vector encode(const BatStatusValue& v) const { return encode(&v); } - bool decode(const std::vector& frame, BatStatusValue& v) const { - return decode(frame, static_cast(&v)); - } -private: - ChecksumPolicy m_checksum{ChecksumType::Sum32}; -}; - -class BatPackAlarmMessage : public Message { -public: - uint16_t id() const override { return 0x0004; } - const char* name() const override { return "bat_pack_alarm"; } - const ChecksumPolicy& checksum() const override { return m_checksum; } - size_t payloadLength() const override { return 68; } - std::vector encode(const void* obj) const override; - bool decode(const std::vector& frame, void* obj) const override; - std::vector encode(const BatPackAlarmValue& v) const { return encode(&v); } - bool decode(const std::vector& frame, BatPackAlarmValue& v) const { - return decode(frame, static_cast(&v)); - } -private: - ChecksumPolicy m_checksum{ChecksumType::Sum32}; -}; - -// 注册锂电池消息到给定注册表 -void registerBatMessages(class MessageRegistry& reg); - -} // namespace ccu - -#endif // PCCU_BAT_PROTOCOL_H diff --git a/src/pCCU/protocol/CanBms.cpp b/src/pCCU/protocol/CanBms.cpp new file mode 100644 index 0000000..9bc00e6 --- /dev/null +++ b/src/pCCU/protocol/CanBms.cpp @@ -0,0 +1,57 @@ +#include "CanBms.h" + +namespace ccu { + +namespace { + +// 大端 u16 +inline uint16_t rdU16BE(const uint8_t* p) { + return static_cast((static_cast(p[0]) << 8) | p[1]); +} + +// 大端 16 位有符号整数(补码) +inline int16_t rdS16BE(const uint8_t* p) { + return static_cast(rdU16BE(p)); +} + +} // namespace + +bool decodeCanBmsFrame(uint32_t canId, const uint8_t* data, int dlc, BmsStatusValue& v) { + if (!data || dlc <= 0) return false; + + // 缩放用除法(而非乘 0.1/0.001),保证结果与十进制字面量严格一致。 + switch (canId) { + case CAN_BMS_ID_STATUS: // 0x10010000:总电压 / SOC / 故障码 / 状态位 + if (dlc >= 8) { + v.totalVoltage = rdU16BE(data + 0) / 10.0; // 0.1V + v.soc = rdU16BE(data + 4) / 10.0; // 0.1% + v.faultCode = data[6]; + v.statusBits = data[7]; + } + break; + + case CAN_BMS_ID_CURRENT: + // 0x10010005:最大电流限制 / 总电流 / 最高单体电压 + // 注:xlsx 表中"总电流"标注为 3 字节(s24),但与其自身示例 + // (总电流 00 00 => 0.0A、最高单体电压 0x0EA8 => 3.752V, + // 对应实车帧 07 d0 | 00 00 | 0e a8 | 00 00)矛盾——s24@2 会与 + // 单体电压@4 重叠。按示例与实测采用 2 字节 s16(负电流示例 + // 0xFF06 => -25.0A 亦为 2 字节补码)。 + if (dlc >= 2) v.maxCurrentLimit = rdU16BE(data + 0) / 10.0; // 0.1A + if (dlc >= 4) v.totalCurrent = rdS16BE(data + 2) / 10.0; // 0.1A,放电为正 + if (dlc >= 6) v.maxCellVoltage = rdU16BE(data + 4) / 1000.0; // 0.001V + break; + + case CAN_BMS_ID_VOLT_CHK: // 0x10010006:总电压(校验) + if (dlc >= 6) { + v.totalVoltageChk = rdU16BE(data + 4) / 10.0; // 0.1V + } + break; + + default: + return false; + } + return true; +} + +} // namespace ccu diff --git a/src/pCCU/protocol/CanBms.h b/src/pCCU/protocol/CanBms.h new file mode 100644 index 0000000..cb120c0 --- /dev/null +++ b/src/pCCU/protocol/CanBms.h @@ -0,0 +1,63 @@ +#ifndef PCCU_CAN_BMS_H +#define PCCU_CAN_BMS_H + +#include + +namespace ccu { + +//============================================================================ +// CAN BMS 协议(锂电池 BMS 报文,经 USBCAN-8E-U/CANET 上 CAN 总线) +// 依据:docs/BMS_协议字段定义.xlsx +// +// 数据来源:pCanBridge 透传的 MOOS 消息 +// 变量名 CAN_0x%08X = CAN ID(扩展帧),m_sVal = 二进制数据域, +// m_dfVal2 = 原始帧信息字节。本模块只负责按报文 ID 解析数据域字段。 +// +// 报文(均为扩展帧,字段大端,起始字节相对 8 字节数据域): +// 0x10010000 总电压 u16@0 ÷10 V +// SOC u16@4 ÷10 % +// 故障码 u8 @6 ×1 +// 状态位 u8 @7 ×1 +// 0x10010005 最大电流限制 u16@0 ÷10 A +// 总电流 s16@2 ÷10 A(补码,放电为正;xlsx 表标注 +// 3 字节与其示例/实测冲突,按 2 字节实现, +// 详见 CanBms.cpp 注释) +// 最高单体电压 u16@4 ÷1000 V +// 0x10010006 总电压(校验) u16@4 ÷10 V +//============================================================================ + +// BMS 报文 ID(扩展帧 CAN ID) +enum CanBmsId : uint32_t { + CAN_BMS_ID_STATUS = 0x10010000, // 总电压 / SOC / 故障码 / 状态位 + CAN_BMS_ID_CURRENT = 0x10010005, // 最大电流限制 / 总电流 / 最高单体电压 + CAN_BMS_ID_VOLT_CHK = 0x10010006, // 总电压(校验) +}; + +// BMS 最新状态(各报文字段按 ID 部分更新,最终合成完整快照) +struct BmsStatusValue { + double totalVoltage = 0; // 总电压 V(0x10010000) + double soc = 0; // SOC %(0x10010000) + int faultCode = 0; // 故障码(0x10010000 @6) + int statusBits = 0; // 状态位(0x10010000 @7) + double maxCurrentLimit = 0; // 最大电流限制 A(0x10010005) + double totalCurrent = 0; // 总电流 A,放电为正(0x10010005,s24 补码) + double maxCellVoltage = 0; // 最高单体电压 V(0x10010005) + double totalVoltageChk = 0; // 总电压校验 V(0x10010006) + double lastRxTime = 0; // 最近收到任一 BMS 报文的时刻(MOOSTime) + bool valid = false; // 是否收到过 BMS 报文 +}; + +// 解析一帧 CAN 数据域;命中 BMS 报文 ID 返回 true 并把字段合并进 v +// (部分更新语义:每个 ID 只更新自己的字段)。 +// data 为数据域首指针(有效长度 dlc 字节),data/dlc 非法或非 BMS ID 返回 false。 +bool decodeCanBmsFrame(uint32_t canId, const uint8_t* data, int dlc, BmsStatusValue& v); + +// 是否为 BMS 报文 ID +inline bool isCanBmsId(uint32_t canId) { + return canId == CAN_BMS_ID_STATUS || canId == CAN_BMS_ID_CURRENT || + canId == CAN_BMS_ID_VOLT_CHK; +} + +} // namespace ccu + +#endif // PCCU_CAN_BMS_H diff --git a/src/pCCU/web/pages/index.h b/src/pCCU/web/pages/index.h index ddbc242..aa2c999 100644 --- a/src/pCCU/web/pages/index.h +++ b/src/pCCU/web/pages/index.h @@ -74,8 +74,7 @@ th{color:var(--dim);font-weight:500;} - - +
发送 @@ -93,7 +92,7 @@ th{color:var(--dim);font-weight:500;} const $ = id => document.getElementById(id); const pagesEl = $('pages'); const statusEl = $('status'); -const TABS = ['fcStatus','pmCmd','pmParam','batDyn','batIns','fcCmd','pmStatus','pmFb','links','raw']; +const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','raw']; let activeTab = 'fcStatus'; function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); } @@ -129,15 +128,16 @@ function statusText(s){ } function flameText(f){ return {0:'无效',1:'火焰报警',2:'探测器故障'}[f] || ('未知('+hex(f)+')'); } -//------------------ 锂电池(0x0003/0x0004) 状态码解析 ------------------ -function batWcText(w){ return {0x00:'无效',0x10:'工房调试',0x30:'试验实航',0x50:'科研模式'}[w] || ('未知('+hex(w)+')'); } -function batScText(s){ return {0x00:'无效',0x11:'自检过程中',0x22:'自检完成正常',0x33:'自检完成异常'}[s] || ('未知('+hex(s)+')'); } -function batPwrCfgText(s){ return {0x00:'无效',0x11:'功率计算中',0x22:'满足功率要求',0x33:'不满足功率要求',0x44:'满足要求但无法执行'}[s] || ('未知('+hex(s)+')'); } -function batBusText(s){ return {0x00:'无效',0x01:'执行中',0x11:'闭合完成',0x22:'断开完成',0x33:'闭合故障',0x44:'断开故障'}[s] || ('未知('+hex(s)+')'); } -function batNibText(n){ return {0:'无效',1:'执行中',2:'闭合完成',3:'断开完成',4:'闭合故障',5:'断开故障',6:'负载端无电压'}[n] || ('未知('+hex(n)+')'); } -function batChargeText(s){ return {0x00:'无效',0x10:'电池充电中',0x20:'电池功率输出中'}[s] || ('未知('+hex(s)+')'); } -function batAlarmHex(f){ return f===0 ? ''+hex(f)+' 无' - : ''+hex(f)+''; } +//------------------ 锂电池 BMS(CAN) 解析 ------------------ +// 报文定义见 docs/BMS_协议字段定义.xlsx: +// 0x10010000 总电压/SOC/故障码/状态位 +// 0x10010005 最大电流限制/总电流/最高单体电压 +// 0x10010006 总电压(校验) +// 故障码高亮:0 显示绿色正常,非 0 显示红色 +function bmsFault(c){ return c===0 ? ''+c+' 正常' + : ''+c+''; } +// 数据新鲜度(10s 内视为在线) +function bmsOnline(t){ return (Date.now()/1000 - t) < 10; } // 带解析的值:hex 码 + 括号解析 function parsed(v, text){ return hex(v)+' ('+text+')'; } // 故障码高亮:0 显示绿色正常,非 0 显示红色 @@ -303,16 +303,12 @@ function linkCard(s){ '收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err); h+=row('PM 链路', '收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err); - h+=row('动力锂电池链路', - '收:'+l.batDyn.rx+' 发:'+l.batDyn.tx+' 误:'+l.batDyn.err); - h+=row('仪表锂电池链路', - '收:'+l.batIns.rx+' 发:'+l.batIns.tx+' 误:'+l.batIns.err); + h+=row('CAN 链路 (pCanBridge)', 'CAN帧:'+s.canFrameCount+' BMS帧:'+s.bmsStatusCount); h+=row('FC 状态接收计数', s.fcStatusCount); h+=row('PM 指令接收计数', s.pmControlCount); h+=row('FC 控制转发计数', s.fcControlCount); h+=row('PM 状态发送计数', s.pmStatusCount); - h+=row('动力电池状态接收计数', s.batDynStatusCount); - h+=row('仪表电池状态接收计数', s.batInsStatusCount); return card('链路状态', h); } @@ -329,9 +325,9 @@ function cmdCard(d){ h+=row('应急允许', parsed(d.emergencyAllow, emerAllowText(d.emergencyAllow))); h+=row('潜深深度', d.depth+' m'); h+=row('补给/排放指令', parsed(d.supplyCmd, supplyCmdText(d.supplyCmd))); - h+=row('仪表锂电池启停', parsed(d.insBatCmd, batCmdText(d.insBatCmd))); - h+=row('动力锂电池启停', parsed(d.dynBatCmd, batCmdText(d.dynBatCmd))); - h+=row('动力锂电池功率配置', d.dynBatPower+' kW'); + h+=row('仪表锂电池启停', parsed(d.insBatCmd, batCmdText(d.insBatCmd))+' (BMS/CAN暂不支持,未转发)'); + h+=row('动力锂电池启停', parsed(d.dynBatCmd, batCmdText(d.dynBatCmd))+' (BMS/CAN暂不支持,未转发)'); + h+=row('动力锂电池功率配置', d.dynBatPower+' kW (未转发)'); h+=row('通信心跳', d.heartbeat); h+=row('主机状态', parsed(d.hostState, hostStateText(d.hostState))); return card('指令内容', h); @@ -377,87 +373,22 @@ function pmFbCard(d){ return card('设定结果', h); } -//------------------ 锂电池状态(0x0003) / 电池包报警(0x0004) 卡片 ------------------ -function batRunCard(d){ - if(!d) return ''; +//------------------ 锂电池 BMS(CAN) 卡片 ------------------ +function bmsCard(d){ + if(!d || !d.valid) return card('锂电池BMS (CAN)', + '
数据状态等待报文...
'); let h=''; - h+=row('工况', parsed(d.workCondition, batWcText(d.workCondition))); - h+=row('电池自检状态', parsed(d.selfCheckState, batScText(d.selfCheckState))); - h+=row('功率配置状态', parsed(d.powerCfgState, batPwrCfgText(d.powerCfgState))); - h+=row('充放电状态', parsed(d.chargeStatus, batChargeText(d.chargeStatus))); - h+=row('电池紧急状态', hex(d.emergencyState)); - h+=row('SOC', (d.soc*0.1).toFixed(1)+' %'); - h+=row('电池电量', (d.energy*0.1).toFixed(1)+' kWh'); - h+=row('允许最高放电功率', (d.maxDischargePower*0.1).toFixed(1)+' kW'); - h+=row('允许充电功率', (d.maxChargePower*0.1).toFixed(1)+' kW'); - h+=row('当前接入功率', (d.currentAccessPower*0.1).toFixed(1)+' kW'); - h+=row('电池包在线标志', hex32(d.packOnlineFlag)); - h+=row('电池包接入标志', hex32(d.packAccessFlag)); - h+=row('通信心跳', d.heartbeat); - return card('运行状态', h); -} - -function batElecCard(d){ - if(!d) return ''; - let h=''; - h+=row('母线接触器状态', parsed(d.busContactorState, batBusText(d.busContactorState))); - h+=row('正极接触器', batNibText(d.posDiodeState & 0x0F)); - h+=row('二极管回路', batNibText((d.posDiodeState>>4) & 0x0F)); - h+=row('正极+充电继电器', hex(d.relayPosCharge)); - h+=row('预充+负极继电器', hex(d.relayPreNeg)); - h+=row('负载端电压', (d.voltageLoad*0.01).toFixed(2)+' V'); - h+=row('电池端电压', (d.voltagePack*0.01).toFixed(2)+' V'); - h+=row('放电电流', (d.current*0.05).toFixed(2)+' A'); - h+=row('正端绝缘电阻', d.insulationPos+'0 kΩ'); - h+=row('负端绝缘电阻', d.insulationNeg+'0 kΩ'); - return card('接触器与电气量', h); -} - -function batPackCard(d){ - if(!d) return ''; - let h=''; - h+=row('中值电压', (d.packVoltageMid*0.1).toFixed(1)+' V'); - h+=row('最低电压', (d.packVoltageMin*0.1).toFixed(1)+' V (包号'+d.packVoltageMinNo+')'); - h+=row('最高电压', (d.packVoltageMax*0.1).toFixed(1)+' V (包号'+d.packVoltageMaxNo+')'); - h+=row('单包最大压差', (d.packDeltaMax*0.1).toFixed(1)+' mV (包号'+d.packDeltaMaxNo+')'); - h+=row('单体均值温度', (d.cellTempAvg-40)+' ℃'); - h+=row('单体最低温度', (d.cellTempMin-40)+' ℃ (包号'+d.cellTempMinNo+')'); - h+=row('单体最高温度', (d.cellTempMax-40)+' ℃ (包号'+d.cellTempMaxNo+')'); - return card('电池包电压温度', h); -} - -function batAlarmWordCard(d){ - if(!d) return ''; - let h=''; - h+=row('报警标识字1', batAlarmHex(d.alarmFlag1)); - h+=row('报警标识字2', batAlarmHex(d.alarmFlag2)); - h+=row('报警标识字3', batAlarmHex(d.alarmFlag3)); - h+=row('报警标识字4', batAlarmHex(d.alarmFlag4)); - h+=row('报警标识字5', batAlarmHex(d.alarmFlag5)); - h+=row('报警标识字6', batAlarmHex(d.alarmFlag6)); - h+=row('报警位置标识字', hex32(d.alarmPosFlag)); - return card('电池组报警', h); -} - -function batPackAlarmCard(d){ - if(!d) return ''; - let h=''; - h+=row('电池包号', d.packNo); - h+=row('报警标识字1', batAlarmHex(d.alarmFlag1)); - h+=row('报警标识字2', batAlarmHex(d.alarmFlag2)); - h+=row('SOC', (d.soc*0.1).toFixed(1)+' %'); - h+=row('正极+负极继电器', hex(d.relayPosNeg)); - h+=row('负载端电压', (d.voltageLoad*0.1).toFixed(1)+' V'); - h+=row('电池端电压', (d.voltagePack*0.1).toFixed(1)+' V'); - h+=row('放电电流', (d.current*0.05).toFixed(2)+' A'); - h+=row('单体最高温度', (d.tempMax1-40)+' ℃ (号'+d.tempMaxNo+')'); - h+=row('单体最低温度', (d.tempMin1-40)+' ℃ (号'+d.tempMinNo+')'); - h+=row('单体平均温度', (d.tempAvg-40)+' ℃'); - h+=row('均衡状态', d.balanceState===1?'有均衡':'无'); - h+=row('单体最高电压', (d.voltMax1*0.1).toFixed(1)+' mV (号'+d.voltMaxNo+')'); - h+=row('单体最低电压', (d.voltMin1*0.1).toFixed(1)+' mV (号'+d.voltMinNo+')'); - h+=row('单体平均电压', (d.voltAvg*0.1).toFixed(1)+' mV'); - return card('电池包报警信息', h); + h+=row('总电压', d.totalVoltage.toFixed(1)+' V'); + h+=row('总电流', d.totalCurrent.toFixed(1)+' A'); + h+=row('SOC', d.soc.toFixed(1)+' %'); + h+=row('最大电流限制', d.maxCurrentLimit.toFixed(1)+' A'); + h+=row('最高单体电压', d.maxCellVoltage.toFixed(3)+' V'); + h+=row('总电压(校验帧)', d.totalVoltageChk.toFixed(1)+' V'); + h+=row('故障码', bmsFault(d.faultCode)); + h+=row('状态位', hex(d.statusBits)); + h+=row('数据状态', bmsOnline(d.lastRx) + ? '在线' : '超时'); + return card('锂电池BMS (CAN)', h); } function pmStatusCard(d){ @@ -551,13 +482,13 @@ function render(snap){ +tankCard(snap.fc)+cabinCard(snap.fc)+'
', pmCmd: pageHeader(0x0001,'PM操控指令','PM→CCU')+'
'+cmdCard(snap.pmCmd)+'
', pmParam: pageHeader(0x0002,'PM参数设定','PM→CCU')+'
'+pmParamCard(snap.pmParam)+'
', - // 0x0003/0x0004 锂电池状态反馈(锂电池→CCU):动力 / 仪表 - batDyn: pageHeader(0x0003,'动力锂电池状态','动力电池→CCU')+'
'+ - batRunCard(snap.batDyn)+batElecCard(snap.batDyn)+batPackCard(snap.batDyn)+ - batAlarmWordCard(snap.batDyn)+batPackAlarmCard(snap.batDynAlarm)+'
', - batIns: pageHeader(0x0003,'仪表锂电池状态','仪表电池→CCU')+'
'+ - batRunCard(snap.batIns)+batElecCard(snap.batIns)+batPackCard(snap.batIns)+ - batAlarmWordCard(snap.batIns)+batPackAlarmCard(snap.batInsAlarm)+'
', + // 锂电池 BMS(CAN 总线,经 pCanBridge 透传) + bms: '
'+bmsCard(snap.bms)+ + '

报文来源 (CAN ID)

'+ + row('0x10010000','总电压 / SOC / 故障码 / 状态位')+ + row('0x10010005','最大电流限制 / 总电流 / 最高单体电压')+ + row('0x10010006','总电压(校验)')+ + row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'
', fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'
'+fcCmdCard(snap.fcCmd)+'
', // 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据 pmStatus: pageHeader(0x0004,'PM状态报文','CCU→PM')+'
'+ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9810d4b..a3064b2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -137,7 +137,7 @@ add_executable(pccuTest ${CCU_DIR}/protocol/MessageRegistry.cpp ${CCU_DIR}/protocol/FcProtocol.cpp ${CCU_DIR}/protocol/PmProtocol.cpp - ${CCU_DIR}/protocol/BatProtocol.cpp + ${CCU_DIR}/protocol/CanBms.cpp ${CCU_DIR}/comm/UdpEndpoint.cpp ${CCU_DIR}/comm/LinkManager.cpp ${CCU_DIR}/store/DbStore.cpp diff --git a/test/pccu/pccuTest.cpp b/test/pccu/pccuTest.cpp index 60517dd..eb5a5c7 100644 --- a/test/pccu/pccuTest.cpp +++ b/test/pccu/pccuTest.cpp @@ -15,11 +15,10 @@ #include "../../src/pCCU/protocol/MessageRegistry.h" #include "../../src/pCCU/protocol/FcProtocol.h" #include "../../src/pCCU/protocol/PmProtocol.h" -#include "../../src/pCCU/protocol/BatProtocol.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/BatLinkManager.h" #include "../../src/pCCU/store/DbStore.h" using namespace ccu; @@ -290,244 +289,50 @@ static void testDbStore() { } //-------------------------------------------------------------------------- -// 9. 锂电池帧长度必须与协议文档一致(docs/锂电池协议20230324.docx) -static void testBatFrameLengths() { - BatSelfCheckMessage bsc; - BatControlMessage bcm; - BatStatusMessage bsm; - BatPackAlarmMessage bam; +// 9. CAN BMS 报文解码(docs/BMS_协议字段定义.xlsx,测试向量为实车抓包数据) +static void testCanBmsDecode() { + BmsStatusValue v; - CHECK(bsc.totalLength() == 12); // 自检指令:6+2+4 - CHECK(bcm.totalLength() == 27); // 设备控制指令:6+17+4 - CHECK(bsm.totalLength() == 90); // 电池组状态:6+80+4 - CHECK(bam.totalLength() == 78); // 电池包报警:6+68+4 -} + // 0x10010000:14 da 17 70 00 c9 31 01(实车抓包) + // 总电压 0x14DA=5338 -> 533.8V;SOC 0x00C9=201 -> 20.1% + // 故障码 0x31=49;状态位 0x01 + const uint8_t d0[8] = {0x14,0xDA,0x17,0x70,0x00,0xC9,0x31,0x01}; + CHECK(decodeCanBmsFrame(0x10010000, d0, 8, v)); + CHECK(v.totalVoltage == 533.8); + CHECK(v.soc == 20.1); + CHECK(v.faultCode == 0x31); + CHECK(v.statusBits == 0x01); -//-------------------------------------------------------------------------- -// 10. 锂电池自检/控制指令编解码往返 -static void testBatControlRoundTrip() { - // 自检指令 - BatSelfCheckValue sc; - sc.selfCheck = BAT_SC_START; - sc.selfCheckReset = BAT_SCR_RESET; - BatSelfCheckMessage scMsg; - std::vector scFrame = scMsg.encode(sc); - CHECK(scFrame.size() == 12); - CHECK(scFrame[0] == 0x40 && scFrame[1] == 0x40); - CHECK(scFrame[2] == 0x00 && scFrame[3] == 0x00); // id 0x0000 小端 - CHECK(scFrame[6] == 0x01 && scFrame[7] == 0x55); - BatSelfCheckValue scOut; - CHECK(scMsg.decode(scFrame, static_cast(&scOut))); - CHECK(scOut.selfCheck == BAT_SC_START && scOut.selfCheckReset == BAT_SCR_RESET); + // 文档示例帧:14 d9 => 533.7V + const uint8_t d0doc[8] = {0x14,0xD9,0x17,0x70,0x00,0xC9,0x31,0x01}; + CHECK(decodeCanBmsFrame(0x10010000, d0doc, 8, v)); + CHECK(v.totalVoltage == 533.7); - // 设备控制指令 - BatControlValue v; - v.year = 2026; v.month = 8; v.day = 30; v.hour = 11; v.minute = 20; v.second = 33; - v.ms10 = 4; - v.workCondition = BAT_WC_SEA_TRIAL; - v.busContactor = BAT_CTR_CLOSE; - v.posContactor = BAT_CTR_OPEN; - v.diodeContactor = BAT_CTR_CLOSE; - v.powerKw = 350; - v.reserved = 0x1234; - v.heartbeat = 66; + // 0x10010005:07 d0 00 00 0e a8 00 00 + // 最大电流限制 0x07D0=2000 -> 200.0A;总电流 00 00 00 -> 0.0A + // 最高单体电压 0x0EA8=3752 -> 3.752V + const uint8_t d5[8] = {0x07,0xD0,0x00,0x00,0x0E,0xA8,0x00,0x00}; + CHECK(decodeCanBmsFrame(0x10010005, d5, 8, v)); + CHECK(v.maxCurrentLimit == 200.0); + CHECK(v.totalCurrent == 0.0); + CHECK(v.maxCellVoltage == 3.752); - BatControlMessage msg; - std::vector frame = msg.encode(v); - CHECK(frame.size() == 27); - CHECK(frame[2] == 0x01 && frame[3] == 0x00); // id 0x0001 小端 - CHECK(frame[6 + 8] == BAT_WC_SEA_TRIAL); // 工况 - CHECK(frame[6 + 9] == BAT_CTR_CLOSE); // 母线接触器 - CHECK(frame[6 + 10] == BAT_CTR_OPEN); // 正极接触器 - CHECK(frame[6 + 11] == BAT_CTR_CLOSE); // 二极管接触器 - CHECK(frame[6 + 12] == 0x5E && frame[6 + 13] == 0x01); // 350 = 0x015E 小端 - CHECK(frame[6 + 14] == 0x34 && frame[6 + 15] == 0x12); // 预留 0x1234 小端 - CHECK(frame[6 + 16] == 66); // 心跳 + // 总电流负数:s16 补码 FF 06 = -250 -> -25.0A(xlsx 负数示例) + const uint8_t d5n[8] = {0x07,0xD0,0xFF,0x06,0x0E,0xA8,0x00,0x00}; + CHECK(decodeCanBmsFrame(0x10010005, d5n, 8, v)); + CHECK(v.totalCurrent == -25.0); - BatControlValue out; - CHECK(msg.decode(frame, static_cast(&out))); - CHECK(out.year == 2026 && out.month == 8 && out.day == 30); - CHECK(out.hour == 11 && out.minute == 20 && out.second == 33 && out.ms10 == 4); - CHECK(out.workCondition == BAT_WC_SEA_TRIAL); - CHECK(out.busContactor == BAT_CTR_CLOSE); - CHECK(out.posContactor == BAT_CTR_OPEN); - CHECK(out.diodeContactor == BAT_CTR_CLOSE); - CHECK(out.powerKw == 350); - CHECK(out.reserved == 0x1234); - CHECK(out.heartbeat == 66); -} + // 0x10010006:07 d0 07 d0 14 da 00 00 -> 总电压校验 0x14DA=533.8V + const uint8_t d6[8] = {0x07,0xD0,0x07,0xD0,0x14,0xDA,0x00,0x00}; + CHECK(decodeCanBmsFrame(0x10010006, d6, 8, v)); + CHECK(v.totalVoltageChk == 533.8); -//-------------------------------------------------------------------------- -// 11. 锂电池电池组状态(0x0003)编解码往返(关键字段抽查) -static void testBatStatusRoundTrip() { - BatStatusValue v; - v.year = 2026; v.month = 3; v.day = 24; v.hour = 10; v.minute = 0; v.second = 1; - v.ms10 = 9; - v.workCondition = BAT_WC_WORKSHOP; - v.selfCheckState = BAT_SCST_OK; - v.powerCfgState = 0x22; - v.busContactorState = BAT_BUSST_CLOSED; - v.posDiodeState = 0x32; // 二极管闭合完成(3?) 高4位=3 断开完成; 低4位=2 正极闭合完成 - v.emergencyState = 0x1C; - v.soc = 855; // 85.5% - v.energy = 1234; // 123.4 kWh - v.maxDischargePower = 5000; // 500.0 kW - v.maxChargePower = 2000; - v.currentAccessPower = 1500; // 150.0 kW - v.packOnlineFlag = 0x00FFFFFF; - v.packAccessFlag = 0x000000FF; - v.relayPosCharge = 0x12; - v.relayPreNeg = 0x11; - v.voltageLoad = 5400; // 54.00 V - v.voltagePack = 5500; - v.current = -200; // -10 A(充电) - v.insulationPos = 50; // 500 kΩ - v.insulationNeg = 60; - v.masterTemp1 = 1; v.masterTemp2 = 2; - v.packVoltageMid = 540; // 54.0 V - v.packVoltageMin = 535; - v.packVoltageMinNo = 3; - v.packVoltageMax = 545; - v.packVoltageMaxNo = 7; - v.packDeltaMax = 200; // 20.0 mV - v.packDeltaMaxNo = 5; - v.packDeltaMaxVoltage = 5450; - v.cellTempAvg = 65; // 25 ℃ - v.cellTempMin = 55; - v.cellTempMinNo = 2; - v.cellTempMax = 70; - v.cellTempMaxNo = 4; - v.alarmFlag[0] = 0x42; v.alarmFlag[5] = 0xC1; - v.alarmPosFlag = 0x00FF00AA; - v.chargeStatus = 0x20; - v.reserved = 0x5A5A; - v.heartbeat = 88; - - BatStatusMessage msg; - std::vector frame = msg.encode(v); - CHECK(frame.size() == 90); - CHECK(frame[2] == 0x03 && frame[3] == 0x00); // id 0x0003 - CHECK(frame[4] == 0x5A && frame[5] == 0x00); // 域字节数=整包长度 90=0x005A - CHECK(frame[6 + 9] == BAT_SCST_OK); // 自检状态 offset9 - CHECK(frame[6 + 11] == BAT_BUSST_CLOSED); // 母线接触器状态 offset11 - CHECK(frame[6 + 12] == 0x32); // 正极/二极管 offset12 - CHECK(frame[6 + 14] == 0x57 && frame[6 + 15] == 0x03); // SOC 855=0x0357 小端 - CHECK(frame[6 + 40] == 0x38 && frame[6 + 41] == 0xFF); // current -200=0xFF38 小端 - CHECK(frame[6 + 79] == 88); // 心跳 offset79 - - BatStatusValue out; - CHECK(msg.decode(frame, static_cast(&out))); - CHECK(out.year == 2026 && out.month == 3 && out.day == 24); - CHECK(out.selfCheckState == BAT_SCST_OK); - CHECK(out.powerCfgState == 0x22); - CHECK(out.busContactorState == BAT_BUSST_CLOSED); - CHECK(out.posDiodeState == 0x32); - CHECK(batPosStateOf(out.posDiodeState) == BAT_POSST_CLOSED); - CHECK(out.emergencyState == 0x1C); - CHECK(out.soc == 855 && out.energy == 1234); - CHECK(out.maxDischargePower == 5000 && out.maxChargePower == 2000); - CHECK(out.currentAccessPower == 1500); - CHECK(out.packOnlineFlag == 0x00FFFFFF && out.packAccessFlag == 0x000000FF); - CHECK(out.relayPosCharge == 0x12 && out.relayPreNeg == 0x11); - CHECK(out.voltageLoad == 5400 && out.voltagePack == 5500); - CHECK(out.current == -200); - CHECK(out.insulationPos == 50 && out.insulationNeg == 60); - CHECK(out.packVoltageMid == 540 && out.packVoltageMin == 535 && out.packVoltageMinNo == 3); - CHECK(out.packVoltageMax == 545 && out.packVoltageMaxNo == 7); - CHECK(out.packDeltaMax == 200 && out.packDeltaMaxNo == 5 && out.packDeltaMaxVoltage == 5450); - CHECK(out.cellTempAvg == 65 && out.cellTempMax == 70 && out.cellTempMaxNo == 4); - CHECK(out.alarmFlag[0] == 0x42 && out.alarmFlag[5] == 0xC1); - CHECK(out.alarmPosFlag == 0x00FF00AA); - CHECK(out.chargeStatus == 0x20 && out.reserved == 0x5A5A); - CHECK(out.heartbeat == 88); -} - -//-------------------------------------------------------------------------- -// 12. 锂电池电池包报警(0x0004)编解码往返 -static void testBatPackAlarmRoundTrip() { - BatPackAlarmValue v; - v.year = 2026; v.month = 3; v.day = 24; v.hour = 9; v.minute = 1; v.second = 2; - v.ms10 = 5; - v.packNo = 12; - v.alarmFlag[0] = 0x45; v.alarmFlag[3] = 0x82; - v.soc = 700; // 70.0% - v.relayPosNeg = 0x12; // 正极接通 + 负极断开 - v.voltageLoad = 530; // 53.0 V - v.voltagePack = 528; - v.current = 400; // 20 A(放电) - v.insulationPos = 100; - v.insulationNeg = 110; - v.tempMaxNo = 6; v.tempMax1 = 75; - v.tempMin1 = 50; - v.tempAvg = 62; - v.balanceState = 1; - v.voltMaxNo = 9; v.voltMax1 = 41; // 4.1 mV*10 => 0.1mV 精度 - v.voltMin1 = 33; - v.voltMinNo = 11; - v.voltAvg = 38; - - BatPackAlarmMessage msg; - std::vector frame = msg.encode(v); - CHECK(frame.size() == 78); - CHECK(frame[2] == 0x04 && frame[3] == 0x00); // id 0x0004 - CHECK(frame[6 + 8] == 12); // 电池包号 - CHECK(frame[6 + 9] == 0x45); // 报警标识字1 - CHECK(frame[6 + 17] == 0x12); // 正极/负极继电器 - CHECK(frame[6 + 66] == 0x26 && frame[6 + 67] == 0x00); // 平均电压 38=0x0026 小端 - - BatPackAlarmValue out; - CHECK(msg.decode(frame, static_cast(&out))); - CHECK(out.packNo == 12); - CHECK(out.alarmFlag[0] == 0x45 && out.alarmFlag[3] == 0x82); - CHECK(out.soc == 700); - CHECK(out.relayPosNeg == 0x12); - CHECK(out.voltageLoad == 530 && out.voltagePack == 528); - CHECK(out.current == 400); - CHECK(out.insulationPos == 100 && out.insulationNeg == 110); - CHECK(out.tempMaxNo == 6 && out.tempMax1 == 75); - CHECK(out.tempMin1 == 50 && out.tempAvg == 62); - CHECK(out.balanceState == 1); - CHECK(out.voltMaxNo == 9 && out.voltMax1 == 41); - CHECK(out.voltMin1 == 33 && out.voltMinNo == 11); - CHECK(out.voltAvg == 38); -} - -//-------------------------------------------------------------------------- -// 13. 锂电池链路分发与注册表隔离 -static void testBatLinkDispatch() { - MessageRegistry batReg; - registerBatMessages(batReg); - CHECK(batReg.size() == 4); - CHECK(batReg.find(0x0000) != nullptr); - CHECK(batReg.find(0x0001) != nullptr); - CHECK(batReg.find(0x0003) != nullptr); - CHECK(batReg.find(0x0004) != nullptr); - - // 锂电池链路收帧:0x0003 状态解码 - BatStatusValue v; - v.soc = 999; v.heartbeat = 21; - BatStatusMessage bsm; - std::vector frame = bsm.encode(v); - - BatLinkManager blm(BatRole::Dyn, 0, "192.168.100.137", 7000); - CHECK(blm.linkName() == "bat_dyn"); - - bool got = false; - blm.setOnMessage([&](Message* m, const std::vector& f) { - if (m && m->id() == 0x0003) { - BatStatusValue out; - if (m->decode(f, static_cast(&out))) { - got = (out.soc == 999 && out.heartbeat == 21); - } - } - }); - CHECK(blm.handleIncoming(frame)); - CHECK(got); - - // 校验失败帧拒收 - std::vector bad = frame; - bad[10] ^= 0xFF; - CHECK(!blm.handleIncoming(bad)); + // 非 BMS 报文 ID 不命中 + const uint8_t dx[8] = {0,0,0,0,0,0,0,0}; + CHECK(!decodeCanBmsFrame(0x10010035, dx, 8, v)); + CHECK(!decodeCanBmsFrame(0x10010000, nullptr, 0, v)); + CHECK(isCanBmsId(0x10010000) && isCanBmsId(0x10010005) && isCanBmsId(0x10010006)); + CHECK(!isCanBmsId(0x10010001)); } //-------------------------------------------------------------------------- @@ -540,11 +345,7 @@ int main() { testChecksum(); testLinkDispatch(); testDbStore(); - testBatFrameLengths(); - testBatControlRoundTrip(); - testBatStatusRoundTrip(); - testBatPackAlarmRoundTrip(); - testBatLinkDispatch(); + testCanBmsDecode(); 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 f0f926c..d3aac41 100644 --- a/test/pccu/pccu_integration_test.py +++ b/test/pccu/pccu_integration_test.py @@ -4,21 +4,19 @@ 拓扑: 测试脚本(FC模拟) --16000--> pCCU(fc_local) [FC状态 0x0002] 测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002] - 测试脚本(电池模拟) --17001/17002--> pCCU [电池组状态 0x0003] pCCU --16001--> 测试脚本 [FC控制 0x0001 转发] pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003] - pCCU --17003/17004--> 测试脚本 [电池控制指令 0x0001 / 自检指令 0x0000] + +锂电池已切换为 CAN 总线(BMS 协议,经 pCanBridge 透传 CAN_0x* 消息), +不在本脚本 UDP 拓扑内:BMS 解码由 pccuTest 单元测试覆盖 +(testCanBmsDecode,向量取自实车抓包)。 校验: 1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003 2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001 3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003 - 4. PM 操控中锂电池启停指令映射为电池控制指令(0x0001, 27B): - 动力 0x20 关闭 -> 母线接触器 0x77;仪表 0x10 启动 -> 母线接触器 0x55 - 5. pCCU 周期(1s)向动力/仪表锂电池下发心跳控制指令 - 6. 电池组状态(0x0003)收到后落库 SQLite(link=bat_dyn/bat_ins) - 7. SQLite 记录了收/发原始帧 - 8. Web 页面可访问 + 4. SQLite 记录了收/发原始帧 + 5. Web 页面可访问 """ import socket import struct @@ -35,10 +33,6 @@ FC_STATUS_PORT = 16000 FC_CTRL_RECV_PORT = 16001 PM_CTRL_PORT = 16002 PM_STATUS_RECV_PORT = 16003 -DYN_BAT_PORT = 17001 # pCCU 监听动力电池状态 -INS_BAT_PORT = 17002 # pCCU 监听仪表电池状态 -DYN_BAT_RECV_PORT = 17003 # 测试监听(pCCU 发往"动力电池"的目标端口) -INS_BAT_RECV_PORT = 17004 # 测试监听(pCCU 发往"仪表电池"的目标端口) WEB_PORT = 18081 DB_PATH = "/tmp/pccu_it_test.db" MOOSDB_EXE = "/usr/local/bin/MOOSDB" @@ -104,26 +98,6 @@ def pm_paramset_frame(): return frame(0x0002, bytes(p)) -def bat_status_frame(): - """电池组状态 0x0003:payload 80B,带 uint32 校验和""" - p = bytearray(80) - struct.pack_into(" 母线接触器 0x77;仪表 0x10 启动 -> 母线接触器 0x55 - dyn_ctl = [d for d in dyn_bat_results if len(d) == 27 and d[2] == 0x01] - ins_ctl = [d for d in ins_bat_results if len(d) == 27 and d[2] == 0x01] - print(f"[动力电池控制指令] 收到 {len(dyn_ctl)} 帧 (期望>=5, 每帧27B, 1s心跳)") - if dyn_ctl: - bus = dyn_ctl[-1][6 + 9] # 最后一帧:PM 指令生效后的期望状态 - print(f" -> 母线接触器=0x{bus:02X} (期望0x77 关闭)") - ok = ok and (bus == 0x77) - else: - ok = False - print(f"[仪表电池控制指令] 收到 {len(ins_ctl)} 帧 (期望>=5, 每帧27B, 1s心跳)") - if ins_ctl: - bus = ins_ctl[-1][6 + 9] - print(f" -> 母线接触器=0x{bus:02X} (期望0x55 接通)") - ok = ok and (bus == 0x55) - else: - ok = False - - # 校验 5:数据库有记录(含锂电池链路收发) + # 校验 4:数据库有记录(FC/PM 链路收发) time.sleep(1.0) db_count = -1 - bat_dyn_rx = -1 - bat_tx = -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] - # 电池组状态接收(link=bat_dyn, 方向=收, msg_id=3) - bat_dyn_rx = conn.execute( - "SELECT COUNT(*) FROM comm_log WHERE link='bat_dyn' AND direction=0 AND msg_id=3" - ).fetchone()[0] - # 电池控制指令发送(direction=1, msg_id=1, bat 链路) - bat_tx = conn.execute( - "SELECT COUNT(*) FROM comm_log WHERE link IN ('bat_dyn','bat_ins') AND direction=1 AND msg_id=1" - ).fetchone()[0] conn.close() print(f"[数据库] comm_log 记录数 = {db_count} (期望>=6)") - print(f"[数据库] 动力电池状态接收记录 = {bat_dyn_rx} (期望>=1), 电池控制发送记录 = {bat_tx} (期望>=2)") ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1) - ok = ok and (bat_dyn_rx >= 1) and (bat_tx >= 2) - - # 校验 6:Web 页面 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 bb7c2b4..5d39377 100644 --- a/test/pccu/pccu_it.moos +++ b/test/pccu/pccu_it.moos @@ -16,13 +16,8 @@ ProcessConfig = pCCU pm_remote_ip = 127.0.0.1 pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文) - // 锂电池链路(测试端口) - dyn_bat_local_port = 17001 // 监听动力电池状态 - dyn_bat_remote_ip = 127.0.0.1 - dyn_bat_remote_port = 17003 // 动力电池(测试监听此端口收控制/心跳指令) - ins_bat_local_port = 17002 // 监听仪表电池状态 - ins_bat_remote_ip = 127.0.0.1 - ins_bat_remote_port = 17004 // 仪表电池(测试监听此端口收控制/心跳指令) + // 锂电池:CAN 总线 BMS 协议,经 pCanBridge 的 CAN_0x* 消息透传 + // (集成测试不覆盖 CAN 链路,BMS 解码由 pccuTest 单元测试覆盖) dbpath = /tmp/pccu_it_test.db logpath = /tmp/pccu_it_test.log