锂电池改用 BCU-MBMS CAN 协议按节点解析 + bcu_node 解析数据落库 + 板卡对时脚本

- pCCU/CanBms:按《04KT38电池BCU-MBMS通信(CAN)定义》重写解码,
  BmsStatus 单状态模型改为 BcuNodeStatus 多节点模型(0x10XX00YY,
  节点地址 01~36h),支持 0x0000 电压/电流/SOC/告警码、0x0001 单体
  电压、0x0002 单体温度、0x0003 继电器、0x0006 绝缘/端口电压、
  0x0010 告警位(附录1 中文码表)六类报文
- 平均单体温度偏移修正:协议文档 BYTE5 写"偏移0"有误,实测固件与
  最高/最低一致均带 -40℃ 偏移(实车 0x10020002 原始 68/69 减 40 后
  为 28/29℃,落在最低28~最高30区间内,按文档直读则超出物理范围)
- pCCU/DbStore:新增 bcu_node 解析数据表,锂电池 BMS 报文每帧落一行
  节点合成状态(原 comm_log 仅原始帧,BMS/CAN 帧此前不落 pCCU 库),
  buildReport 增加 BMS 记录数
- 快照/网页:BCU 按节点分组展示(告警位解析中文含义、数据 age),
  PM 状态报文锂电池/应急电池卡片
- pPowerManger:新增 iport 本地输入端口配置(UDP bind 延迟到
  OnStartUp 读取配置后执行,保证 iport/ccuhost/ccuport 生效),
  各 mission 文件补充注释
- test:CAN BCU 解码用例按新协议/新偏移更新(实车抓包 + 文档示例值),
  133 项全部通过
- docs:删除旧 BMS 协议(xlsx/20230324docx),归档 04KT38 BCU-MBMS
  CAN 定义、配电控制器通信协议 20260824、配电系统 CAN 通讯协议
- scripts:新增 sync-board-time.sh,223 板卡(RK3588)时间校正
  (本机为基准 + RTT 折半补偿对时,尽力写 RTC,支持 status/--time)
This commit is contained in:
zjk
2026-09-01 12:58:33 +08:00
parent 88bed850ee
commit 5b8f0b1421
27 changed files with 894 additions and 170 deletions
+93 -29
View File
@@ -65,6 +65,9 @@ th{color:var(--dim);font-weight:500;}
.lvl4{background:rgba(248,81,73,.38);color:#ff7b72;border:1px solid #f85149;}
.fc-bad{color:#f85149;font-weight:700;background:rgba(248,81,73,.12);padding:1px 6px;border-radius:4px;}
.fc-ok{color:#3fb950;}
/* 卡片内分组小标题 */
.sub{font-size:11px;color:#79c0ff;margin:10px 0 2px;border-bottom:1px solid #22304a;padding-bottom:2px;}
.sub:first-child{margin-top:0;}
</style>
</head>
<body>
@@ -97,6 +100,7 @@ let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
function row(k, v){ return '<div class="row"><span class="k">'+k+'</span><span class="v">'+v+'</span></div>'; }
function sub(t){ return '<div class="sub">'+t+'</div>'; }
function hex(v){ return '0x' + (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
function hex16(v){ return '0x' + (v & 0xFFFF).toString(16).toUpperCase().padStart(4,'0'); }
function hex32(v){ return '0x' + (v >>> 0).toString(16).toUpperCase().padStart(8,'0'); }
@@ -128,16 +132,32 @@ function statusText(s){
}
function flameText(f){ return {0:'无效',1:'火焰报警',2:'探测器故障'}[f] || ('未知('+hex(f)+')'); }
//------------------ 锂电池 BMS(CAN) 解析 ------------------
// 报文定义见 docs/BMS_协议字段定义.xlsx:
// 0x10010000 总电压/SOC/故障码/状态位
// 0x10010005 最大电流限制/总电流/最高单体电压
// 0x10010006 总电压(校验)
// 故障码高亮:0 显示绿色正常,非 0 显示红色
function bmsFault(c){ return c===0 ? '<span class="fc-ok">'+c+' 正常</span>'
: '<span class="fc-bad">'+c+'</span>'; }
// 数据新鲜度(10s 内视为在线)
function bmsOnline(t){ return (Date.now()/1000 - t) < 10; }
//------------------ 锂电池 BCU 节点(CAN) 解析 ------------------
// 报文定义见 docs/04KT38电池BCU-MBMS通信(CAN)定义.docx:
// 0x10XX0000 累加电压/回路电流/SOC/告警码/自检状态
// 0x10XX0001 单体电压 0x10XX0002 单体温度 0x10XX0003 继电器
// 0x10XX0006 绝缘阻值/端口电压 0x10XX0010 告警位(附录1)
// XX(01h~36h) 为 BCU 节点地址:同一节点的全部报文归入同一张卡片;
// 告警含义由服务器端按附录1 解析为中文列表(alarms 字段)
function pad2(v){ return (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
function relayText(v){ return v===1 ? '<span class="fc-ok">闭合</span>' : '断开'; }
// 告警码(0x10XX0000 BYTE7,协议未附码表,显示原始值)
function bmsFault(c){ return c===0 ? '<span class="fc-ok">'+hex(c)+'</span>'
: '<span class="fc-bad">'+hex(c)+'</span>'; }
// 数据新鲜度:10s 内视为在线。优先用后端算好的 age(距最后收到该节点
// 报文的秒数,不依赖浏览器与板卡时钟同步),无 age 时退回 lastRx 比对本地时钟
function bmsOnline(n){
if(!n) return undefined;
if(typeof n.age === 'number') return n.age < 10;
if(typeof n.lastRx === 'number') return (Date.now()/1000 - n.lastRx) < 10;
return undefined;
}
// 最后更新时间文本(a:距最后收到报文的秒数)
function ageText(a){
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)+' 分前';
}
// 带解析的值:hex 码 + 括号解析
function parsed(v, text){ return hex(v)+' ('+text+')'; }
// 故障码高亮:0 显示绿色正常,非 0 显示红色
@@ -303,7 +323,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);
h+=row('<span><span class="status-dot '+dot(s.bms && s.bms.valid ? bmsOnline(s.bms.lastRx) : undefined)+
// CAN/BMS 行:任一 BCU 节点在线即视为链路在线
const bmsNodes = Array.isArray(s.bms) ? s.bms : [];
const bmsAnyOnline = bmsNodes.some(function(n){ return bmsOnline(n); });
h+=row('<span><span class="status-dot '+dot(bmsNodes.length ? bmsAnyOnline : undefined)+
'"></span>CAN 链路 (pCanBridge)</span>', 'CAN帧:'+s.canFrameCount+' BMS帧:'+s.bmsStatusCount);
h+=row('FC 状态接收计数', s.fcStatusCount);
h+=row('PM 指令接收计数', s.pmControlCount);
@@ -373,22 +396,59 @@ function pmFbCard(d){
return card('设定结果', h);
}
//------------------ 锂电池 BMS(CAN) 卡片 ------------------
//------------------ 锂电池 BCU 节点(CAN) 卡片 ------------------
// 每个节点(CAN ID 0x10XX00YY 中的 XX)一张卡片,
// 同一节点的 0000/0001/0002/0003/0006/0010 各报文归并在卡片内分组展示
function bmsCard(d){
if(!d || !d.valid) return card('锂电池BMS (CAN)',
const nodes = (d && d.length) ? d : [];
if(!nodes.length) return card('锂电池BMS (CAN)',
'<div class="row"><span class="k">数据状态</span><span class="v">等待报文...</span></div>');
let h='';
h+=row('总电压', d.totalVoltage.toFixed(1)+' V');
h+=row('总电流', d.totalCurrent.toFixed(1)+' A');
h+=row('SOC', d.soc.toFixed(1)+' %');
h+=row('最大电流限制', d.maxCurrentLimit.toFixed(1)+' A');
h+=row('最高单体电压', d.maxCellVoltage.toFixed(3)+' V');
h+=row('总电压(校验帧)', d.totalVoltageChk.toFixed(1)+' V');
h+=row('故障码', bmsFault(d.faultCode));
h+=row('状态位', hex(d.statusBits));
h+=row('数据状态', bmsOnline(d.lastRx)
? '<span class="fc-ok">在线</span>' : '<span class="fc-bad">超时</span>');
return card('锂电池BMS (CAN)', h);
let html='';
for(const n of nodes){
const addrTxt = 'BCU节点 '+pad2(n.addr)+'h (#'+n.addr+')';
let h='';
// 概要(0x10XX0000)
h+=sub('概要 0x10'+pad2(n.addr)+'0000');
h+=row('累加电压', n.totalVoltage.toFixed(1)+' V');
h+=row('回路电流', n.current.toFixed(1)+' A');
h+=row('SOC', n.soc.toFixed(1)+' %');
h+=row('告警码', bmsFault(n.alarmCode));
h+=row('自检状态', hex(n.selfCheck));
// 单体电压(0x10XX0001)
h+=sub('单体电压 0x10'+pad2(n.addr)+'0001');
h+=row('最高单体电压', n.maxCellVoltage.toFixed(3)+' V (#'+n.maxCellVoltageNo+')');
h+=row('最低单体电压', n.minCellVoltage.toFixed(3)+' V (#'+n.minCellVoltageNo+')');
h+=row('单体平均电压', n.avgCellVoltage.toFixed(3)+' V');
// 单体温度(0x10XX0002)
h+=sub('单体温度 0x10'+pad2(n.addr)+'0002');
h+=row('最高单体温度', n.maxCellTemp.toFixed(0)+' ℃ (#'+n.maxCellTempNo+')');
h+=row('最低单体温度', n.minCellTemp.toFixed(0)+' ℃ (#'+n.minCellTempNo+')');
h+=row('单体平均温度', n.avgCellTemp.toFixed(0)+' ℃');
// 继电器(0x10XX0003)
h+=sub('继电器 0x10'+pad2(n.addr)+'0003');
h+=row('正极继电器', relayText(n.posRelay));
h+=row('负极继电器', relayText(n.negRelay));
// 绝缘/端口电压(0x10XX0006)
h+=sub('绝缘/端口 0x10'+pad2(n.addr)+'0006');
h+=row('正极绝缘阻抗', n.posInsulationKohm.toFixed(0)+' kΩ');
h+=row('负极绝缘阻抗', n.negInsulationKohm.toFixed(0)+' kΩ');
h+=row('正负极端口电压', n.portVoltage.toFixed(1)+' V');
h+=row('正极继电器外侧电压', n.posRelayOuterVoltage.toFixed(1)+' V');
// 告警(0x10XX0010,附录1 解析)
h+=sub('告警明细 0x10'+pad2(n.addr)+'0010');
if(n.alarms && n.alarms.length){
h+=row('告警(' + n.alarms.length + '条)',
n.alarms.map(function(a){ return '<span class="fc-bad">'+a+'</span>'; }).join('<br>'));
}else{
h+=row('告警', '<span class="fc-ok">无</span>');
}
// 最后更新时间(判断该节点消息是否持续正常发送)
const online = bmsOnline(n);
h+=row('最后更新', (online ? '<span class="fc-ok">' : '<span class="fc-bad">')
+ ageText(n.age || 0) + (online ? ' (在线)' : ' (超时)') + '</span>');
html+=card(addrTxt, h);
}
return html;
}
function pmStatusCard(d){
@@ -482,12 +542,16 @@ function render(snap){
+tankCard(snap.fc)+cabinCard(snap.fc)+'</div>',
pmCmd: pageHeader(0x0001,'PM操控指令','PM→CCU')+'<div class="grid">'+cmdCard(snap.pmCmd)+'</div>',
pmParam: pageHeader(0x0002,'PM参数设定','PM→CCU')+'<div class="grid">'+pmParamCard(snap.pmParam)+'</div>',
// 锂电池 BMS(CAN 总线,经 pCanBridge 透传)
// 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传,按节点分组展示)
bms: '<div class="grid">'+bmsCard(snap.bms)+
'<div class="card"><h2>报文来源 (CAN ID)</h2>'+
row('0x10010000','总电压 / SOC / 故障码 / 状态位')+
row('0x10010005','最大电流限制 / 总电流 / 最高单体电压')+
row('0x10010006','总电压(校验)')+
row('0x10XX0000','累加电压 / 回路电流 / SOC / 告警码 / 自检状态')+
row('0x10XX0001','最高/最低/平均单体电压及编号')+
row('0x10XX0002','最高/最低/平均单体温度及编号')+
row('0x10XX0003','正/负极继电器状态')+
row('0x10XX0006','正/负极绝缘阻抗 / 端口电压 / 继电器外侧电压')+
row('0x10XX0010','告警位(附录1,按位解析显示告警含义)')+
row('XX','BCU 节点地址 01h~36h,同一节点的报文归入同一卡片')+
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'</div></div>',
fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>',
// 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据