Files
H100PowerManger/src/pCCU/CCU.cpp
T
zjk 67e732045a pCanBridge 扩展多通道支持:每通道独立TCP链路 + 上下行对称通道标注(m_sSrcAux)
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 通过。
2026-09-01 21:23:52 +08:00

654 lines
26 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}