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:
Binary file not shown.
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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
@@ -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,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()(当前为占位,待完善)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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, <);
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -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 条)
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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">'+
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user