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:
+89
-1
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 = 运行状态字符串 ");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">'+
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 字节即解析并回调
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -520,6 +520,25 @@ static void testRcuRegistry() {
|
||||
CHECK(gotName == "pm_status");
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 13. BCU 断路器控制帧(0x10XX81FF 下行)
|
||||
static void testBcuCtrlFrame() {
|
||||
// CAN ID:0x10 XX 81 FF,XX=节点地址
|
||||
CHECK(bcuCtrlCanId(1) == 0x100181FFu);
|
||||
CHECK(bcuCtrlCanId(54) == 0x103681FFu);
|
||||
|
||||
// 数据域:02 00 总正 总负 FF*4
|
||||
uint8_t d[8];
|
||||
buildBcuRelayCtrlData(1, 1, d);
|
||||
CHECK(d[0] == 0x02 && d[1] == 0x00);
|
||||
CHECK(d[2] == 0x01 && d[3] == 0x01);
|
||||
CHECK(d[4] == 0xFF && d[5] == 0xFF && d[6] == 0xFF && d[7] == 0xFF);
|
||||
buildBcuRelayCtrlData(0, 0, d);
|
||||
CHECK(d[2] == 0x00 && d[3] == 0x00);
|
||||
buildBcuRelayCtrlData(1, 0, d);
|
||||
CHECK(d[2] == 0x01 && d[3] == 0x00);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
int main() {
|
||||
testFrameLengths();
|
||||
@@ -535,6 +554,7 @@ int main() {
|
||||
testDisRoundTrip();
|
||||
testRegistryLengthDispatch();
|
||||
testRcuRegistry();
|
||||
testBcuCtrlFrame();
|
||||
|
||||
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
|
||||
@@ -33,6 +33,7 @@ import subprocess
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
# ---- 配置 ----
|
||||
@@ -42,10 +43,12 @@ PM_CTRL_PORT = 16002
|
||||
PM_STATUS_RECV_PORT = 16003
|
||||
RCU_FB_PORT = 16004 # pCCU rcu 链路本地端口(收配电反馈)
|
||||
RCU_CMD_RECV_PORT = 16005 # 模拟真实CCU 接收转发的配电指令
|
||||
CANET_PORT = 16006 # 模拟 USBCAN-8E-U TCP Server(捕获下行 CAN 帧)
|
||||
WEB_PORT = 18081
|
||||
DB_PATH = "/tmp/pccu_it_test.db"
|
||||
MOOSDB_EXE = "/usr/local/bin/MOOSDB"
|
||||
PCCU_EXE = "/home/zjk/project/H100PowerManger/bin/pCCU"
|
||||
CANBRIDGE_EXE = "/home/zjk/project/H100PowerManger/bin/pCanBridge"
|
||||
MISSION = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pccu_it.moos")
|
||||
|
||||
# 帧头
|
||||
@@ -184,6 +187,72 @@ def dis_lv_cmd_frame():
|
||||
return dis_frame(0x0004, bytes(p))
|
||||
|
||||
|
||||
#------------------ BCU 断路器控制下行测试 ------------------
|
||||
|
||||
class CanetServer(threading.Thread):
|
||||
"""最小 CANET TCP Server 模拟:接受 pCanBridge 连接,捕获下行 13 字节帧"""
|
||||
def __init__(self, port):
|
||||
super().__init__(daemon=True)
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.sock.bind(("127.0.0.1", port))
|
||||
self.sock.listen(2)
|
||||
self.frames = []
|
||||
self.lock = threading.Lock()
|
||||
self.stop_flag = False
|
||||
|
||||
def run(self):
|
||||
self.sock.settimeout(1.0)
|
||||
conn = None
|
||||
while not self.stop_flag:
|
||||
try:
|
||||
if conn is None:
|
||||
conn, _ = self.sock.accept()
|
||||
conn.settimeout(0.5)
|
||||
buf = b""
|
||||
while not self.stop_flag:
|
||||
try:
|
||||
d = conn.recv(4096)
|
||||
except socket.timeout:
|
||||
break
|
||||
if not d:
|
||||
conn.close()
|
||||
conn = None
|
||||
break
|
||||
buf += d
|
||||
while len(buf) >= 13:
|
||||
with self.lock:
|
||||
self.frames.append(buf[:13])
|
||||
buf = buf[13:]
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
|
||||
def snapshot(self):
|
||||
with self.lock:
|
||||
return list(self.frames)
|
||||
|
||||
|
||||
def bcu_ctrl_expect(addr, pos, neg):
|
||||
"""期望的下行 13 字节 CANET 帧:扩展帧 DLC8 + 0x10XX81FF + 02 00 pos neg FF*4"""
|
||||
return bytes([0x80 | 8,
|
||||
(0x10000000 | (addr << 16) | 0x81FF) >> 24 & 0xFF,
|
||||
(0x10000000 | (addr << 16) | 0x81FF) >> 16 & 0xFF,
|
||||
(0x10000000 | (addr << 16) | 0x81FF) >> 8 & 0xFF,
|
||||
(0x10000000 | (addr << 16) | 0x81FF) & 0xFF,
|
||||
0x02, 0x00, 0x01 if pos else 0x00, 0x01 if neg else 0x00,
|
||||
0xFF, 0xFF, 0xFF, 0xFF])
|
||||
|
||||
|
||||
def api_get(path):
|
||||
try:
|
||||
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}{path}", timeout=3)
|
||||
return json.loads(resp.read().decode())
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
|
||||
def recv_loop(sock, label, results, msg_ids, timeout):
|
||||
"""持续接收指定 msg_id 的帧,记录到 results"""
|
||||
sock.settimeout(timeout)
|
||||
@@ -204,11 +273,13 @@ def recv_loop(sock, label, results, msg_ids, timeout):
|
||||
|
||||
def main():
|
||||
# 清理
|
||||
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log"):
|
||||
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log",
|
||||
"/tmp/pccu_it_can.db", "/tmp/pccu_it_can.db-wal", "/tmp/pccu_it_can.db-shm"):
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
|
||||
procs = []
|
||||
canet = CanetServer(CANET_PORT)
|
||||
try:
|
||||
# 1. 启动 MOOSDB
|
||||
moosdb = subprocess.Popen([MOOSDB_EXE, "--moos", "--port", "9000"],
|
||||
@@ -216,11 +287,15 @@ def main():
|
||||
procs.append(moosdb)
|
||||
time.sleep(1.0)
|
||||
|
||||
# 2. 启动 pCCU
|
||||
# 2. 启动 pCCU + pCanBridge(pCanBridge 连接模拟 USBCAN,用于下行测试)
|
||||
pccu = subprocess.Popen([PCCU_EXE, MISSION],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
procs.append(pccu)
|
||||
time.sleep(2.0)
|
||||
canet.start()
|
||||
pcan = subprocess.Popen([CANBRIDGE_EXE, MISSION],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
procs.append(pcan)
|
||||
time.sleep(2.5)
|
||||
if pccu.poll() is not None:
|
||||
print("FAIL: pCCU exited early, code", pccu.returncode)
|
||||
return 1
|
||||
@@ -349,6 +424,39 @@ def main():
|
||||
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
|
||||
ok = ok and (db_rcu >= 8) # 4条配电反馈(收) + 4条配电指令(发)
|
||||
|
||||
# 校验 8:BCU 断路器控制下行(网页API -> pCCU -> MOOS -> pCanBridge -> 模拟USBCAN)
|
||||
# 闭合(addr=5, 总正+总负=1)
|
||||
j = api_get("/api/bcu_ctrl?addr=5&pos=1&neg=1")
|
||||
print(f"[断路器闭合 API] {j}")
|
||||
ok = ok and j.get("ok") is True
|
||||
# action 语法(addr=6, action=1 等价 pos=neg=1)
|
||||
j = api_get("/api/bcu_ctrl?addr=6&action=1")
|
||||
print(f"[断路器闭合 API(action)] {j}")
|
||||
ok = ok and j.get("ok") is True
|
||||
# 非法地址
|
||||
j = api_get("/api/bcu_ctrl?addr=99&action=1")
|
||||
print(f"[非法地址 API] {j}")
|
||||
ok = ok and j.get("ok") is False
|
||||
# 断开(addr=5, 总正+总负=0)
|
||||
j = api_get("/api/bcu_ctrl?addr=5&pos=0&neg=0")
|
||||
print(f"[断路器断开 API] {j}")
|
||||
ok = ok and j.get("ok") is True
|
||||
time.sleep(3.0)
|
||||
frames = canet.snapshot()
|
||||
print(f"[下行帧捕获] {len(frames)} 帧")
|
||||
exp_close5 = bcu_ctrl_expect(5, 1, 1)
|
||||
exp_close6 = bcu_ctrl_expect(6, 1, 1)
|
||||
exp_open5 = bcu_ctrl_expect(5, 0, 0)
|
||||
hit5 = any(f == exp_close5 for f in frames)
|
||||
hit6 = any(f == exp_close6 for f in frames)
|
||||
hit5o = any(f == exp_open5 for f in frames)
|
||||
leak99 = any(f[1] == 0x63 and f[2] == 0x00 for f in frames) # addr=99 未下发
|
||||
print(f"[断路器闭合下行(addr=5)] {'OK' if hit5 else 'MISSING'}")
|
||||
print(f"[断路器闭合下行(addr=6)] {'OK' if hit6 else 'MISSING'}")
|
||||
print(f"[断路器断开下行(addr=5)] {'OK' if hit5o else 'MISSING'}")
|
||||
print(f"[非法地址未下发(addr=99)] {'OK' if not leak99 else 'LEAK'}")
|
||||
ok = ok and hit5 and hit6 and hit5o and not leak99
|
||||
|
||||
web_ok = False
|
||||
try:
|
||||
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)
|
||||
@@ -363,6 +471,7 @@ def main():
|
||||
return 0 if ok else 1
|
||||
|
||||
finally:
|
||||
canet.stop_flag = True
|
||||
for p in reversed(procs):
|
||||
try:
|
||||
p.terminate()
|
||||
|
||||
@@ -30,4 +30,16 @@ ProcessConfig = pCCU
|
||||
|
||||
web_port = 18081 // 避开 pPowerMangerHost(18080)
|
||||
web_enable = true
|
||||
}
|
||||
|
||||
ProcessConfig = pCanBridge
|
||||
{
|
||||
AppTick = 4
|
||||
CommsTick = 4
|
||||
|
||||
// 测试用 CANET TCP Server(本脚本模拟,捕获 pCanBridge 下行帧)
|
||||
can_host = 127.0.0.1
|
||||
can_port = 16006
|
||||
can_channel_name = CAN0
|
||||
dbpath = /tmp/pccu_it_can.db
|
||||
}
|
||||
Reference in New Issue
Block a user