Files
H100PowerManger/test/pccu/pccuTest.cpp
T
zjk 5b8f0b1421 锂电池改用 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)
2026-09-01 12:58:33 +08:00

407 lines
17 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.
//============================================================================
// pccuTest:pCCU 协议编解码 / 校验和 / 帧长度 / 链路分发 / 数据库 单元测试
// 无第三方测试库,独立可执行。参照 test/until/testSqlitdb.cpp 的轻量风格。
//============================================================================
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include <iostream>
#include "../../src/pCCU/protocol/FieldCodec.h"
#include "../../src/pCCU/protocol/Frame.h"
#include "../../src/pCCU/protocol/ChecksumPolicy.h"
#include "../../src/pCCU/protocol/MessageRegistry.h"
#include "../../src/pCCU/protocol/FcProtocol.h"
#include "../../src/pCCU/protocol/PmProtocol.h"
#include "../../src/pCCU/protocol/CanBms.h"
#include "../../src/pCCU/comm/LinkManager.h"
#include "../../src/pCCU/comm/FcLinkManager.h"
#include "../../src/pCCU/comm/PmLinkManager.h"
#include "../../src/pCCU/store/DbStore.h"
using namespace ccu;
static int g_fail = 0;
static int g_pass = 0;
#define CHECK(cond) do { \
if (cond) { ++g_pass; } \
else { ++g_fail; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
} while (0)
//--------------------------------------------------------------------------
// 1. 帧长度必须与协议文档一致
static void testFrameLengths() {
FcControlMessage fcm;
FcStatusMessage fsm;
PmControlMessage pcm;
PmParamSetMessage pps;
PmParamSetFbMessage ppf;
PmStatusMessage psm;
CHECK(fcm.totalLength() == 24); // FC 控制指令 24B(1.1.1:域字节数0x0018)
CHECK(fsm.totalLength() == 150); // FC 状态反馈 150B
CHECK(pcm.totalLength() == 45); // PM 操控指令 45B
CHECK(pps.totalLength() == 28); // PM 参数设定 28B
CHECK(ppf.totalLength() == 12); // PM 参数反馈 12B
CHECK(psm.totalLength() == 248); // PM 状态报文 248B
}
//--------------------------------------------------------------------------
// 2. FC 控制指令编解码往返
static void testFcControlRoundTrip() {
FcControlValue v;
v.mode = 2; v.cmd = 2; v.outputPower = 100; v.pitch1 = 35;
v.roll1 = -12;
v.emergencyAllow = 0x03; v.depth = 150;
v.supplyCmd = 1; v.reservedCmd5 = 5; v.reservedCmd6 = 6; v.heartbeat = 7;
FcControlMessage msg;
std::vector<uint8_t> frame = msg.encode(v);
CHECK(frame.size() == 24);
CHECK(frame[0] == 0x40 && frame[1] == 0x40);
CHECK(frame[4] == 0x18 && frame[5] == 0x00); // 域字节数 0x0018 小端
// 字节位置校验(数据域起点=帧头6):姿态数据1 int16 小端,数据2字节预留恒0
CHECK(frame[6 + 3] == 0x23 && frame[6 + 4] == 0x00); // 纵倾数据1 = 35
CHECK(frame[6 + 5] == 0x00 && frame[6 + 6] == 0x00); // 预留(0827协议,原纵倾数据2)
CHECK(frame[6 + 7] == 0xF4 && frame[6 + 8] == 0xFF); // 横倾数据1 = -12 (0xFFF4)
CHECK(frame[6 + 9] == 0x00 && frame[6 + 10] == 0x00); // 预留(0827协议,原横倾数据2)
FcControlValue out;
CHECK(msg.decode(frame, static_cast<void*>(&out)));
CHECK(out.mode == 2);
CHECK(out.cmd == 2);
CHECK(out.outputPower == 100);
CHECK(out.pitch1 == 35);
CHECK(out.roll1 == -12);
CHECK(out.emergencyAllow == 0x03);
CHECK(out.depth == 150);
CHECK(out.supplyCmd == 1);
CHECK(out.reservedCmd5 == 5 && out.reservedCmd6 == 6);
CHECK(out.heartbeat == 7);
}
//--------------------------------------------------------------------------
// 3. FC 状态编解码往返(关键字段抽查)
static void testFcStatusRoundTrip() {
FcStatusValue v;
v.fc_mode = 2; v.fc_status = 6; v.fc_fault_level = 0;
v.fault_level_1 = 0x0102; v.fault_level_2 = 0x0304;
v.fault_level_3 = 0x0506; v.fault_level_4 = 0x0708;
v.total_generation_time = 12345;
v.generation_power = 23456;
v.hydrogen_capacity = 80; v.liquid_oxygen_capacity = 70;
v.palladium_temp = 350; v.buffer_tank_pressure = 200;
v.dcdc1_in_voltage = 12; v.dcdc_out_current = 33;
v.methanol_total_use = 100;
v.cabin_temp1 = 25; v.h2_concentration1 = 500;
v.heartbeat = 9; v.emergency_cmd = 0x01;
v.reserved[0] = 0xAAAA; v.remaining_generation = 0xBBBB;
FcStatusMessage msg;
std::vector<uint8_t> frame = msg.encode(v);
CHECK(frame.size() == 150);
// 字节位置校验(对照 0821 docx):发电功率在字节21-22(offset 14),心跳在字节149(offset 142)
CHECK(frame[6 + 12] == 0); // 故障等级 offset12
CHECK(frame[6 + 14] == 0xA0 && frame[6 + 15] == 0x5B); // 发电功率 23456 -> 0x5BA0 小端
CHECK(frame[6 + 142] == 9); // 心跳 offset142
CHECK(frame[6 + 143] == 0x01);// 应急指令 offset143
FcStatusValue out;
CHECK(msg.decode(frame, static_cast<void*>(&out)));
CHECK(out.fc_mode == 2 && out.fc_status == 6);
CHECK(out.fault_level_1 == 0x0102 && out.fault_level_4 == 0x0708);
CHECK(out.total_generation_time == 12345);
CHECK(out.generation_power == 23456);
CHECK(out.hydrogen_capacity == 80 && out.liquid_oxygen_capacity == 70);
CHECK(out.palladium_temp == 350);
CHECK(out.dcdc1_in_voltage == 12 && out.dcdc_out_current == 33);
CHECK(out.methanol_total_use == 100);
CHECK(out.cabin_temp1 == 25 && out.h2_concentration1 == 500);
CHECK(out.heartbeat == 9 && out.emergency_cmd == 0x01);
CHECK(out.reserved[0] == 0xAAAA && out.remaining_generation == 0xBBBB);
}
//--------------------------------------------------------------------------
// 4. PM 操控指令编解码往返
static void testPmControlRoundTrip() {
PmControlValue v;
v.year = 2026; v.month = 8; v.day = 26; v.hour = 17; v.minute = 30; v.second = 5;
v.millisecond10 = 3;
v.mode = 2; v.cmd = 2; v.outputPower = 120;
v.pitch = 35; v.roll = -12;
v.emergencyAllow = 0x03; v.depth = 200;
v.supplyCmd = 1; v.reservedCmd5 = 5; v.reservedCmd6 = 6;
v.insBatCmd = 0x10; v.dynBatCmd = 0x20; v.dynBatPower = 300;
v.heartbeat = 11; v.hostState = 0xAA;
v.reserved1 = 0x11223344; v.reserved2 = 0x55667788;
PmControlMessage msg;
std::vector<uint8_t> frame = msg.encode(v);
CHECK(frame.size() == 45);
PmControlValue out;
CHECK(msg.decode(frame, static_cast<void*>(&out)));
CHECK(out.year == 2026 && out.month == 8 && out.day == 26);
CHECK(out.hour == 17 && out.minute == 30 && out.second == 5);
CHECK(out.millisecond10 == 3);
CHECK(out.mode == 2 && out.cmd == 2 && out.outputPower == 120);
CHECK(out.pitch == 35 && out.roll == -12);
CHECK(out.depth == 200);
CHECK(out.insBatCmd == 0x10 && out.dynBatCmd == 0x20 && out.dynBatPower == 300);
CHECK(out.heartbeat == 11 && out.hostState == 0xAA);
CHECK(out.reserved1 == 0x11223344 && out.reserved2 == 0x55667788);
}
//--------------------------------------------------------------------------
// 5. PM 状态报文编解码往返(抽查关键字段,覆盖各分组)
static void testPmStatusRoundTrip() {
PmStatusValue v;
v.fc_mode = 2; v.fc_status = 6;
v.fault_level_1 = 1; v.fault_level_2 = 2; v.fault_level_3 = 3; v.fault_level_4 = 4;
v.total_generation_time = 500; v.fc_fault_level = 0;
v.output_power_limit = 30000; v.generation_power = 100;
v.hydrogen_capacity = 80; v.liquid_oxygen_capacity = 70;
v.main_pipe_pressure = 500; v.aux_pipe_pressure = 400;
v.dcdc_out_voltage = 240; v.dcdc_out_current = 100;
v.cabin_temp1 = 2500; v.cabin_temp2 = 2400;
v.h2_concentration1 = 300; v.o2_concentration1 = 200;
v.emergency_battery1_voltage = 24; v.emergency_battery2_voltage = 26;
v.ins_soc = 60; v.dyn_soc = 55;
v.ins_voltage_link = 4800; v.dyn_voltage_link = 4900;
v.ins_current = 120; v.dyn_current = 130;
v.device_online_flag1 = 0x0F; v.device_online_flag2 = 0x07;
v.heartbeat = 42; v.emergency_cmd = 0x01;
v.dyn_alarm_flag[0] = 0xAA; v.ins_alarm_flag[5] = 0x55;
PmStatusMessage msg;
std::vector<uint8_t> frame = msg.encode(v);
CHECK(frame.size() == 248);
PmStatusValue out;
CHECK(msg.decode(frame, static_cast<void*>(&out)));
CHECK(out.fc_mode == 2 && out.fc_status == 6);
CHECK(out.fault_level_1 == 1 && out.fault_level_4 == 4);
CHECK(out.total_generation_time == 500 && out.fc_fault_level == 0);
CHECK(out.output_power_limit == 30000 && out.generation_power == 100);
CHECK(out.hydrogen_capacity == 80 && out.liquid_oxygen_capacity == 70);
CHECK(out.main_pipe_pressure == 500 && out.aux_pipe_pressure == 400);
CHECK(out.dcdc_out_voltage == 240 && out.dcdc_out_current == 100);
CHECK(out.cabin_temp1 == 2500 && out.h2_concentration1 == 300);
CHECK(out.emergency_battery1_voltage == 24 && out.emergency_battery2_voltage == 26);
CHECK(out.ins_soc == 60 && out.dyn_soc == 55);
CHECK(out.ins_voltage_link == 4800 && out.dyn_voltage_link == 4900);
CHECK(out.device_online_flag1 == 0x0F && out.device_online_flag2 == 0x07);
CHECK(out.heartbeat == 42 && out.emergency_cmd == 0x01);
CHECK(out.dyn_alarm_flag[0] == 0xAA && out.ins_alarm_flag[5] == 0x55);
}
//--------------------------------------------------------------------------
// 6. PM 校验和:正确帧通过,篡改字节校验失败
static void testChecksum() {
PmControlValue v;
v.mode = 2; v.cmd = 3; v.heartbeat = 1;
PmControlMessage msg;
std::vector<uint8_t> frame = msg.encode(v);
PmControlValue tmp;
CHECK(msg.decode(frame, static_cast<void*>(&tmp))); // 校验通过
// 篡改数据域中间字节
std::vector<uint8_t> bad = frame;
bad[10] ^= 0xFF; // 数据域某字节
CHECK(!msg.decode(bad, static_cast<void*>(&tmp)));
// 篡改帧头长度
std::vector<uint8_t> badLen = frame;
badLen[4] = 0x01; // length 低字节改小 -> 0x012D != 45
CHECK(!msg.decode(badLen, static_cast<void*>(&tmp)));
}
//--------------------------------------------------------------------------
// 7. 链路分发:FC 链路收到状态帧 -> 回调解码
static void testLinkDispatch() {
// 注册表按链路隔离:FC 与 PM 共用 0x0001/0x0002 不应冲突
MessageRegistry fcReg;
registerFcMessages(fcReg);
MessageRegistry pmReg;
registerPmMessages(pmReg);
CHECK(fcReg.size() == 2);
CHECK(pmReg.size() == 4);
CHECK(fcReg.find(0x0001)->id() == 0x0001);
CHECK(pmReg.find(0x0001)->id() == 0x0001); // id 重叠但隔离
// 用 LinkManager 的 handleIncoming 模拟收帧
FcStatusValue fv;
fv.fc_status = 6; fv.heartbeat = 8;
FcStatusMessage fsMsg;
std::vector<uint8_t> frame = fsMsg.encode(fv);
LinkManager lm("test_fc", 0);
registerFcMessages(lm.registry());
bool got = false;
lm.setOnMessage([&](Message* m, const std::vector<uint8_t>& f) {
if (m && m->id() == 0x0002) {
FcStatusValue out;
if (m->decode(f, static_cast<void*>(&out))) {
got = (out.fc_status == 6 && out.heartbeat == 8);
}
}
});
CHECK(lm.handleIncoming(frame));
CHECK(got);
}
//--------------------------------------------------------------------------
// 8. 数据库落库与查询
static void testDbStore() {
std::string path = "test_pccu.db";
std::remove(path.c_str());
{
DbStore db(path);
CHECK(db.open());
std::vector<uint8_t> frame = {0x40,0x40,0x01,0x00,0x14,0x00, 1,2,3,4,5,6,7,8,9,10,11,12,13,14};
db.onRawFrame(0, 0x0001, "fc", frame, true);
db.onRawFrame(1, 0x0004, "pm", frame, false);
CHECK(db.count() == 2);
auto rows = db.queryRecent("", -1, 0, 10);
CHECK(rows.size() == 2);
CHECK(rows[0].msgId == 0x0004 || rows[0].msgId == 0x0001);
CHECK(rows[0].checksumOk == 0); // 最近一条是 pm 的 false
auto fcRows = db.queryRecent("fc", 0, 0, 10);
CHECK(fcRows.size() == 1);
CHECK(fcRows[0].msgId == 0x0001 && fcRows[0].direction == 0);
db.close();
}
std::remove(path.c_str());
}
//--------------------------------------------------------------------------
// 9. CAN BCU 报文解码(docs/04KT38电池BCU-MBMS通信(CAN)定义.docx,
// 测试向量为实车抓包数据与协议文档示例值)
static void testCanBmsDecode() {
uint8_t addr = 0;
// 0x10010000(节点01,实车抓包):14 da 17 70 00 c9 31 01
// 累加电压 0x14DA=5338 -> 533.8V;回路电流 0x1770=6000 -> 600.0-600=0.0A
// SOC 0x00C9=201 -> 20.1%;告警码 0x31;自检状态 0x01
BcuNodeStatus n1;
const uint8_t d0[8] = {0x14,0xDA,0x17,0x70,0x00,0xC9,0x31,0x01};
CHECK(decodeCanBcuFrame(0x10010000, d0, 8, addr, n1));
CHECK(addr == 1);
CHECK(n1.totalVoltage == 533.8);
CHECK(n1.current == 0.0);
CHECK(n1.soc == 20.1);
CHECK(n1.alarmCode == 0x31);
CHECK(n1.selfCheck == 0x01);
// 回路电流:充电 -25A -> raw 0x1676=5750 -> 575.0-600=-25.0A
const uint8_t d0n[8] = {0x14,0xDA,0x16,0x76,0x00,0xC9,0x31,0x00};
decodeCanBcuFrame(0x10010000, d0n, 8, addr, n1);
CHECK(n1.current == -25.0);
// 0x10010001(节点01)单体电压:0e a8 05 0e a0 03 0e a4
// 最高 0x0EA8=3752 -> 3.752V (#5);最低 0x0EA0=3744 -> 3.744V (#3)
// 平均 0x0EA4=3748 -> 3.748V;部分更新,概要字段保留
const uint8_t dv[8] = {0x0E,0xA8,0x05,0x0E,0xA0,0x03,0x0E,0xA4};
CHECK(decodeCanBcuFrame(0x10010001, dv, 8, addr, n1));
CHECK(n1.maxCellVoltage == 3.752);
CHECK(n1.maxCellVoltageNo == 5);
CHECK(n1.minCellVoltage == 3.744);
CHECK(n1.minCellVoltageNo == 3);
CHECK(n1.avgCellVoltage == 3.748);
CHECK(n1.totalVoltage == 533.8); // 同节点部分更新,前一帧字段不被清除
CHECK(n1.rxMask == ((1u << BCU_FUNC_BASE) | (1u << BCU_FUNC_CELL_V)));
// 0x10010002(节点01)单体温度:3c 01 2d 02 33
// 最高 0x3C=60-40=20℃ (#1);最低 0x2D=45-40=5℃ (#2);平均 0x33=51-40=11℃
// (实测固件平均温度与最高/最低一致均带 -40℃ 偏移,协议文档"偏移0"有误:
// 实车 0x10020002 原始 68/69 减 40 后为 28/29℃,落在最低28~最高30区间内)
const uint8_t dt[8] = {0x3C,0x01,0x2D,0x02,0x33,0x00,0x00,0x00};
CHECK(decodeCanBcuFrame(0x10010002, dt, 8, addr, n1));
CHECK(n1.maxCellTemp == 20.0);
CHECK(n1.maxCellTempNo == 1);
CHECK(n1.minCellTemp == 5.0);
CHECK(n1.minCellTempNo == 2);
CHECK(n1.avgCellTemp == 11.0);
// 0x10010003(节点01)继电器:01 01 -> 正/负极均闭合
const uint8_t dr[8] = {0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00};
CHECK(decodeCanBcuFrame(0x10010003, dr, 8, addr, n1));
CHECK(n1.posRelay == 1 && n1.negRelay == 1);
// 0x10010006(节点01,实车抓包):07 d0 07 d0 14 da 00 00
// 正/负极绝缘 2000kΩ;端口电压 0x14DA -> 533.8V;外侧电压 0.0V
const uint8_t di[8] = {0x07,0xD0,0x07,0xD0,0x14,0xDA,0x00,0x00};
CHECK(decodeCanBcuFrame(0x10010006, di, 8, addr, n1));
CHECK(n1.posInsulationKohm == 2000.0);
CHECK(n1.negInsulationKohm == 2000.0);
CHECK(n1.portVoltage == 533.8);
CHECK(n1.posRelayOuterVoltage == 0.0);
// 0x10010010(节点01)告警位:byte0=0x01 -> BCU绝缘2级;byte1=0x04 -> 烟雾报警1级
const uint8_t da[8] = {0x01,0x04,0x00,0x00,0x00,0x00,0x00,0x00};
CHECK(decodeCanBcuFrame(0x10010010, da, 8, addr, n1));
CHECK(n1.alarmBits[0] == 0x01 && n1.alarmBits[1] == 0x04);
const char* texts[8] = {nullptr};
int cnt = bcuAlarmTexts(n1, texts, 8);
CHECK(cnt == 2);
CHECK(cnt > 0 && std::string(texts[0]) == "BCU绝缘2级");
CHECK(cnt > 1 && std::string(texts[1]) == "BCU烟雾报警1级");
// 无告警
const uint8_t dz[8] = {0,0,0,0,0,0,0,0};
decodeCanBcuFrame(0x10010010, dz, 8, addr, n1);
CHECK(bcuAlarmTexts(n1, texts, 8) == 0);
// 节点分离:0x10020000(节点02)解析到独立状态
BcuNodeStatus n2;
const uint8_t d02[8] = {0x0D,0x40,0x17,0x70,0x01,0x2C,0x00,0x00};
CHECK(decodeCanBcuFrame(0x10020000, d02, 8, addr, n2));
CHECK(addr == 2);
CHECK(n2.totalVoltage == 339.2);
CHECK(n2.soc == 30.0);
CHECK(n1.alarmCode == 0x31); // 节点01 快照不受节点02 帧影响
// 非 BCU 报文 / 预留功能码 / 地址越界 / 下发指令 均不命中
const uint8_t dx[8] = {0,0,0,0,0,0,0,0};
CHECK(!decodeCanBcuFrame(0x10010035, dx, 8, addr, n1)); // 未定义功能码
CHECK(!decodeCanBcuFrame(0x10010004, dx, 8, addr, n1)); // Reserve
CHECK(!decodeCanBcuFrame(0x10010005, dx, 8, addr, n1)); // Reserve
CHECK(!decodeCanBcuFrame(0x10370000, dx, 8, addr, n1)); // 地址 0x37 越界
CHECK(!decodeCanBcuFrame(0x10000000, dx, 8, addr, n1)); // 地址 0 非法
CHECK(!decodeCanBcuFrame(0x100181FF, dx, 8, addr, n1)); // MBMS 下发指令
CHECK(!decodeCanBcuFrame(0x10010000, nullptr, 0, addr, n1));
CHECK(isCanBcuId(0x10010000) && isCanBcuId(0x10010001) &&
isCanBcuId(0x10010002) && isCanBcuId(0x10010003) &&
isCanBcuId(0x10010006) && isCanBcuId(0x10010010));
CHECK(!isCanBcuId(0x10010004) && !isCanBcuId(0x10010005) &&
!isCanBcuId(0x100181FF) && !isCanBcuId(0x10370000));
}
//--------------------------------------------------------------------------
int main() {
testFrameLengths();
testFcControlRoundTrip();
testFcStatusRoundTrip();
testPmControlRoundTrip();
testPmStatusRoundTrip();
testChecksum();
testLinkDispatch();
testDbStore();
testCanBmsDecode();
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
}