pCCU 锂电池页新增 BCU 断路器控制(0x10XX81FF 经 pCanBridge 下行 CAN 总线)

- CanBms: bcuCtrlCanId(0x10XX81FF) + buildBcuRelayCtrlData(02 00 总正 总负 FF*4)
- SystemData: BcuCtrlCmd 下行队列(Web线程入队->Iterate/MOOS线程出队Notify) + 下发统计
- CCU: /api/bcu_ctrl(addr/action 或 pos/neg, 校验地址01h~36h) + Iterate 经 MOOS CAN_TX_0x* 下发
- pCanBridge 变双向透传: 订阅 CAN_TX_0x*,CanEndpoint::sendFrame(13字节CANET帧,TCP写,互斥保护)
- 网页: 每BCU节点断路器闭合/断开按钮(confirm确认) + 下发统计卡片
- 测试: pccuTest 控制帧编码用例(183项);集成测试新增假CANET服务器验证完整下行链路(闭合/断开/非法地址)
This commit is contained in:
zjk
2026-09-01 15:23:30 +08:00
parent 0639b5904d
commit 90caacf9a2
15 changed files with 517 additions and 22 deletions
+89 -1
View File
@@ -5,6 +5,7 @@
#include "protocol/CanBms.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
using namespace ccu;
using namespace std;
@@ -277,6 +278,9 @@ bool CCU::Iterate() {
m_lastStatusTx = now;
}
// BCU 断路器控制指令下发(网页 -> 队列 -> MOOS CAN_TX_*)
sendPendingBcuCtrl();
// 电源协调器:协调算法占位
if (m_coord)
m_coord->tick(now);
@@ -503,16 +507,95 @@ std::string CCU::buildSnapshot() {
}
//---------------------------------------------------------
// handleApi:/api/logs
// 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* -> pCanBridge 订阅 -> CanEndpoint(TCP) -> USBCAN
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);
bool ok = m_Comms.Post(msg);
m_sysData->noteBcuCtrlSent(c, ok);
LOG_F(INFO, "[BCU/CAN] 断路器指令 0x%08X 节点%d 总正=%d 总负=%d %s",
canId, c.addr, c.pos, c.neg, ok ? "已投递MOOS" : "投递失败");
}
}
//---------------------------------------------------------
// buildReport
@@ -551,6 +634,11 @@ bool CCU::buildReport() {
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";
+5
View File
@@ -60,6 +60,11 @@ private:
std::string buildSnapshot();
std::string handleApi(const std::string& uri, const std::string& query);
// BCU 断路器控制:/api/bcu_ctrl 解析与下发(经 MOOS CAN_TX_* -> pCanBridge)
std::string handleBcuCtrlApi(const std::string& query);
// 出队并下发一条 BCU 控制指令(Iterate 调用,MOOS 线程内 Notify 安全)
void sendPendingBcuCtrl();
// 配置
long m_fcLocalPort = 6000;
std::string m_fcHost = "192.168.1.162";
+2
View File
@@ -101,6 +101,8 @@ void showInterfaceAndExit() {
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CAN_TX_0x%08X = BCU 断路器控制帧 0x10XX81FF(二进制 ");
blk(" 数据域,经 pCanBridge 下行到 CAN 总线) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
+15
View File
@@ -679,6 +679,20 @@ std::string SnapshotBuilder::build() const {
root["canFrameCount"] = JsonVal(static_cast<Json::UInt>(m_sys->canFrameCount()));
// 真实CCU 配电桥接数据(RCU 链路)
root["realCcu"] = realCcuJson(m_sys->realCcu(), MOOSTime(false));
// BCU 断路器控制下发统计(网页 -> 0x10XX81FF -> pCanBridge)
{
BcuCtrlStat bc = m_sys->bcuCtrlStat();
JsonVal jc(Json::objectValue);
jc["sentCount"] = JsonVal(static_cast<Json::UInt>(bc.sentCount));
jc["errCount"] = JsonVal(static_cast<Json::UInt>(bc.errCount));
jc["valid"] = JsonVal(bc.lastTime > 0);
jc["age"] = JsonVal(bc.lastTime > 0 ? (MOOSTime(false) - bc.lastTime) : -1.0);
put(jc, "addr", bc.last.addr);
put(jc, "pos", bc.last.pos);
put(jc, "neg", bc.last.neg);
jc["lastOk"] = JsonVal(bc.lastOk);
root["bcuCtrl"] = jc;
}
} else {
root["fc"] = JsonVal(Json::objectValue);
root["pmCmd"] = JsonVal(Json::objectValue);
@@ -688,6 +702,7 @@ std::string SnapshotBuilder::build() const {
root["pmStatus"] = JsonVal(Json::objectValue);
root["bms"] = JsonVal(Json::arrayValue);
root["realCcu"] = JsonVal(Json::objectValue);
root["bcuCtrl"] = JsonVal(Json::objectValue);
}
JsonVal links(Json::objectValue);
+57
View File
@@ -3,6 +3,8 @@
#include <mutex>
#include <atomic>
#include <vector>
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/CanBms.h"
@@ -58,6 +60,29 @@ struct RealCcuState {
// 通过互斥锁保护读写。
//============================================================================
//============================================================================
// BcuCtrlCmd:BCU 断路器(总正/总负继电器)控制指令。
//
// 链路:网页 /api/bcu_ctrl(Web 线程)-> 入队 -> CCU::Iterate(MOOS 线程)
// 出队编码 0x10XX81FF -> MOOS CAN_TX_0x* -> pCanBridge 下行 CAN 总线。
// 队列化是为了让 MOOS Notify 只发生在 MOOS 线程(Web 线程不可直接 Notify)。
//============================================================================
struct BcuCtrlCmd {
uint8_t addr = 0; // BCU 节点地址 1~54
uint8_t pos = 0; // 总正继电器 1 闭合 / 0 断开
uint8_t neg = 0; // 总负继电器 1 闭合 / 0 断开
};
// BCU 断路器控制的下发统计(网页展示)
struct BcuCtrlStat {
unsigned long sentCount = 0; // 已下发帧数(成功投递 MOOS)
unsigned long errCount = 0; // 投递失败帧数
BcuCtrlCmd last{};
bool lastOk = false;
double lastTime = 0; // 最近下发时刻(MOOSTime)
};
class SystemData {
public:
// 更新/获取燃料电池最新状态
@@ -189,6 +214,36 @@ public:
return m_realCcu;
}
//-------- BCU 断路器控制(网页 -> MOOS 下行队列) --------
// 入队一条控制指令(Web API 线程调用);队列上限 16,满则丢弃并返回 false
bool pushBcuCtrlCmd(const BcuCtrlCmd& c) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_bcuCtrlQueue.size() >= 16) return false;
m_bcuCtrlQueue.push_back(c);
return true;
}
// 出队一条控制指令(MOOS 主循环 Iterate 调用);空队列返回 false
bool popBcuCtrlCmd(BcuCtrlCmd& out) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_bcuCtrlQueue.empty()) return false;
out = m_bcuCtrlQueue.front();
m_bcuCtrlQueue.erase(m_bcuCtrlQueue.begin());
return true;
}
// 记录一次下发结果(Iterate 调用,供网页统计展示)
void noteBcuCtrlSent(const BcuCtrlCmd& c, bool ok) {
std::lock_guard<std::mutex> lock(m_mutex);
if (ok) m_bcuCtrlStat.sentCount++; else m_bcuCtrlStat.errCount++;
m_bcuCtrlStat.last = c;
m_bcuCtrlStat.lastOk = ok;
m_bcuCtrlStat.lastTime = MOOSTime(false);
}
BcuCtrlStat bcuCtrlStat() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_bcuCtrlStat;
}
private:
mutable std::mutex m_mutex;
FcStatusValue m_fcStatus;
@@ -199,6 +254,8 @@ private:
PmParamSetFbValue m_pmParamFb;
BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1
RealCcuState m_realCcu;
std::vector<BcuCtrlCmd> m_bcuCtrlQueue;
BcuCtrlStat m_bcuCtrlStat;
unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0;
+30 -1
View File
@@ -15,7 +15,13 @@ namespace ccu {
// 【节点地址】一个 MBMS 下最多挂 54 个 BCU,CAN ID 中间的 XX(01h~36h)
// 为 BCU 节点地址编号,其余位固定:
// 0x10XX00YY BCU 主动上报消息(YY=功能码 00/01/02/03/06/10)
// 0x10XX81FF MBMS 下发指令(继电器控制,pCCU 只收不发,不解析)
// 0x10XX81FF MBMS 下发指令(继电器控制,经 MOOS CAN_TX_0x* 下行)
//
// 【下发指令 0x10XX81FF】数据域 8 字节(BYTE1~8 = byte0~7):
// byte0 = 0x02(索引号) byte1 = 0x00(索引号)
// byte2 = 总正继电器控制(1 闭合 / 0 断开)
// byte3 = 总负继电器控制(1 闭合 / 0 断开)
// byte4~7 = Reserve(填充 0xFF)
//
// 【上报报文】均为扩展帧,字段大端,BYTE1~8 对应数据域 byte0~7:
// 0x10XX0000 (200ms) 累加电压 u16@0 ÷10 V
@@ -68,6 +74,29 @@ inline uint8_t bcuFuncOf(uint32_t canId) {
return static_cast<uint8_t>(canId & 0xFF);
}
//--------------------------------------------------------------------------
// BCU 断路器(总正/总负继电器)下行控制
//--------------------------------------------------------------------------
// BCU 节点继电器控制指令 CAN ID(0x10XX81FF,XX=节点地址 01h~36h)
inline uint32_t bcuCtrlCanId(uint8_t addr) {
return 0x10000000u | (static_cast<uint32_t>(addr) << 16) | 0x81FFu;
}
// 编码继电器控制数据域(8 字节,经 MOOS CAN_TX_0x%08X 下发,pCanBridge 发送):
// [0]=0x02 [1]=0x00 [2]=总正(1闭合/0断开) [3]=总负 [4..7]=0xFF
// pos/neg 非 0 值均视为闭合(1)。
inline void buildBcuRelayCtrlData(int pos, int neg, uint8_t out[8]) {
out[0] = 0x02;
out[1] = 0x00;
out[2] = pos ? 0x01 : 0x00;
out[3] = neg ? 0x01 : 0x00;
out[4] = 0xFF;
out[5] = 0xFF;
out[6] = 0xFF;
out[7] = 0xFF;
}
// 是否为 BCU 主动上报报文 ID(0x10XX00YY,YY 为已定义功能码)
inline bool isCanBcuId(uint32_t canId) {
uint8_t addr = bcuAddrOf(canId);
+48 -3
View File
@@ -68,6 +68,11 @@ th{color:var(--dim);font-weight:500;}
/* 卡片内分组小标题 */
.sub{font-size:11px;color:#79c0ff;margin:10px 0 2px;border-bottom:1px solid #22304a;padding-bottom:2px;}
.sub:first-child{margin-top:0;}
.btn{padding:3px 10px;margin:0 4px 4px 0;border:1px solid var(--line);border-radius:6px;background:#222b3d;color:var(--fg);font-size:11px;cursor:pointer;}
.btn:hover{border-color:#3b4a63;}
.btn.on{background:#1f6feb;border-color:#1f6feb;color:#fff;font-weight:600;}
.btn.off{background:#3d2226;border-color:#f85149;color:#f85149;}
.btn:disabled{opacity:.4;cursor:not-allowed;}
</style>
</head>
<body>
@@ -402,6 +407,39 @@ function pmFbCard(d){
return card('设定结果', h);
}
//------------------ BCU 断路器控制(下行 0x10XX81FF) ------------------
// pos/neg: 1 闭合 / 0 断开(总正+总负同帧下发,经 pCanBridge 写 USBCAN)
function bcuCtrl(addr, pos, neg, label){
if(!confirm('确认向 BCU 节点 '+addr+' 下发「'+label+'」指令?\n该指令将直接控制电池簇总正/总负继电器。')) return;
fetch('/api/bcu_ctrl?addr='+addr+'&pos='+pos+'&neg='+neg)
.then(function(r){ return r.json(); })
.then(function(j){
if(j.ok){
alert('已下发: 节点'+j.addr+' 总正='+(j.pos?'闭合':'断开')+' 总负='+(j.neg?'闭合':'断开')+
'\n继电器状态以 0x10'+pad2(j.addr)+'0003 上报刷新为准');
}else{
alert('下发失败: '+(j.error||'未知错误'));
}
})
.catch(function(){ alert('网络错误,下发未完成'); });
}
// 断路器控制下发统计卡片(最近一次指令与成败计数)
function bcuCtrlStatCard(s){
let c='';
const v = s && s.valid;
c+=row('累计下发', (s && s.sentCount || 0) + ' 成功 / ' + (s && s.errCount || 0) + ' 失败');
if(v){
c+=row('最近指令', '节点'+s.addr+' 总正='+(s.pos?'闭合':'断开')+' 总负='+(s.neg?'闭合':'断开')
+ ' ' + (s.lastOk ? '<span class="fc-ok">已投递</span>' : '<span class="fc-bad">投递失败</span>'));
c+=row('下发时间', ageCell(s.age, true));
}else{
c+=row('最近指令', '无');
}
c+=sub('链路: 网页 → pCCU(0x10XX81FF) → MOOS CAN_TX_* → pCanBridge → USBCAN → BCU');
return card('断路器控制下发', c);
}
//------------------ 锂电池 BCU 节点(CAN) 卡片 ------------------
// 每个节点(CAN ID 0x10XX00YY 中的 XX)一张卡片,
// 同一节点的 0000/0001/0002/0003/0006/0010 各报文归并在卡片内分组展示
@@ -410,8 +448,7 @@ function bmsCard(d){
if(!nodes.length) return card('锂电池BMS (CAN)',
'<div class="row"><span class="k">数据状态</span><span class="v">等待报文...</span></div>');
let html='';
for(const n of nodes){
const addrTxt = 'BCU节点 '+pad2(n.addr)+'h (#'+n.addr+')';
for(const n of nodes){ const addrTxt = 'BCU节点 '+pad2(n.addr)+'h (#'+n.addr+')';
let h='';
// 概要(0x10XX0000)
h+=sub('概要 0x10'+pad2(n.addr)+'0000');
@@ -434,6 +471,11 @@ function bmsCard(d){
h+=sub('继电器 0x10'+pad2(n.addr)+'0003');
h+=row('正极继电器', relayText(n.posRelay));
h+=row('负极继电器', relayText(n.negRelay));
// 断路器控制(0x10XX81FF 下发,总正+总负继电器同帧控制)
h+=sub('断路器控制 (0x10'+pad2(n.addr)+'81FF)');
h+=row('操作',
'<button class="btn on" onclick="bcuCtrl('+n.addr+',1,1,\'断路器闭合\')">断路器闭合</button>'+
'<button class="btn off" onclick="bcuCtrl('+n.addr+',0,0,\'断路器断开\')">断路器断开</button>');
// 绝缘/端口电压(0x10XX0006)
h+=sub('绝缘/端口 0x10'+pad2(n.addr)+'0006');
h+=row('正极绝缘阻抗', n.posInsulationKohm.toFixed(0)+' kΩ');
@@ -758,6 +800,7 @@ function render(snap){
pmParam: pageHeader(0x0002,'PM参数设定','PM→CCU')+'<div class="grid">'+pmParamCard(snap.pmParam)+'</div>',
// 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传,按节点分组展示)
bms: '<div class="grid">'+bmsCard(snap.bms)+
bcuCtrlStatCard(snap.bcuCtrl)+
'<div class="card"><h2>报文来源 (CAN ID)</h2>'+
row('0x10XX0000','累加电压 / 回路电流 / SOC / 告警码 / 自检状态')+
row('0x10XX0001','最高/最低/平均单体电压及编号')+
@@ -765,8 +808,10 @@ function render(snap){
row('0x10XX0003','正/负极继电器状态')+
row('0x10XX0006','正/负极绝缘阻抗 / 端口电压 / 继电器外侧电压')+
row('0x10XX0010','告警位(附录1,按位解析显示告警含义)')+
row('0x10XX81FF','断路器控制下发(MBMS→BCU,总正/总负继电器,总正+总负同帧)')+
row('XX','BCU 节点地址 01h~36h,同一节点的报文归入同一卡片')+
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'</div></div>',
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+
row('下行通道','pCCU 发布 CAN_TX_0x* → pCanBridge → USBCAN')+'</div></div>',
fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>',
// 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据
pmStatus: pageHeader(0x0004,'PM状态报文','CCU→PM')+'<div class="grid">'+
+52 -4
View File
@@ -90,25 +90,70 @@ bool CanBridge::OnConnectToServer() {
}
//---------------------------------------------------------
// registerVariables:纯发布应用,无需注册订阅变量
// registerVariables:订阅下行指令(CAN_TX_0x*,pCCU 发布的
// BCU 断路器控制帧等)。
void CanBridge::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// 通配订阅必须用三参数重载(变量模式 + 来源模式)
Register("CAN_TX_0x*", "*", 0);
}
//---------------------------------------------------------
// OnNewMail:当前无订阅指令,预留
// OnNewMail:下行指令 CAN_TX_0x%08X(二进制数据域)
bool CanBridge::OnNewMail(MOOSMSG_LIST &NewMail) {
AppCastingMOOSApp::OnNewMail(NewMail);
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
(void)msg;
if (msg.m_sKey.rfind("CAN_TX_0x", 0) == 0)
handleTxMessage(msg);
}
return true;
}
//---------------------------------------------------------
// handleTxMessage:MOOS 下行消息 -> CANET 13 字节帧 -> USBCAN
//
// m_sKey = "CAN_TX_0x%08X"(CAN ID)
// m_sVal = 二进制数据域(dlc<=8)
// 帧信息:id>0x7FF 视为扩展帧(bit7=1),DLC=数据字节数。
void CanBridge::handleTxMessage(CMOOSMsg& msg) {
if (!m_endpoint) return;
uint32_t id = 0;
// "CAN_TX_0x" 前缀长 9 字符(注意不是 8)
int sr = std::sscanf(msg.m_sKey.c_str() + 9, "%x", &id);
if (sr != 1) {
reportRunWarning("CAN_TX bad key: " + msg.m_sKey);
return;
}
if (!msg.IsBinary() || msg.GetBinaryDataSize() <= 0 ||
msg.GetBinaryDataSize() > 8) {
reportRunWarning("CAN_TX bad data size: " + msg.m_sKey);
return;
}
const unsigned char* d = msg.GetBinaryData();
if (!d) return;
int dlc = static_cast<int>(msg.GetBinaryDataSize());
uint8_t frameInfo = static_cast<uint8_t>(((id > 0x7FF) ? 0x80 : 0x00) | dlc);
if (m_endpoint->sendFrame(frameInfo, id, d, dlc)) {
++m_txCount;
char hex[32] = {0};
for (int i = 0; i < dlc && i < 8; ++i)
std::snprintf(hex + i * 2, 3, "%02X", d[i]);
std::cout << "[CAN-TX] 0x" << std::hex << id << std::dec
<< " dlc=" << dlc << " data=" << hex << std::endl;
} else {
reportRunWarning("CAN_TX send failed: " + msg.m_sKey +
(m_endpoint->isConnected() ? "" : " (未连接)"));
}
}
//---------------------------------------------------------
// Iterate:周期任务
@@ -146,7 +191,7 @@ void CanBridge::handleFrame(uint8_t frameInfo, uint32_t id,
bool CanBridge::buildReport() {
m_msgs << "============================================" << "\n";
m_msgs << "pCanBridge CAN->MOOSDB 透传" << "\n";
m_msgs << "pCanBridge CAN<->MOOSDB 双向透传" << "\n";
m_msgs << "============================================" << "\n";
if (m_endpoint) {
m_msgs << "目标: " << m_endpoint->host() << ":"
@@ -154,6 +199,9 @@ bool CanBridge::buildReport() {
m_msgs << "连接状态: " << (m_endpoint->isConnected() ? "已连接" : "断开") << "\n";
m_msgs << "收帧数: " << m_endpoint->frameCount() << "\n";
m_msgs << "发布数: " << m_notifyCount << "\n";
m_msgs << "下行帧数: " << m_endpoint->txCount() << " (错误 "
<< m_endpoint->txErrorCount() << ")" << "\n";
m_msgs << "MOOS下发数: " << m_txCount << "\n";
m_msgs << "错误数: " << m_endpoint->errorCount() << "\n";
m_msgs << "重连次数: " << m_endpoint->reconnectCount() << "\n";
m_msgs << "最近收帧: " << m_endpoint->lastFrameTime() << "\n";
+14 -3
View File
@@ -10,14 +10,20 @@
namespace canbridge {
//============================================================================
// CanBridge:CAN -> MOOSDB 透传桥(MOOS 应用外壳)。
// CanBridge:CAN <-> MOOSDB 双向透传桥(MOOS 应用外壳)。
//
// 数据流:
// 数据流(上行):
// USBCAN-8E-U (TCP Server, 默认 192.168.0.222:4001 = CAN0)
// -> CanEndpoint(TCP Client,13 字节切帧)
// -> handleFrame() 解析
// -> Notify 到 MOOSDB
//
// 数据流(下行,BCU 断路器/继电器控制等):
// MOOS "CAN_TX_0x%08X"(二进制数据域,dlc<=8)
// -> OnNewMail 解析 CAN ID 与数据
// -> CanEndpoint::sendFrame(同一 TCP 连接写出 13 字节帧)
// -> USBCAN -> CAN 总线
//
// MOOS 消息映射:
// m_sKey = "CAN_0x%08X" CAN ID(完整 4 字节,标准/扩展帧同格式)
// m_sVal = 二进制 data(dlc 字节,MOOS_BINARY_STRING)
@@ -25,7 +31,8 @@ namespace canbridge {
// m_dfVal2 = 原始帧信息字节 byte0(bit7 FF / bit6 RTR / bit3~0 DLC)
// m_nID 不使用(MOOS 内部消息序号)
//
// 每帧同时落库 SQLite(can_frame 表,配置项 dbpath)。
// 每帧(上行)同时落库 SQLite(can_frame 表,配置项 dbpath);
// 下行帧仅日志记录(can_frame 表无方向列,保持旧库兼容)。
//============================================================================
class CanBridge : public AppCastingMOOSApp {
@@ -45,6 +52,9 @@ private:
// 收帧回调(CanEndpoint 接收线程调用)
void handleFrame(uint8_t frameInfo, uint32_t id, const uint8_t* data, int dlc);
// 下行指令处理(OnNewMail 调用):CAN_TX_0x%08X -> CanEndpoint 发送
void handleTxMessage(CMOOSMsg& msg);
// 配置
std::string m_canHost = "192.168.0.222";
long m_canPort = 4001;
@@ -55,6 +65,7 @@ private:
CanEndpoint* m_endpoint = nullptr;
CanDbStore* m_db = nullptr;
std::atomic<unsigned long> m_notifyCount{0};
std::atomic<unsigned long> m_txCount{0};
};
} // namespace canbridge
+43 -1
View File
@@ -12,7 +12,6 @@
#include <iostream>
#include <chrono>
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
namespace canbridge {
namespace {
@@ -128,6 +127,49 @@ void CanEndpoint::closeSocket() {
}
}
//----------------------------------------------------------------------
// 下行发送:组装 13 字节 CANET 帧并写出
// 接收线程可能并发断线重连(m_fd 被关闭),用 m_txMutex 串行化:
// - 持锁期间检查 m_connected,避免对已关闭 fd 发送
// - 接收线程的 closeSocket 不持此锁,若恰逢发送中出现 EPIPE 等
// 错误按发送失败计,断线由接收线程统一检出
//----------------------------------------------------------------------
bool CanEndpoint::sendFrame(uint8_t frameInfo, uint32_t id,
const uint8_t* data, int dlc) {
if (dlc < 0 || dlc > 8) return false;
uint8_t frame[kFrameLen] = {0};
frame[0] = frameInfo;
frame[1] = static_cast<uint8_t>((id >> 24) & 0xFF);
frame[2] = static_cast<uint8_t>((id >> 16) & 0xFF);
frame[3] = static_cast<uint8_t>((id >> 8) & 0xFF);
frame[4] = static_cast<uint8_t>(id & 0xFF);
if (data && dlc > 0)
std::memcpy(frame + 5, data, static_cast<size_t>(dlc));
std::lock_guard<std::mutex> lock(m_txMutex);
if (!m_connected || m_fd < 0) {
++m_txErrorCount;
std::cerr << "[CanEndpoint] send failed: not connected" << std::endl;
return false;
}
size_t sent = 0;
while (sent < kFrameLen) {
ssize_t n = ::send(m_fd, frame + sent, kFrameLen - sent, MSG_NOSIGNAL);
if (n > 0) {
sent += static_cast<size_t>(n);
continue;
}
if (n < 0 && (errno == EINTR)) continue;
++m_txErrorCount;
std::cerr << "[CanEndpoint] send error: "
<< std::strerror(errno) << std::endl;
return false;
}
++m_txCount;
return true;
}
//----------------------------------------------------------------------
// 切帧:缓冲 >= 13 字节即解析并回调
//----------------------------------------------------------------------
+11
View File
@@ -4,6 +4,7 @@
#include <cstdint>
#include <string>
#include <vector>
#include <mutex>
#include <atomic>
#include <thread>
#include <functional>
@@ -48,8 +49,15 @@ public:
bool isConnected() const { return m_connected; }
// 下行发送一帧 CAN(MOOS->CAN 方向,须已连接)。
// frameInfo: bit7 FF(1扩展/0标准) bit6 RTR bit3~0 DLC;
// data 为 dlc(0~8) 字节数据域。返回是否成功写出。
bool sendFrame(uint8_t frameInfo, uint32_t id, const uint8_t* data, int dlc);
// 统计
unsigned long frameCount() const { return m_frameCount; }
unsigned long txCount() const { return m_txCount; }
unsigned long txErrorCount() const { return m_txErrorCount; }
unsigned long errorCount() const { return m_errorCount; }
unsigned long reconnectCount() const { return m_reconnectCount; }
double lastFrameTime() const { return m_lastFrameTime; }
@@ -70,6 +78,7 @@ private:
std::atomic<bool> m_running{false};
std::atomic<bool> m_connected{false};
std::thread m_thread;
std::mutex m_txMutex; // 下行 send 与断线 close 的并发保护
CanFrameCallback m_onFrame;
@@ -77,6 +86,8 @@ private:
std::vector<uint8_t> m_buf;
std::atomic<unsigned long> m_frameCount{0};
std::atomic<unsigned long> m_txCount{0};
std::atomic<unsigned long> m_txErrorCount{0};
std::atomic<unsigned long> m_errorCount{0};
std::atomic<unsigned long> m_reconnectCount{0};
std::atomic<double> m_lastFrameTime{0.0};
+7 -6
View File
@@ -1,12 +1,13 @@
//============================================================================
// pCanBridge 配置示例
//
// 拓扑:
// USBCAN-8E-U(CANET 系列,TCP Server 模式)
// 192.168.0.222:4001 = CAN0 工作端口(CAN1=4002 ... CAN7=4008)
// pCanBridge 以 TCP Client 连接目标,收帧后透传 MOOSDB:
// 变量名 CAN_0x%08X = CAN ID;m_sVal = 二进制 data;
// m_sSrcAux = 通道名;m_dfVal2 = 原始帧信息字节(FF/RTR/DLC)
// 拓扑(双向透传):
// 上行:USBCAN-8E-U(CANET TCP Server,192.168.0.222:4001=CAN0)
// -> pCanBridge(TCP Client) -> MOOS "CAN_0x%08X"(二进制 data)
// 下行:MOOS "CAN_TX_0x%08X"(二进制 data,dlc<=8,如 BCU 断路器控制
// 帧 0x10XX81FF,由 pCCU 发布)-> pCanBridge -> USBCAN -> CAN 总线
// 变量名 CAN_0x%08X = CAN ID;m_sVal = 二进制 data;
// m_sSrcAux = 通道名;m_dfVal2 = 原始帧信息字节(FF/RTR/DLC)
//============================================================================
ProcessConfig = pCanBridge