From 7e4d08808addfeff4b77a8a810a56eda0bbdf43c Mon Sep 17 00:00:00 2001 From: zjk <1553836110@qq.com> Date: Sun, 30 Aug 2026 14:40:27 +0800 Subject: [PATCH] =?UTF-8?q?pCCU=E6=96=B0=E5=A2=9E=E9=94=82=E7=94=B5?= =?UTF-8?q?=E6=B1=A0=E9=80=9A=E4=BF=A1/=E5=8D=8F=E8=B0=83=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=20+=20=E7=94=B5=E6=BA=90=E5=8D=8F=E8=B0=83=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA=E9=AA=A8=E6=9E=B6(=E6=83=B0=E6=80=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 锂电池通信(docs/锂电池协议20230324.docx): - 协议层 BatProtocol:自检0x0000(2B)/设备控制0x0001(17B)/电池组状态0x0003(80B)/ 电池包报警0x0004(68B),uint32字节和校验、小端 - 链路层 BatLinkManager:动力/仪表锂电池独立UDP链路,本机监听与远端IP端口 均由.moos配置(dyn_bat_*/ins_bat_*/bat_work_condition) - SystemData/SnapshotBuilder/网页:电池数据按动力/仪表归类展示(运行状态/接触器 电气量/电池包电压温度/报警),收发帧落库SQLite,网页新增电池标签页 操作->指令下发(PowerCoordinator): - 操作接口:自检/上下电(母线接触器55H接通77H断开,含预充)/功率设定(0~500kW)/ 电池切换(低压->高压6步、高压->低压5步,按协议时序) - 步骤引擎:每电池独立步骤队列,下发->状态反馈确认->超时兜底;周期1s心跳控制帧 - PM操控指令insBatCmd/dynBatCmd/dynBatPower自动映射为电池操作 电源协调状态机骨架(CoordFsm, TinyFSM): - 事件Tick/PmControl/StepDone + 状态Normal/Switching/Fault,react均为TODO占位 - 当前未接线(无start/dispatch调用,运行行为与接入前一致),CoordFsm.hpp头部附接线指南 测试与部署: - pccuTest新增电池协议编解码/帧长/链路分发用例(175项通过);集成测试覆盖电池 心跳、启停映射(0x10->0x55/0x20->0x77)、状态落库;同步pccu_it.moos与ws_check - h100.moos与pCCU.moos增加锂电池链路配置;归档锂电池协议文档 --- docs/锂电池协议20230324.docx | Bin 0 -> 45747 bytes missions/h100.moos | 11 + src/pCCU/CCU.cpp | 114 +++++++- src/pCCU/CCU.h | 20 ++ src/pCCU/CCU_Info.cpp | 16 +- src/pCCU/CCU软件需求.txt | 5 +- src/pCCU/CMakeLists.txt | 3 + src/pCCU/comm/BatLinkManager.h | 47 +++ src/pCCU/core/CoordFsm.cpp | 122 ++++++++ src/pCCU/core/CoordFsm.hpp | 168 +++++++++++ src/pCCU/core/PowerCoordinator.cpp | 388 +++++++++++++++++++++++++ src/pCCU/core/PowerCoordinator.h | 147 ++++++++++ src/pCCU/core/SnapshotBuilder.cpp | 113 +++++++- src/pCCU/core/SnapshotBuilder.h | 5 +- src/pCCU/core/SystemData.h | 38 +++ src/pCCU/fsm/tinyfsm.hpp | 251 ++++++++++++++++ src/pCCU/pCCU.moos | 20 +- src/pCCU/protocol/BatProtocol.cpp | 446 +++++++++++++++++++++++++++++ src/pCCU/protocol/BatProtocol.h | 334 +++++++++++++++++++++ src/pCCU/web/pages/index.h | 110 ++++++- test/CMakeLists.txt | 1 + test/pccu/pccuTest.cpp | 248 ++++++++++++++++ test/pccu/pccu_integration_test.py | 93 +++++- test/pccu/pccu_it.moos | 8 + test/pccu/pccu_ws_check.py | 6 +- 25 files changed, 2691 insertions(+), 23 deletions(-) create mode 100644 docs/锂电池协议20230324.docx create mode 100644 src/pCCU/comm/BatLinkManager.h create mode 100644 src/pCCU/core/CoordFsm.cpp create mode 100644 src/pCCU/core/CoordFsm.hpp create mode 100644 src/pCCU/core/PowerCoordinator.cpp create mode 100644 src/pCCU/core/PowerCoordinator.h create mode 100644 src/pCCU/fsm/tinyfsm.hpp create mode 100644 src/pCCU/protocol/BatProtocol.cpp create mode 100644 src/pCCU/protocol/BatProtocol.h diff --git a/docs/锂电池协议20230324.docx b/docs/锂电池协议20230324.docx new file mode 100644 index 0000000000000000000000000000000000000000..0bfd8f560e5ea6701a4deed9e6b4a578c7af67c3 GIT binary patch literal 45747 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 m82~{1hv~n#&itM5H#x_j2S<6>JHSxC8o literal 0 HcmV?d00001 diff --git a/missions/h100.moos b/missions/h100.moos index 091dc9b..e541719 100644 --- a/missions/h100.moos +++ b/missions/h100.moos @@ -51,6 +51,17 @@ 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 + 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 fa2f596..c4306c1 100644 --- a/src/pCCU/CCU.cpp +++ b/src/pCCU/CCU.cpp @@ -14,8 +14,11 @@ CCU::CCU() {} CCU::~CCU() { if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; } + if (m_coord) { delete m_coord; m_coord = nullptr; } if (m_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_snap) { delete m_snap; m_snap = nullptr; } if (m_sysData) { delete m_sysData; m_sysData = nullptr; } @@ -46,6 +49,13 @@ 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()); @@ -101,8 +111,33 @@ bool CCU::OnStartUp() { 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"); + + 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"); + + // 电源协调器(操作->指令下发 + 锂电池/燃料电池协调占位) + 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_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_db); + m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, + m_dynBatLink, m_insBatLink, m_db); // 网页 if (m_webEnable) { @@ -118,9 +153,12 @@ bool CCU::OnStartUp() { } } - LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld", + 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", m_fcLocalPort, m_fcHost.c_str(), m_fcPort, - m_pmLocalPort, m_pmHost.c_str(), m_pmPort); + m_pmLocalPort, m_pmHost.c_str(), m_pmPort, + m_dynBatLocalPort, m_dynBatHost.c_str(), m_dynBatPort, + m_insBatLocalPort, m_insBatHost.c_str(), m_insBatPort); return true; } @@ -140,6 +178,8 @@ void CCU::registerVariables() { // pCCU 数据交换主要走 UDP;MOOSDB 仅登记运行状态变量 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); } //--------------------------------------------------------- @@ -169,6 +209,10 @@ bool CCU::Iterate() { m_lastStatusTx = now; } + // 电源协调器:操作步骤推进 + 锂电池心跳指令(1s) + 协调算法占位 + if (m_coord) + m_coord->tick(now); + // 周期(1Hz)推送网页快照 if (m_web) { static double lastWebPush = 0; @@ -234,6 +278,16 @@ void CCU::handlePmMessage(Message* msg, const std::vector& frame) { m_sysData->updateFcControl(f); 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); + } } else { LOG_F(ERROR, "[PM] control decode failed"); } @@ -262,6 +316,43 @@ 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 状态报文发送 @@ -398,9 +489,26 @@ 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 << "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"; diff --git a/src/pCCU/CCU.h b/src/pCCU/CCU.h index 322a276..a8e9026 100644 --- a/src/pCCU/CCU.h +++ b/src/pCCU/CCU.h @@ -5,8 +5,10 @@ #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" #include "store/DbStore.h" #include "web/WebServer.h" @@ -18,7 +20,10 @@ namespace ccu { // 数据流: // 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger // 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池 +// -> 锂电池启停/功率指令映射为电池操作序列(协调器) // 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003) +// 收 电池组状态(0x0003)/电池包报警(0x0004)(动力/仪表锂电池) -> SystemData 快照 +// 周期向锂电池下发设备控制指令(0x0001,1s 心跳);协调器按操作时序下发自检(0x0000)等 // 收发全部帧落库 SQLite;网页周期推送 JSON 快照。 //============================================================================ @@ -39,6 +44,7 @@ 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); // 整合并发送 PM 状态报文 void sendPmStatus(); @@ -56,6 +62,17 @@ 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) 错开 @@ -68,10 +85,13 @@ 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; WebServer* m_web = nullptr; + PowerCoordinator* m_coord = nullptr; }; } // namespace ccu diff --git a/src/pCCU/CCU_Info.cpp b/src/pCCU/CCU_Info.cpp index e4a3407..74664db 100644 --- a/src/pCCU/CCU_Info.cpp +++ b/src/pCCU/CCU_Info.cpp @@ -15,9 +15,10 @@ void showSynopsis() { blk("SYNOPSIS: "); blk("------------------------------------ "); blk(" The pCCU application is the composite controller (CCU) "); - blk(" gateway between the fuel cell (FC) system and pPowerManger. "); - blk(" It receives FC status, integrates & forwards to pPowerManger, "); - blk(" forwards pPowerManger commands to FC, stores data in SQLite "); + 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(" "); } @@ -67,6 +68,13 @@ 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(" dbpath = pccu_data.db // 数据库路径 "); blk(" logpath = pCCU.log // 日志路径 "); blk(" web_port = 8080 // 网页端口(避开8090/18080) "); @@ -92,6 +100,8 @@ void showInterfaceAndExit() { 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/CCU软件需求.txt b/src/pCCU/CCU软件需求.txt index c370c84..46d162c 100644 --- a/src/pCCU/CCU软件需求.txt +++ b/src/pCCU/CCU软件需求.txt @@ -4,4 +4,7 @@ 4.具备sqlit数据库存储功能,可以把接收和发送的数据都存储到sqlit数据库中 5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置 6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介 -7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署 \ No newline at end of file +7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署 +8.接收动力/仪表锂电池的消息,协议见docs/锂电池协议20230324.docx;动力/仪表锂电池的ip和端口均可通过.moos文件配置 +9.锂电池需要额外操作(自检/上下电/电池切换/功率设定),pCCU根据操作按协议时序下发指令(0x0000自检/0x0001控制,1s心跳) +10.pCCU按自身算法进行锂电池与燃料电池的协调操作,算法入口为core/PowerCoordinator.cpp的coordinationTick()(当前为占位,待完善) \ No newline at end of file diff --git a/src/pCCU/CMakeLists.txt b/src/pCCU/CMakeLists.txt index 3ea4d8b..e72a243 100644 --- a/src/pCCU/CMakeLists.txt +++ b/src/pCCU/CMakeLists.txt @@ -24,6 +24,7 @@ SET(CCU_PROTOCOL_SRC protocol/MessageRegistry.cpp protocol/FcProtocol.cpp protocol/PmProtocol.cpp + protocol/BatProtocol.cpp ) SET(CCU_COMM_SRC @@ -33,6 +34,8 @@ SET(CCU_COMM_SRC SET(CCU_CORE_SRC core/SnapshotBuilder.cpp + core/PowerCoordinator.cpp + core/CoordFsm.cpp ) SET(CCU_STORE_SRC diff --git a/src/pCCU/comm/BatLinkManager.h b/src/pCCU/comm/BatLinkManager.h new file mode 100644 index 0000000..145c90f --- /dev/null +++ b/src/pCCU/comm/BatLinkManager.h @@ -0,0 +1,47 @@ +#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 new file mode 100644 index 0000000..69d2f3d --- /dev/null +++ b/src/pCCU/core/CoordFsm.cpp @@ -0,0 +1,122 @@ +#include "CoordFsm.hpp" +#include "SystemData.h" +#include "loguru.hpp" + +namespace ccu { + +// 静态上下文(接线时由 CoordFsm::init 注入) +PowerCoordinator* CoordFsm::m_coord = nullptr; +SystemData* CoordFsm::m_sys = nullptr; + +//============================================================================ +// CoordNormalState 正常运行(初始状态) +//============================================================================ + +void CoordNormalState::entry() { + LOG_F(INFO, "[CoordFsm] 进入状态: CoordNormalState"); +} + +void CoordNormalState::exit() { + LOG_F(INFO, "[CoordFsm] 离开状态: CoordNormalState"); +} + +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() 主机最新操控指令 + // + // 下发操作(提交后由协调器步骤引擎按序执行并等待确认): + // 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); +} + +void CoordNormalState::react(StepDoneEvent const &e) { + // 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); +} + +//============================================================================ +// CoordSwitchingState 电池切换中 +//============================================================================ + +void CoordSwitchingState::entry() { + LOG_F(INFO, "[CoordFsm] 进入状态: CoordSwitchingState"); +} + +void CoordSwitchingState::exit() { + LOG_F(INFO, "[CoordFsm] 离开状态: CoordSwitchingState"); +} + +void CoordSwitchingState::react(TickEvent const &e) { + // TODO(策略填写):切换过程中的周期监视(如各步骤进度/超时统计、 + // 是否需要中止切换并回退)。状态可通过 sys()->batStatus(role) 获取。 + (void)e; +} + +void CoordSwitchingState::react(StepDoneEvent const &e) { + // TODO(策略填写):切换步骤完成推进。 + // 全部步骤完成后返回正常运行态(coord()->pendingSteps()==0 可作为判据): + // if (coord()->pendingSteps() == 0) transit(); + // 步骤失败(超时或接触器故障)可中止并进入故障态: + // 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); +} + +//============================================================================ +// CoordFaultState 故障处理 +//============================================================================ + +void CoordFaultState::entry() { + LOG_F(INFO, "[CoordFsm] 进入状态: CoordFaultState"); +} + +void CoordFaultState::exit() { + LOG_F(INFO, "[CoordFsm] 离开状态: CoordFaultState"); +} + +void CoordFaultState::react(TickEvent const &e) { + // TODO(策略填写):故障处理与恢复判断(周期检查故障是否消除, + // 消除后 transit() 回正常运行态)。 + // 可用:sys()->fcStatus().fc_fault_level、sys()->batStatus(role).emergencyState / + // alarmFlag、sys()->batAlarm(role) 等。 + (void)e; +} + +void CoordFaultState::react(StepDoneEvent const &e) { + // TODO(策略填写):故障态下操作步骤结果处理(如降级下电指令的确认)。 + (void)e; +} + +} // namespace ccu + +// 初始状态声明(TinyFSM 宏要求全局作用域 + 命名空间限定名) +FSM_INITIAL_STATE(ccu::CoordFsm, ccu::CoordNormalState) diff --git a/src/pCCU/core/CoordFsm.hpp b/src/pCCU/core/CoordFsm.hpp new file mode 100644 index 0000000..bd30a35 --- /dev/null +++ b/src/pCCU/core/CoordFsm.hpp @@ -0,0 +1,168 @@ +#ifndef PCCU_COORD_FSM_HPP +#define PCCU_COORD_FSM_HPP + +#include "../fsm/tinyfsm.hpp" +#include "../protocol/FcProtocol.h" +#include "../protocol/PmProtocol.h" +#include "../protocol/BatProtocol.h" +#include "PowerCoordinator.h" + +namespace ccu { + +class SystemData; + +//============================================================================ +// CoordFsm:pCCU 电源协调状态机骨架(TinyFSM,事件驱动)。 +// +// 【当前状态:框架已参与编译,未接入运行流程(惰性)】 +// - 无任何代码调用 CoordFsm::start() / CoordFsm::dispatch(), +// 运行行为与接入前完全一致; +// - 各状态 react() 均为 TODO 空实现,由开发者填写协调策略。 +// +// 【设计约定】 +// - 边沿事件走 dispatch:指令到达(PmControlEvent)、操作步骤完成(StepDoneEvent)、 +// 周期节拍(TickEvent); +// - 电平状态不进事件:最新 FC/电池状态由状态机在 TickEvent 中直接读取 +// SystemData 快照,避免 1Hz 状态报文淹没事件队列; +// - TinyFSM 非线程安全:dispatch 必须收敛到单线程(MOOS 主循环线程), +// 链路接收线程产生的事件一律经 PowerCoordinator::pushEvent() 排队, +// 由 tick() 在主线程统一 drain 后逐个 dispatch(见接线指南第 1 步)。 +// +// 【接线指南:将来启用时按以下步骤操作】 +// +// 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 非空为前提:未接线时零开销、零行为) +// +// 2) CCU::OnStartUp(m_coord->setup(...) 之后): +// 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)); }); +// (c 为已解析的 PmControlValue,按值捕获) +// +// 4) 验证:编译运行后日志出现 "[CoordFsm] 进入状态: CoordNormalState" 即接线成功。 +// +// 【状态说明】 +// CoordNormalState 正常运行(初始状态):周期功率分配/阈值判断、响应主机指令 +// CoordSwitchingState 电池切换中:等待 StepDoneEvent 推进,完成后返回 Normal +// CoordFaultState 故障处理:告警抑制/降级运行/恢复判断 +//============================================================================ + +//---- 事件定义(边沿触发) ------------------------------------------------ + +// 周期节拍:由 coordinationTick 转发(AppTick 频率,默认 4Hz) +struct TickEvent : tinyfsm::Event { + double now; // MOOSTime + TickEvent() : now(0) {} + explicit TickEvent(double t) : now(t) {} +}; + +// 主机操控指令到达:PM 0x0001 每次成功解析触发一次 +struct PmControlEvent : tinyfsm::Event { + PmControlValue cmd; + PmControlEvent() {} + explicit PmControlEvent(const PmControlValue& c) : cmd(c) {} +}; + +// 电池操作步骤完成/超时:由协调器步骤引擎产生 +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) {} +}; + +//---- 状态机 --------------------------------------------------------------- + +class CoordFsm : public tinyfsm::Fsm { +public: + // 启用时注入上下文(见接线指南) + static void init(PowerCoordinator* coord, SystemData* sys) { + m_coord = coord; + m_sys = sys; + } + static PowerCoordinator* coord() { return m_coord; } + static SystemData* sys() { return m_sys; } + + // 事件处理默认实现:状态类按需覆盖;未覆盖的事件静默忽略 + virtual void react(tinyfsm::Event const &) {} + virtual void react(TickEvent const &) {} + virtual void react(PmControlEvent const &) {} + virtual void react(StepDoneEvent const &) {} + + // 状态进入/退出钩子(状态类覆盖以打日志/做进出动作) + virtual void entry() {} + virtual void exit() {} + +private: + static PowerCoordinator* m_coord; + static SystemData* m_sys; +}; + +//---- 状态类 --------------------------------------------------------------- + +// 正常运行(初始状态) +class CoordNormalState : public CoordFsm { +public: + void entry() override; + void exit() override; + void react(TickEvent const &e) override; + void react(PmControlEvent const &e) override; + void react(StepDoneEvent const &e) override; +}; + +// 电池切换中(提交 submitSwitchLowToHigh/HighToLow 后进入) +class CoordSwitchingState : public CoordFsm { +public: + void entry() override; + void exit() override; + void react(TickEvent const &e) override; + void react(StepDoneEvent const &e) override; +}; + +// 故障处理 +class CoordFaultState : public CoordFsm { +public: + void entry() override; + void exit() override; + void react(TickEvent const &e) override; + void react(StepDoneEvent const &e) override; +}; + +} // namespace ccu + +#endif // PCCU_COORD_FSM_HPP diff --git a/src/pCCU/core/PowerCoordinator.cpp b/src/pCCU/core/PowerCoordinator.cpp new file mode 100644 index 0000000..90737fc --- /dev/null +++ b/src/pCCU/core/PowerCoordinator.cpp @@ -0,0 +1,388 @@ +#include "PowerCoordinator.h" +#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) { + 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 周期调用。 +// +// 可用输入(通过 m_sys 读取最新状态): +// - m_sys->fcStatus() : 燃料电池最新状态 +// - m_sys->batStatus((int)role) : 动力/仪表锂电池最新状态 +// - m_sys->batAlarm((int)role) : 动力/仪表电池包报警信息 +// - m_sys->pmControl() : 控制主机最新操控指令 +// +// 可用操作(提交后将按步骤下发指令): +// - submitPowerOn/Off(role) : 电池上下电(母线接触器) +// - submitSetPower(role, kW) : 电池功率设定 +// - submitSelfCheck(role) : 电池自检 +// - submitSwitchLowToHigh(low, high) : 正常切换(低压->高压) +// - submitSwitchHighToLow(high, low) : 异常切换(高压->低压) +// - setWorkCondition(wc) : 工况设定 +// - sendFcControl(FcControlValue) : 直接下发燃料电池控制指令 +// +// TODO(开发者完善):在此实现具体的功率分配、电池切换时机、 +// 燃料电池与锂电池联合调度等协调策略。当前为空实现。 +//--------------------------------------------------------- +void PowerCoordinator::coordinationTick(double now) { + (void)now; // 占位:暂不执行任何协调动作 +} + +} // namespace ccu diff --git a/src/pCCU/core/PowerCoordinator.h b/src/pCCU/core/PowerCoordinator.h new file mode 100644 index 0000000..5bfd6d4 --- /dev/null +++ b/src/pCCU/core/PowerCoordinator.h @@ -0,0 +1,147 @@ +#ifndef PCCU_POWER_COORDINATOR_H +#define PCCU_POWER_COORDINATOR_H + +#include +#include +#include +#include +#include "../protocol/BatProtocol.h" +#include "../protocol/FcProtocol.h" + +namespace ccu { + +class SystemData; +class FcLinkManager; +class PmLinkManager; +class BatLinkManager; + +//============================================================================ +// PowerCoordinator:电源协调器。 +// +// 职责: +// 1. 操作 -> 指令:把"自检/上下电/电池切换/功率设定"等高级操作, +// 按协议文档要求的时序拆解为操作步骤队列,逐步下发锂电池指令, +// 并以电池状态反馈确认每一步的执行结果(超时兜底)。 +// 2. 心跳维持:周期(1s)向动力/仪表锂电池下发设备控制指令(0x0001), +// 携带当前期望状态(工况/接触器/功率)与自增心跳。 +// 3. 协调算法占位:coordinationTick() 为锂电池与燃料电池协调策略的 +// 占位入口,由后续完善(当前为空实现)。 +// +// 电池切换时序(docs/锂电池协议20230324.docx): +// 正常切换(低压->高压): +// 高压电池自检 -> 低压二极管闭合 -> 低压正极断开 +// -> 高压母线闭合 -> 高压二极管闭合 -> 低压二极管断开 +// 异常切换(高压->低压): +// 低压电池自检 -> 高压二极管闭合 -> 低压二极管闭合 +// -> 高压母线断开 -> 低压正极闭合 +//============================================================================ + +// 操作步骤类型 +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(); + + // 注入依赖(均在 CCU::OnStartUp 中创建完成后调用) + void setup(SystemData* sys, + FcLinkManager* fc, + PmLinkManager* pm, + BatLinkManager* dynBat, + BatLinkManager* insBat); + + //---------------- 操作接口(生成指令序列) ---------------- + // 单电池自检(随状态反馈确认自检完成) + 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()) + void tick(double now); + + // 直接向燃料电池下发控制指令(供协调算法联动使用) + bool sendFcControl(const FcControlValue& fcCmd); + + //---------------- 协调算法占位 ---------------- + // TODO(算法完善):锂电池与燃料电池协调策略入口。 + 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 + +#endif // PCCU_POWER_COORDINATOR_H diff --git a/src/pCCU/core/SnapshotBuilder.cpp b/src/pCCU/core/SnapshotBuilder.cpp index c0063b9..11eb55d 100644 --- a/src/pCCU/core/SnapshotBuilder.cpp +++ b/src/pCCU/core/SnapshotBuilder.cpp @@ -13,6 +13,7 @@ namespace { void put(JsonVal& j, const char* k, uint8_t v) { j[k] = JsonVal(v); } void put(JsonVal& j, const char* k, uint16_t v){ j[k] = JsonVal(v); } void put(JsonVal& j, const char* k, uint32_t v){ j[k] = JsonVal(static_cast(v)); } +void putI(JsonVal& j, const char* k, int16_t v) { j[k] = JsonVal(static_cast(v)); } //-------------------------------------------------------------------------- // 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。 @@ -33,6 +34,14 @@ 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); @@ -40,7 +49,10 @@ std::string describeMessageName(const std::string& link, uint16_t id) { } std::string describeDirection(const std::string& link, int direction) { - const std::string peer = (link == "fc") ? "FC" : "PM"; + 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"); } @@ -206,6 +218,87 @@ JsonVal pmFbJson(const PmParamSetFbValue& v) { return j; } +// 锂电池组状态(0x0003 收)-> JSON(关键字段) +JsonVal batStatusJson(const BatStatusValue& 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); + return j; +} + // PM 状态报文(整合后发送给控制主机)-> JSON(关键字段) JsonVal pmStatusJson(const PmStatusValue& v) { JsonVal j(Json::objectValue); @@ -320,8 +413,9 @@ JsonVal linkJson(const LinkManager* lm) { } // namespace -SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db) - : m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {} +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) {} std::string SnapshotBuilder::build() const { JsonVal root(Json::objectValue); @@ -333,10 +427,17 @@ 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)); 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))); } else { root["fc"] = JsonVal(Json::objectValue); root["pmCmd"] = JsonVal(Json::objectValue); @@ -344,11 +445,17 @@ 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); } 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 a3f9ab4..208591a 100644 --- a/src/pCCU/core/SnapshotBuilder.h +++ b/src/pCCU/core/SnapshotBuilder.h @@ -18,7 +18,8 @@ class DbStore; class SnapshotBuilder { public: - SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db); + SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, + LinkManager* batDyn, LinkManager* batIns, DbStore* db); // 生成完整快照 JSON std::string build() const; @@ -30,6 +31,8 @@ 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 2c12133..3d259d1 100644 --- a/src/pCCU/core/SystemData.h +++ b/src/pCCU/core/SystemData.h @@ -5,6 +5,7 @@ #include #include "../protocol/FcProtocol.h" #include "../protocol/PmProtocol.h" +#include "../protocol/BatProtocol.h" namespace ccu { @@ -12,6 +13,7 @@ namespace ccu { // SystemData:跨线程共享的最新状态快照。 // // - 接收线程(FC 链路)写 FcStatus 快照 +// - 接收线程(锂电池链路)写 动力/仪表 BatStatus 快照(role: 0=动力 1=仪表) // - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送 // - 网页线程读快照展示 // 通过互斥锁保护读写。 @@ -99,6 +101,38 @@ public: return m_pmParamFb; } + //-------- 锂电池(role: 0=动力 1=仪表,见 BatRole) -------- + + // 更新/获取最新电池组状态(0x0003) + void updateBatStatus(int role, const BatStatusValue& v) { + std::lock_guard lock(m_mutex); + m_batStatus[role] = v; + m_batStatusCount[role]++; + } + BatStatusValue batStatus(int role) const { + std::lock_guard lock(m_mutex); + return m_batStatus[role]; + } + unsigned long batStatusCount(int role) const { + std::lock_guard lock(m_mutex); + return m_batStatusCount[role]; + } + + // 更新/获取最新电池包报警信息(0x0004) + void updateBatAlarm(int role, const BatPackAlarmValue& v) { + std::lock_guard lock(m_mutex); + m_batAlarm[role] = v; + m_batAlarmCount[role]++; + } + BatPackAlarmValue batAlarm(int role) 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]; + } + private: mutable std::mutex m_mutex; FcStatusValue m_fcStatus; @@ -107,10 +141,14 @@ private: FcControlValue m_fcControl; PmStatusValue m_pmStatus; PmParamSetFbValue m_pmParamFb; + BatStatusValue m_batStatus[2]; + BatPackAlarmValue m_batAlarm[2]; 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}; }; } // namespace ccu diff --git a/src/pCCU/fsm/tinyfsm.hpp b/src/pCCU/fsm/tinyfsm.hpp new file mode 100644 index 0000000..5a11f8a --- /dev/null +++ b/src/pCCU/fsm/tinyfsm.hpp @@ -0,0 +1,251 @@ +/* + * TinyFSM - Tiny Finite State Machine Processor + * + * Copyright (c) 2012-2022 Axel Burri + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* --------------------------------------------------------------------- + * Version: 0.3.3 + * + * API documentation: see "../doc/50-API.md" + * + * The official TinyFSM website is located at: + * https://digint.ch/tinyfsm/ + * + * Author: + * Axel Burri + * --------------------------------------------------------------------- + */ + +#ifndef TINYFSM_HPP_INCLUDED +#define TINYFSM_HPP_INCLUDED + +#ifndef TINYFSM_NOSTDLIB +#include +#endif + +// #include +// #define DBG(str) do { std::cerr << str << std::endl; } while( false ) +// DBG("*** dbg_example *** " << __PRETTY_FUNCTION__); + +namespace tinyfsm +{ + + // -------------------------------------------------------------------------- + + struct Event { }; + + // -------------------------------------------------------------------------- + +#ifdef TINYFSM_NOSTDLIB + // remove dependency on standard library (silent fail!). + // useful in conjunction with -nostdlib option, e.g. if your compiler + // does not provide a standard library. + // NOTE: this silently disables all static_assert() calls below! + template + struct is_same_fsm { static constexpr bool value = true; }; +#else + // check if both fsm and state class share same fsmtype + template + struct is_same_fsm : std::is_same< typename F::fsmtype, typename S::fsmtype > { }; +#endif + + template + struct _state_instance + { + using value_type = S; + using type = _state_instance; + static S value; + }; + + template + typename _state_instance::value_type _state_instance::value; + + // -------------------------------------------------------------------------- + + template + class Fsm + { + public: + + using fsmtype = Fsm; + using state_ptr_t = F *; + + static state_ptr_t current_state_ptr; + + // public, leaving ability to access state instance (e.g. on reset) + template + static constexpr S & state(void) { + static_assert(is_same_fsm::value, "accessing state of different state machine"); + return _state_instance::value; + } + + template + static constexpr bool is_in_state(void) { + static_assert(is_same_fsm::value, "accessing state of different state machine"); + return current_state_ptr == &_state_instance::value; + } + + /// state machine functions + public: + + // explicitely specialized in FSM_INITIAL_STATE macro + static void set_initial_state(); + + static void reset() { }; + + static void enter() { + current_state_ptr->entry(); + } + + static void start() { + set_initial_state(); + enter(); + } + + template + static void dispatch(E const & event) { + current_state_ptr->react(event); + } + + + /// state transition functions + protected: + + template + void transit(void) { + static_assert(is_same_fsm::value, "transit to different state machine"); + current_state_ptr->exit(); + current_state_ptr = &_state_instance::value; + current_state_ptr->entry(); + } + + template + void transit(ActionFunction action_function) { + static_assert(is_same_fsm::value, "transit to different state machine"); + current_state_ptr->exit(); + // NOTE: do not send events in action_function definisions. + action_function(); + current_state_ptr = &_state_instance::value; + current_state_ptr->entry(); + } + + template + void transit(ActionFunction action_function, ConditionFunction condition_function) { + if(condition_function()) { + transit(action_function); + } + } + }; + + template + typename Fsm::state_ptr_t Fsm::current_state_ptr; + + // -------------------------------------------------------------------------- + + template + struct FsmList; + + template<> struct FsmList<> { + static void set_initial_state() { } + static void reset() { } + static void enter() { } + template + static void dispatch(E const &) { } + }; + + template + struct FsmList + { + using fsmtype = Fsm; + + static void set_initial_state() { + fsmtype::set_initial_state(); + FsmList::set_initial_state(); + } + + static void reset() { + F::reset(); + FsmList::reset(); + } + + static void enter() { + fsmtype::enter(); + FsmList::enter(); + } + + static void start() { + set_initial_state(); + enter(); + } + + template + static void dispatch(E const & event) { + fsmtype::template dispatch(event); + FsmList::template dispatch(event); + } + }; + + // -------------------------------------------------------------------------- + + template struct StateList; + template<> struct StateList<> { + static void reset() { } + }; + template + struct StateList + { + static void reset() { + _state_instance::value = S(); + StateList::reset(); + } + }; + + // -------------------------------------------------------------------------- + + template + struct MooreMachine : tinyfsm::Fsm + { + virtual void entry(void) { }; /* entry actions in some states */ + void exit(void) { }; /* no exit actions */ + }; + + template + struct MealyMachine : tinyfsm::Fsm + { + // input actions are modeled in react(): + // - conditional dependent of event type or payload + // - transit<>(ActionFunction) + void entry(void) { }; /* no entry actions */ + void exit(void) { }; /* no exit actions */ + }; + +} /* namespace tinyfsm */ + + +#define FSM_INITIAL_STATE(_FSM, _STATE) \ +namespace tinyfsm { \ + template<> void Fsm< _FSM >::set_initial_state(void) { \ + current_state_ptr = &_state_instance< _STATE >::value; \ + } \ +} + +#endif /* TINYFSM_HPP_INCLUDED */ diff --git a/src/pCCU/pCCU.moos b/src/pCCU/pCCU.moos index afed511..a61aecb 100644 --- a/src/pCCU/pCCU.moos +++ b/src/pCCU/pCCU.moos @@ -2,9 +2,12 @@ // pCCU 配置示例 // // 链路拓扑: -// 燃料电池控制器(FC) 192.168.1.162:7000 +// 燃料电池控制器(FC) 192.168.1.162:7000 // 控制主机(pPowerManger) 192.168.0.140:5001 -// pCCU 本机监听:FC 状态端口 6000、PM 指令端口 7000 +// 动力锂电池 192.168.100.137:7000 +// 仪表锂电池 192.168.100.136:7000 +// pCCU 本机监听:FC 状态端口 6000、PM 指令端口 7000、 +// 动力电池端口 7001、仪表电池端口 7002 //============================================================================ ProcessConfig = pCCU @@ -26,6 +29,19 @@ 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 + //======== 存储与日志 ======== // 数据库路径(SQLite) dbpath = pccu_data.db diff --git a/src/pCCU/protocol/BatProtocol.cpp b/src/pCCU/protocol/BatProtocol.cpp new file mode 100644 index 0000000..cbc885b --- /dev/null +++ b/src/pCCU/protocol/BatProtocol.cpp @@ -0,0 +1,446 @@ +#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 new file mode 100644 index 0000000..b7bf9ec --- /dev/null +++ b/src/pCCU/protocol/BatProtocol.h @@ -0,0 +1,334 @@ +#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/web/pages/index.h b/src/pCCU/web/pages/index.h index 3068343..ddbc242 100644 --- a/src/pCCU/web/pages/index.h +++ b/src/pCCU/web/pages/index.h @@ -74,6 +74,8 @@ th{color:var(--dim);font-weight:500;} + +
发送 @@ -91,7 +93,7 @@ th{color:var(--dim);font-weight:500;} const $ = id => document.getElementById(id); const pagesEl = $('pages'); const statusEl = $('status'); -const TABS = ['fcStatus','pmCmd','pmParam','fcCmd','pmStatus','pmFb','links','raw']; +const TABS = ['fcStatus','pmCmd','pmParam','batDyn','batIns','fcCmd','pmStatus','pmFb','links','raw']; let activeTab = 'fcStatus'; function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); } @@ -126,6 +128,16 @@ function statusText(s){ return m[s] || ('未知('+hex(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)+''; } // 带解析的值:hex 码 + 括号解析 function parsed(v, text){ return hex(v)+' ('+text+')'; } // 故障码高亮:0 显示绿色正常,非 0 显示红色 @@ -291,10 +303,16 @@ 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('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); } @@ -359,6 +377,89 @@ function pmFbCard(d){ return card('设定结果', h); } +//------------------ 锂电池状态(0x0003) / 电池包报警(0x0004) 卡片 ------------------ +function batRunCard(d){ + if(!d) return ''; + 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); +} + function pmStatusCard(d){ if(!d) return ''; let h=''; @@ -450,6 +551,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)+'
', 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 bf4f14c..9810d4b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -137,6 +137,7 @@ add_executable(pccuTest ${CCU_DIR}/protocol/MessageRegistry.cpp ${CCU_DIR}/protocol/FcProtocol.cpp ${CCU_DIR}/protocol/PmProtocol.cpp + ${CCU_DIR}/protocol/BatProtocol.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 e6e474f..60517dd 100644 --- a/test/pccu/pccuTest.cpp +++ b/test/pccu/pccuTest.cpp @@ -15,9 +15,11 @@ #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/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; @@ -287,6 +289,247 @@ static void testDbStore() { std::remove(path.c_str()); } +//-------------------------------------------------------------------------- +// 9. 锂电池帧长度必须与协议文档一致(docs/锂电池协议20230324.docx) +static void testBatFrameLengths() { + BatSelfCheckMessage bsc; + BatControlMessage bcm; + BatStatusMessage bsm; + BatPackAlarmMessage bam; + + 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 +} + +//-------------------------------------------------------------------------- +// 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); + + // 设备控制指令 + 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; + + 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); // 心跳 + + 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); +} + +//-------------------------------------------------------------------------- +// 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)); +} + //-------------------------------------------------------------------------- int main() { testFrameLengths(); @@ -297,6 +540,11 @@ int main() { testChecksum(); testLinkDispatch(); testDbStore(); + testBatFrameLengths(); + testBatControlRoundTrip(); + testBatStatusRoundTrip(); + testBatPackAlarmRoundTrip(); + testBatLinkDispatch(); 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 b6010a7..f0f926c 100644 --- a/test/pccu/pccu_integration_test.py +++ b/test/pccu/pccu_integration_test.py @@ -4,15 +4,21 @@ 拓扑: 测试脚本(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] 校验: 1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003 2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001 3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003 - 4. SQLite 记录了收/发原始帧 - 5. Web 页面可访问 + 4. PM 操控中锂电池启停指令映射为电池控制指令(0x0001, 27B): + 动力 0x20 关闭 -> 母线接触器 0x77;仪表 0x10 启动 -> 母线接触器 0x55 + 5. pCCU 周期(1s)向动力/仪表锂电池下发心跳控制指令 + 6. 电池组状态(0x0003)收到后落库 SQLite(link=bat_dyn/bat_ins) + 7. SQLite 记录了收/发原始帧 + 8. Web 页面可访问 """ import socket import struct @@ -29,6 +35,10 @@ 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" @@ -94,6 +104,26 @@ 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:数据库有记录(含锂电池链路收发) 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}") + 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) - # 校验 5:Web 页面 + # 校验 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 2e4d92d..bb7c2b4 100644 --- a/test/pccu/pccu_it.moos +++ b/test/pccu/pccu_it.moos @@ -16,6 +16,14 @@ 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 // 仪表电池(测试监听此端口收控制/心跳指令) + dbpath = /tmp/pccu_it_test.db logpath = /tmp/pccu_it_test.log diff --git a/test/pccu/pccu_ws_check.py b/test/pccu/pccu_ws_check.py index 3aa753e..9c6e829 100644 --- a/test/pccu/pccu_ws_check.py +++ b/test/pccu/pccu_ws_check.py @@ -82,11 +82,15 @@ def main(): print("FAIL: 快照非合法JSON:", text[:200]) return 1 ok = True - for key in ("fc", "pmCmd", "links", "fcStatusCount", "pmControlCount", "logs"): + for key in ("fc", "pmCmd", "links", "fcStatusCount", "pmControlCount", "logs", + "batDyn", "batIns", "batDynAlarm", "batInsAlarm", + "batDynStatusCount", "batInsStatusCount"): ok = ok and key in data + ok = ok and ("batDyn" in data.get("links", {})) and ("batIns" in data.get("links", {})) print(f"PASS: 快照 JSON 有效, 分组keys={sorted(data.keys())}") print(f" fc.fc_status={data['fc']['fc_status']}, fc.fc_mode={data['fc']['fc_mode']}, " f"heartbeat={data['fc']['heartbeat']}") + print(f" batDyn.soc={data['batDyn'].get('soc')}, batIns.soc={data['batIns'].get('soc')}") print(f" links={json.dumps(data['links'], ensure_ascii=False)}") print(f" logs条数={len(data['logs'])}, fcStatusCount={data['fcStatusCount']}") ws.s.close()