pCCU 新增真实CCU配电桥接(rcu链路原帧透传+网页显示)并适配板卡端口拓扑

- protocol/DisProtocol: 配电协议8条消息(指令0x0001~4/反馈0x0005~8, Sum8),复用pmSysvariable结构体+static_assert核对
- ChecksumPolicy 新增 Sum8;Frame 支持 1 字节校验和
- MessageRegistry 支持 (id+帧总长) 二维查找:配电指令与PM操控指令共用id按帧长区分(帧头length字段不可信)
- LinkManager: 按实际帧长分发 + sendRaw原帧透传 + 非本协议帧头落库节流告警;UdpEndpoint sendTo 加锁
- comm/RcuLinkManager: 真实CCU链路;core/DisBridge: 配电桥接器(反馈转pPowerManger+解码显示,指令转真实CCU,0x0004仅显示不转发)
- 网页新增"真实CCU"页签全字段展示;SnapshotBuilder realCcu 节 + rcu 链路描述
- h100.moos: 端口腾挪(真实CCU固定上报5001归pCCU,pPowerManger iport=5002),rcu_remote=192.168.0.140:7000
- 测试: pccuTest 新增配电/Sum8/长度分发/RCU注册表用例(176项);集成测试新增桥接字节一致+不误转发用例
This commit is contained in:
zjk
2026-09-01 15:06:11 +08:00
parent 5b8f0b1421
commit 0639b5904d
28 changed files with 1609 additions and 53 deletions
+216 -1
View File
@@ -86,6 +86,7 @@ th{color:var(--dim);font-weight:500;}
</div>
<div class="tab-group"><span class="tg-label">系统</span>
<button class="tab" data-tab="links">链路状态</button>
<button class="tab" data-tab="realCcu">真实CCU</button>
<button class="tab" data-tab="raw">原始报文</button>
</div>
</div>
@@ -95,7 +96,7 @@ th{color:var(--dim);font-weight:500;}
const $ = id => document.getElementById(id);
const pagesEl = $('pages');
const statusEl = $('status');
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','raw'];
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','realCcu','raw'];
let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
@@ -154,6 +155,7 @@ function bmsOnline(n){
}
// 最后更新时间文本(a:距最后收到报文的秒数)
function ageText(a){
if(typeof a !== 'number' || isNaN(a)) return '—';
if(a < 90) return a.toFixed(1)+' 秒前';
if(a < 3600) return Math.floor(a/60)+' 分 '+Math.floor(a%60)+' 秒前';
return Math.floor(a/3600)+' 时 '+Math.floor((a%3600)/60)+' 分前';
@@ -323,6 +325,10 @@ function linkCard(s){
'收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err);
h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>',
'收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err);
if(l.rcu){
h+=row('<span><span class="status-dot '+dot(online(l.rcu.lastRx))+'"></span>RCU 链路 (真实CCU)</span>',
'收:'+l.rcu.rx+' 发:'+l.rcu.tx+' 误:'+l.rcu.err);
}
// CAN/BMS 行:任一 BCU 节点在线即视为链路在线
const bmsNodes = Array.isArray(s.bms) ? s.bms : [];
const bmsAnyOnline = bmsNodes.some(function(n){ return bmsOnline(n); });
@@ -490,6 +496,214 @@ function pmStatusCard(d){
return card('整合后状态', h);
}
//------------------ 真实 CCU(配电桥接) 卡片 ------------------
// 数据来源:RCU 链路。配电反馈(0x0005~0x0008)由 pCCU 原帧转发
// pPowerManger 并在此全字段展示;配电指令(0x0001~0x0004)由 pPowerManger
// 经 pCCU 原帧转发真实CCU。断路器/开关量:0x55 闭合 0xAA 断开 0x5A 故障
function brkText(v){
if(v===0x55) return '<span class="fc-ok">闭合</span>';
if(v===0xAA) return '断开';
if(v===0x5A) return '<span class="fc-bad">故障脱扣</span>';
return hex(v);
}
// 双值信号(0x55 正常 / 0xAA 异常)
function sigText(v){
if(v===0x55) return '<span class="fc-ok">正常</span>';
if(v===0xAA) return '<span class="fc-bad">异常</span>';
return hex(v);
}
function insStatText(v){ return v===0x55 ? '<span class="fc-ok">正常</span>'
: (v===0xAA ? '<span class="fc-bad">绝缘低</span>' : hex(v)); }
// age:距最后收到该类帧的秒数;valid:是否收到过(未收到显示"从未收到")
function ageCell(age, valid){
if(!valid || typeof age !== 'number' || isNaN(age))
return '<span style="color:var(--dim)">从未收到</span>';
const online = age >= 0 && age < 5;
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">') + ageText(age)
+ (online ? ' (在线)' : ' (超时)') + '</span>';
}
function fwdLine(okN, errN){
return okN + ' 成功' + (errN ? ' <span class="fc-bad">'+errN+' 失败</span>' : '');
}
// 母线卡片通用:breakers 子表 + 信号/电气量行(无数据时只显示概要行)
function busCard(title, bus, age, rows){
let h='';
const b = bus && bus.breakers ? bus.breakers : {};
const keys = Object.keys(bNamesDis).filter(function(k){ return b[k] !== undefined; });
if(!keys.length){
h+=sub('断路器状态');
h+=row('状态','暂无数据');
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
h+=sub('断路器状态');
for(const k of keys) h+=row(bNamesDis[k], brkText(b[k]));
for(const r of rows) h+=row(r[0], r[1]);
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
// 母线断路器中文名表(busCard 共用)
const bNamesDis = {
fuelCell:'燃料电池断路器', powerLithiumBattery:'动力锂电池断路器',
propulsionMotor:'推进电机断路器', lithiumBatteryGroupInstrument:'锂电池组(仪表)断路器',
dcDc5Module:'DC/DC5模块断路器', bowHighVoltageDistributionBox:'艏部高压配电箱断路器',
sternFTDevice45:'艉部FT装置4/5断路器', fbReserved:'FB预留断路器',
sternRudderSwitch1:'艉舵开关1断路器', sternRudderSwitch2:'艉舵开关2断路器',
tyzReserved:'TYZ预留断路器', reserved:'预留断路器',
bowFTDevice123:'艏部FT装置1/2/3断路器', actuator:'启闭机构断路器',
mastSteeringGearControlBox:'桅杆舵机控制箱断路器', xcz:'XCZ断路器',
bowRudderControlBox:'艏舵控制箱断路器', openWaterCoverStartCylinder:'敞水盖启动电缸断路器',
bowLowVoltageDistributionBox:'艏部低压配电箱断路器', unit4InstrumentDC48V:'UNIT4仪表DC48V断路器',
dcC1DCDistributionPanel:'DC-C1直流配电板断路器', reserved1:'预留断路器1', reserved2:'预留断路器2',
emergencyLithiumBatteryGroup2:'应急锂电池组2断路器',
unit1:'UNIT1断路器', unit2:'UNIT2断路器', unit3:'UNIT3断路器', unit5:'UNIT5断路器',
bowPZDevice:'艏部PZ装置断路器', emergencyLithiumBatteryGroup1:'应急锂电池组1断路器',
dcdc5ModuleStart:'DC/DC5模块启动', coolingSystemStart:'冷却系统启动'
};
function realCcuPage(d){
if(!d) return '<div class="card"><h2>真实CCU</h2><div class="row"><span class="k">数据状态</span><span class="v">等待数据...</span></div></div>';
let h='<div class="page-hdr"><span class="raw-id">RCU</span> <span class="raw-name">真实CCU 配电桥接</span> '+
'<span class="badge ok">原帧透传</span></div><div class="grid">';
// 桥接概览卡片
{
let c='';
const fwd = d.forward || {};
c+=row('配电反馈收帧', fwd.fbCount || 0);
c+=row('反馈转发PM', fwdLine(fwd.fwdFbOk || 0, fwd.fwdFbErr || 0));
c+=row('配电指令收帧', fwd.cmdCount || 0);
c+=row('指令转发真实CCU', fwdLine(fwd.fwdCmdOk || 0, fwd.fwdCmdErr || 0));
const ccu = d.ccuStatus || {};
c+=sub('真实CCU 状态报文(仅显示,不转发)');
c+=row('在线', ageCell(ccu.age, ccu.valid));
if(ccu.valid){
c+=row('运行模式', modeText(ccu.fc_mode));
c+=row('运行状态', statusText(ccu.fc_status));
c+=row('系统故障等级', faultLvlBadge(ccu.fc_fault_level));
c+=row('系统发电功率', (ccu.generation_power*0.01).toFixed(2)+' kW');
c+=row('仪表/动力SOC', ccu.ins_soc+'% / '+ccu.dyn_soc+'%');
c+=row('通信心跳', ccu.heartbeat);
}
h+=card('桥接概览', c);
}
// 高压母线(0x0005 反馈 / 0x0001 指令)
{
const hv = d.hv || {};
const rows = [
['DC/DC5故障字低', hex(hv.dcDcFaultWord1)],
['DC/DC5故障字高', hex(hv.dcDcFaultWord2)],
['动力母排绝缘状态', insStatText(hv.powerBusInsulationStatus)],
['A汇流排绝缘状态', insStatText(hv.aBusInsulationStatus)],
['仪表电源状态', sigText(hv.meterPowerLossSignal)],
['应急电源状态', sigText(hv.emergencyPowerLossSignal)],
['DC/DC温度', hv.dcDcTemperature],
['动力汇流排电压', hv.powerBusVoltage],
['DC/DC5模块电流', hv.dcDc5ModuleCurrent],
['动力汇流排电流', hv.powerBusCurrent],
['A汇流排电压', hv.busbarAVoltage],
['推进电机控制箱电流', hv.propulsionMotorControlBoxCurrent],
['A汇流排电流', hv.busbarACurrent],
['锂电池组(仪表)电流', hv.lithiumBatteryGroupMeterCurrent],
['艏部高压配电箱电流', hv.bowHighVoltageDistributionBoxCurrent],
['艉部FT装置4/5电流', hv.sternFTDevice45Current],
['TYZ预留开关电流', hv.tyzReservedSwitchCurrent],
['艉舵开关1电流', hv.sternRudderSwitch1Current],
['预留电流1', hv.reservedCurrent1],
['艉舵开关2电流', hv.sternRudderSwitch2Current],
['预留电流2', hv.reservedCurrent2],
['FB预留电流', hv.fbReservedSwitchCurrent],
['预留电流3', hv.reservedCurrent3],
['冷却水压力', hv.coolWaterPressure],
];
h+=busCard('高压母线 (0x0005 反馈 / 0x0001 指令)', hv, d.hvAge, rows);
}
// 高压母线A(0x0006 反馈 / 0x0002 指令)
{
const b = d.hva || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['汇流排B电压', b.busbarBVoltage],
['汇流排B电流', b.busbarBCurrent],
['艏部FT装置1/2/3电流', b.bowFTDevice123Current],
['启闭机构电流', b.actuatorCurrent],
['桅杆舵机控制箱电流', b.mastSteeringGearControlBoxCurrent],
['XCZ电流', b.xczCurrent],
['艏舵控制箱电流', b.bowRudderControlBoxCurrent],
['敞水盖启动电缸电流', b.openWaterCoverStartCylinderCurrent],
];
h+=busCard('高压母线A (0x0006 反馈 / 0x0002 指令)', b, d.hvaAge, rows);
}
// 低压主母线(0x0007 反馈 / 0x0003 指令)
{
const b = d.hvb || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['仪表汇流排绝缘', insStatText(b.instrumentBusbarInsulationLow)],
['仪表母排电压', b.instrumentBusbarVoltage],
['艏部低压配电箱电流', b.bowLowVoltageDistributionBoxCurrent],
['DC-C1仪表DC48V电流', b.dcC1InstrumentDC48VCurrent],
['预留电流1', b.reservedCurrent1],
['应急锂电池组2电流', b.emergencyLithiumBatteryGroup2Current],
['锂电池组(仪表)电流', b.lithiumBatteryGroupInstrumentCurrent],
['UNIT4仪表DC48V电流', b.unit4InstrumentDC48VCurrent],
['预留电流2', b.reservedCurrent2],
['应急2汇流排电压', b.emergency2BusbarVoltage],
['复合能源应急DC48V电流', b.compositeEnergyEmergencyDC48VCurrent],
['燃料电池安全应急DC48V电流', b.fuelCellSecurityEmergencyDC48VCurrent],
['PLC控制电源电流', b.plcControlPowerCurrent],
];
h+=busCard('低压主母线 (0x0007 反馈 / 0x0003 指令)', b, d.hvbAge, rows);
}
// 仪表汇流排(0x0008 反馈 / 0x0004 指令)
{
const b = d.lv || {};
const rows = [
['仪表电源失电信号', sigText(b.powerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['仪表母排电压', b.instrumentBusbarVoltage],
['仪表母排电流', b.instrumentBusbarCurrent],
['UNIT1电流', b.unit1Current],
['UNIT2电流', b.unit2Current],
['UNIT3电流', b.unit3Current],
['UNIT5电流', b.unit5Current],
['艏部PZ装置电流', b.bowPZDeviceCurrent],
['应急锂电池组1电流', b.emergencyLithiumBatteryGroup1Current],
['应急1汇流排电压', b.emergency1BusbarVoltage],
];
h+=busCard('仪表汇流排 (0x0008 反馈 / 0x0004 指令)', b, d.lvAge, rows);
}
// 最近下发指令卡片
{
const lc = d.lastCmd || {};
let c='';
c+=row('最后指令时间', ageCell(lc.age, lc.valid));
const cmds = lc.cmds || {};
const groups = [['hv','高压母线'],['hva','高压母线A'],['hvb','低压主母线'],['lv','仪表汇流排']];
for(const g of groups){
const gb = cmds[g[0]] || {};
const keys = Object.keys(gb);
if(!keys.length) continue;
c+=sub(g[1]+' (0x'+({hv:'0001',hva:'0002',hvb:'0003',lv:'0004'})[g[0]]+')');
for(const k of keys) c+=row(bNamesDis[k]||k, brkText(gb[k]));
}
h+=card('最近下发的配电指令 (PM→真实CCU)', c);
}
h+='</div>';
return h;
}
//------------------ 标签页头部 ------------------
function pageHeader(id, name, dir){
const idHex = '0x'+id.toString(16).toUpperCase().padStart(4,'0');
@@ -559,6 +773,7 @@ function render(snap){
pmStatusCard(snap.pmStatus)+batteryCard(snap.pmStatus)+emergCard(snap.pmStatus)+'</div>',
pmFb: pageHeader(0x0003,'PM参数反馈','CCU→PM')+'<div class="grid">'+pmFbCard(snap.pmFb)+'</div>',
links: '<div class="grid">'+linkCard(snap)+'</div>',
realCcu: realCcuPage(snap.realCcu),
raw: rawCard(snap.logs)
};