锂电池改用 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
+2
View File
@@ -6,4 +6,6 @@ ProcessConfig = pPowerManger
CommsTick = 4 CommsTick = 4
log_level = INFO log_level = INFO
log_file = /path/to/your/logfile.log log_file = /path/to/your/logfile.log
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -20,6 +20,8 @@ ProcessConfig = pPowerManger
// CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000 // CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000
ccuhost = 127.0.0.1 ccuhost = 127.0.0.1
ccuport = 7000 ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
} }
ProcessConfig = pPowerMangerHost ProcessConfig = pPowerMangerHost
+2
View File
@@ -5,4 +5,6 @@ ProcessConfig = pPowerManger
// CCU 地址;不配置则默认 127.0.0.1:7000 // CCU 地址;不配置则默认 127.0.0.1:7000
// ccuhost = 127.0.0.1 // ccuhost = 127.0.0.1
// ccuport = 7000 // ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
} }
+176
View File
@@ -0,0 +1,176 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/sync-board-time.sh
# DESC: 校正 223 板卡(RK3588,默认 192.168.0.223)的系统时间:以
# 本机时间(或 --time 指定时间)为基准,经 SSH 设置板卡系统时
# 钟,并尽力写入硬件时钟 (RTC)。板卡网络隔离无 NTP,用本脚本对时。
#
# 用法:
# ./scripts/sync-board-time.sh # 用本机时间校正板卡
# ./scripts/sync-board-time.sh status # 只查看时间与偏差
# ./scripts/sync-board-time.sh --time "2026-09-01 12:00:00"
# # 用指定时间校正(本地时区)
#
# 参数:
# --host <ip> 目标主机(默认 192.168.0.223)
# --user <user> 登录用户(默认 root)
# --time <str> 手动指定基准时间(date 可解析的格式,按本地时区解释)
# --keep-ntp 不执行 timedatectl set-ntp false
# --no-rtc 不写硬件时钟(跳过 hwclock --systohc)
# -h, --help 显示帮助
#
# 说明:
# - 需要 root 登录与免密 SSH(deploy.sh setup-ssh)。
# - 对时经 SSH 往返时延 (RTT) 折半补偿,精度约 ±0.1s(受网络抖动影响)。
# - 板卡若无可用 RTC,hwclock 失败仅告警;重启后时间可能回跳。
# - 对时会调整系统时钟,运行中的服务日志时间戳会跳变,属正常现象。
#=======================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOST="192.168.0.223"
USER="root"
SET_TIME=""
KEEP_NTP=0
NO_RTC=0
info() { printf '\033[1;36m[time]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[time]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[time]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[time]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
#-------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
--host) [ $# -ge 2 ] || { err "--host 需要参数"; exit 1; }; HOST="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--time) [ $# -ge 2 ] || { err "--time 需要参数"; exit 1; }; SET_TIME="$2"; shift 2 ;;
--keep-ntp) KEEP_NTP=1; shift ;;
--no-rtc) NO_RTC=1; shift ;;
status|help|-h|--help) ACTION="$1"; shift ;;
"") shift ;;
*) err "未知参数: $1(见 help)"; exit 1 ;;
esac
done
[ -n "${ACTION}" ] || ACTION="sync"
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
#-------------------------------------------------------------------
# 读取板卡时间并测 RTT
# 输出: "<epoch>|<板卡可读时间> <t0> <t1>"(t0/t1 为本机采样时刻)
#-------------------------------------------------------------------
read_board_time() {
local t0 t1 out
t0="$(date +%s.%N)"
out="$(${SSH} "${SSH_TARGET}" 'date "+%s.%N|%F %T %Z"' 2>/dev/null)" || {
err "无法免密 SSH 到 ${SSH_TARGET}(先执行: ./scripts/deploy.sh setup-ssh)" >&2
return 1
}
t1="$(date +%s.%N)"
printf '%s %.3f %.3f\n' "${out}" "${t0}" "${t1}"
}
# 从 read_board_time 输出解析: set -- <out> 后取 $1/$2/$3
parse_board_time() {
B_EPOCH="${1%%|*}"
B_DATE="${1#*|}"
T0="$2"
T1="$3"
MID="$(awk -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t0 + (t1-t0)/2}')"
DIFF="$(awk -v b="${B_EPOCH}" -v m="${MID}" 'BEGIN{printf "%+.3f", b - m}')"
}
#-------------------------------------------------------------------
# 命令: status
#-------------------------------------------------------------------
cmd_status() {
local out
out="$(read_board_time)" || return 1
parse_board_time ${out}
local rtt
rtt="$(awk -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t1-t0}')"
printf '本机时间: %s (RTT %.3fs)\n' "$(date -d "@${MID}" '+%F %T')" "${rtt}"
printf '板卡时间: %s\n' "${B_DATE}"
printf '偏差: 板卡比本机快 %s 秒\n' "${DIFF}"
}
#-------------------------------------------------------------------
# 命令: sync(默认)
#-------------------------------------------------------------------
cmd_sync() {
local out target board_reply
# 前置检查:SSH 连通 + 板卡 root 权限
out="$(read_board_time)" || return 1
parse_board_time ${out}
if ! ${SSH} "${SSH_TARGET}" '[ "$(id -u)" = 0 ]' 2>/dev/null; then
err "板卡登录用户需要 root 权限(当前 --user ${USER})"
return 1
fi
ok "SSH 连接正常: ${SSH_TARGET}"
info "当前偏差: 板卡比本机快 ${DIFF} 秒"
# 关闭 NTP 自动同步(板卡网络隔离本就无 NTP 源,防止干扰手动对时)
if [ "${KEEP_NTP}" = "0" ]; then
info "关闭板卡 NTP 自动同步 (timedatectl set-ntp false) ..."
${SSH} "${SSH_TARGET}" "command -v timedatectl >/dev/null 2>&1 && timedatectl set-ntp false || true"
fi
# 计算目标时间:手动指定值直接用;否则取本机时间 + RTT/2 补偿
if [ -n "${SET_TIME}" ]; then
target="$(date -d "${SET_TIME}" '+%s.%N' 2>/dev/null)" || {
err "无法解析时间: ${SET_TIME}"; return 1;
}
info "基准时间(手动): ${SET_TIME}"
else
local t2
t2="$(date +%s.%N)"
target="$(awk -v t2="${t2}" -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t2 + (t1-t0)/2}')"
info "基准时间(本机 + RTT/2 补偿)"
fi
info "设置板卡系统时间 -> $(date -d "@${target}" '+%F %T') ..."
board_reply="$(${SSH} "${SSH_TARGET}" "date -s @${target} 2>&1")" || {
err "date -s 失败: ${board_reply}"
return 1
}
ok "板卡返回: ${board_reply}"
# 写硬件时钟(无 RTC 时仅告警)
if [ "${NO_RTC}" = "0" ]; then
info "同步硬件时钟 (hwclock --systohc) ..."
if ${SSH} "${SSH_TARGET}" "hwclock --systohc" 2>/dev/null; then
ok "RTC 已同步"
else
warn "hwclock 失败(板卡可能无 RTC 或驱动未加载),重启后时间可能回跳"
fi
fi
# 校验
out="$(read_board_time)" || return 1
parse_board_time ${out}
info "校正后偏差: 板卡比本机快 ${DIFF} 秒"
ok "板卡时间: ${B_DATE}"
}
#-------------------------------------------------------------------
# 命令入口
#-------------------------------------------------------------------
case "${ACTION}" in
sync) cmd_sync ;;
status) cmd_status ;;
help|-h|--help) usage ;;
*) usage ;;
esac
+16 -9
View File
@@ -197,20 +197,26 @@ void CCU::handleCanMessage(CMOOSMsg& msg) {
unsigned char* d = msg.GetBinaryData(); unsigned char* d = msg.GetBinaryData();
if (!d) return; if (!d) return;
// 合并语义:取当前快照 -> 解码部分更新 -> 写回 // 合并语义:先取该节点当前快照 -> 解码部分更新 -> 写回
BmsStatusValue v = m_sysData->bmsStatus(); // (每个功能码报文只更新自己的字段,不能整体覆盖)
if (decodeCanBmsFrame(canId, d, static_cast<int>(n), v)) { uint8_t addr = bcuAddrOf(canId);
v.valid = true; if (!addr) return;
v.lastRxTime = MOOSTime(false); BcuNodeStatus node = m_sysData->bcuNode(addr);
m_sysData->updateBmsStatus(v); if (decodeCanBcuFrame(canId, d, static_cast<int>(n), addr, node)) {
node.valid = true;
node.lastRxTime = MOOSTime(false);
m_sysData->updateBcuNode(addr, node);
// 锂电池解析数据落库(bcu_node 表,每帧一行,含节点最新合成状态)
if (m_db) m_db->insertBcuNode(addr, bcuFuncOf(canId), node);
// 节流日志:1s 一条,便于联调观察 // 节流日志:1s 一条,便于联调观察
double now = MOOSTime(); double now = MOOSTime();
if (now - m_lastBmsLog >= 1.0) { if (now - m_lastBmsLog >= 1.0) {
m_lastBmsLog = now; m_lastBmsLog = now;
LOG_F(INFO, "[BMS/CAN] u=%.1fV i=%.1fA soc=%.1f%% fault=%d status=%d cellMax=%.3fV", LOG_F(INFO, "[BMS/CAN] 节点%d u=%.1fV i=%.1fA soc=%.1f%% alarm=%02X self=%d vMax=%.3fV",
v.totalVoltage, v.totalCurrent, v.soc, addr, node.totalVoltage, node.current, node.soc,
v.faultCode, v.statusBits, v.maxCellVoltage); node.alarmCode, node.selfCheck, node.maxCellVoltage);
} }
} }
} }
@@ -479,6 +485,7 @@ bool CCU::buildReport() {
} }
if (m_db) { if (m_db) {
m_msgs << "数据库记录数:" << m_db->count() << "\n"; m_msgs << "数据库记录数:" << m_db->count() << "\n";
m_msgs << "数据库BMS记录数:" << m_db->countBcu() << "\n";
} }
return true; return true;
} }
+1 -1
View File
@@ -23,7 +23,7 @@ namespace ccu {
// 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文 // 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文
// (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照 // (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照
// (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用) // (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用)
// 收发全部帧落库 SQLite;网页周期推送 JSON 快照。 // 收发全部帧落库 SQLite(锂电池 BMS 解析数据另存 bcu_node 表);网页周期推送 JSON 快照。
//============================================================================ //============================================================================
class CCU : public AppCastingMOOSApp { class CCU : public AppCastingMOOSApp {
+3 -3
View File
@@ -25,7 +25,7 @@ void CoordNormalState::react(TickEvent const &e) {
// //
// 读取最新状态: // 读取最新状态:
// sys()->fcStatus() 燃料电池状态 // sys()->fcStatus() 燃料电池状态
// sys()->bmsStatus() 锂电池 BMS 状态(总电压/SOC/电流/故障码,CAN) // sys()->bcuNode(addr) 锂电池 BCU 节点状态(电压/SOC/电流/告警,CAN)
// sys()->pmControl() 主机最新操控指令 // sys()->pmControl() 主机最新操控指令
// //
// 下发操作: // 下发操作:
@@ -61,7 +61,7 @@ void CoordBusyState::exit() {
void CoordBusyState::react(TickEvent const &e) { void CoordBusyState::react(TickEvent const &e) {
// TODO(策略填写):操作进行中的周期监视(进度/超时统计、是否需要中止)。 // TODO(策略填写):操作进行中的周期监视(进度/超时统计、是否需要中止)。
// 状态可通过 sys()->bmsStatus() / sys()->fcStatus() 获取。 // 状态可通过 sys()->bcuNode(addr) / sys()->fcStatus() 获取。
(void)e; (void)e;
} }
@@ -88,7 +88,7 @@ void CoordFaultState::exit() {
void CoordFaultState::react(TickEvent const &e) { void CoordFaultState::react(TickEvent const &e) {
// TODO(策略填写):故障处理与恢复判断(周期检查故障是否消除, // TODO(策略填写):故障处理与恢复判断(周期检查故障是否消除,
// 消除后 transit<CoordNormalState>() 回正常运行态)。 // 消除后 transit<CoordNormalState>() 回正常运行态)。
// 可用:sys()->fcStatus().fc_fault_level、sys()->bmsStatus().faultCode 等。 // 可用:sys()->fcStatus().fc_fault_level、sys()->bcuNode(addr).alarmBits 等。
(void)e; (void)e;
} }
+3 -3
View File
@@ -67,12 +67,12 @@ struct PmControlEvent : tinyfsm::Event {
}; };
// 协调操作步骤完成/超时:由协调动作引擎产生(预留)。 // 协调操作步骤完成/超时:由协调动作引擎产生(预留)。
// status 携带完成时刻的 BMS 状态快照(SOC/电压/电流/故障码,可判故障) // status 携带完成时刻的 BCU 节点状态快照(SOC/电压/电流/告警,可判故障)
struct StepDoneEvent : tinyfsm::Event { struct StepDoneEvent : tinyfsm::Event {
bool confirmed; // true=确认完成;false=超时未确认 bool confirmed; // true=确认完成;false=超时未确认
BmsStatusValue status; // 完成时刻 BMS 状态快照 BcuNodeStatus status; // 完成时刻 BCU 节点状态快照
StepDoneEvent() : confirmed(false) {} StepDoneEvent() : confirmed(false) {}
StepDoneEvent(bool ok, const BmsStatusValue& s) : confirmed(ok), status(s) {} StepDoneEvent(bool ok, const BcuNodeStatus& s) : confirmed(ok), status(s) {}
}; };
//---- 状态机 --------------------------------------------------------------- //---- 状态机 ---------------------------------------------------------------
+1 -1
View File
@@ -33,7 +33,7 @@ bool PowerCoordinator::sendFcControl(const FcControlValue& fcCmd) {
// //
// 可用输入(通过 m_sys 读取最新状态): // 可用输入(通过 m_sys 读取最新状态):
// - m_sys->fcStatus() : 燃料电池最新状态 // - m_sys->fcStatus() : 燃料电池最新状态
// - m_sys->bmsStatus() : 锂电池 BMS 最新状态(总电压/SOC/电流/故障码, // - m_sys->bcuNode(addr) : 锂电池 BCU 节点最新状态(电压/SOC/电流/告警,
// CAN 经 pCanBridge 透传,protocol/CanBms.h) // CAN 经 pCanBridge 透传,protocol/CanBms.h)
// - m_sys->pmControl() : 控制主机最新操控指令 // - m_sys->pmControl() : 控制主机最新操控指令
// //
+69 -13
View File
@@ -1,6 +1,7 @@
#include "SnapshotBuilder.h" #include "SnapshotBuilder.h"
#include "../store/DbStore.h" #include "../store/DbStore.h"
#include "../protocol/CanBms.h" #include "../protocol/CanBms.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "json/json.h" #include "json/json.h"
#include <sstream> #include <sstream>
#include <cstdio> #include <cstdio>
@@ -209,19 +210,72 @@ JsonVal pmFbJson(const PmParamSetFbValue& v) {
return j; return j;
} }
// 锂电池 BMS 状态(CAN,docs/BMS_协议字段定义.xlsx)-> JSON // 锂电池 BCU 节点(CAN,docs/04KT38电池BCU-MBMS通信(CAN)定义.docx)-> JSON
JsonVal bmsStatusJson(const BmsStatusValue& v) { // 同一节点(0x10XX00YY 中 XX)的全部报文字段归并在一个对象内,
// 告警位由附录1 表解析出中文含义放入 alarms 数组。
// nowSec 为当前 MOOSTime,用于计算节点数据最后更新距今的秒数 age。
JsonVal bcuNodeJson(uint8_t addr, const BcuNodeStatus& v, double nowSec) {
JsonVal j(Json::objectValue); JsonVal j(Json::objectValue);
j["totalVoltage"] = JsonVal(v.totalVoltage); // V j["addr"] = JsonVal(addr);
j["soc"] = JsonVal(v.soc); // %
j["faultCode"] = JsonVal(v.faultCode); // 故障码
j["statusBits"] = JsonVal(v.statusBits); // 状态位
j["maxCurrentLimit"] = JsonVal(v.maxCurrentLimit); // A
j["totalCurrent"] = JsonVal(v.totalCurrent); // A(放电为正)
j["maxCellVoltage"] = JsonVal(v.maxCellVoltage); // V
j["totalVoltageChk"] = JsonVal(v.totalVoltageChk); // V(校验)
j["valid"] = JsonVal(v.valid); j["valid"] = JsonVal(v.valid);
j["lastRx"] = JsonVal(v.lastRxTime); j["lastRx"] = JsonVal(v.lastRxTime);
j["age"] = JsonVal(nowSec - v.lastRxTime); // 距最后收到该节点报文的秒数
j["rx"] = JsonVal(static_cast<Json::UInt>(v.rxMask));
// 0x10XX0000:电压 / 电流 / SOC / 告警码 / 自检
j["totalVoltage"] = JsonVal(v.totalVoltage);
j["current"] = JsonVal(v.current);
j["soc"] = JsonVal(v.soc);
j["alarmCode"] = JsonVal(v.alarmCode);
j["selfCheck"] = JsonVal(v.selfCheck);
// 0x10XX0001:单体电压
j["maxCellVoltage"] = JsonVal(v.maxCellVoltage);
j["maxCellVoltageNo"] = JsonVal(v.maxCellVoltageNo);
j["minCellVoltage"] = JsonVal(v.minCellVoltage);
j["minCellVoltageNo"] = JsonVal(v.minCellVoltageNo);
j["avgCellVoltage"] = JsonVal(v.avgCellVoltage);
// 0x10XX0002:单体温度
j["maxCellTemp"] = JsonVal(v.maxCellTemp);
j["maxCellTempNo"] = JsonVal(v.maxCellTempNo);
j["minCellTemp"] = JsonVal(v.minCellTemp);
j["minCellTempNo"] = JsonVal(v.minCellTempNo);
j["avgCellTemp"] = JsonVal(v.avgCellTemp);
// 0x10XX0003:继电器
j["posRelay"] = JsonVal(v.posRelay);
j["negRelay"] = JsonVal(v.negRelay);
// 0x10XX0006:绝缘 / 端口电压
j["posInsulationKohm"] = JsonVal(v.posInsulationKohm);
j["negInsulationKohm"] = JsonVal(v.negInsulationKohm);
j["portVoltage"] = JsonVal(v.portVoltage);
j["posRelayOuterVoltage"] = JsonVal(v.posRelayOuterVoltage);
// 0x10XX0010:告警位(原始 hex + 附录1 解释)
JsonVal bits(Json::arrayValue);
for (int i = 0; i < 6; ++i)
bits.append(JsonVal(static_cast<Json::UInt>(v.alarmBits[i])));
j["alarmBits"] = bits;
JsonVal alarms(Json::arrayValue);
const char* texts[32];
int n = bcuAlarmTexts(v, texts, 32);
for (int i = 0; i < n; ++i)
alarms.append(JsonVal(texts[i]));
j["alarms"] = alarms;
j["alarmCount"] = JsonVal(n);
return j;
}
// 全部 BCU 节点 -> JSON(只输出收到过报文的节点,按地址升序)
JsonVal bcuNodesJson(const BcuNodeStatus nodes[kBcuNodeCount], double nowSec) {
JsonVal j(Json::arrayValue);
for (int i = 0; i < kBcuNodeCount; ++i) {
if (nodes[i].valid)
j.append(bcuNodeJson(static_cast<uint8_t>(i + kBcuAddrMin), nodes[i], nowSec));
}
return j; return j;
} }
@@ -353,8 +407,10 @@ std::string SnapshotBuilder::build() const {
root["pmParam"] = pmParamJson(m_sys->pmParamSet()); root["pmParam"] = pmParamJson(m_sys->pmParamSet());
root["pmFb"] = pmFbJson(m_sys->pmParamFb()); root["pmFb"] = pmFbJson(m_sys->pmParamFb());
root["pmStatus"] = pmStatusJson(m_sys->pmStatus()); root["pmStatus"] = pmStatusJson(m_sys->pmStatus());
// 锂电池 BMS(CAN,经 pCanBridge 透传) // 锂电池 BCU 节点(CAN,经 pCanBridge 透传,按节点分组)
root["bms"] = bmsStatusJson(m_sys->bmsStatus()); BcuNodeStatus nodes[kBcuNodeCount];
m_sys->bcuNodes(nodes);
root["bms"] = bcuNodesJson(nodes, MOOSTime(false));
root["fcStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcStatusCount())); root["fcStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcStatusCount()));
root["pmControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmControlCount())); root["pmControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmControlCount()));
root["fcControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcControlCount())); root["fcControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcControlCount()));
@@ -368,7 +424,7 @@ std::string SnapshotBuilder::build() const {
root["pmParam"] = JsonVal(Json::objectValue); root["pmParam"] = JsonVal(Json::objectValue);
root["pmFb"] = JsonVal(Json::objectValue); root["pmFb"] = JsonVal(Json::objectValue);
root["pmStatus"] = JsonVal(Json::objectValue); root["pmStatus"] = JsonVal(Json::objectValue);
root["bms"] = JsonVal(Json::objectValue); root["bms"] = JsonVal(Json::arrayValue);
} }
JsonVal links(Json::objectValue); JsonVal links(Json::objectValue);
+18 -8
View File
@@ -13,7 +13,7 @@ namespace ccu {
// SystemData:跨线程共享的最新状态快照。 // SystemData:跨线程共享的最新状态快照。
// //
// - 接收线程(FC 链路)写 FcStatus 快照 // - 接收线程(FC 链路)写 FcStatus 快照
// - MOOS 主线程(OnNewMail)写 BMS 快照(CAN 经 pCanBridge 透传) // - MOOS 主线程(OnNewMail)写 BCU 节点快照(CAN 经 pCanBridge 透传)
// - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送 // - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送
// - 网页线程读快照展示 // - 网页线程读快照展示
// 通过互斥锁保护读写。 // 通过互斥锁保护读写。
@@ -101,17 +101,27 @@ public:
return m_pmParamFb; return m_pmParamFb;
} }
//-------- 锂电池 BMS(CAN 总线,经 pCanBridge 透传) -------- //-------- 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传) --------
// 更新最新 BMS 状态(每帧部分更新,由调用方先取出快照再合并) // 更新单个 BCU 节点状态(每帧部分更新,由调用方先取出快照再合并)
void updateBmsStatus(const BmsStatusValue& v) { void updateBcuNode(uint8_t addr, const BcuNodeStatus& v) {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
m_bmsStatus = v; if (addr >= kBcuAddrMin && addr <= kBcuAddrMax) {
m_bcuNodes[addr - kBcuAddrMin] = v;
m_bmsStatusCount++; m_bmsStatusCount++;
} }
BmsStatusValue bmsStatus() const { }
// 读取单个 BCU 节点快照(地址非法返回 valid=false 的空快照)
BcuNodeStatus bcuNode(uint8_t addr) const {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
return m_bmsStatus; if (addr >= kBcuAddrMin && addr <= kBcuAddrMax)
return m_bcuNodes[addr - kBcuAddrMin];
return BcuNodeStatus();
}
// 读取全部节点快照(数组下标 = 地址-1)
void bcuNodes(BcuNodeStatus out[kBcuNodeCount]) const {
std::lock_guard<std::mutex> lock(m_mutex);
for (int i = 0; i < kBcuNodeCount; ++i) out[i] = m_bcuNodes[i];
} }
unsigned long bmsStatusCount() const { unsigned long bmsStatusCount() const {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
@@ -136,7 +146,7 @@ private:
FcControlValue m_fcControl; FcControlValue m_fcControl;
PmStatusValue m_pmStatus; PmStatusValue m_pmStatus;
PmParamSetFbValue m_pmParamFb; PmParamSetFbValue m_pmParamFb;
BmsStatusValue m_bmsStatus; BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1
unsigned long m_fcStatusCount = 0; unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0; unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0; unsigned long m_fcControlCount = 0;
+124 -22
View File
@@ -14,41 +14,143 @@ inline int16_t rdS16BE(const uint8_t* p) {
return static_cast<int16_t>(rdU16BE(p)); return static_cast<int16_t>(rdU16BE(p));
} }
// 有符号 8 位(温度)
inline int8_t rdS8(uint8_t b) { return static_cast<int8_t>(b); }
// 设置功能码已收位
inline void markRx(BcuNodeStatus& n, uint8_t func) {
n.rxMask |= (1u << (func & 31));
}
} // namespace } // namespace
bool decodeCanBmsFrame(uint32_t canId, const uint8_t* data, int dlc, BmsStatusValue& v) { //============================================================================
// 附录1:0x10XX0010 告警位定义(04KT38电池BCU-MBMS通信(CAN)定义.docx)
//============================================================================
const BcuAlarmDesc kBcuAlarmTable[] = {
// byte0:2 级告警
{0, 0, "BCU绝缘2级"},
{0, 1, "BCU最高温度过高2级"},
{0, 2, "BCU最低温度过低2级"},
{0, 3, "BCU单体电压不均衡2级"},
{0, 4, "BCU单体温度不均衡2级"},
{0, 5, "BCU放电过流2级"},
{0, 6, "BCU单体电压过高2级"},
{0, 7, "BCU单体电压过低2级"},
// byte1
{1, 0, "BCU总电压过高2级"},
{1, 1, "BCU总电压过低2级"},
{1, 2, "BCU烟雾报警1级"},
{1, 3, "烟雾报警器不在线"},
{1, 4, "烟雾报警器上报自身故障"},
{1, 5, "BCU单包绝缘3级"},
{1, 6, "BCU最高温度过高3级"},
{1, 7, "BCU最低温度过低3级"},
// byte2
{2, 0, "单体电压不均衡3级"},
{2, 1, "单体温度不均衡3级"},
{2, 2, "单包放电过流3级"},
{2, 3, "BCU单体电压过高3级"},
{2, 4, "BCU单体电压过低3级"},
{2, 5, "BCU总电压过高3级"},
{2, 6, "BCU总电压过低3级"},
{2, 7, "BCU单体温度传感器检测故障"},
// byte3
{3, 0, "BCU正极继电器失效"},
{3, 1, "BCU负极继电器失效"},
{3, 3, "单包BMS内部通讯故障"},
{3, 4, "BCU电流传感器故障"},
{3, 5, "BCU电压传感器故障"},
{3, 7, "BCU高压回路连接异常"},
// byte4
{4, 1, "烟雾报警2级"},
{4, 4, "烟雾报警3级"},
{4, 6, "烟雾报警4级"},
{4, 7, "回馈电流过大2级"},
// byte5
{5, 0, "回馈电流过大3级"},
{5, 1, "充电电流过大3级"},
{0xFF, 0, nullptr}, // 表尾
};
int bcuAlarmTexts(const BcuNodeStatus& node, const char* texts[], int cap) {
if (!texts || cap <= 0) return 0;
int count = 0;
for (int i = 0; kBcuAlarmTable[i].text; ++i) {
const BcuAlarmDesc& d = kBcuAlarmTable[i];
if (d.byteIdx < sizeof(node.alarmBits) &&
(node.alarmBits[d.byteIdx] >> d.bitIdx) & 0x01) {
if (count >= cap) break;
texts[count++] = d.text;
}
}
return count;
}
bool decodeCanBcuFrame(uint32_t canId, const uint8_t* data, int dlc,
uint8_t& nodeAddr, BcuNodeStatus& node) {
if (!data || dlc <= 0) return false; if (!data || dlc <= 0) return false;
uint8_t addr = bcuAddrOf(canId);
if (!addr || !isCanBcuId(canId)) return false;
nodeAddr = addr;
// 缩放用除法(而非乘 0.1/0.001),保证结果与十进制字面量严格一致。 // 缩放用除法(而非乘 0.1/0.001),保证结果与十进制字面量严格一致。
switch (canId) { switch (bcuFuncOf(canId)) {
case CAN_BMS_ID_STATUS: // 0x10010000:总电压 / SOC / 故障码 / 状态位 case BCU_FUNC_BASE: // 0x10XX0000:电压 / 电流 / SOC / 告警码 / 自检
if (dlc >= 8) { if (dlc >= 8) {
v.totalVoltage = rdU16BE(data + 0) / 10.0; // 0.1V node.totalVoltage = rdU16BE(data + 0) / 10.0; // 0.1V
v.soc = rdU16BE(data + 4) / 10.0; // 0.1% node.current = rdS16BE(data + 2) / 10.0 - 600.0; // 0.1A,偏移-600A
v.faultCode = data[6]; node.soc = rdU16BE(data + 4) / 10.0; // 0.1%
v.statusBits = data[7]; node.alarmCode = data[6];
node.selfCheck = data[7];
markRx(node, BCU_FUNC_BASE);
} }
break; break;
case CAN_BMS_ID_CURRENT: case BCU_FUNC_CELL_V: // 0x10XX0001:最高/最低/平均单体电压
// 0x10010005:最大电流限制 / 总电流 / 最高单体电压 if (dlc >= 2) node.maxCellVoltage = rdU16BE(data + 0) / 1000.0; // 1mV
// 注:xlsx 表中"总电流"标注为 3 字节(s24),但与其自身示例 if (dlc >= 3) node.maxCellVoltageNo = data[2];
// (总电流 00 00 => 0.0A、最高单体电压 0x0EA8 => 3.752V, if (dlc >= 5) node.minCellVoltage = rdU16BE(data + 3) / 1000.0; // 1mV
// 对应实车帧 07 d0 | 00 00 | 0e a8 | 00 00)矛盾——s24@2 会与 if (dlc >= 6) node.minCellVoltageNo = data[5];
// 单体电压@4 重叠。按示例与实测采用 2 字节 s16(负电流示例 if (dlc >= 8) node.avgCellVoltage = rdU16BE(data + 6) / 1000.0; // 1mV
// 0xFF06 => -25.0A 亦为 2 字节补码)。 markRx(node, BCU_FUNC_CELL_V);
if (dlc >= 2) v.maxCurrentLimit = rdU16BE(data + 0) / 10.0; // 0.1A
if (dlc >= 4) v.totalCurrent = rdS16BE(data + 2) / 10.0; // 0.1A,放电为正
if (dlc >= 6) v.maxCellVoltage = rdU16BE(data + 4) / 1000.0; // 0.001V
break; break;
case CAN_BMS_ID_VOLT_CHK: // 0x10010006:总电压(校验) case BCU_FUNC_CELL_T: // 0x10XX0002:最高/最低/平均单体温度
if (dlc >= 6) { if (dlc >= 1) node.maxCellTemp = rdS8(data[0]) - 40.0; // 偏移-40℃
v.totalVoltageChk = rdU16BE(data + 4) / 10.0; // 0.1V if (dlc >= 2) node.maxCellTempNo = data[1];
} if (dlc >= 3) node.minCellTemp = rdS8(data[2]) - 40.0; // 偏移-40℃
if (dlc >= 4) node.minCellTempNo = data[3];
if (dlc >= 5) node.avgCellTemp = rdS8(data[4]) - 40.0; // 偏移-40℃(文档写偏移0,
// 但实测固件与最高/最低一致均带-40偏移,见0x10020002:
// 原始68/69减40后为28/29℃,落在最低28~最高30区间内)
markRx(node, BCU_FUNC_CELL_T);
break; break;
default: case BCU_FUNC_RELAY: // 0x10XX0003:正/负极继电器状态
if (dlc >= 1) node.posRelay = data[0];
if (dlc >= 2) node.negRelay = data[1];
markRx(node, BCU_FUNC_RELAY);
break;
case BCU_FUNC_ISU: // 0x10XX0006:绝缘阻值 / 端口电压
if (dlc >= 2) node.posInsulationKohm = rdU16BE(data + 0); // 1kΩ
if (dlc >= 4) node.negInsulationKohm = rdU16BE(data + 2); // 1kΩ
if (dlc >= 6) node.portVoltage = rdU16BE(data + 4) / 10.0; // 0.1V
if (dlc >= 8) node.posRelayOuterVoltage = rdU16BE(data + 6) / 10.0; // 0.1V
markRx(node, BCU_FUNC_ISU);
break;
case BCU_FUNC_ALARM: // 0x10XX0010:告警位(附录1)
for (int i = 0; i < 6 && i < dlc; ++i) node.alarmBits[i] = data[i];
if (dlc >= 7) node.alarmExtraByte6 = data[6];
if (dlc >= 8) node.alarmExtraByte7 = data[7];
markRx(node, BCU_FUNC_ALARM);
break;
default: // Reserve(0x10XX0004/0005 等)不解析
return false; return false;
} }
return true; return true;
+123 -40
View File
@@ -6,58 +6,141 @@
namespace ccu { namespace ccu {
//============================================================================ //============================================================================
// CAN BMS 协议(锂电池 BMS 报文,经 USBCAN-8E-U/CANET 上 CAN 总线) // 锂电池 BCU-MBMS CAN 协议(docs/04KT38电池BCU-MBMS通信(CAN)定义.docx)
// 依据:docs/BMS_协议字段定义.xlsx
// //
// 数据来源:pCanBridge 透传的 MOOS 消息 // 数据来源:pCanBridge 透传的 MOOS 消息
// 变量名 CAN_0x%08X = CAN ID(扩展帧),m_sVal = 二进制数据域, // 变量名 CAN_0x%08X = CAN ID(扩展帧),m_sVal = 二进制数据域,
// m_dfVal2 = 原始帧信息字节。本模块只负责按报文 ID 解析数据域字段。 // m_dfVal2 = 原始帧信息字节。本模块只负责按报文 ID 解析数据域字段。
// //
// 报文(均为扩展帧,字段大端,起始字节相对 8 字节数据域): // 【节点地址】一个 MBMS 下最多挂 54 个 BCU,CAN ID 中间的 XX(01h~36h)
// 0x10010000 总电压 u16@0 ÷10 V // 为 BCU 节点地址编号,其余位固定:
// 0x10XX00YY BCU 主动上报消息(YY=功能码 00/01/02/03/06/10)
// 0x10XX81FF MBMS 下发指令(继电器控制,pCCU 只收不发,不解析)
//
// 【上报报文】均为扩展帧,字段大端,BYTE1~8 对应数据域 byte0~7:
// 0x10XX0000 (200ms) 累加电压 u16@0 ÷10 V
// 回路电流 s16@2 ÷10-600 A(偏移 -600A,放电为正)
// SOC u16@4 ÷10 % // SOC u16@4 ÷10 %
// 故障码 u8 @6 ×1 // 告警码 u8 @6(循环上报,协议未附码表,原样显示)
// 状态位 u8 @7 ×1 // 自检状态 u8 @7
// 0x10010005 最大电流限制 u16@0 ÷10 A // 0x10XX0001 (200ms) 最高单体电压 u16@0 mV,编号 u8@2
// 总电流 s16@2 ÷10 A(补码,放电为正;xlsx 表标注 // 最低单体电压 u16@3 mV,编号 u8@5
// 3 字节与其示例/实测冲突,按 2 字节实现, // 单体平均电压 u16@6 mV
// 详见 CanBms.cpp 注释) // 0x10XX0002 (1000ms) 最高单体温度 s8@0(偏移 -40℃),编号 u8@1
// 最高单体电压 u16@4 ÷1000 V // 最低单体温度 s8@2(偏移 -40℃),编号 u8@3
// 0x10010006 总电压(校验) u16@4 ÷10 V // 单体平均温度 s8@4(偏移 -40℃,协议文档写"偏移0"
// 有误:实测原始值减 40 后才落在最低~最高区间内)
// 0x10XX0003 (200ms) 正极继电器状态 u8@0(1 闭合 / 0 断开)
// 负极继电器状态 u8@1(1 闭合 / 0 断开)
// 0x10XX0006 (200ms) 正极绝缘阻抗 u16@0 kΩ
// 负极绝缘阻抗 u16@2 kΩ
// 正负极端口电压 u16@4 ÷10 V
// 正极继电器外侧电压 u16@6 ÷10 V
// 0x10XX0010 (200ms) byte0~5 告警位(bit 定义见附录1,
// kBcuAlarmTable,可经 bcuAlarmTexts() 取告警描述)
// 0x10XX0004/0005 Reserve(调试预留,不解析)
//============================================================================ //============================================================================
// BMS 报文 ID(扩展帧 CAN ID) // BCU 节点地址范围:01h~36h(1~54)
enum CanBmsId : uint32_t { static const int kBcuAddrMin = 0x01;
CAN_BMS_ID_STATUS = 0x10010000, // 总电压 / SOC / 故障码 / 状态位 static const int kBcuAddrMax = 0x36;
CAN_BMS_ID_CURRENT = 0x10010005, // 最大电流限制 / 总电流 / 最高单体电压 static const int kBcuNodeCount = kBcuAddrMax - kBcuAddrMin + 1;
CAN_BMS_ID_VOLT_CHK = 0x10010006, // 总电压(校验)
// BCU 上报报文功能码(CAN ID 低字节 YY)
enum BcuFunc : uint8_t {
BCU_FUNC_BASE = 0x00, // 0x10XX0000 电压 / 电流 / SOC / 告警码 / 自检
BCU_FUNC_CELL_V = 0x01, // 0x10XX0001 单体电压
BCU_FUNC_CELL_T = 0x02, // 0x10XX0002 单体温度
BCU_FUNC_RELAY = 0x03, // 0x10XX0003 继电器状态
BCU_FUNC_ISU = 0x06, // 0x10XX0006 绝缘阻值 / 端口电压
BCU_FUNC_ALARM = 0x10, // 0x10XX0010 告警位(附录1)
}; };
// BMS 最新状态(各报文字段按 ID 部分更新,最终合成完整快照) // 从 CAN ID 提取 BCU 节点地址(0x10XX00YY -> XX);非 BCU 报文格式返回 0
struct BmsStatusValue { inline uint8_t bcuAddrOf(uint32_t canId) {
double totalVoltage = 0; // 总电压 V(0x10010000) if ((canId & 0xFF00FF00u) != 0x10000000u) return 0;
double soc = 0; // SOC %(0x10010000) uint8_t addr = static_cast<uint8_t>((canId >> 16) & 0xFF);
int faultCode = 0; // 故障码(0x10010000 @6) return (addr >= kBcuAddrMin && addr <= kBcuAddrMax) ? addr : 0;
int statusBits = 0; // 状态位(0x10010000 @7)
double maxCurrentLimit = 0; // 最大电流限制 A(0x10010005)
double totalCurrent = 0; // 总电流 A,放电为正(0x10010005,s24 补码)
double maxCellVoltage = 0; // 最高单体电压 V(0x10010005)
double totalVoltageChk = 0; // 总电压校验 V(0x10010006)
double lastRxTime = 0; // 最近收到任一 BMS 报文的时刻(MOOSTime)
bool valid = false; // 是否收到过 BMS 报文
};
// 解析一帧 CAN 数据域;命中 BMS 报文 ID 返回 true 并把字段合并进 v
// (部分更新语义:每个 ID 只更新自己的字段)。
// data 为数据域首指针(有效长度 dlc 字节),data/dlc 非法或非 BMS ID 返回 false。
bool decodeCanBmsFrame(uint32_t canId, const uint8_t* data, int dlc, BmsStatusValue& v);
// 是否为 BMS 报文 ID
inline bool isCanBmsId(uint32_t canId) {
return canId == CAN_BMS_ID_STATUS || canId == CAN_BMS_ID_CURRENT ||
canId == CAN_BMS_ID_VOLT_CHK;
} }
// 从 CAN ID 提取功能码(0x10XX00YY -> YY)
inline uint8_t bcuFuncOf(uint32_t canId) {
return static_cast<uint8_t>(canId & 0xFF);
}
// 是否为 BCU 主动上报报文 ID(0x10XX00YY,YY 为已定义功能码)
inline bool isCanBcuId(uint32_t canId) {
uint8_t addr = bcuAddrOf(canId);
if (!addr) return false;
switch (bcuFuncOf(canId)) {
case BCU_FUNC_BASE:
case BCU_FUNC_CELL_V:
case BCU_FUNC_CELL_T:
case BCU_FUNC_RELAY:
case BCU_FUNC_ISU:
case BCU_FUNC_ALARM:
return true;
default:
return false;
}
}
// 单个 BCU 节点最新状态(各报文字段按功能码部分更新,最终合成完整快照)
struct BcuNodeStatus {
// 0x10XX0000 基本信息
double totalVoltage = 0; // 累加电压 V
double current = 0; // 回路电流 A(放电为正,偏移 -600A 已折算)
double soc = 0; // SOC %
int alarmCode = 0; // 告警码(原始值,协议未附码表)
int selfCheck = 0; // 自检状态(原始值)
// 0x10XX0001 单体电压
double maxCellVoltage = 0; // 最高单体电压 V
int maxCellVoltageNo = 0; // 最高单体电压编号
double minCellVoltage = 0; // 最低单体电压 V
int minCellVoltageNo = 0; // 最低单体电压编号
double avgCellVoltage = 0; // 单体平均电压 V
// 0x10XX0002 单体温度
double maxCellTemp = 0; // 最高单体温度 ℃
int maxCellTempNo = 0; // 最高单体温度编号
double minCellTemp = 0; // 最低单体温度 ℃
int minCellTempNo = 0; // 最低单体温度编号
double avgCellTemp = 0; // 单体平均温度 ℃
// 0x10XX0003 继电器(1 闭合 / 0 断开)
int posRelay = 0;
int negRelay = 0;
// 0x10XX0006 绝缘 / 端口电压
double posInsulationKohm = 0; // 正极绝缘阻抗 kΩ
double negInsulationKohm = 0; // 负极绝缘阻抗 kΩ
double portVoltage = 0; // 正负极端口电压 V
double posRelayOuterVoltage = 0; // 正极继电器外侧电压 V
// 0x10XX0010 告警位(附录1,byte0~5)
uint8_t alarmBits[6] = {0, 0, 0, 0, 0, 0};
// 0x10XX0010 附加字节(协议未详列,原样保留)
int alarmExtraByte6 = 0;
int alarmExtraByte7 = 0;
// 元数据
uint32_t rxMask = 0; // 收到过的功能码位图(bit = BcuFunc)
double lastRxTime = 0; // 最近收到该节点任一报文的时刻(MOOSTime)
bool valid = false; // 是否收到过该节点报文
};
// 解析一帧 CAN 数据域;命中 BCU 上报报文返回 true,nodeAddr 为节点地址(1~54),
// 字段按功能码部分更新合并进 node。data/dlc 非法或非 BCU 报文返回 false。
bool decodeCanBcuFrame(uint32_t canId, const uint8_t* data, int dlc,
uint8_t& nodeAddr, BcuNodeStatus& node);
// 附录1:0x10XX0010 告警位定义表(byteIdx/bitIdx -> 告警描述),表尾以 byteIdx=0xFF 结束
struct BcuAlarmDesc {
uint8_t byteIdx;
uint8_t bitIdx;
const char* text;
};
extern const BcuAlarmDesc kBcuAlarmTable[];
// 收集节点当前置位的告警描述;返回条数(不超过 cap,texts 填描述指针)
int bcuAlarmTexts(const BcuNodeStatus& node, const char* texts[], int cap);
} // namespace ccu } // namespace ccu
#endif // PCCU_CAN_BMS_H #endif // PCCU_CAN_BMS_H
+121 -1
View File
@@ -65,7 +65,45 @@ bool DbStore::open() {
");")) return false; ");")) return false;
exec("CREATE INDEX IF NOT EXISTS idx_comm_log_time ON comm_log(time);"); exec("CREATE INDEX IF NOT EXISTS idx_comm_log_time ON comm_log(time);");
return prepareInsert(); // bcu_node:锂电池 BCU 节点解析数据(每收到一帧 BMS 报文落一行,
// 记录该节点合并后的最新状态;BMS/CAN 协议字段固定故用结构化表)
if (!exec(
"CREATE TABLE IF NOT EXISTS bcu_node ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" time REAL NOT NULL,"
" addr INTEGER NOT NULL," // BCU 节点地址 1~54
" func INTEGER NOT NULL," // 触发本行的报文功能码
" total_voltage REAL," // 累加电压 V
" current REAL," // 回路电流 A(放电为正)
" soc REAL," // SOC %
" alarm_code INTEGER," // 告警码(原始值)
" self_check INTEGER," // 自检状态(原始值)
" max_cell_voltage REAL," // 最高单体电压 V
" max_cell_voltage_no INTEGER,"
" min_cell_voltage REAL," // 最低单体电压 V
" min_cell_voltage_no INTEGER,"
" avg_cell_voltage REAL," // 单体平均电压 V
" max_cell_temp REAL," // 最高单体温度 ℃
" max_cell_temp_no INTEGER,"
" min_cell_temp REAL," // 最低单体温度 ℃
" min_cell_temp_no INTEGER,"
" avg_cell_temp REAL," // 单体平均温度 ℃
" pos_relay INTEGER," // 正极继电器 1闭合/0断开
" neg_relay INTEGER," // 负极继电器 1闭合/0断开
" pos_insulation REAL," // 正极绝缘阻抗 kΩ
" neg_insulation REAL," // 负极绝缘阻抗 kΩ
" port_voltage REAL," // 正负极端口电压 V
" pos_relay_outer_voltage REAL," // 正极继电器外侧电压 V
" alarm_bits TEXT," // 告警位 byte0~5 十六进制
" alarm_extra_byte6 INTEGER,"
" alarm_extra_byte7 INTEGER,"
" rx_mask INTEGER," // 收到过的功能码位图
" last_rx_time REAL" // 节点最近收帧时刻(MOOSTime)
");")) return false;
exec("CREATE INDEX IF NOT EXISTS idx_bcu_node_time ON bcu_node(time);");
exec("CREATE INDEX IF NOT EXISTS idx_bcu_node_addr ON bcu_node(addr);");
return prepareInsert() && prepareBcuInsert();
} }
bool DbStore::prepareInsert() { bool DbStore::prepareInsert() {
@@ -79,12 +117,33 @@ bool DbStore::prepareInsert() {
return true; return true;
} }
bool DbStore::prepareBcuInsert() {
const char* sql =
"INSERT INTO bcu_node (time, addr, func, total_voltage, current, soc, "
"alarm_code, self_check, "
"max_cell_voltage, max_cell_voltage_no, min_cell_voltage, min_cell_voltage_no, avg_cell_voltage, "
"max_cell_temp, max_cell_temp_no, min_cell_temp, min_cell_temp_no, avg_cell_temp, "
"pos_relay, neg_relay, pos_insulation, neg_insulation, port_voltage, pos_relay_outer_voltage, "
"alarm_bits, alarm_extra_byte6, alarm_extra_byte7, rx_mask, last_rx_time) "
"VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,"
"?19,?20,?21,?22,?23,?24,?25,?26,?27,?28,?29);";
if (sqlite3_prepare_v2(m_db, sql, -1, &m_stmtBcuInsert, nullptr) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
return false;
}
return true;
}
void DbStore::close() { void DbStore::close() {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
if (m_stmtInsert) { if (m_stmtInsert) {
sqlite3_finalize(m_stmtInsert); sqlite3_finalize(m_stmtInsert);
m_stmtInsert = nullptr; m_stmtInsert = nullptr;
} }
if (m_stmtBcuInsert) {
sqlite3_finalize(m_stmtBcuInsert);
m_stmtBcuInsert = nullptr;
}
if (m_db) { if (m_db) {
sqlite3_close(m_db); sqlite3_close(m_db);
m_db = nullptr; m_db = nullptr;
@@ -119,6 +178,55 @@ void DbStore::onRawFrame(int direction, uint16_t msgId, const std::string& link,
} }
} }
void DbStore::insertBcuNode(uint8_t addr, uint8_t func, const BcuNodeStatus& v) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db || !m_stmtBcuInsert) return;
char bits[16];
std::snprintf(bits, sizeof(bits), "%02X%02X%02X%02X%02X%02X",
v.alarmBits[0], v.alarmBits[1], v.alarmBits[2],
v.alarmBits[3], v.alarmBits[4], v.alarmBits[5]);
sqlite3_reset(m_stmtBcuInsert);
sqlite3_clear_bindings(m_stmtBcuInsert);
// 时间:与 comm_log 一致用 unix 时间(秒)
int i = 1;
sqlite3_bind_double(m_stmtBcuInsert, i++, static_cast<double>(::time(nullptr)));
sqlite3_bind_int(m_stmtBcuInsert, i++, addr);
sqlite3_bind_int(m_stmtBcuInsert, i++, func);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.totalVoltage);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.current);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.soc);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.alarmCode);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.selfCheck);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.maxCellVoltage);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.maxCellVoltageNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.minCellVoltage);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.minCellVoltageNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.avgCellVoltage);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.maxCellTemp);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.maxCellTempNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.minCellTemp);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.minCellTempNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.avgCellTemp);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.posRelay);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.negRelay);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.posInsulationKohm);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.negInsulationKohm);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.portVoltage);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.posRelayOuterVoltage);
sqlite3_bind_text(m_stmtBcuInsert, i++, bits, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.alarmExtraByte6);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.alarmExtraByte7);
sqlite3_bind_int64(m_stmtBcuInsert, i++, static_cast<sqlite3_int64>(v.rxMask));
sqlite3_bind_double(m_stmtBcuInsert, i++, v.lastRxTime);
int rc = sqlite3_step(m_stmtBcuInsert);
if (rc != SQLITE_DONE) {
m_lastError = sqlite3_errmsg(m_db);
}
}
std::vector<CommLogRow> DbStore::queryRecent(const std::string& link, int direction, std::vector<CommLogRow> DbStore::queryRecent(const std::string& link, int direction,
uint16_t msgId, int limit) { uint16_t msgId, int limit) {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
@@ -175,4 +283,16 @@ long long DbStore::count() const {
return n; return n;
} }
long long DbStore::countBcu() const {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return 0;
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(m_db, "SELECT COUNT(*) FROM bcu_node;", -1, &stmt, nullptr) != SQLITE_OK)
return 0;
long long n = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) n = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
return n;
}
} // namespace ccu } // namespace ccu
+11
View File
@@ -6,6 +6,7 @@
#include <mutex> #include <mutex>
#include <vector> #include <vector>
#include "../comm/LinkManager.h" #include "../comm/LinkManager.h"
#include "../protocol/CanBms.h"
struct sqlite3; struct sqlite3;
struct sqlite3_stmt; struct sqlite3_stmt;
@@ -19,6 +20,9 @@ namespace ccu {
// - 以“原始帧日志”为主表(comm_log),收发全部数据按帧落库, // - 以“原始帧日志”为主表(comm_log),收发全部数据按帧落库,
// 协议字段变化不会导致建表失败。 // 协议字段变化不会导致建表失败。
// - 提供按 link / direction / msg_id 的查询接口供网页展示。 // - 提供按 link / direction / msg_id 的查询接口供网页展示。
// - 锂电池 BMS/CAN 协议字段固定,另设解析数据表 bcu_node:
// 每收到一帧 BMS 报文,把该 BCU 节点的合成状态落一行
// (电压/电流/SOC/单体电压温度/继电器/绝缘/告警位)。
// 复用仓库内 src/pPowerManger/sqlit3/sqlite3.c 与 sqlite3.h。 // 复用仓库内 src/pPowerManger/sqlit3/sqlite3.c 与 sqlite3.h。
//============================================================================ //============================================================================
@@ -48,19 +52,26 @@ public:
void onRawFrame(int direction, uint16_t msgId, const std::string& link, void onRawFrame(int direction, uint16_t msgId, const std::string& link,
const std::vector<uint8_t>& data, bool checksumOk) override; const std::vector<uint8_t>& data, bool checksumOk) override;
// 锂电池解析数据落库(bcu_node 表):addr=节点地址(1~54),
// func=本次触发落库的 BCU 报文功能码,v=合并后的节点最新状态
void insertBcuNode(uint8_t addr, uint8_t func, const BcuNodeStatus& v);
// 查询最近 N 条记录(可限定链路/方向/消息id;limit<=0 表示默认 100) // 查询最近 N 条记录(可限定链路/方向/消息id;limit<=0 表示默认 100)
std::vector<CommLogRow> queryRecent(const std::string& link, int direction, std::vector<CommLogRow> queryRecent(const std::string& link, int direction,
uint16_t msgId, int limit); uint16_t msgId, int limit);
long long count() const; long long count() const;
long long countBcu() const;
private: private:
bool exec(const char* sql); bool exec(const char* sql);
bool prepareInsert(); bool prepareInsert();
bool prepareBcuInsert();
std::string m_dbPath; std::string m_dbPath;
sqlite3* m_db = nullptr; sqlite3* m_db = nullptr;
sqlite3_stmt* m_stmtInsert = nullptr; sqlite3_stmt* m_stmtInsert = nullptr;
sqlite3_stmt* m_stmtBcuInsert = nullptr;
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
std::string m_lastError; std::string m_lastError;
}; };
+92 -28
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;} .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-bad{color:#f85149;font-weight:700;background:rgba(248,81,73,.12);padding:1px 6px;border-radius:4px;}
.fc-ok{color:#3fb950;} .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> </style>
</head> </head>
<body> <body>
@@ -97,6 +100,7 @@ let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); } 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 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 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 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'); } 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)+')'); } function flameText(f){ return {0:'无效',1:'火焰报警',2:'探测器故障'}[f] || ('未知('+hex(f)+')'); }
//------------------ 锂电池 BMS(CAN) 解析 ------------------ //------------------ 锂电池 BCU 节点(CAN) 解析 ------------------
// 报文定义见 docs/BMS_协议字段定义.xlsx: // 报文定义见 docs/04KT38电池BCU-MBMS通信(CAN)定义.docx:
// 0x10010000 总电压/SOC/故障码/状态位 // 0x10XX0000 累加电压/回路电流/SOC/告警码/自检状态
// 0x10010005 最大电流限制/总电流/最高单体电压 // 0x10XX0001 单体电压 0x10XX0002 单体温度 0x10XX0003 继电器
// 0x10010006 总电压(校验) // 0x10XX0006 绝缘阻值/端口电压 0x10XX0010 告警位(附录1)
// 故障码高亮:0 显示绿色正常,非 0 显示红色 // XX(01h~36h) 为 BCU 节点地址:同一节点的全部报文归入同一张卡片;
function bmsFault(c){ return c===0 ? '<span class="fc-ok">'+c+' 正常</span>' // 告警含义由服务器端按附录1 解析为中文列表(alarms 字段)
: '<span class="fc-bad">'+c+'</span>'; } function pad2(v){ return (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
// 数据新鲜度(10s 内视为在线) function relayText(v){ return v===1 ? '<span class="fc-ok">闭合</span>' : '断开'; }
function bmsOnline(t){ return (Date.now()/1000 - t) < 10; } // 告警码(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 码 + 括号解析 // 带解析的值:hex 码 + 括号解析
function parsed(v, text){ return hex(v)+' ('+text+')'; } function parsed(v, text){ return hex(v)+' ('+text+')'; }
// 故障码高亮:0 显示绿色正常,非 0 显示红色 // 故障码高亮:0 显示绿色正常,非 0 显示红色
@@ -303,7 +323,10 @@ function linkCard(s){
'收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err); '收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err);
h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>', h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>',
'收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err); '收:'+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); '"></span>CAN 链路 (pCanBridge)</span>', 'CAN帧:'+s.canFrameCount+' BMS帧:'+s.bmsStatusCount);
h+=row('FC 状态接收计数', s.fcStatusCount); h+=row('FC 状态接收计数', s.fcStatusCount);
h+=row('PM 指令接收计数', s.pmControlCount); h+=row('PM 指令接收计数', s.pmControlCount);
@@ -373,22 +396,59 @@ function pmFbCard(d){
return card('设定结果', h); return card('设定结果', h);
} }
//------------------ 锂电池 BMS(CAN) 卡片 ------------------ //------------------ 锂电池 BCU 节点(CAN) 卡片 ------------------
// 每个节点(CAN ID 0x10XX00YY 中的 XX)一张卡片,
// 同一节点的 0000/0001/0002/0003/0006/0010 各报文归并在卡片内分组展示
function bmsCard(d){ 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>'); '<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+')';
let h=''; let h='';
h+=row('总电压', d.totalVoltage.toFixed(1)+' V'); // 概要(0x10XX0000)
h+=row('总电流', d.totalCurrent.toFixed(1)+' A'); h+=sub('概要 0x10'+pad2(n.addr)+'0000');
h+=row('SOC', d.soc.toFixed(1)+' %'); h+=row('累加电压', n.totalVoltage.toFixed(1)+' V');
h+=row('最大电流限制', d.maxCurrentLimit.toFixed(1)+' A'); h+=row('回路电流', n.current.toFixed(1)+' A');
h+=row('最高单体电压', d.maxCellVoltage.toFixed(3)+' V'); h+=row('SOC', n.soc.toFixed(1)+' %');
h+=row('总电压(校验帧)', d.totalVoltageChk.toFixed(1)+' V'); h+=row('告警码', bmsFault(n.alarmCode));
h+=row('故障码', bmsFault(d.faultCode)); h+=row('自检状态', hex(n.selfCheck));
h+=row('状态位', hex(d.statusBits)); // 单体电压(0x10XX0001)
h+=row('数据状态', bmsOnline(d.lastRx) h+=sub('单体电压 0x10'+pad2(n.addr)+'0001');
? '<span class="fc-ok">在线</span>' : '<span class="fc-bad">超时</span>'); h+=row('最高单体电压', n.maxCellVoltage.toFixed(3)+' V (#'+n.maxCellVoltageNo+')');
return card('锂电池BMS (CAN)', h); 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){ function pmStatusCard(d){
@@ -482,12 +542,16 @@ function render(snap){
+tankCard(snap.fc)+cabinCard(snap.fc)+'</div>', +tankCard(snap.fc)+cabinCard(snap.fc)+'</div>',
pmCmd: pageHeader(0x0001,'PM操控指令','PM→CCU')+'<div class="grid">'+cmdCard(snap.pmCmd)+'</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>', 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)+ bms: '<div class="grid">'+bmsCard(snap.bms)+
'<div class="card"><h2>报文来源 (CAN ID)</h2>'+ '<div class="card"><h2>报文来源 (CAN ID)</h2>'+
row('0x10010000','总电压 / SOC / 故障码 / 状态位')+ row('0x10XX0000','累加电压 / 回路电流 / SOC / 告警码 / 自检状态')+
row('0x10010005','最大电流限制 / 总电流 / 最高单体电压')+ row('0x10XX0001','最高/最低/平均单体电压及编号')+
row('0x10010006','总电压(校验)')+ row('0x10XX0002','最高/最低/平均单体温度及编号')+
row('0x10XX0003','正/负极继电器状态')+
row('0x10XX0006','正/负极绝缘阻抗 / 端口电压 / 继电器外侧电压')+
row('0x10XX0010','告警位(附录1,按位解析显示告警含义)')+
row('XX','BCU 节点地址 01h~36h,同一节点的报文归入同一卡片')+
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'</div></div>', row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'</div></div>',
fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>', fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>',
// 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据 // 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据
+10
View File
@@ -75,6 +75,16 @@ bool LowerCommManager::initUdpComm(){
return true; return true;
} }
void LowerCommManager::setLocalPort(long port){
if (port <= 0 || port == m_port) return;
m_port = port;
if (m_udpComm && m_udpComm->udpScoket) {
// 套接字构造时即记录接收端口,改端口须重建(此时尚未 bind,重建安全)
delete m_udpComm->udpScoket;
m_udpComm->udpScoket = new XPCUdpSocket(m_port);
}
}
void LowerCommManager::setCcuAddress(const std::string& host, long port){ void LowerCommManager::setCcuAddress(const std::string& host, long port){
m_ccuHost = host; m_ccuHost = host;
m_ccuPort = port; m_ccuPort = port;
+3
View File
@@ -35,6 +35,9 @@ public:
// 设置 CCU 地址(moos 配置读取后调用;须在监听/发送线程启动前调用) // 设置 CCU 地址(moos 配置读取后调用;须在监听/发送线程启动前调用)
void setCcuAddress(const std::string& host, long port); void setCcuAddress(const std::string& host, long port);
// 设置本地输入端口(CCU -> pPowerManger,moos 配置读取后调用;须在 bind/监听线程启动前调用)
void setLocalPort(long port);
// 设置原始帧日志回调(转发给 udpComm,可为空) // 设置原始帧日志回调(转发给 udpComm,可为空)
void setCommLogger(ICommLogger* logger); void setCommLogger(ICommLogger* logger);
+19 -1
View File
@@ -51,6 +51,8 @@ PowerManger::PowerManger()
// CCU 地址默认值,可在 moos 文件中用 ccuhost/ccuport 覆盖 // CCU 地址默认值,可在 moos 文件中用 ccuhost/ccuport 覆盖
m_ccuHost = CCUHOST; m_ccuHost = CCUHOST;
m_ccuPort = CCUPORT; m_ccuPort = CCUPORT;
// 本地输入端口(接收 CCU 数据)默认值,可在 moos 文件中用 iport 覆盖
m_localPort = IPORT;
m_pmCurrentState = {}; m_pmCurrentState = {};
m_currentDisBreakerState = {}; m_currentDisBreakerState = {};
@@ -69,7 +71,7 @@ PowerManger::PowerManger()
//初始化下位机通信 //初始化下位机通信
m_lowerCommManager = new LowerCommManager(this,IPORT,m_ccuPort,m_ccuHost); m_lowerCommManager = new LowerCommManager(this,IPORT,m_ccuPort,m_ccuHost);
m_lowerCommManager->initUdpComm(); // UDP 端口 bind 延迟到 OnStartUp 读取配置后进行,保证 iport/ccuhost/ccuport 配置生效
//初始化上位机通信 //初始化上位机通信
m_upperCommManager = new UpperCommManager(this); m_upperCommManager = new UpperCommManager(this);
@@ -195,6 +197,7 @@ bool PowerManger::OnStartUp()
std::string logPath; // 日志文件路径,不配置则默认当前目录 pPowerManger.log std::string logPath; // 日志文件路径,不配置则默认当前目录 pPowerManger.log
std::string ccuHost; // CCU 地址,不配置则沿用默认值 std::string ccuHost; // CCU 地址,不配置则沿用默认值
long ccuPort = 0; // CCU 端口,不配置则沿用默认值 long ccuPort = 0; // CCU 端口,不配置则沿用默认值
long localPort = 0; // 本地输入端口(CCU->pPowerManger),不配置则沿用默认值
STRING_LIST::iterator p; STRING_LIST::iterator p;
for(p=sParams.begin(); p!=sParams.end(); p++) { for(p=sParams.begin(); p!=sParams.end(); p++) {
@@ -226,6 +229,10 @@ bool PowerManger::OnStartUp()
ccuPort = atoi(value.c_str()); ccuPort = atoi(value.c_str());
handled = true; handled = true;
} }
else if(param == "iport") {
localPort = atoi(value.c_str());
handled = true;
}
if(!handled) if(!handled)
reportUnhandledConfigWarning(orig); reportUnhandledConfigWarning(orig);
@@ -251,6 +258,17 @@ bool PowerManger::OnStartUp()
m_lowerCommManager->setCcuAddress(m_ccuHost, m_ccuPort); m_lowerCommManager->setCcuAddress(m_ccuHost, m_ccuPort);
LOG_F(INFO, "CCU 地址: %s:%ld", m_ccuHost.c_str(), m_ccuPort); LOG_F(INFO, "CCU 地址: %s:%ld", m_ccuHost.c_str(), m_ccuPort);
// 应用 moos 文件中的本地输入端口配置(接收 CCU 数据),须在 bind 前生效
if (localPort > 0) m_localPort = localPort;
m_lowerCommManager->setLocalPort(m_localPort);
LOG_F(INFO, "本地输入端口(CCU->pPowerManger): %ld", m_localPort);
// 配置生效后再 bind 本地 UDP 输入端口
if (!m_lowerCommManager->initUdpComm()) {
LOG_F(ERROR, "UDP bind failed on port %ld", m_localPort);
reportRunWarning("UDP bind failed on local input port");
}
// 注入原始帧日志回调(RX/TX 双向落库) // 注入原始帧日志回调(RX/TX 双向落库)
m_lowerCommManager->setCommLogger(m_db); m_lowerCommManager->setCommLogger(m_db);
// 数据库就绪后再启动下位机监听线程 // 数据库就绪后再启动下位机监听线程
+1
View File
@@ -125,6 +125,7 @@ class PowerManger : public AppCastingMOOSApp
bool isDeviceCmdEmpty() const; bool isDeviceCmdEmpty() const;
//===========================CONFIG======================================== //===========================CONFIG========================================
long m_lPort; long m_lPort;
long m_localPort; // 本地输入端口(CCU->pPowerManger),moos 配置项 iport
std::string m_ccuHost; // CCU 地址,moos 配置项 ccuhost std::string m_ccuHost; // CCU 地址,moos 配置项 ccuhost
long m_ccuPort; // CCU 端口,moos 配置项 ccuport long m_ccuPort; // CCU 端口,moos 配置项 ccuport
unsigned int m_nReceiveBufferSizeKB; unsigned int m_nReceiveBufferSizeKB;
+2
View File
@@ -11,4 +11,6 @@ ProcessConfig = pPowerManger
// CCU 地址;不配置则默认 127.0.0.1:7000 // CCU 地址;不配置则默认 127.0.0.1:7000
// ccuhost = 127.0.0.1 // ccuhost = 127.0.0.1
// ccuport = 7000 // ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
} }
+90 -35
View File
@@ -289,50 +289,105 @@ static void testDbStore() {
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// 9. CAN BMS 报文解码(docs/BMS_协议字段定义.xlsx,测试向量为实车抓包数据) // 9. CAN BCU 报文解码(docs/04KT38电池BCU-MBMS通信(CAN)定义.docx,
// 测试向量为实车抓包数据与协议文档示例值)
static void testCanBmsDecode() { static void testCanBmsDecode() {
BmsStatusValue v; uint8_t addr = 0;
// 0x10010000:14 da 17 70 00 c9 31 01(实车抓包) // 0x10010000(节点01,实车抓包):14 da 17 70 00 c9 31 01
// 总电压 0x14DA=5338 -> 533.8V;SOC 0x00C9=201 -> 20.1% // 累加电压 0x14DA=5338 -> 533.8V;回路电流 0x1770=6000 -> 600.0-600=0.0A
// 故障码 0x31=49;状态位 0x01 // SOC 0x00C9=201 -> 20.1%;告警码 0x31;自检状态 0x01
BcuNodeStatus n1;
const uint8_t d0[8] = {0x14,0xDA,0x17,0x70,0x00,0xC9,0x31,0x01}; const uint8_t d0[8] = {0x14,0xDA,0x17,0x70,0x00,0xC9,0x31,0x01};
CHECK(decodeCanBmsFrame(0x10010000, d0, 8, v)); CHECK(decodeCanBcuFrame(0x10010000, d0, 8, addr, n1));
CHECK(v.totalVoltage == 533.8); CHECK(addr == 1);
CHECK(v.soc == 20.1); CHECK(n1.totalVoltage == 533.8);
CHECK(v.faultCode == 0x31); CHECK(n1.current == 0.0);
CHECK(v.statusBits == 0x01); CHECK(n1.soc == 20.1);
CHECK(n1.alarmCode == 0x31);
CHECK(n1.selfCheck == 0x01);
// 文档示例帧:14 d9 => 533.7V // 回路电流:充电 -25A -> raw 0x1676=5750 -> 575.0-600=-25.0A
const uint8_t d0doc[8] = {0x14,0xD9,0x17,0x70,0x00,0xC9,0x31,0x01}; const uint8_t d0n[8] = {0x14,0xDA,0x16,0x76,0x00,0xC9,0x31,0x00};
CHECK(decodeCanBmsFrame(0x10010000, d0doc, 8, v)); decodeCanBcuFrame(0x10010000, d0n, 8, addr, n1);
CHECK(v.totalVoltage == 533.7); CHECK(n1.current == -25.0);
// 0x10010005:07 d0 00 00 0e a8 00 00 // 0x10010001(节点01)单体电压:0e a8 05 0e a0 03 0e a4
// 最大电流限制 0x07D0=2000 -> 200.0A;总电流 00 00 00 -> 0.0A // 最高 0x0EA8=3752 -> 3.752V (#5);最低 0x0EA0=3744 -> 3.744V (#3)
// 最高单体电压 0x0EA8=3752 -> 3.752V // 平均 0x0EA4=3748 -> 3.748V;部分更新,概要字段保留
const uint8_t d5[8] = {0x07,0xD0,0x00,0x00,0x0E,0xA8,0x00,0x00}; const uint8_t dv[8] = {0x0E,0xA8,0x05,0x0E,0xA0,0x03,0x0E,0xA4};
CHECK(decodeCanBmsFrame(0x10010005, d5, 8, v)); CHECK(decodeCanBcuFrame(0x10010001, dv, 8, addr, n1));
CHECK(v.maxCurrentLimit == 200.0); CHECK(n1.maxCellVoltage == 3.752);
CHECK(v.totalCurrent == 0.0); CHECK(n1.maxCellVoltageNo == 5);
CHECK(v.maxCellVoltage == 3.752); 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)));
// 总电流负数:s16 补码 FF 06 = -250 -> -25.0A(xlsx 负数示例) // 0x10010002(节点01)单体温度:3c 01 2d 02 33
const uint8_t d5n[8] = {0x07,0xD0,0xFF,0x06,0x0E,0xA8,0x00,0x00}; // 最高 0x3C=60-40=20℃ (#1);最低 0x2D=45-40=5℃ (#2);平均 0x33=51-40=11℃
CHECK(decodeCanBmsFrame(0x10010005, d5n, 8, v)); // (实测固件平均温度与最高/最低一致均带 -40℃ 偏移,协议文档"偏移0"有误:
CHECK(v.totalCurrent == -25.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);
// 0x10010006:07 d0 07 d0 14 da 00 00 -> 总电压校验 0x14DA=533.8V // 0x10010003(节点01)继电器:01 01 -> 正/负极均闭合
const uint8_t d6[8] = {0x07,0xD0,0x07,0xD0,0x14,0xDA,0x00,0x00}; const uint8_t dr[8] = {0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00};
CHECK(decodeCanBmsFrame(0x10010006, d6, 8, v)); CHECK(decodeCanBcuFrame(0x10010003, dr, 8, addr, n1));
CHECK(v.totalVoltageChk == 533.8); CHECK(n1.posRelay == 1 && n1.negRelay == 1);
// 非 BMS 报文 ID 不命中 // 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}; const uint8_t dx[8] = {0,0,0,0,0,0,0,0};
CHECK(!decodeCanBmsFrame(0x10010035, dx, 8, v)); CHECK(!decodeCanBcuFrame(0x10010035, dx, 8, addr, n1)); // 未定义功能码
CHECK(!decodeCanBmsFrame(0x10010000, nullptr, 0, v)); CHECK(!decodeCanBcuFrame(0x10010004, dx, 8, addr, n1)); // Reserve
CHECK(isCanBmsId(0x10010000) && isCanBmsId(0x10010005) && isCanBmsId(0x10010006)); CHECK(!decodeCanBcuFrame(0x10010005, dx, 8, addr, n1)); // Reserve
CHECK(!isCanBmsId(0x10010001)); 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));
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------