pCanBridge: - 配置改为 channel = <通道名>,<IP>,<工作端口> 重复行(兼容旧 can_host/can_port 单通道回退), 每通道一条独立 CanEndpoint(TCP Client) + 接收线程,对应 CANET 的 CAN0=4001..CAN7=4008 - 上行 CAN_0x%08X 不变,m_sSrcAux=通道名区分来源通道 - 下行按 m_sSrcAux 路由到对应通道;回退规则:default_channel > 单通道 > 多通道告警丢弃 - buildReport 逐通道统计(连接/收发帧数/错误/重连) pCCU: - 新增 bcu_can_channel 配置(默认 CAN0),BCU 断路器下行 Post 前 SetSourceAux 指定目标通道, key 保持 CAN_TX_0x%08X(与上行通道标注对称,key 格式不变) 配置/文档:pCanBridge.moos、CanBridge_Info、pCCU.moos、missions/h100.moos、 test/pccu/pccu_it.moos 同步新格式。 验证:伪造 CANET Server 冒烟通过(多通道建链/上行解析发布/SQLite channel 列/ 下行通道路由与回退丢弃/srcAux 经 MOOSDB 往返); pccu 集成测试 PASSED(BCU 断路器下行 3 帧经新路由全部正确); pccuTest 183/183、PowerMangerTests 64/64 通过。
654 lines
26 KiB
C++
654 lines
26 KiB
C++
#include "CCU.h"
|
||
#include "MBUtils.h"
|
||
#include "../pPowerManger/logc/loguru.hpp"
|
||
#include "protocol/Frame.h"
|
||
#include "protocol/CanBms.h"
|
||
#include <chrono>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
|
||
using namespace ccu;
|
||
using namespace std;
|
||
|
||
//---------------------------------------------------------
|
||
// Constructor / Destructor
|
||
|
||
CCU::CCU() {}
|
||
|
||
CCU::~CCU() {
|
||
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
|
||
if (m_coord) { delete m_coord; m_coord = nullptr; }
|
||
if (m_bridge) { delete m_bridge; m_bridge = nullptr; }
|
||
if (m_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
|
||
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; }
|
||
if (m_rcuLink) { m_rcuLink->stop(); delete m_rcuLink; m_rcuLink = nullptr; }
|
||
if (m_db) { m_db->close(); delete m_db; m_db = nullptr; }
|
||
if (m_snap) { delete m_snap; m_snap = nullptr; }
|
||
if (m_sysData) { delete m_sysData; m_sysData = nullptr; }
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// OnStartUp:读取配置并初始化各组件
|
||
|
||
bool CCU::OnStartUp() {
|
||
AppCastingMOOSApp::OnStartUp();
|
||
|
||
STRING_LIST sParams;
|
||
m_MissionReader.EnableVerbatimQuoting(false);
|
||
if (!m_MissionReader.GetConfiguration(GetAppName(), sParams))
|
||
reportConfigWarning("No config block found for " + GetAppName());
|
||
|
||
STRING_LIST::iterator p;
|
||
for (p = sParams.begin(); p != sParams.end(); p++) {
|
||
string orig = *p;
|
||
string line = *p;
|
||
string param = stripBlankEnds(tolower(biteStringX(line, '=')));
|
||
string value = stripBlankEnds(line);
|
||
|
||
bool handled = true;
|
||
if (param == "fc_local_port") m_fcLocalPort = atol(value.c_str());
|
||
else if (param == "fc_remote_ip") m_fcHost = value;
|
||
else if (param == "fc_remote_port") m_fcPort = 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_port") m_pmPort = atol(value.c_str());
|
||
else if (param == "rcu_enable") m_rcuEnable = (tolower(value) == "true" || value == "1");
|
||
else if (param == "rcu_local_port") m_rcuLocalPort = atol(value.c_str());
|
||
else if (param == "rcu_remote_ip") m_rcuHost = value;
|
||
else if (param == "rcu_remote_port") m_rcuPort = atol(value.c_str());
|
||
else if (param == "dbpath") m_dbPath = value;
|
||
else if (param == "logpath") m_logPath = value;
|
||
else if (param == "bcu_can_channel") m_bcuCanChannel = value;
|
||
else if (param == "web_port") m_webPort = atoi(value.c_str());
|
||
else if (param == "web_enable") {
|
||
m_webEnable = (tolower(value) == "true" || value == "1");
|
||
}
|
||
else handled = false;
|
||
|
||
if (!handled)
|
||
reportUnhandledConfigWarning(orig);
|
||
}
|
||
|
||
registerVariables();
|
||
|
||
// 日志文件
|
||
if (m_logPath.empty()) m_logPath = "pCCU.log";
|
||
loguru::add_file(m_logPath.c_str(), loguru::Append, loguru::Verbosity_MAX);
|
||
LOG_F(INFO, "pCCU log path: %s", m_logPath.c_str());
|
||
|
||
// 数据库
|
||
m_db = new DbStore(m_dbPath);
|
||
if (!m_db->open()) {
|
||
LOG_F(ERROR, "open database failed: %s", m_db->lastError().c_str());
|
||
delete m_db;
|
||
m_db = nullptr;
|
||
} else {
|
||
LOG_F(INFO, "database path: %s", m_dbPath.c_str());
|
||
}
|
||
|
||
// 系统数据快照
|
||
m_sysData = new SystemData();
|
||
|
||
// 配电桥接器(须在链路 start 前完成依赖注入,避免接收线程空指针)
|
||
m_bridge = new DisBridge();
|
||
|
||
// FC 链路
|
||
m_fcLink = new FcLinkManager(m_fcLocalPort, m_fcHost, m_fcPort);
|
||
m_fcLink->setLogSink(m_db);
|
||
m_fcLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
|
||
handleFcMessage(msg, frame);
|
||
});
|
||
m_fcLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) {
|
||
// 原始帧已通过 LogSink 落库;如需 Web 实时原始报文可在此扩展
|
||
});
|
||
|
||
// PM 链路
|
||
m_pmLink = new PmLinkManager(m_pmLocalPort, m_pmHost, m_pmPort);
|
||
m_pmLink->setLogSink(m_db);
|
||
m_pmLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
|
||
// 配电指令(0x0001~0x0004,Sum8 短帧)优先桥接转发真实CCU;
|
||
// 注册表按帧总长区分配电指令与 PM 操控/参数指令(共用 id)
|
||
if (m_bridge && m_bridge->onPmFrame(msg, frame)) return;
|
||
handlePmMessage(msg, frame);
|
||
});
|
||
m_pmLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
|
||
});
|
||
|
||
// 真实 CCU 链路(配电桥接,rcu_enable 开启时创建)
|
||
if (m_rcuEnable) {
|
||
m_rcuLink = new RcuLinkManager(m_rcuLocalPort, m_rcuHost, m_rcuPort);
|
||
m_rcuLink->setLogSink(m_db);
|
||
m_rcuLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
|
||
handleRcuMessage(msg, frame);
|
||
});
|
||
m_rcuLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
|
||
});
|
||
}
|
||
|
||
// 注入桥接依赖(m_rcuLink 可为空:未启用桥接时相关入口自动失效)
|
||
m_bridge->setup(m_pmLink, m_rcuLink, m_sysData);
|
||
|
||
// 启动链路
|
||
if (!m_fcLink->start())
|
||
LOG_F(ERROR, "FC link start failed");
|
||
if (!m_pmLink->start())
|
||
LOG_F(ERROR, "PM link start failed");
|
||
if (m_rcuLink && !m_rcuLink->start())
|
||
LOG_F(ERROR, "RCU link start failed on port %ld", m_rcuLocalPort);
|
||
|
||
// 锂电池:CAN 总线(BMS 协议),数据经 pCanBridge 以 CAN_0x* 消息透传,
|
||
// 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。
|
||
|
||
// 电源协调器(FC 联动 + 协调算法占位)
|
||
m_coord = new PowerCoordinator();
|
||
m_coord->setup(m_sysData, m_fcLink, m_pmLink);
|
||
|
||
// 快照构建器(链路就绪后创建)
|
||
m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_rcuLink, m_db);
|
||
|
||
// 网页
|
||
if (m_webEnable) {
|
||
m_web = new WebServer();
|
||
m_web->setOnOpen([this]() { return buildSnapshot(); });
|
||
m_web->setApiHandler([this](const std::string& uri, const std::string& query) {
|
||
return handleApi(uri, query);
|
||
});
|
||
if (!m_web->start(m_webPort)) {
|
||
LOG_F(ERROR, "web server start failed on port %d", m_webPort);
|
||
delete m_web;
|
||
m_web = nullptr;
|
||
}
|
||
}
|
||
|
||
if (m_rcuLink) {
|
||
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
|
||
"rcu_link local=%ld -> %s:%ld (配电桥接), "
|
||
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
|
||
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
|
||
m_pmLocalPort, m_pmHost.c_str(), m_pmPort,
|
||
m_rcuLocalPort, m_rcuHost.c_str(), m_rcuPort);
|
||
} else {
|
||
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
|
||
"rcu_link disabled, "
|
||
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
|
||
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
|
||
m_pmLocalPort, m_pmHost.c_str(), m_pmPort);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// OnConnectToServer
|
||
|
||
bool CCU::OnConnectToServer() {
|
||
registerVariables();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// registerVariables
|
||
|
||
void CCU::registerVariables() {
|
||
AppCastingMOOSApp::RegisterVariables();
|
||
// 订阅 pCanBridge 透传的全部 CAN 帧(变量名 CAN_0x%08X,二进制数据域)。
|
||
// 注意:MOOS 通配订阅必须用三参数重载(变量模式 + 来源模式),
|
||
// 单参数 Register("CAN_0x*") 不会做通配匹配。
|
||
Register("CAN_0x*", "*", 0);
|
||
// 运行状态变量
|
||
Register("CCU_FC_LINK_STATE", 0);
|
||
Register("CCU_PM_LINK_STATE", 0);
|
||
Register("CCU_RCU_LINK_STATE", 0);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// OnNewMail
|
||
|
||
bool CCU::OnNewMail(MOOSMSG_LIST &NewMail) {
|
||
AppCastingMOOSApp::OnNewMail(NewMail);
|
||
MOOSMSG_LIST::iterator p;
|
||
for (p = NewMail.begin(); p != NewMail.end(); p++) {
|
||
CMOOSMsg &msg = *p;
|
||
// pCanBridge 透传的 CAN 帧(变量名前缀 CAN_0x)
|
||
if (msg.m_sKey.rfind("CAN_0x", 0) == 0) {
|
||
handleCanMessage(msg);
|
||
continue;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// handleCanMessage:解码 pCanBridge 透传的 CAN 帧
|
||
//
|
||
// 消息格式(pCanBridge 约定):
|
||
// m_sKey = "CAN_0x%08X"(CAN ID,扩展帧)
|
||
// m_sVal = 二进制数据域(dlc 字节)
|
||
// m_dfVal2 = 原始帧信息字节(bit7 FF / bit6 RTR / bit3~0 DLC)
|
||
|
||
void CCU::handleCanMessage(CMOOSMsg& msg) {
|
||
if (!m_sysData) return;
|
||
|
||
// 从变量名解析 CAN ID(跳过前缀 "CAN_0x" 共 6 字符)
|
||
uint32_t canId = 0;
|
||
if (std::sscanf(msg.m_sKey.c_str() + 6, "%x", &canId) != 1) return;
|
||
|
||
m_sysData->incCanFrameCount();
|
||
|
||
// 远程帧无数据域,直接跳过
|
||
if (msg.IsBinary() && msg.GetBinaryDataSize() > 0) {
|
||
unsigned int n = msg.GetBinaryDataSize();
|
||
unsigned char* d = msg.GetBinaryData();
|
||
if (!d) return;
|
||
|
||
// 合并语义:先取该节点当前快照 -> 解码部分更新 -> 写回
|
||
// (每个功能码报文只更新自己的字段,不能整体覆盖)
|
||
uint8_t addr = bcuAddrOf(canId);
|
||
if (!addr) return;
|
||
BcuNodeStatus node = m_sysData->bcuNode(addr);
|
||
if (decodeCanBcuFrame(canId, d, static_cast<int>(n), addr, node)) {
|
||
node.valid = true;
|
||
node.lastRxTime = MOOSTime(false);
|
||
m_sysData->updateBcuNode(addr, node);
|
||
|
||
// 锂电池解析数据落库(bcu_node 表,每帧一行,含节点最新合成状态)
|
||
if (m_db) m_db->insertBcuNode(addr, bcuFuncOf(canId), node);
|
||
|
||
// 节流日志:1s 一条,便于联调观察
|
||
double now = MOOSTime();
|
||
if (now - m_lastBmsLog >= 1.0) {
|
||
m_lastBmsLog = now;
|
||
LOG_F(INFO, "[BMS/CAN] 节点%d u=%.1fV i=%.1fA soc=%.1f%% alarm=%02X self=%d vMax=%.3fV",
|
||
addr, node.totalVoltage, node.current, node.soc,
|
||
node.alarmCode, node.selfCheck, node.maxCellVoltage);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// Iterate:周期任务(AppTick 次/秒)
|
||
|
||
bool CCU::Iterate() {
|
||
AppCastingMOOSApp::Iterate();
|
||
|
||
double now = MOOSTime();
|
||
|
||
// 周期(1Hz)整合 FC 状态并发送 PM 状态报文给控制主机
|
||
if (now - m_lastStatusTx >= 1.0) {
|
||
sendPmStatus();
|
||
m_lastStatusTx = now;
|
||
}
|
||
|
||
// BCU 断路器控制指令下发(网页 -> 队列 -> MOOS CAN_TX_*)
|
||
sendPendingBcuCtrl();
|
||
|
||
// 电源协调器:协调算法占位
|
||
if (m_coord)
|
||
m_coord->tick(now);
|
||
|
||
// 周期(1Hz)推送网页快照
|
||
if (m_web) {
|
||
static double lastWebPush = 0;
|
||
if (now - lastWebPush >= 1.0) {
|
||
lastWebPush = now;
|
||
m_web->broadcast(buildSnapshot());
|
||
}
|
||
}
|
||
|
||
AppCastingMOOSApp::PostReport();
|
||
return true;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// handleFcMessage:处理 FC 链路收到的消息
|
||
|
||
void CCU::handleFcMessage(Message* msg, const std::vector<uint8_t>& frame) {
|
||
if (!msg) return;
|
||
switch (msg->id()) {
|
||
case 0x0002: { // FC 状态反馈
|
||
FcStatusValue v;
|
||
if (msg->decode(frame, static_cast<void*>(&v))) {
|
||
m_sysData->updateFcStatus(v);
|
||
LOG_F(INFO, "[FC] status: mode=%d status=%d fault_level=%d",
|
||
v.fc_mode, v.fc_status, v.fc_fault_level);
|
||
} else {
|
||
LOG_F(ERROR, "[FC] status decode failed");
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
LOG_F(WARNING, "[FC] unhandled msg id 0x%04X", msg->id());
|
||
break;
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// handlePmMessage:处理 PM 链路收到的消息
|
||
|
||
void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
|
||
if (!msg) return;
|
||
// 配电指令已在入口由 DisBridge 处理(见 OnStartUp 的 onMessage 接线),
|
||
// 到达此处的均为 PM 协议消息(Sum32 校验)
|
||
switch (msg->id()) {
|
||
case 0x0001: { // PM 操控指令 -> 转发给 FC
|
||
PmControlValue c;
|
||
if (msg->decode(frame, static_cast<void*>(&c))) {
|
||
m_sysData->updatePmControl(c);
|
||
LOG_F(INFO, "[PM] control: mode=%d cmd=%d power=%d",
|
||
c.mode, c.cmd, c.outputPower);
|
||
|
||
// 映射并转发到 FC(0827协议:姿态仅数据1有效,数据2字节预留填0)
|
||
FcControlValue f;
|
||
f.mode = c.mode;
|
||
f.cmd = c.cmd;
|
||
f.outputPower = c.outputPower;
|
||
f.pitch1 = c.pitch;
|
||
f.roll1 = c.roll;
|
||
f.emergencyAllow = c.emergencyAllow;
|
||
f.depth = c.depth;
|
||
f.supplyCmd = c.supplyCmd;
|
||
f.reservedCmd5 = c.reservedCmd5;
|
||
f.reservedCmd6 = c.reservedCmd6;
|
||
f.heartbeat = c.heartbeat;
|
||
m_sysData->updateFcControl(f);
|
||
m_fcLink->sendMessage(0x0001, &f);
|
||
LOG_F(INFO, "[FC] forward control cmd=%d power=%d", f.cmd, f.outputPower);
|
||
|
||
// 锂电池启停/功率指令:BMS CAN 协议未定义控制报文,暂不支持
|
||
if (c.insBatCmd != 0 || c.dynBatCmd != 0 || c.dynBatPower != 0) {
|
||
LOG_F(WARNING, "[PM] battery cmd ignored (insBat=0x%02X dynBat=0x%02X dynPwr=%u): "
|
||
"BMS CAN 协议未定义控制报文",
|
||
c.insBatCmd, c.dynBatCmd, c.dynBatPower);
|
||
}
|
||
} else {
|
||
LOG_F(ERROR, "[PM] control decode failed");
|
||
}
|
||
break;
|
||
}
|
||
case 0x0002: { // PM 参数设定 -> 回参数反馈
|
||
PmParamSetValue p;
|
||
if (msg->decode(frame, static_cast<void*>(&p))) {
|
||
m_sysData->updatePmParamSet(p);
|
||
LOG_F(INFO, "[PM] param set received");
|
||
// 当前阶段直接回成功
|
||
PmParamSetFbValue fb;
|
||
fb.flag = 0x10; // 设定成功
|
||
fb.failCode = 0;
|
||
m_sysData->updatePmParamFb(fb);
|
||
m_pmLink->sendMessage(0x0003, &fb);
|
||
LOG_F(INFO, "[PM] param set feedback sent");
|
||
} else {
|
||
LOG_F(ERROR, "[PM] param set decode failed");
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
LOG_F(WARNING, "[PM] unhandled msg id 0x%04X", msg->id());
|
||
break;
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// handleRcuMessage:处理真实 CCU 链路收到的消息(配电桥接)
|
||
//
|
||
// 配电反馈(0x0005~0x0008)与真实CCU状态报文(0x0004)由 DisBridge
|
||
// 处理(原帧转发 pPowerManger / 仅解码显示);其余(如配电指令
|
||
// 回显)仅记录,不转发。
|
||
|
||
void CCU::handleRcuMessage(Message* msg, const std::vector<uint8_t>& frame) {
|
||
if (!msg) return;
|
||
if (m_bridge && m_bridge->onRcuFrame(msg, frame)) return;
|
||
LOG_F(WARNING, "[RCU] unhandled msg id 0x%04X, len=%zu", msg->id(), frame.size());
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// sendPmStatus:整合最新 FC 状态,编码 PM 状态报文发送
|
||
|
||
void CCU::sendPmStatus() {
|
||
if (!m_pmLink || !m_sysData) return;
|
||
|
||
const FcStatusValue& fc = m_sysData->fcStatus();
|
||
|
||
PmStatusValue s;
|
||
s.fc_mode = fc.fc_mode;
|
||
s.fc_status = fc.fc_status;
|
||
s.fault_level_1 = fc.fault_level_1;
|
||
s.fault_level_2 = fc.fault_level_2;
|
||
s.fault_level_3 = fc.fault_level_3;
|
||
s.fault_level_4 = fc.fault_level_4;
|
||
s.total_generation_time = fc.total_generation_time;
|
||
s.fc_fault_level = fc.fc_fault_level;
|
||
s.output_power_limit = 0; // FC 状态未含此字段,置 0
|
||
s.generation_power = fc.generation_power;
|
||
s.hydrogen_capacity = fc.hydrogen_capacity;
|
||
s.liquid_oxygen_capacity = fc.liquid_oxygen_capacity;
|
||
|
||
s.fc1_min_cell_voltage = fc.fc1_min_cell_voltage;
|
||
s.fc1_min_cell_pos = fc.fc1_min_cell_pos;
|
||
s.fc1_avg_cell_voltage = fc.fc1_avg_cell_voltage;
|
||
s.fc2_min_cell_voltage = fc.fc2_min_cell_voltage;
|
||
s.fc2_min_cell_pos = fc.fc2_min_cell_pos;
|
||
s.fc2_avg_cell_voltage = fc.fc2_avg_cell_voltage;
|
||
|
||
s.palladium_temp = fc.palladium_temp;
|
||
s.buffer_tank_pressure = fc.buffer_tank_pressure;
|
||
s.flue_total_emission = fc.flue_total_emission;
|
||
s.flue_pressure = fc.flue_pressure;
|
||
s.reactor_pressure = fc.reactor_pressure;
|
||
s.electric_valve_open = fc.electric_valve_open;
|
||
s.main_pipe_pressure = 0;
|
||
s.aux_pipe_pressure = 0;
|
||
|
||
s.dcdc1_in_voltage = fc.dcdc1_in_voltage;
|
||
s.dcdc1_in_current = fc.dcdc1_in_current;
|
||
s.dcdc2_in_voltage = fc.dcdc2_in_voltage;
|
||
s.dcdc2_in_current = fc.dcdc2_in_current;
|
||
s.dcdc_out_voltage = fc.dcdc_out_voltage;
|
||
s.dcdc_out_current = fc.dcdc_out_current;
|
||
s.dcdc_ctrl_voltage = fc.dcdc_ctrl_voltage;
|
||
s.dcdc_aux_voltage = fc.dcdc_aux_voltage;
|
||
|
||
s.methanol_total_use = fc.methanol_total_use;
|
||
s.methanol_feed = fc.methanol_feed;
|
||
s.oxygen_side_water_level = fc.oxygen_side_water_level;
|
||
s.hydrogen_side_water_level = fc.hydrogen_side_water_level;
|
||
s.ballast_water_level = fc.ballast_water_level;
|
||
|
||
s.exhaust_inlet_pressure = fc.exhaust_inlet_pressure;
|
||
s.exhaust_outlet_pressure = fc.exhaust_outlet_pressure;
|
||
s.exhaust_run_freq = fc.exhaust_run_freq;
|
||
s.exhaust_inlet_temp = fc.exhaust_inlet_temp;
|
||
s.exhaust_outlet_temp = fc.exhaust_outlet_temp;
|
||
s.exhaust_water_in_pressure = fc.exhaust_water_in_pressure;
|
||
s.exhaust_water_out_pressure = fc.exhaust_water_out_pressure;
|
||
|
||
s.tank_lo2_pressure = fc.tank_lo2_pressure;
|
||
s.tank_co2_pressure = fc.tank_co2_pressure;
|
||
s.tank_lo2_level = fc.tank_lo2_level;
|
||
s.alloy_h2_flow = fc.alloy_h2_flow;
|
||
s.fc_h2_flow = fc.fc_h2_flow;
|
||
s.fc_o2_flow = fc.fc_o2_flow;
|
||
|
||
s.emergency_float_depth = fc.emergency_float_depth;
|
||
s.emergency_float_time = fc.emergency_float_time;
|
||
|
||
s.cabin_pressure1 = fc.cabin_pressure1;
|
||
s.cabin_pressure2 = fc.cabin_pressure2;
|
||
s.cabin_temp1 = fc.cabin_temp1;
|
||
s.cabin_temp2 = fc.cabin_temp2;
|
||
s.cabin_humidity1 = fc.cabin_humidity1;
|
||
s.cabin_humidity2 = fc.cabin_humidity2;
|
||
s.h2_concentration1 = fc.h2_concentration1;
|
||
s.h2_concentration2 = fc.h2_concentration2;
|
||
s.h2_concentration3 = fc.h2_concentration3;
|
||
s.o2_concentration1 = fc.o2_concentration1;
|
||
s.o2_concentration2 = fc.o2_concentration2;
|
||
s.ch3oh_concentration1 = fc.ch3oh_concentration1;
|
||
s.ch3oh_concentration2 = fc.ch3oh_concentration2;
|
||
|
||
s.flame_detector1 = fc.flame_detector1;
|
||
s.flame_detector2 = fc.flame_detector2;
|
||
|
||
// 通信心跳自增
|
||
s.heartbeat = m_pmHeartbeat++;
|
||
s.emergency_cmd = fc.emergency_cmd;
|
||
|
||
m_sysData->updatePmStatus(s);
|
||
m_pmLink->sendMessage(0x0004, &s);
|
||
LOG_F(INFO, "[PM] status sent: mode=%d status=%d heartbeat=%d",
|
||
s.fc_mode, s.fc_status, s.heartbeat);
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// buildSnapshot
|
||
|
||
std::string CCU::buildSnapshot() {
|
||
if (m_snap) return m_snap->build();
|
||
return "{}";
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// handleApi:/api/logs、/api/bcu_ctrl
|
||
|
||
std::string CCU::handleApi(const std::string& uri, const std::string& query) {
|
||
if (uri == "/api/logs") {
|
||
if (m_snap) return m_snap->buildLogs(50);
|
||
return "[]";
|
||
}
|
||
if (uri == "/api/bcu_ctrl") {
|
||
return handleBcuCtrlApi(query);
|
||
}
|
||
return "";
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// handleBcuCtrlApi:BCU 断路器控制指令下发
|
||
//
|
||
// GET /api/bcu_ctrl?addr=5&action=1 action: 1 闭合 / 0 断开(总正+总负)
|
||
// GET /api/bcu_ctrl?addr=5&pos=1&neg=0 也可单独指定总正/总负
|
||
// Web 线程不可直接 Notify,指令入队后由 Iterate(MOOS 线程)下发。
|
||
|
||
std::string CCU::handleBcuCtrlApi(const std::string& query) {
|
||
if (!m_sysData) return "{\"ok\":false,\"error\":\"not ready\"}";
|
||
|
||
// 简易 query 解析(k=v&k=v)
|
||
auto getParam = [&query](const char* key, int& out) -> bool {
|
||
std::string k = std::string(key) + "=";
|
||
size_t p = query.find(k);
|
||
if (p == std::string::npos) return false;
|
||
size_t e = query.find('&', p);
|
||
std::string v = query.substr(p + k.size(),
|
||
(e == std::string::npos) ? std::string::npos
|
||
: e - p - k.size());
|
||
out = atoi(v.c_str());
|
||
return true;
|
||
};
|
||
|
||
int addr = 0, action = -1, pos = -1, neg = -1;
|
||
getParam("addr", addr);
|
||
getParam("action", action);
|
||
getParam("pos", pos);
|
||
getParam("neg", neg);
|
||
|
||
if (addr < kBcuAddrMin || addr > kBcuAddrMax)
|
||
return "{\"ok\":false,\"error\":\"bad addr\"}";
|
||
if (pos < 0) pos = action; // 未单独指定时跟随 action
|
||
if (neg < 0) neg = action;
|
||
if (action < 0 && (pos < 0 || neg < 0))
|
||
return "{\"ok\":false,\"error\":\"missing action\"}";
|
||
if (action > 1 || pos > 1 || neg > 1)
|
||
return "{\"ok\":false,\"error\":\"bad value\"}";
|
||
|
||
BcuCtrlCmd c;
|
||
c.addr = static_cast<uint8_t>(addr);
|
||
c.pos = static_cast<uint8_t>(pos);
|
||
c.neg = static_cast<uint8_t>(neg);
|
||
if (!m_sysData->pushBcuCtrlCmd(c))
|
||
return "{\"ok\":false,\"error\":\"queue full\"}";
|
||
|
||
LOG_F(INFO, "[BCU/CAN] 断路器指令入队: 节点%d 总正=%d 总负=%d", addr, pos, neg);
|
||
char buf[96];
|
||
std::snprintf(buf, sizeof(buf),
|
||
"{\"ok\":true,\"addr\":%d,\"pos\":%d,\"neg\":%d}", addr, pos, neg);
|
||
return buf;
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// sendPendingBcuCtrl:出队控制指令,编码 0x10XX81FF 经 MOOS 下发
|
||
// 链路:MOOS CAN_TX_0x*(m_sSrcAux=目标通道) -> pCanBridge 订阅
|
||
// -> 按通道路由 CanEndpoint(TCP) -> CANET -> CAN 总线
|
||
|
||
void CCU::sendPendingBcuCtrl() {
|
||
if (!m_sysData) return;
|
||
BcuCtrlCmd c;
|
||
while (m_sysData->popBcuCtrlCmd(c)) {
|
||
uint8_t data[8];
|
||
buildBcuRelayCtrlData(c.pos, c.neg, data);
|
||
uint32_t canId = bcuCtrlCanId(c.addr);
|
||
|
||
char key[32];
|
||
std::snprintf(key, sizeof(key), "CAN_TX_0x%08X", canId);
|
||
// 二进制构造同 pCanBridge 上行发布(MOOS_BINARY_STRING)
|
||
CMOOSMsg msg(MOOS_NOTIFY, key,
|
||
static_cast<unsigned int>(sizeof(data)), data);
|
||
// 目标通道经 m_sSrcAux 指定(与上行 CAN_0x* 的通道标注对称)
|
||
msg.SetSourceAux(m_bcuCanChannel);
|
||
bool ok = m_Comms.Post(msg);
|
||
m_sysData->noteBcuCtrlSent(c, ok);
|
||
LOG_F(INFO, "[BCU/CAN] 断路器指令 0x%08X 节点%d 总正=%d 总负=%d 通道=%s %s",
|
||
canId, c.addr, c.pos, c.neg, m_bcuCanChannel.c_str(),
|
||
ok ? "已投递MOOS" : "投递失败");
|
||
}
|
||
}
|
||
|
||
//---------------------------------------------------------
|
||
// buildReport
|
||
|
||
bool CCU::buildReport() {
|
||
m_msgs << "============================================" << "\n";
|
||
m_msgs << "pCCU 复合管控器" << "\n";
|
||
m_msgs << "============================================" << "\n";
|
||
if (m_fcLink) {
|
||
m_msgs << "FC 链路 local:" << m_fcLink->localPort()
|
||
<< " rx:" << m_fcLink->rxCount()
|
||
<< " tx:" << m_fcLink->txCount()
|
||
<< " err:" << m_fcLink->errorCount() << "\n";
|
||
}
|
||
if (m_pmLink) {
|
||
m_msgs << "PM 链路 local:" << m_pmLink->localPort()
|
||
<< " rx:" << m_pmLink->rxCount()
|
||
<< " tx:" << m_pmLink->txCount()
|
||
<< " err:" << m_pmLink->errorCount() << "\n";
|
||
}
|
||
if (m_rcuLink) {
|
||
m_msgs << "RCU 链路 local:" << m_rcuLink->localPort()
|
||
<< " -> " << m_rcuLink->rcuHost() << ":" << m_rcuLink->rcuPort()
|
||
<< " rx:" << m_rcuLink->rxCount()
|
||
<< " tx:" << m_rcuLink->txCount()
|
||
<< " err:" << m_rcuLink->errorCount() << "\n";
|
||
if (m_sysData) {
|
||
const RealCcuState& st = m_sysData->realCcu();
|
||
m_msgs << "配电桥接: 反馈转发 " << st.fwdFbOk << " 成功 / " << st.fwdFbErr
|
||
<< " 失败, 指令转发 " << st.fwdCmdOk << " 成功 / " << st.fwdCmdErr
|
||
<< " 失败\n";
|
||
}
|
||
}
|
||
if (m_sysData) {
|
||
m_msgs << "锂电池: BMS/CAN (经 pCanBridge CAN_0x* 订阅)\n";
|
||
m_msgs << "CAN 帧接收计数:" << m_sysData->canFrameCount() << "\n";
|
||
m_msgs << "BMS 报文接收计数:" << m_sysData->bmsStatusCount() << "\n";
|
||
m_msgs << "FC 状态接收次数:" << m_sysData->fcStatusCount() << "\n";
|
||
m_msgs << "PM 指令接收次数:" << m_sysData->pmControlCount() << "\n";
|
||
const BcuCtrlStat& bc = m_sysData->bcuCtrlStat();
|
||
m_msgs << "断路器指令下发:" << bc.sentCount << " 成功 / " << bc.errCount
|
||
<< " 失败 (最近: 节点" << static_cast<int>(bc.last.addr)
|
||
<< " 总正=" << static_cast<int>(bc.last.pos)
|
||
<< " 总负=" << static_cast<int>(bc.last.neg) << ")\n";
|
||
}
|
||
if (m_db) {
|
||
m_msgs << "数据库记录数:" << m_db->count() << "\n";
|
||
m_msgs << "数据库BMS记录数:" << m_db->countBcu() << "\n";
|
||
}
|
||
return true;
|
||
}
|