pCCU新增锂电池通信/协调操作 + 电源协调状态机骨架(惰性)

锂电池通信(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增加锂电池链路配置;归档锂电池协议文档
This commit is contained in:
zjk
2026-08-30 14:40:27 +08:00
parent 2590282aed
commit 7e4d08808a
25 changed files with 2691 additions and 23 deletions
Binary file not shown.
+11
View File
@@ -51,6 +51,17 @@ ProcessConfig = pCCU
pm_remote_ip = 127.0.0.1 pm_remote_ip = 127.0.0.1
pm_remote_port = 5001 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 dbpath = /root/work/h100/data/pccu_data.db
logpath = /root/work/h100/data/pCCU.log logpath = /root/work/h100/data/pCCU.log
+111 -3
View File
@@ -14,8 +14,11 @@ CCU::CCU() {}
CCU::~CCU() { CCU::~CCU() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; } if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
if (m_coord) { delete m_coord; m_coord = nullptr; }
if (m_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; } if (m_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; } if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; }
if (m_dynBatLink) { m_dynBatLink->stop(); delete m_dynBatLink; m_dynBatLink = nullptr; }
if (m_insBatLink) { m_insBatLink->stop(); delete m_insBatLink; m_insBatLink = nullptr; }
if (m_db) { delete m_db; m_db = nullptr; } if (m_db) { delete m_db; m_db = nullptr; }
if (m_snap) { delete m_snap; m_snap = nullptr; } if (m_snap) { delete m_snap; m_snap = nullptr; }
if (m_sysData) { delete m_sysData; m_sysData = nullptr; } if (m_sysData) { delete m_sysData; m_sysData = nullptr; }
@@ -46,6 +49,13 @@ bool CCU::OnStartUp() {
else if (param == "pm_local_port") m_pmLocalPort = atol(value.c_str()); else if (param == "pm_local_port") m_pmLocalPort = atol(value.c_str());
else if (param == "pm_remote_ip") m_pmHost = value; else if (param == "pm_remote_ip") m_pmHost = value;
else if (param == "pm_remote_port") m_pmPort = atol(value.c_str()); else if (param == "pm_remote_port") m_pmPort = atol(value.c_str());
else if (param == "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 == "dbpath") m_dbPath = value;
else if (param == "logpath") m_logPath = value; else if (param == "logpath") m_logPath = value;
else if (param == "web_port") m_webPort = atoi(value.c_str()); else if (param == "web_port") m_webPort = atoi(value.c_str());
@@ -101,8 +111,33 @@ bool CCU::OnStartUp() {
if (!m_pmLink->start()) if (!m_pmLink->start())
LOG_F(ERROR, "PM link start failed"); 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<uint8_t>& frame) {
handleBatMessage(BatRole::Dyn, msg, frame);
});
m_dynBatLink->setOnRawFrame([](int, const std::vector<uint8_t>&, 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<uint8_t>& frame) {
handleBatMessage(BatRole::Ins, msg, frame);
});
m_insBatLink->setOnRawFrame([](int, const std::vector<uint8_t>&, 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<uint8_t>(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) { 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_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; return true;
} }
@@ -140,6 +178,8 @@ void CCU::registerVariables() {
// pCCU 数据交换主要走 UDP;MOOSDB 仅登记运行状态变量 // pCCU 数据交换主要走 UDP;MOOSDB 仅登记运行状态变量
Register("CCU_FC_LINK_STATE", 0); Register("CCU_FC_LINK_STATE", 0);
Register("CCU_PM_LINK_STATE", 0); Register("CCU_PM_LINK_STATE", 0);
Register("CCU_DYN_BAT_LINK_STATE", 0);
Register("CCU_INS_BAT_LINK_STATE", 0);
} }
//--------------------------------------------------------- //---------------------------------------------------------
@@ -169,6 +209,10 @@ bool CCU::Iterate() {
m_lastStatusTx = now; m_lastStatusTx = now;
} }
// 电源协调器:操作步骤推进 + 锂电池心跳指令(1s) + 协调算法占位
if (m_coord)
m_coord->tick(now);
// 周期(1Hz)推送网页快照 // 周期(1Hz)推送网页快照
if (m_web) { if (m_web) {
static double lastWebPush = 0; static double lastWebPush = 0;
@@ -234,6 +278,16 @@ void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
m_sysData->updateFcControl(f); m_sysData->updateFcControl(f);
m_fcLink->sendMessage(0x0001, &f); m_fcLink->sendMessage(0x0001, &f);
LOG_F(INFO, "[FC] forward control cmd=%d power=%d", f.cmd, f.outputPower); 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 { } else {
LOG_F(ERROR, "[PM] control decode failed"); LOG_F(ERROR, "[PM] control decode failed");
} }
@@ -262,6 +316,43 @@ void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
} }
} }
//---------------------------------------------------------
// handleBatMessage:处理锂电池链路收到的消息(动力/仪表共用协议)
void CCU::handleBatMessage(BatRole role, Message* msg, const std::vector<uint8_t>& 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<void*>(&v))) {
m_sysData->updateBatStatus(static_cast<int>(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<void*>(&v))) {
m_sysData->updateBatAlarm(static_cast<int>(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 状态报文发送 // sendPmStatus:整合最新 FC 状态,编码 PM 状态报文发送
@@ -398,9 +489,26 @@ bool CCU::buildReport() {
<< " tx:" << m_pmLink->txCount() << " tx:" << m_pmLink->txCount()
<< " err:" << m_pmLink->errorCount() << "\n"; << " 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) { if (m_sysData) {
m_msgs << "FC 状态接收次数:" << m_sysData->fcStatusCount() << "\n"; m_msgs << "FC 状态接收次数:" << m_sysData->fcStatusCount() << "\n";
m_msgs << "PM 指令接收次数:" << m_sysData->pmControlCount() << "\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) { if (m_db) {
m_msgs << "数据库记录数:" << m_db->count() << "\n"; m_msgs << "数据库记录数:" << m_db->count() << "\n";
+20
View File
@@ -5,8 +5,10 @@
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h" #include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "comm/FcLinkManager.h" #include "comm/FcLinkManager.h"
#include "comm/PmLinkManager.h" #include "comm/PmLinkManager.h"
#include "comm/BatLinkManager.h"
#include "core/SystemData.h" #include "core/SystemData.h"
#include "core/SnapshotBuilder.h" #include "core/SnapshotBuilder.h"
#include "core/PowerCoordinator.h"
#include "store/DbStore.h" #include "store/DbStore.h"
#include "web/WebServer.h" #include "web/WebServer.h"
@@ -18,7 +20,10 @@ namespace ccu {
// 数据流: // 数据流:
// 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger // 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger
// 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池 // 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池
// -> 锂电池启停/功率指令映射为电池操作序列(协调器)
// 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003) // 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003)
// 收 电池组状态(0x0003)/电池包报警(0x0004)(动力/仪表锂电池) -> SystemData 快照
// 周期向锂电池下发设备控制指令(0x0001,1s 心跳);协调器按操作时序下发自检(0x0000)等
// 收发全部帧落库 SQLite;网页周期推送 JSON 快照。 // 收发全部帧落库 SQLite;网页周期推送 JSON 快照。
//============================================================================ //============================================================================
@@ -39,6 +44,7 @@ private:
// 链路收帧处理 // 链路收帧处理
void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame); void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame);
void handlePmMessage(Message* msg, const std::vector<uint8_t>& frame); void handlePmMessage(Message* msg, const std::vector<uint8_t>& frame);
void handleBatMessage(BatRole role, Message* msg, const std::vector<uint8_t>& frame);
// 整合并发送 PM 状态报文 // 整合并发送 PM 状态报文
void sendPmStatus(); void sendPmStatus();
@@ -56,6 +62,17 @@ private:
std::string m_pmHost = "192.168.0.140"; std::string m_pmHost = "192.168.0.140";
long m_pmPort = 5001; long m_pmPort = 5001;
// 锂电池链路(动力/仪表,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_dbPath = "pccu_data.db";
std::string m_logPath = "pCCU.log"; std::string m_logPath = "pCCU.log";
int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开 int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开
@@ -68,10 +85,13 @@ private:
// 组件 // 组件
FcLinkManager* m_fcLink = nullptr; FcLinkManager* m_fcLink = nullptr;
PmLinkManager* m_pmLink = nullptr; PmLinkManager* m_pmLink = nullptr;
BatLinkManager* m_dynBatLink = nullptr;
BatLinkManager* m_insBatLink = nullptr;
SystemData* m_sysData = nullptr; SystemData* m_sysData = nullptr;
DbStore* m_db = nullptr; DbStore* m_db = nullptr;
SnapshotBuilder* m_snap = nullptr; SnapshotBuilder* m_snap = nullptr;
WebServer* m_web = nullptr; WebServer* m_web = nullptr;
PowerCoordinator* m_coord = nullptr;
}; };
} // namespace ccu } // namespace ccu
+13 -3
View File
@@ -15,9 +15,10 @@ void showSynopsis() {
blk("SYNOPSIS: "); blk("SYNOPSIS: ");
blk("------------------------------------ "); blk("------------------------------------ ");
blk(" The pCCU application is the composite controller (CCU) "); blk(" The pCCU application is the composite controller (CCU) ");
blk(" gateway between the fuel cell (FC) system and pPowerManger. "); blk(" gateway between the fuel cell (FC) system / lithium battery ");
blk(" It receives FC status, integrates & forwards to pPowerManger, "); blk(" packs and pPowerManger. It receives FC & battery status, ");
blk(" forwards pPowerManger commands to FC, stores data in SQLite "); 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(" and provides a web monitoring page. ");
blk(" "); blk(" ");
} }
@@ -67,6 +68,13 @@ void showExampleConfigAndExit() {
blk(" pm_local_port = 7000 // PM 指令接收端口 "); blk(" pm_local_port = 7000 // PM 指令接收端口 ");
blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP "); blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP ");
blk(" pm_remote_port = 5001 // 控制主机接收端口 "); blk(" pm_remote_port = 5001 // 控制主机接收端口 ");
blk(" 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(" dbpath = pccu_data.db // 数据库路径 ");
blk(" logpath = pCCU.log // 日志路径 "); blk(" logpath = pCCU.log // 日志路径 ");
blk(" web_port = 8080 // 网页端口(避开8090/18080) "); blk(" web_port = 8080 // 网页端口(避开8090/18080) ");
@@ -92,6 +100,8 @@ void showInterfaceAndExit() {
blk("------------------------------------ "); blk("------------------------------------ ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 "); blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 "); blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_DYN_BAT_LINK_STATE = 运行状态字符串 ");
blk(" CCU_INS_BAT_LINK_STATE = 运行状态字符串 ");
blk(" "); blk(" ");
exit(0); exit(0);
} }
+4 -1
View File
@@ -4,4 +4,7 @@
4.具备sqlit数据库存储功能,可以把接收和发送的数据都存储到sqlit数据库中 4.具备sqlit数据库存储功能,可以把接收和发送的数据都存储到sqlit数据库中
5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置 5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置
6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介 6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介
7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署 7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署
8.接收动力/仪表锂电池的消息,协议见docs/锂电池协议20230324.docx;动力/仪表锂电池的ip和端口均可通过.moos文件配置
9.锂电池需要额外操作(自检/上下电/电池切换/功率设定),pCCU根据操作按协议时序下发指令(0x0000自检/0x0001控制,1s心跳)
10.pCCU按自身算法进行锂电池与燃料电池的协调操作,算法入口为core/PowerCoordinator.cpp的coordinationTick()(当前为占位,待完善)
+3
View File
@@ -24,6 +24,7 @@ SET(CCU_PROTOCOL_SRC
protocol/MessageRegistry.cpp protocol/MessageRegistry.cpp
protocol/FcProtocol.cpp protocol/FcProtocol.cpp
protocol/PmProtocol.cpp protocol/PmProtocol.cpp
protocol/BatProtocol.cpp
) )
SET(CCU_COMM_SRC SET(CCU_COMM_SRC
@@ -33,6 +34,8 @@ SET(CCU_COMM_SRC
SET(CCU_CORE_SRC SET(CCU_CORE_SRC
core/SnapshotBuilder.cpp core/SnapshotBuilder.cpp
core/PowerCoordinator.cpp
core/CoordFsm.cpp
) )
SET(CCU_STORE_SRC SET(CCU_STORE_SRC
+47
View File
@@ -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
+122
View File
@@ -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<CoordFaultState>();
LOG_F(INFO, "[CoordFsm][Normal] StepDoneEvent: role=%d type=%d confirmed=%d",
static_cast<int>(e.role), static_cast<int>(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<CoordNormalState>();
// 步骤失败(超时或接触器故障)可中止并进入故障态:
// if (!e.confirmed) transit<CoordFaultState>();
LOG_F(INFO, "[CoordFsm][Switching] StepDoneEvent: role=%d type=%d confirmed=%d pending=%zu",
static_cast<int>(e.role), static_cast<int>(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<CoordNormalState>() 回正常运行态)。
// 可用: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)
+168
View File
@@ -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<void(double)> m_tickHook = nullptr; // 节拍转发
// std::mutex m_evtMutex;
// std::deque<std::function<void()>> m_events; // 事件队列(闭包)
// 新增方法:
// void setTickHook(std::function<void(double)> h) { m_tickHook = std::move(h); }
// void pushEvent(std::function<void()> fn) { // 线程安全;带上限防积压
// std::lock_guard<std::mutex> 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<std::function<void()>> evs;
// { std::lock_guard<std::mutex> 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<int>(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<CoordFsm> {
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
+388
View File
@@ -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 <ctime>
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<bool(const BatStatusValue&)> doneSelfCheck() {
return std::function<bool(const BatStatusValue&)>(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<int>(BatRole::Dyn)] = dynBat;
m_bat[static_cast<int>(BatRole::Ins)] = insBat;
}
//---------------------------------------------------------
// 操作接口
// 提交一个步骤:挂入其目标电池的队列(同一电池内的步骤按序执行)。
// 复合操作(电池切换)的各步骤依次调用本函数即可保持时序。
static void pushStep(std::deque<BatOpStep>& q, const BatOpStep& s) {
q.push_back(s);
}
bool PowerCoordinator::submit(const BatOpStep& step) {
std::deque<BatOpStep>& q = m_steps[static_cast<int>(step.role)];
pushStep(q, step);
LOG_F(INFO, "[Coord] operation step queued: role=%s type=%d (pending=%zu)",
batRoleName(step.role), static_cast<int>(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<BatOpStep>& q = m_steps[static_cast<int>(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<BatOpStep>& q = m_steps[static_cast<int>(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<BatOpStep>& 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<int>(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<int>(s.type));
} else {
LOG_F(INFO, "[Coord] step confirmed (role=%s type=%d)",
batRoleName(s.role), static_cast<int>(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<BatRole>(i), now);
}
}
//---------------------------------------------------------
// 指令下发
void PowerCoordinator::applyStep(const BatOpStep& s, double now) {
const int idx = static_cast<int>(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, &lt);
c.year = static_cast<uint16_t>(lt.tm_year + 1900);
c.month = static_cast<uint8_t>(lt.tm_mon + 1);
c.day = static_cast<uint8_t>(lt.tm_mday);
c.hour = static_cast<uint8_t>(lt.tm_hour);
c.minute = static_cast<uint8_t>(lt.tm_min);
c.second = static_cast<uint8_t>(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<int>(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
+147
View File
@@ -0,0 +1,147 @@
#ifndef PCCU_POWER_COORDINATOR_H
#define PCCU_POWER_COORDINATOR_H
#include <cstdint>
#include <cstddef>
#include <deque>
#include <functional>
#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<bool(const BatStatusValue&)> 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<int>(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<BatOpStep> 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
+110 -3
View File
@@ -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, 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, uint16_t v){ j[k] = JsonVal(v); }
void put(JsonVal& j, const char* k, uint32_t v){ j[k] = JsonVal(static_cast<Json::UInt>(v)); } void put(JsonVal& j, const char* k, uint32_t v){ j[k] = JsonVal(static_cast<Json::UInt>(v)); }
void putI(JsonVal& j, const char* k, int16_t v) { j[k] = JsonVal(static_cast<int>(v)); }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。 // 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。
@@ -33,6 +34,14 @@ std::string describeMessageName(const std::string& link, uint16_t id) {
case 0x0004: return "PM状态报文"; case 0x0004: return "PM状态报文";
default: break; 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]; char buf[32];
std::snprintf(buf, sizeof(buf), "未知(0x%04X)", id); 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) { 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"); return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU");
} }
@@ -206,6 +218,87 @@ JsonVal pmFbJson(const PmParamSetFbValue& v) {
return j; return j;
} }
// 锂电池组状态(0x0003 收)-> JSON(关键字段)
JsonVal batStatusJson(const BatStatusValue& v) {
JsonVal j(Json::objectValue);
j["time"] = JsonVal(static_cast<Json::UInt>(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(关键字段) // PM 状态报文(整合后发送给控制主机)-> JSON(关键字段)
JsonVal pmStatusJson(const PmStatusValue& v) { JsonVal pmStatusJson(const PmStatusValue& v) {
JsonVal j(Json::objectValue); JsonVal j(Json::objectValue);
@@ -320,8 +413,9 @@ JsonVal linkJson(const LinkManager* lm) {
} // namespace } // namespace
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db) SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
: m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {} 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 { std::string SnapshotBuilder::build() const {
JsonVal root(Json::objectValue); JsonVal root(Json::objectValue);
@@ -333,10 +427,17 @@ std::string SnapshotBuilder::build() const {
root["pmParam"] = pmParamJson(m_sys->pmParamSet()); root["pmParam"] = pmParamJson(m_sys->pmParamSet());
root["pmFb"] = pmFbJson(m_sys->pmParamFb()); root["pmFb"] = pmFbJson(m_sys->pmParamFb());
root["pmStatus"] = pmStatusJson(m_sys->pmStatus()); 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<Json::UInt>(m_sys->fcStatusCount())); root["fcStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcStatusCount()));
root["pmControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmControlCount())); root["pmControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmControlCount()));
root["fcControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcControlCount())); root["fcControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcControlCount()));
root["pmStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmStatusCount())); root["pmStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmStatusCount()));
root["batDynStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->batStatusCount(0)));
root["batInsStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->batStatusCount(1)));
} else { } else {
root["fc"] = JsonVal(Json::objectValue); root["fc"] = JsonVal(Json::objectValue);
root["pmCmd"] = JsonVal(Json::objectValue); root["pmCmd"] = JsonVal(Json::objectValue);
@@ -344,11 +445,17 @@ std::string SnapshotBuilder::build() const {
root["pmParam"] = JsonVal(Json::objectValue); root["pmParam"] = JsonVal(Json::objectValue);
root["pmFb"] = JsonVal(Json::objectValue); root["pmFb"] = JsonVal(Json::objectValue);
root["pmStatus"] = 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); JsonVal links(Json::objectValue);
links["fc"] = linkJson(m_fc); links["fc"] = linkJson(m_fc);
links["pm"] = linkJson(m_pm); links["pm"] = linkJson(m_pm);
links["batDyn"] = linkJson(m_batDyn);
links["batIns"] = linkJson(m_batIns);
root["links"] = links; root["links"] = links;
// 最近原始帧(默认 20 条) // 最近原始帧(默认 20 条)
+4 -1
View File
@@ -18,7 +18,8 @@ class DbStore;
class SnapshotBuilder { class SnapshotBuilder {
public: public:
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db); SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
LinkManager* batDyn, LinkManager* batIns, DbStore* db);
// 生成完整快照 JSON // 生成完整快照 JSON
std::string build() const; std::string build() const;
@@ -30,6 +31,8 @@ private:
SystemData* m_sys; SystemData* m_sys;
LinkManager* m_fc; LinkManager* m_fc;
LinkManager* m_pm; LinkManager* m_pm;
LinkManager* m_batDyn; // 动力锂电池链路
LinkManager* m_batIns; // 仪表锂电池链路
DbStore* m_db; DbStore* m_db;
}; };
+38
View File
@@ -5,6 +5,7 @@
#include <atomic> #include <atomic>
#include "../protocol/FcProtocol.h" #include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h" #include "../protocol/PmProtocol.h"
#include "../protocol/BatProtocol.h"
namespace ccu { namespace ccu {
@@ -12,6 +13,7 @@ namespace ccu {
// SystemData:跨线程共享的最新状态快照。 // SystemData:跨线程共享的最新状态快照。
// //
// - 接收线程(FC 链路)写 FcStatus 快照 // - 接收线程(FC 链路)写 FcStatus 快照
// - 接收线程(锂电池链路)写 动力/仪表 BatStatus 快照(role: 0=动力 1=仪表)
// - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送 // - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送
// - 网页线程读快照展示 // - 网页线程读快照展示
// 通过互斥锁保护读写。 // 通过互斥锁保护读写。
@@ -99,6 +101,38 @@ public:
return m_pmParamFb; return m_pmParamFb;
} }
//-------- 锂电池(role: 0=动力 1=仪表,见 BatRole) --------
// 更新/获取最新电池组状态(0x0003)
void updateBatStatus(int role, const BatStatusValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_batStatus[role] = v;
m_batStatusCount[role]++;
}
BatStatusValue batStatus(int role) const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_batStatus[role];
}
unsigned long batStatusCount(int role) const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_batStatusCount[role];
}
// 更新/获取最新电池包报警信息(0x0004)
void updateBatAlarm(int role, const BatPackAlarmValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_batAlarm[role] = v;
m_batAlarmCount[role]++;
}
BatPackAlarmValue batAlarm(int role) const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_batAlarm[role];
}
unsigned long batAlarmCount(int role) const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_batAlarmCount[role];
}
private: private:
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
FcStatusValue m_fcStatus; FcStatusValue m_fcStatus;
@@ -107,10 +141,14 @@ private:
FcControlValue m_fcControl; FcControlValue m_fcControl;
PmStatusValue m_pmStatus; PmStatusValue m_pmStatus;
PmParamSetFbValue m_pmParamFb; PmParamSetFbValue m_pmParamFb;
BatStatusValue m_batStatus[2];
BatPackAlarmValue m_batAlarm[2];
unsigned long m_fcStatusCount = 0; unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0; unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0; unsigned long m_fcControlCount = 0;
unsigned long m_pmStatusCount = 0; unsigned long m_pmStatusCount = 0;
unsigned long m_batStatusCount[2] = {0, 0};
unsigned long m_batAlarmCount[2] = {0, 0};
}; };
} // namespace ccu } // namespace ccu
+251
View File
@@ -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 <axel@tty0.ch>
* ---------------------------------------------------------------------
*/
#ifndef TINYFSM_HPP_INCLUDED
#define TINYFSM_HPP_INCLUDED
#ifndef TINYFSM_NOSTDLIB
#include <type_traits>
#endif
// #include <iostream>
// #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<typename F, typename S>
struct is_same_fsm { static constexpr bool value = true; };
#else
// check if both fsm and state class share same fsmtype
template<typename F, typename S>
struct is_same_fsm : std::is_same< typename F::fsmtype, typename S::fsmtype > { };
#endif
template<typename S>
struct _state_instance
{
using value_type = S;
using type = _state_instance<S>;
static S value;
};
template<typename S>
typename _state_instance<S>::value_type _state_instance<S>::value;
// --------------------------------------------------------------------------
template<typename F>
class Fsm
{
public:
using fsmtype = Fsm<F>;
using state_ptr_t = F *;
static state_ptr_t current_state_ptr;
// public, leaving ability to access state instance (e.g. on reset)
template<typename S>
static constexpr S & state(void) {
static_assert(is_same_fsm<F, S>::value, "accessing state of different state machine");
return _state_instance<S>::value;
}
template<typename S>
static constexpr bool is_in_state(void) {
static_assert(is_same_fsm<F, S>::value, "accessing state of different state machine");
return current_state_ptr == &_state_instance<S>::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<typename E>
static void dispatch(E const & event) {
current_state_ptr->react(event);
}
/// state transition functions
protected:
template<typename S>
void transit(void) {
static_assert(is_same_fsm<F, S>::value, "transit to different state machine");
current_state_ptr->exit();
current_state_ptr = &_state_instance<S>::value;
current_state_ptr->entry();
}
template<typename S, typename ActionFunction>
void transit(ActionFunction action_function) {
static_assert(is_same_fsm<F, S>::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<S>::value;
current_state_ptr->entry();
}
template<typename S, typename ActionFunction, typename ConditionFunction>
void transit(ActionFunction action_function, ConditionFunction condition_function) {
if(condition_function()) {
transit<S>(action_function);
}
}
};
template<typename F>
typename Fsm<F>::state_ptr_t Fsm<F>::current_state_ptr;
// --------------------------------------------------------------------------
template<typename... FF>
struct FsmList;
template<> struct FsmList<> {
static void set_initial_state() { }
static void reset() { }
static void enter() { }
template<typename E>
static void dispatch(E const &) { }
};
template<typename F, typename... FF>
struct FsmList<F, FF...>
{
using fsmtype = Fsm<F>;
static void set_initial_state() {
fsmtype::set_initial_state();
FsmList<FF...>::set_initial_state();
}
static void reset() {
F::reset();
FsmList<FF...>::reset();
}
static void enter() {
fsmtype::enter();
FsmList<FF...>::enter();
}
static void start() {
set_initial_state();
enter();
}
template<typename E>
static void dispatch(E const & event) {
fsmtype::template dispatch<E>(event);
FsmList<FF...>::template dispatch<E>(event);
}
};
// --------------------------------------------------------------------------
template<typename... SS> struct StateList;
template<> struct StateList<> {
static void reset() { }
};
template<typename S, typename... SS>
struct StateList<S, SS...>
{
static void reset() {
_state_instance<S>::value = S();
StateList<SS...>::reset();
}
};
// --------------------------------------------------------------------------
template<typename F>
struct MooreMachine : tinyfsm::Fsm<F>
{
virtual void entry(void) { }; /* entry actions in some states */
void exit(void) { }; /* no exit actions */
};
template<typename F>
struct MealyMachine : tinyfsm::Fsm<F>
{
// 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 */
+18 -2
View File
@@ -2,9 +2,12 @@
// pCCU 配置示例 // pCCU 配置示例
// //
// 链路拓扑: // 链路拓扑:
// 燃料电池控制器(FC) 192.168.1.162:7000 // 燃料电池控制器(FC) 192.168.1.162:7000
// 控制主机(pPowerManger) 192.168.0.140:5001 // 控制主机(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 ProcessConfig = pCCU
@@ -26,6 +29,19 @@ ProcessConfig = pCCU
pm_remote_ip = 192.168.0.140 pm_remote_ip = 192.168.0.140
pm_remote_port = 5001 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) // 数据库路径(SQLite)
dbpath = pccu_data.db dbpath = pccu_data.db
+446
View File
@@ -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<Message>(new BatSelfCheckMessage()));
reg.registerMessage(std::unique_ptr<Message>(new BatControlMessage()));
reg.registerMessage(std::unique_ptr<Message>(new BatStatusMessage()));
reg.registerMessage(std::unique_ptr<Message>(new BatPackAlarmMessage()));
}
//============================================================================
// 0x0000 自检指令
// 数据域 2 字节,偏移 0~1
//============================================================================
namespace {
enum : size_t {
BO_SELFCHECK = 0, // 电池自检 00/01
BO_SELFCHECK_RST = 1, // 自检复位指令 00/55
};
}
std::vector<uint8_t> BatSelfCheckMessage::encode(const void* obj) const {
const BatSelfCheckValue* v = static_cast<const BatSelfCheckValue*>(obj);
std::vector<uint8_t> 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<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
BatSelfCheckValue* v = static_cast<BatSelfCheckValue*>(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<uint8_t> BatControlMessage::encode(const void* obj) const {
const BatControlValue* v = static_cast<const BatControlValue*>(obj);
std::vector<uint8_t> 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<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
BatControlValue* v = static_cast<BatControlValue*>(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<uint8_t>& 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<uint8_t>& 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<uint8_t> BatStatusMessage::encode(const void* obj) const {
const BatStatusValue* v = static_cast<const BatStatusValue*>(obj);
std::vector<uint8_t> 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<uint16_t>(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<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
BatStatusValue* v = static_cast<BatStatusValue*>(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<int16_t>(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<uint8_t> BatPackAlarmMessage::encode(const void* obj) const {
const BatPackAlarmValue* v = static_cast<const BatPackAlarmValue*>(obj);
std::vector<uint8_t> 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<uint16_t>(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<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
BatPackAlarmValue* v = static_cast<BatPackAlarmValue*>(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<int16_t>(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
+334
View File
@@ -0,0 +1,334 @@
#ifndef PCCU_BAT_PROTOCOL_H
#define PCCU_BAT_PROTOCOL_H
#include <cstdint>
#include <vector>
#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<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const BatSelfCheckValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, BatSelfCheckValue& v) const {
return decode(frame, static_cast<void*>(&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<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const BatControlValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, BatControlValue& v) const {
return decode(frame, static_cast<void*>(&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<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const BatStatusValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, BatStatusValue& v) const {
return decode(frame, static_cast<void*>(&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<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const BatPackAlarmValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, BatPackAlarmValue& v) const {
return decode(frame, static_cast<void*>(&v));
}
private:
ChecksumPolicy m_checksum{ChecksumType::Sum32};
};
// 注册锂电池消息到给定注册表
void registerBatMessages(class MessageRegistry& reg);
} // namespace ccu
#endif // PCCU_BAT_PROTOCOL_H
+109 -1
View File
@@ -74,6 +74,8 @@ th{color:var(--dim);font-weight:500;}
<button class="tab active" data-tab="fcStatus">FC状态反馈</button> <button class="tab active" data-tab="fcStatus">FC状态反馈</button>
<button class="tab" data-tab="pmCmd">PM操控指令</button> <button class="tab" data-tab="pmCmd">PM操控指令</button>
<button class="tab" data-tab="pmParam">PM参数设定</button> <button class="tab" data-tab="pmParam">PM参数设定</button>
<button class="tab" data-tab="batDyn">动力锂电池</button>
<button class="tab" data-tab="batIns">仪表锂电池</button>
</div> </div>
<div class="tab-group"><span class="tg-label">发送</span> <div class="tab-group"><span class="tg-label">发送</span>
<button class="tab" data-tab="fcCmd">FC控制指令</button> <button class="tab" data-tab="fcCmd">FC控制指令</button>
@@ -91,7 +93,7 @@ th{color:var(--dim);font-weight:500;}
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
const pagesEl = $('pages'); const pagesEl = $('pages');
const statusEl = $('status'); const statusEl = $('status');
const TABS = ['fcStatus','pmCmd','pmParam','fcCmd','pmStatus','pmFb','links','raw']; const TABS = ['fcStatus','pmCmd','pmParam','batDyn','batIns','fcCmd','pmStatus','pmFb','links','raw'];
let activeTab = 'fcStatus'; let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); } function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
@@ -126,6 +128,16 @@ function statusText(s){
return m[s] || ('未知('+hex(s)+')'); return m[s] || ('未知('+hex(s)+')');
} }
function flameText(f){ return {0:'无效',1:'火焰报警',2:'探测器故障'}[f] || ('未知('+hex(f)+')'); } 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 ? '<span class="fc-ok">'+hex(f)+' 无</span>'
: '<span class="fc-bad">'+hex(f)+'</span>'; }
// 带解析的值:hex 码 + 括号解析 // 带解析的值:hex 码 + 括号解析
function parsed(v, text){ return hex(v)+' ('+text+')'; } function parsed(v, text){ return hex(v)+' ('+text+')'; }
// 故障码高亮:0 显示绿色正常,非 0 显示红色 // 故障码高亮:0 显示绿色正常,非 0 显示红色
@@ -291,10 +303,16 @@ function linkCard(s){
'收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err); '收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err);
h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>', h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>',
'收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err); '收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err);
h+=row('<span><span class="status-dot '+dot(online(l.batDyn.lastRx))+'"></span>动力锂电池链路</span>',
'收:'+l.batDyn.rx+' 发:'+l.batDyn.tx+' 误:'+l.batDyn.err);
h+=row('<span><span class="status-dot '+dot(online(l.batIns.lastRx))+'"></span>仪表锂电池链路</span>',
'收:'+l.batIns.rx+' 发:'+l.batIns.tx+' 误:'+l.batIns.err);
h+=row('FC 状态接收计数', s.fcStatusCount); h+=row('FC 状态接收计数', s.fcStatusCount);
h+=row('PM 指令接收计数', s.pmControlCount); h+=row('PM 指令接收计数', s.pmControlCount);
h+=row('FC 控制转发计数', s.fcControlCount); h+=row('FC 控制转发计数', s.fcControlCount);
h+=row('PM 状态发送计数', s.pmStatusCount); h+=row('PM 状态发送计数', s.pmStatusCount);
h+=row('动力电池状态接收计数', s.batDynStatusCount);
h+=row('仪表电池状态接收计数', s.batInsStatusCount);
return card('链路状态', h); return card('链路状态', h);
} }
@@ -359,6 +377,89 @@ function pmFbCard(d){
return card('设定结果', h); 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){ function pmStatusCard(d){
if(!d) return ''; if(!d) return '';
let h=''; let h='';
@@ -450,6 +551,13 @@ function render(snap){
+tankCard(snap.fc)+cabinCard(snap.fc)+'</div>', +tankCard(snap.fc)+cabinCard(snap.fc)+'</div>',
pmCmd: pageHeader(0x0001,'PM操控指令','PM→CCU')+'<div class="grid">'+cmdCard(snap.pmCmd)+'</div>', pmCmd: pageHeader(0x0001,'PM操控指令','PM→CCU')+'<div class="grid">'+cmdCard(snap.pmCmd)+'</div>',
pmParam: pageHeader(0x0002,'PM参数设定','PM→CCU')+'<div class="grid">'+pmParamCard(snap.pmParam)+'</div>', pmParam: pageHeader(0x0002,'PM参数设定','PM→CCU')+'<div class="grid">'+pmParamCard(snap.pmParam)+'</div>',
// 0x0003/0x0004 锂电池状态反馈(锂电池→CCU):动力 / 仪表
batDyn: pageHeader(0x0003,'动力锂电池状态','动力电池→CCU')+'<div class="grid">'+
batRunCard(snap.batDyn)+batElecCard(snap.batDyn)+batPackCard(snap.batDyn)+
batAlarmWordCard(snap.batDyn)+batPackAlarmCard(snap.batDynAlarm)+'</div>',
batIns: pageHeader(0x0003,'仪表锂电池状态','仪表电池→CCU')+'<div class="grid">'+
batRunCard(snap.batIns)+batElecCard(snap.batIns)+batPackCard(snap.batIns)+
batAlarmWordCard(snap.batIns)+batPackAlarmCard(snap.batInsAlarm)+'</div>',
fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>', fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>',
// 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据 // 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据
pmStatus: pageHeader(0x0004,'PM状态报文','CCU→PM')+'<div class="grid">'+ pmStatus: pageHeader(0x0004,'PM状态报文','CCU→PM')+'<div class="grid">'+
+1
View File
@@ -137,6 +137,7 @@ add_executable(pccuTest
${CCU_DIR}/protocol/MessageRegistry.cpp ${CCU_DIR}/protocol/MessageRegistry.cpp
${CCU_DIR}/protocol/FcProtocol.cpp ${CCU_DIR}/protocol/FcProtocol.cpp
${CCU_DIR}/protocol/PmProtocol.cpp ${CCU_DIR}/protocol/PmProtocol.cpp
${CCU_DIR}/protocol/BatProtocol.cpp
${CCU_DIR}/comm/UdpEndpoint.cpp ${CCU_DIR}/comm/UdpEndpoint.cpp
${CCU_DIR}/comm/LinkManager.cpp ${CCU_DIR}/comm/LinkManager.cpp
${CCU_DIR}/store/DbStore.cpp ${CCU_DIR}/store/DbStore.cpp
+248
View File
@@ -15,9 +15,11 @@
#include "../../src/pCCU/protocol/MessageRegistry.h" #include "../../src/pCCU/protocol/MessageRegistry.h"
#include "../../src/pCCU/protocol/FcProtocol.h" #include "../../src/pCCU/protocol/FcProtocol.h"
#include "../../src/pCCU/protocol/PmProtocol.h" #include "../../src/pCCU/protocol/PmProtocol.h"
#include "../../src/pCCU/protocol/BatProtocol.h"
#include "../../src/pCCU/comm/LinkManager.h" #include "../../src/pCCU/comm/LinkManager.h"
#include "../../src/pCCU/comm/FcLinkManager.h" #include "../../src/pCCU/comm/FcLinkManager.h"
#include "../../src/pCCU/comm/PmLinkManager.h" #include "../../src/pCCU/comm/PmLinkManager.h"
#include "../../src/pCCU/comm/BatLinkManager.h"
#include "../../src/pCCU/store/DbStore.h" #include "../../src/pCCU/store/DbStore.h"
using namespace ccu; using namespace ccu;
@@ -287,6 +289,247 @@ static void testDbStore() {
std::remove(path.c_str()); 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<uint8_t> 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<void*>(&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<uint8_t> 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<void*>(&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<uint8_t> 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<void*>(&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<uint8_t> 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<void*>(&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<uint8_t> 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<uint8_t>& f) {
if (m && m->id() == 0x0003) {
BatStatusValue out;
if (m->decode(f, static_cast<void*>(&out))) {
got = (out.soc == 999 && out.heartbeat == 21);
}
}
});
CHECK(blm.handleIncoming(frame));
CHECK(got);
// 校验失败帧拒收
std::vector<uint8_t> bad = frame;
bad[10] ^= 0xFF;
CHECK(!blm.handleIncoming(bad));
}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
int main() { int main() {
testFrameLengths(); testFrameLengths();
@@ -297,6 +540,11 @@ int main() {
testChecksum(); testChecksum();
testLinkDispatch(); testLinkDispatch();
testDbStore(); testDbStore();
testBatFrameLengths();
testBatControlRoundTrip();
testBatStatusRoundTrip();
testBatPackAlarmRoundTrip();
testBatLinkDispatch();
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail); std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1; return g_fail == 0 ? 0 : 1;
+85 -8
View File
@@ -4,15 +4,21 @@
拓扑: 拓扑:
测试脚本(FC模拟) --16000--> pCCU(fc_local) [FC状态 0x0002] 测试脚本(FC模拟) --16000--> pCCU(fc_local) [FC状态 0x0002]
测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002] 测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002]
测试脚本(电池模拟) --17001/17002--> pCCU [电池组状态 0x0003]
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发] pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003] pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003]
pCCU --17003/17004--> 测试脚本 [电池控制指令 0x0001 / 自检指令 0x0000]
校验: 校验:
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003 1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001 2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003 3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
4. SQLite 记录了收/发原始帧 4. PM 操控中锂电池启停指令映射为电池控制指令(0x0001, 27B):
5. Web 页面可访问 动力 0x20 关闭 -> 母线接触器 0x77;仪表 0x10 启动 -> 母线接触器 0x55
5. pCCU 周期(1s)向动力/仪表锂电池下发心跳控制指令
6. 电池组状态(0x0003)收到后落库 SQLite(link=bat_dyn/bat_ins)
7. SQLite 记录了收/发原始帧
8. Web 页面可访问
""" """
import socket import socket
import struct import struct
@@ -29,6 +35,10 @@ FC_STATUS_PORT = 16000
FC_CTRL_RECV_PORT = 16001 FC_CTRL_RECV_PORT = 16001
PM_CTRL_PORT = 16002 PM_CTRL_PORT = 16002
PM_STATUS_RECV_PORT = 16003 PM_STATUS_RECV_PORT = 16003
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 WEB_PORT = 18081
DB_PATH = "/tmp/pccu_it_test.db" DB_PATH = "/tmp/pccu_it_test.db"
MOOSDB_EXE = "/usr/local/bin/MOOSDB" MOOSDB_EXE = "/usr/local/bin/MOOSDB"
@@ -94,6 +104,26 @@ def pm_paramset_frame():
return frame(0x0002, bytes(p)) return frame(0x0002, bytes(p))
def bat_status_frame():
"""电池组状态 0x0003:payload 80B,带 uint32 校验和"""
p = bytearray(80)
struct.pack_into("<H", p, 0, 2026)
p[2], p[3], p[4], p[5], p[6], p[7] = 8, 30, 10, 30, 0, 0
p[8] = 0x10 # 工况 工房调试
p[9] = 0x22 # 自检完成,状态正常
p[10] = 0x22 # 满足功率要求
p[11] = 0x11 # 母线接触器闭合完成
p[12] = 0x22 # 二极管闭合完成(高4位) + 正极闭合完成(低4位)
struct.pack_into("<H", p, 14, 855) # SOC 85.5%
struct.pack_into("<H", p, 16, 1234) # 电量 123.4 kWh
struct.pack_into("<H", p, 18, 5000) # 允许最高放电功率 500 kW
struct.pack_into("<H", p, 22, 1500) # 当前接入功率 150 kW
struct.pack_into("<H", p, 36, 5400) # 负载端电压 54.00V
struct.pack_into("<h", p, 40, -200) # 放电电流 -10A(充电)
p[79] = 33 # 心跳
return frame(0x0003, bytes(p))
def recv_loop(sock, label, results, msg_ids, timeout): def recv_loop(sock, label, results, msg_ids, timeout):
"""持续接收指定 msg_id 的帧,记录到 results""" """持续接收指定 msg_id 的帧,记录到 results"""
sock.settimeout(timeout) sock.settimeout(timeout)
@@ -135,18 +165,27 @@ def main():
print("FAIL: pCCU exited early, code", pccu.returncode) print("FAIL: pCCU exited early, code", pccu.returncode)
return 1 return 1
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003) # 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003,电池控制 17003/17004)
fc_ctl_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) fc_ctl_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fc_ctl_recv.bind(("127.0.0.1", FC_CTRL_RECV_PORT)) fc_ctl_recv.bind(("127.0.0.1", FC_CTRL_RECV_PORT))
pm_status_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) pm_status_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
pm_status_recv.bind(("127.0.0.1", PM_STATUS_RECV_PORT)) pm_status_recv.bind(("127.0.0.1", PM_STATUS_RECV_PORT))
dyn_bat_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
dyn_bat_recv.bind(("127.0.0.1", DYN_BAT_RECV_PORT))
ins_bat_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ins_bat_recv.bind(("127.0.0.1", INS_BAT_RECV_PORT))
fc_ctl_results, pm_status_results = [], [] fc_ctl_results, pm_status_results = [], []
dyn_bat_results, ins_bat_results = [], []
t1 = threading.Thread(target=recv_loop, t1 = threading.Thread(target=recv_loop,
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 12)) args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 12))
t2 = threading.Thread(target=recv_loop, t2 = threading.Thread(target=recv_loop,
args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12)) args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12))
t1.start(); t2.start() t3 = threading.Thread(target=recv_loop,
args=(dyn_bat_recv, "bat-dyn", dyn_bat_results, {0x0000, 0x0001}, 12))
t4 = threading.Thread(target=recv_loop,
args=(ins_bat_recv, "bat-ins", ins_bat_results, {0x0000, 0x0001}, 12))
t1.start(); t2.start(); t3.start(); t4.start()
# 4. 发送 FC 状态(周期模拟) # 4. 发送 FC 状态(周期模拟)
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -160,7 +199,14 @@ def main():
time.sleep(0.5) time.sleep(0.5)
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT)) pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT))
t1.join(); t2.join() # 6. 模拟锂电池上发电池组状态(动力/仪表)
bat_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for _ in range(3):
bat_send.sendto(bat_status_frame(), ("127.0.0.1", DYN_BAT_PORT))
bat_send.sendto(bat_status_frame(), ("127.0.0.1", INS_BAT_PORT))
time.sleep(0.5)
t1.join(); t2.join(); t3.join(); t4.join()
ok = True ok = True
@@ -204,18 +250,49 @@ def main():
else: else:
ok = False ok = False
# 校验 4:数据库有记录 # 校验 4:锂电池控制指令(27B)。
# PM 操控映射:动力 0x20 关闭 -> 母线接触器 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) time.sleep(1.0)
db_count = -1 db_count = -1
bat_dyn_rx = -1
bat_tx = -1
if os.path.exists(DB_PATH): if os.path.exists(DB_PATH):
import sqlite3 import sqlite3
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0] db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0]
# 电池组状态接收(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() 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 (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 web_ok = False
try: try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3) resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)
+8
View File
@@ -16,6 +16,14 @@ ProcessConfig = pCCU
pm_remote_ip = 127.0.0.1 pm_remote_ip = 127.0.0.1
pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文) 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 dbpath = /tmp/pccu_it_test.db
logpath = /tmp/pccu_it_test.log logpath = /tmp/pccu_it_test.log
+5 -1
View File
@@ -82,11 +82,15 @@ def main():
print("FAIL: 快照非合法JSON:", text[:200]) print("FAIL: 快照非合法JSON:", text[:200])
return 1 return 1
ok = True 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 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"PASS: 快照 JSON 有效, 分组keys={sorted(data.keys())}")
print(f" fc.fc_status={data['fc']['fc_status']}, fc.fc_mode={data['fc']['fc_mode']}, " print(f" fc.fc_status={data['fc']['fc_status']}, fc.fc_mode={data['fc']['fc_mode']}, "
f"heartbeat={data['fc']['heartbeat']}") 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" links={json.dumps(data['links'], ensure_ascii=False)}")
print(f" logs条数={len(data['logs'])}, fcStatusCount={data['fcStatusCount']}") print(f" logs条数={len(data['logs'])}, fcStatusCount={data['fcStatusCount']}")
ws.s.close() ws.s.close()