diff --git a/bin/pPowerManger.moos b/bin/pPowerManger.moos index 2de7531..882b3f8 100644 --- a/bin/pPowerManger.moos +++ b/bin/pPowerManger.moos @@ -6,4 +6,6 @@ ProcessConfig = pPowerManger CommsTick = 4 log_level = INFO log_file = /path/to/your/logfile.log + // 本地输入端口(接收 CCU 数据);不配置则默认 5001 + // iport = 5001 } \ No newline at end of file diff --git a/docs/04KT38电池BCU-MBMS通信(CAN)定义.docx b/docs/04KT38电池BCU-MBMS通信(CAN)定义.docx new file mode 100644 index 0000000..6392599 Binary files /dev/null and b/docs/04KT38电池BCU-MBMS通信(CAN)定义.docx differ diff --git a/docs/BMS_协议字段定义.xlsx b/docs/BMS_协议字段定义.xlsx deleted file mode 100644 index a840f99..0000000 Binary files a/docs/BMS_协议字段定义.xlsx and /dev/null differ diff --git a/docs/配电控制器与复合管控器的通信协议20260824.docx b/docs/配电控制器与复合管控器的通信协议20260824.docx new file mode 100644 index 0000000..cf46091 Binary files /dev/null and b/docs/配电控制器与复合管控器的通信协议20260824.docx differ diff --git a/docs/配电系统CAN通讯协议.xlsx b/docs/配电系统CAN通讯协议.xlsx new file mode 100644 index 0000000..ad8acad Binary files /dev/null and b/docs/配电系统CAN通讯协议.xlsx differ diff --git a/docs/锂电池协议20230324.docx b/docs/锂电池协议20230324.docx deleted file mode 100644 index 0bfd8f5..0000000 Binary files a/docs/锂电池协议20230324.docx and /dev/null differ diff --git a/missions/h100.moos b/missions/h100.moos index b836fe1..2d22ddf 100644 --- a/missions/h100.moos +++ b/missions/h100.moos @@ -20,6 +20,8 @@ ProcessConfig = pPowerManger // CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000 ccuhost = 127.0.0.1 ccuport = 7000 + // 本地输入端口(接收 CCU 数据);不配置则默认 5001 + // iport = 5001 } ProcessConfig = pPowerMangerHost diff --git a/missions/pPowerManger.moos b/missions/pPowerManger.moos index 60c307b..b67ccaa 100644 --- a/missions/pPowerManger.moos +++ b/missions/pPowerManger.moos @@ -5,4 +5,6 @@ ProcessConfig = pPowerManger // CCU 地址;不配置则默认 127.0.0.1:7000 // ccuhost = 127.0.0.1 // ccuport = 7000 + // 本地输入端口(接收 CCU 数据);不配置则默认 5001 + // iport = 5001 } \ No newline at end of file diff --git a/scripts/sync-board-time.sh b/scripts/sync-board-time.sh new file mode 100755 index 0000000..d9c91a5 --- /dev/null +++ b/scripts/sync-board-time.sh @@ -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 目标主机(默认 192.168.0.223) +# --user 登录用户(默认 root) +# --time 手动指定基准时间(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 +# 输出: "|<板卡可读时间> "(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 -- 后取 $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 diff --git a/src/pCCU/CCU.cpp b/src/pCCU/CCU.cpp index 1e53b57..39498cc 100644 --- a/src/pCCU/CCU.cpp +++ b/src/pCCU/CCU.cpp @@ -197,20 +197,26 @@ void CCU::handleCanMessage(CMOOSMsg& msg) { unsigned char* d = msg.GetBinaryData(); if (!d) return; - // 合并语义:取当前快照 -> 解码部分更新 -> 写回 - BmsStatusValue v = m_sysData->bmsStatus(); - if (decodeCanBmsFrame(canId, d, static_cast(n), v)) { - v.valid = true; - v.lastRxTime = MOOSTime(false); - m_sysData->updateBmsStatus(v); + // 合并语义:先取该节点当前快照 -> 解码部分更新 -> 写回 + // (每个功能码报文只更新自己的字段,不能整体覆盖) + uint8_t addr = bcuAddrOf(canId); + if (!addr) return; + BcuNodeStatus node = m_sysData->bcuNode(addr); + if (decodeCanBcuFrame(canId, d, static_cast(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 一条,便于联调观察 double now = MOOSTime(); if (now - m_lastBmsLog >= 1.0) { m_lastBmsLog = now; - LOG_F(INFO, "[BMS/CAN] u=%.1fV i=%.1fA soc=%.1f%% fault=%d status=%d cellMax=%.3fV", - v.totalVoltage, v.totalCurrent, v.soc, - v.faultCode, v.statusBits, v.maxCellVoltage); + LOG_F(INFO, "[BMS/CAN] 节点%d u=%.1fV i=%.1fA soc=%.1f%% alarm=%02X self=%d vMax=%.3fV", + addr, node.totalVoltage, node.current, node.soc, + node.alarmCode, node.selfCheck, node.maxCellVoltage); } } } @@ -479,6 +485,7 @@ bool CCU::buildReport() { } if (m_db) { m_msgs << "数据库记录数:" << m_db->count() << "\n"; + m_msgs << "数据库BMS记录数:" << m_db->countBcu() << "\n"; } return true; } diff --git a/src/pCCU/CCU.h b/src/pCCU/CCU.h index f997eb6..3a29f14 100644 --- a/src/pCCU/CCU.h +++ b/src/pCCU/CCU.h @@ -23,7 +23,7 @@ namespace ccu { // 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文 // (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照 // (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用) -// 收发全部帧落库 SQLite;网页周期推送 JSON 快照。 +// 收发全部帧落库 SQLite(锂电池 BMS 解析数据另存 bcu_node 表);网页周期推送 JSON 快照。 //============================================================================ class CCU : public AppCastingMOOSApp { diff --git a/src/pCCU/core/CoordFsm.cpp b/src/pCCU/core/CoordFsm.cpp index ab98a62..e110ea2 100644 --- a/src/pCCU/core/CoordFsm.cpp +++ b/src/pCCU/core/CoordFsm.cpp @@ -25,7 +25,7 @@ void CoordNormalState::react(TickEvent const &e) { // // 读取最新状态: // sys()->fcStatus() 燃料电池状态 - // sys()->bmsStatus() 锂电池 BMS 状态(总电压/SOC/电流/故障码,CAN) + // sys()->bcuNode(addr) 锂电池 BCU 节点状态(电压/SOC/电流/告警,CAN) // sys()->pmControl() 主机最新操控指令 // // 下发操作: @@ -61,7 +61,7 @@ void CoordBusyState::exit() { void CoordBusyState::react(TickEvent const &e) { // TODO(策略填写):操作进行中的周期监视(进度/超时统计、是否需要中止)。 - // 状态可通过 sys()->bmsStatus() / sys()->fcStatus() 获取。 + // 状态可通过 sys()->bcuNode(addr) / sys()->fcStatus() 获取。 (void)e; } @@ -88,7 +88,7 @@ void CoordFaultState::exit() { void CoordFaultState::react(TickEvent const &e) { // TODO(策略填写):故障处理与恢复判断(周期检查故障是否消除, // 消除后 transit() 回正常运行态)。 - // 可用:sys()->fcStatus().fc_fault_level、sys()->bmsStatus().faultCode 等。 + // 可用:sys()->fcStatus().fc_fault_level、sys()->bcuNode(addr).alarmBits 等。 (void)e; } diff --git a/src/pCCU/core/CoordFsm.hpp b/src/pCCU/core/CoordFsm.hpp index dbc287a..b0e12d1 100644 --- a/src/pCCU/core/CoordFsm.hpp +++ b/src/pCCU/core/CoordFsm.hpp @@ -67,12 +67,12 @@ struct PmControlEvent : tinyfsm::Event { }; // 协调操作步骤完成/超时:由协调动作引擎产生(预留)。 -// status 携带完成时刻的 BMS 状态快照(SOC/电压/电流/故障码,可判故障) +// status 携带完成时刻的 BCU 节点状态快照(SOC/电压/电流/告警,可判故障) struct StepDoneEvent : tinyfsm::Event { bool confirmed; // true=确认完成;false=超时未确认 - BmsStatusValue status; // 完成时刻 BMS 状态快照 + BcuNodeStatus status; // 完成时刻 BCU 节点状态快照 StepDoneEvent() : confirmed(false) {} - StepDoneEvent(bool ok, const BmsStatusValue& s) : confirmed(ok), status(s) {} + StepDoneEvent(bool ok, const BcuNodeStatus& s) : confirmed(ok), status(s) {} }; //---- 状态机 --------------------------------------------------------------- diff --git a/src/pCCU/core/PowerCoordinator.cpp b/src/pCCU/core/PowerCoordinator.cpp index 345a41f..5f949a6 100644 --- a/src/pCCU/core/PowerCoordinator.cpp +++ b/src/pCCU/core/PowerCoordinator.cpp @@ -33,7 +33,7 @@ bool PowerCoordinator::sendFcControl(const FcControlValue& fcCmd) { // // 可用输入(通过 m_sys 读取最新状态): // - m_sys->fcStatus() : 燃料电池最新状态 -// - m_sys->bmsStatus() : 锂电池 BMS 最新状态(总电压/SOC/电流/故障码, +// - m_sys->bcuNode(addr) : 锂电池 BCU 节点最新状态(电压/SOC/电流/告警, // CAN 经 pCanBridge 透传,protocol/CanBms.h) // - m_sys->pmControl() : 控制主机最新操控指令 // diff --git a/src/pCCU/core/SnapshotBuilder.cpp b/src/pCCU/core/SnapshotBuilder.cpp index 3fcc37c..6d05031 100644 --- a/src/pCCU/core/SnapshotBuilder.cpp +++ b/src/pCCU/core/SnapshotBuilder.cpp @@ -1,6 +1,7 @@ #include "SnapshotBuilder.h" #include "../store/DbStore.h" #include "../protocol/CanBms.h" +#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h" #include "json/json.h" #include #include @@ -209,19 +210,72 @@ JsonVal pmFbJson(const PmParamSetFbValue& v) { return j; } -// 锂电池 BMS 状态(CAN,docs/BMS_协议字段定义.xlsx)-> JSON -JsonVal bmsStatusJson(const BmsStatusValue& v) { +// 锂电池 BCU 节点(CAN,docs/04KT38电池BCU-MBMS通信(CAN)定义.docx)-> JSON +// 同一节点(0x10XX00YY 中 XX)的全部报文字段归并在一个对象内, +// 告警位由附录1 表解析出中文含义放入 alarms 数组。 +// nowSec 为当前 MOOSTime,用于计算节点数据最后更新距今的秒数 age。 +JsonVal bcuNodeJson(uint8_t addr, const BcuNodeStatus& v, double nowSec) { JsonVal j(Json::objectValue); - j["totalVoltage"] = JsonVal(v.totalVoltage); // V - 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["lastRx"] = JsonVal(v.lastRxTime); + j["addr"] = JsonVal(addr); + j["valid"] = JsonVal(v.valid); + j["lastRx"] = JsonVal(v.lastRxTime); + j["age"] = JsonVal(nowSec - v.lastRxTime); // 距最后收到该节点报文的秒数 + j["rx"] = JsonVal(static_cast(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(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(i + kBcuAddrMin), nodes[i], nowSec)); + } return j; } @@ -353,8 +407,10 @@ std::string SnapshotBuilder::build() const { root["pmParam"] = pmParamJson(m_sys->pmParamSet()); root["pmFb"] = pmFbJson(m_sys->pmParamFb()); root["pmStatus"] = pmStatusJson(m_sys->pmStatus()); - // 锂电池 BMS(CAN,经 pCanBridge 透传) - root["bms"] = bmsStatusJson(m_sys->bmsStatus()); + // 锂电池 BCU 节点(CAN,经 pCanBridge 透传,按节点分组) + BcuNodeStatus nodes[kBcuNodeCount]; + m_sys->bcuNodes(nodes); + root["bms"] = bcuNodesJson(nodes, MOOSTime(false)); root["fcStatusCount"] = JsonVal(static_cast(m_sys->fcStatusCount())); root["pmControlCount"] = JsonVal(static_cast(m_sys->pmControlCount())); root["fcControlCount"] = JsonVal(static_cast(m_sys->fcControlCount())); @@ -368,7 +424,7 @@ std::string SnapshotBuilder::build() const { root["pmParam"] = JsonVal(Json::objectValue); root["pmFb"] = JsonVal(Json::objectValue); root["pmStatus"] = JsonVal(Json::objectValue); - root["bms"] = JsonVal(Json::objectValue); + root["bms"] = JsonVal(Json::arrayValue); } JsonVal links(Json::objectValue); diff --git a/src/pCCU/core/SystemData.h b/src/pCCU/core/SystemData.h index 410df57..5bbdee5 100644 --- a/src/pCCU/core/SystemData.h +++ b/src/pCCU/core/SystemData.h @@ -13,7 +13,7 @@ namespace ccu { // SystemData:跨线程共享的最新状态快照。 // // - 接收线程(FC 链路)写 FcStatus 快照 -// - MOOS 主线程(OnNewMail)写 BMS 快照(CAN 经 pCanBridge 透传) +// - MOOS 主线程(OnNewMail)写 BCU 节点快照(CAN 经 pCanBridge 透传) // - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送 // - 网页线程读快照展示 // 通过互斥锁保护读写。 @@ -101,17 +101,27 @@ public: return m_pmParamFb; } - //-------- 锂电池 BMS(CAN 总线,经 pCanBridge 透传) -------- + //-------- 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传) -------- - // 更新最新 BMS 状态(每帧部分更新,由调用方先取出快照再合并) - void updateBmsStatus(const BmsStatusValue& v) { + // 更新单个 BCU 节点状态(每帧部分更新,由调用方先取出快照再合并) + void updateBcuNode(uint8_t addr, const BcuNodeStatus& v) { std::lock_guard lock(m_mutex); - m_bmsStatus = v; - m_bmsStatusCount++; + if (addr >= kBcuAddrMin && addr <= kBcuAddrMax) { + m_bcuNodes[addr - kBcuAddrMin] = v; + m_bmsStatusCount++; + } } - BmsStatusValue bmsStatus() const { + // 读取单个 BCU 节点快照(地址非法返回 valid=false 的空快照) + BcuNodeStatus bcuNode(uint8_t addr) const { std::lock_guard 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 lock(m_mutex); + for (int i = 0; i < kBcuNodeCount; ++i) out[i] = m_bcuNodes[i]; } unsigned long bmsStatusCount() const { std::lock_guard lock(m_mutex); @@ -136,7 +146,7 @@ private: FcControlValue m_fcControl; PmStatusValue m_pmStatus; PmParamSetFbValue m_pmParamFb; - BmsStatusValue m_bmsStatus; + BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1 unsigned long m_fcStatusCount = 0; unsigned long m_pmControlCount = 0; unsigned long m_fcControlCount = 0; diff --git a/src/pCCU/protocol/CanBms.cpp b/src/pCCU/protocol/CanBms.cpp index 9bc00e6..133820a 100644 --- a/src/pCCU/protocol/CanBms.cpp +++ b/src/pCCU/protocol/CanBms.cpp @@ -14,41 +14,143 @@ inline int16_t rdS16BE(const uint8_t* p) { return static_cast(rdU16BE(p)); } +// 有符号 8 位(温度) +inline int8_t rdS8(uint8_t b) { return static_cast(b); } + +// 设置功能码已收位 +inline void markRx(BcuNodeStatus& n, uint8_t func) { + n.rxMask |= (1u << (func & 31)); +} + } // 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; + uint8_t addr = bcuAddrOf(canId); + if (!addr || !isCanBcuId(canId)) return false; + + nodeAddr = addr; + // 缩放用除法(而非乘 0.1/0.001),保证结果与十进制字面量严格一致。 - switch (canId) { - case CAN_BMS_ID_STATUS: // 0x10010000:总电压 / SOC / 故障码 / 状态位 + switch (bcuFuncOf(canId)) { + case BCU_FUNC_BASE: // 0x10XX0000:电压 / 电流 / SOC / 告警码 / 自检 if (dlc >= 8) { - v.totalVoltage = rdU16BE(data + 0) / 10.0; // 0.1V - v.soc = rdU16BE(data + 4) / 10.0; // 0.1% - v.faultCode = data[6]; - v.statusBits = data[7]; + node.totalVoltage = rdU16BE(data + 0) / 10.0; // 0.1V + node.current = rdS16BE(data + 2) / 10.0 - 600.0; // 0.1A,偏移-600A + node.soc = rdU16BE(data + 4) / 10.0; // 0.1% + node.alarmCode = data[6]; + node.selfCheck = data[7]; + markRx(node, BCU_FUNC_BASE); } break; - case CAN_BMS_ID_CURRENT: - // 0x10010005:最大电流限制 / 总电流 / 最高单体电压 - // 注:xlsx 表中"总电流"标注为 3 字节(s24),但与其自身示例 - // (总电流 00 00 => 0.0A、最高单体电压 0x0EA8 => 3.752V, - // 对应实车帧 07 d0 | 00 00 | 0e a8 | 00 00)矛盾——s24@2 会与 - // 单体电压@4 重叠。按示例与实测采用 2 字节 s16(负电流示例 - // 0xFF06 => -25.0A 亦为 2 字节补码)。 - 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 + case BCU_FUNC_CELL_V: // 0x10XX0001:最高/最低/平均单体电压 + if (dlc >= 2) node.maxCellVoltage = rdU16BE(data + 0) / 1000.0; // 1mV + if (dlc >= 3) node.maxCellVoltageNo = data[2]; + if (dlc >= 5) node.minCellVoltage = rdU16BE(data + 3) / 1000.0; // 1mV + if (dlc >= 6) node.minCellVoltageNo = data[5]; + if (dlc >= 8) node.avgCellVoltage = rdU16BE(data + 6) / 1000.0; // 1mV + markRx(node, BCU_FUNC_CELL_V); break; - case CAN_BMS_ID_VOLT_CHK: // 0x10010006:总电压(校验) - if (dlc >= 6) { - v.totalVoltageChk = rdU16BE(data + 4) / 10.0; // 0.1V - } + case BCU_FUNC_CELL_T: // 0x10XX0002:最高/最低/平均单体温度 + if (dlc >= 1) node.maxCellTemp = rdS8(data[0]) - 40.0; // 偏移-40℃ + 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; - 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 true; diff --git a/src/pCCU/protocol/CanBms.h b/src/pCCU/protocol/CanBms.h index cb120c0..98e5182 100644 --- a/src/pCCU/protocol/CanBms.h +++ b/src/pCCU/protocol/CanBms.h @@ -6,58 +6,141 @@ namespace ccu { //============================================================================ -// CAN BMS 协议(锂电池 BMS 报文,经 USBCAN-8E-U/CANET 上 CAN 总线) -// 依据:docs/BMS_协议字段定义.xlsx +// 锂电池 BCU-MBMS CAN 协议(docs/04KT38电池BCU-MBMS通信(CAN)定义.docx) // // 数据来源:pCanBridge 透传的 MOOS 消息 // 变量名 CAN_0x%08X = CAN ID(扩展帧),m_sVal = 二进制数据域, // m_dfVal2 = 原始帧信息字节。本模块只负责按报文 ID 解析数据域字段。 // -// 报文(均为扩展帧,字段大端,起始字节相对 8 字节数据域): -// 0x10010000 总电压 u16@0 ÷10 V -// SOC u16@4 ÷10 % -// 故障码 u8 @6 ×1 -// 状态位 u8 @7 ×1 -// 0x10010005 最大电流限制 u16@0 ÷10 A -// 总电流 s16@2 ÷10 A(补码,放电为正;xlsx 表标注 -// 3 字节与其示例/实测冲突,按 2 字节实现, -// 详见 CanBms.cpp 注释) -// 最高单体电压 u16@4 ÷1000 V -// 0x10010006 总电压(校验) u16@4 ÷10 V +// 【节点地址】一个 MBMS 下最多挂 54 个 BCU,CAN ID 中间的 XX(01h~36h) +// 为 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 % +// 告警码 u8 @6(循环上报,协议未附码表,原样显示) +// 自检状态 u8 @7 +// 0x10XX0001 (200ms) 最高单体电压 u16@0 mV,编号 u8@2 +// 最低单体电压 u16@3 mV,编号 u8@5 +// 单体平均电压 u16@6 mV +// 0x10XX0002 (1000ms) 最高单体温度 s8@0(偏移 -40℃),编号 u8@1 +// 最低单体温度 s8@2(偏移 -40℃),编号 u8@3 +// 单体平均温度 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) -enum CanBmsId : uint32_t { - CAN_BMS_ID_STATUS = 0x10010000, // 总电压 / SOC / 故障码 / 状态位 - CAN_BMS_ID_CURRENT = 0x10010005, // 最大电流限制 / 总电流 / 最高单体电压 - CAN_BMS_ID_VOLT_CHK = 0x10010006, // 总电压(校验) +// BCU 节点地址范围:01h~36h(1~54) +static const int kBcuAddrMin = 0x01; +static const int kBcuAddrMax = 0x36; +static const int kBcuNodeCount = kBcuAddrMax - kBcuAddrMin + 1; + +// 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 部分更新,最终合成完整快照) -struct BmsStatusValue { - double totalVoltage = 0; // 总电压 V(0x10010000) - double soc = 0; // SOC %(0x10010000) - int faultCode = 0; // 故障码(0x10010000 @6) - 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 提取 BCU 节点地址(0x10XX00YY -> XX);非 BCU 报文格式返回 0 +inline uint8_t bcuAddrOf(uint32_t canId) { + if ((canId & 0xFF00FF00u) != 0x10000000u) return 0; + uint8_t addr = static_cast((canId >> 16) & 0xFF); + return (addr >= kBcuAddrMin && addr <= kBcuAddrMax) ? addr : 0; } +// 从 CAN ID 提取功能码(0x10XX00YY -> YY) +inline uint8_t bcuFuncOf(uint32_t canId) { + return static_cast(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 #endif // PCCU_CAN_BMS_H diff --git a/src/pCCU/store/DbStore.cpp b/src/pCCU/store/DbStore.cpp index 96e8b74..b42a131 100644 --- a/src/pCCU/store/DbStore.cpp +++ b/src/pCCU/store/DbStore.cpp @@ -65,7 +65,45 @@ bool DbStore::open() { ");")) return false; 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() { @@ -79,12 +117,33 @@ bool DbStore::prepareInsert() { 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() { std::lock_guard lock(m_mutex); if (m_stmtInsert) { sqlite3_finalize(m_stmtInsert); m_stmtInsert = nullptr; } + if (m_stmtBcuInsert) { + sqlite3_finalize(m_stmtBcuInsert); + m_stmtBcuInsert = nullptr; + } if (m_db) { sqlite3_close(m_db); 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 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(::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(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 DbStore::queryRecent(const std::string& link, int direction, uint16_t msgId, int limit) { std::lock_guard lock(m_mutex); @@ -175,4 +283,16 @@ long long DbStore::count() const { return n; } +long long DbStore::countBcu() const { + std::lock_guard 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 \ No newline at end of file diff --git a/src/pCCU/store/DbStore.h b/src/pCCU/store/DbStore.h index 8291426..1016ff6 100644 --- a/src/pCCU/store/DbStore.h +++ b/src/pCCU/store/DbStore.h @@ -6,6 +6,7 @@ #include #include #include "../comm/LinkManager.h" +#include "../protocol/CanBms.h" struct sqlite3; struct sqlite3_stmt; @@ -19,6 +20,9 @@ namespace ccu { // - 以“原始帧日志”为主表(comm_log),收发全部数据按帧落库, // 协议字段变化不会导致建表失败。 // - 提供按 link / direction / msg_id 的查询接口供网页展示。 +// - 锂电池 BMS/CAN 协议字段固定,另设解析数据表 bcu_node: +// 每收到一帧 BMS 报文,把该 BCU 节点的合成状态落一行 +// (电压/电流/SOC/单体电压温度/继电器/绝缘/告警位)。 // 复用仓库内 src/pPowerManger/sqlit3/sqlite3.c 与 sqlite3.h。 //============================================================================ @@ -48,19 +52,26 @@ public: void onRawFrame(int direction, uint16_t msgId, const std::string& link, const std::vector& 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) std::vector queryRecent(const std::string& link, int direction, uint16_t msgId, int limit); long long count() const; + long long countBcu() const; private: bool exec(const char* sql); bool prepareInsert(); + bool prepareBcuInsert(); std::string m_dbPath; sqlite3* m_db = nullptr; sqlite3_stmt* m_stmtInsert = nullptr; + sqlite3_stmt* m_stmtBcuInsert = nullptr; mutable std::mutex m_mutex; std::string m_lastError; }; diff --git a/src/pCCU/web/pages/index.h b/src/pCCU/web/pages/index.h index aa2c999..78ecef2 100644 --- a/src/pCCU/web/pages/index.h +++ b/src/pCCU/web/pages/index.h @@ -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;} @@ -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 '
'+k+''+v+'
'; } +function sub(t){ return '
'+t+'
'; } 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 ? ''+c+' 正常' - : ''+c+''; } -// 数据新鲜度(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 ? '闭合' : '断开'; } +// 告警码(0x10XX0000 BYTE7,协议未附码表,显示原始值) +function bmsFault(c){ return c===0 ? ''+hex(c)+'' + : ''+hex(c)+''; } +// 数据新鲜度: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('PM 链路', '收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err); - h+=row('CAN 链路 (pCanBridge)', '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)', '
数据状态等待报文...
'); - 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) - ? '在线' : '超时'); - 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 ''+a+''; }).join('
')); + }else{ + h+=row('告警', '无'); + } + // 最后更新时间(判断该节点消息是否持续正常发送) + const online = bmsOnline(n); + h+=row('最后更新', (online ? '' : '') + + ageText(n.age || 0) + (online ? ' (在线)' : ' (超时)') + ''); + html+=card(addrTxt, h); + } + return html; } function pmStatusCard(d){ @@ -482,12 +542,16 @@ function render(snap){ +tankCard(snap.fc)+cabinCard(snap.fc)+'', pmCmd: pageHeader(0x0001,'PM操控指令','PM→CCU')+'
'+cmdCard(snap.pmCmd)+'
', pmParam: pageHeader(0x0002,'PM参数设定','PM→CCU')+'
'+pmParamCard(snap.pmParam)+'
', - // 锂电池 BMS(CAN 总线,经 pCanBridge 透传) + // 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传,按节点分组展示) bms: '
'+bmsCard(snap.bms)+ '

报文来源 (CAN ID)

'+ - 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* 二进制报文')+'
', fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'
'+fcCmdCard(snap.fcCmd)+'
', // 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据 diff --git a/src/pPowerManger/LowerCommManager.cpp b/src/pPowerManger/LowerCommManager.cpp index f6f4ea7..e6d343f 100644 --- a/src/pPowerManger/LowerCommManager.cpp +++ b/src/pPowerManger/LowerCommManager.cpp @@ -75,6 +75,16 @@ bool LowerCommManager::initUdpComm(){ 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){ m_ccuHost = host; m_ccuPort = port; diff --git a/src/pPowerManger/LowerCommManager.h b/src/pPowerManger/LowerCommManager.h index eb24c4c..8b010cc 100644 --- a/src/pPowerManger/LowerCommManager.h +++ b/src/pPowerManger/LowerCommManager.h @@ -35,6 +35,9 @@ public: // 设置 CCU 地址(moos 配置读取后调用;须在监听/发送线程启动前调用) void setCcuAddress(const std::string& host, long port); + // 设置本地输入端口(CCU -> pPowerManger,moos 配置读取后调用;须在 bind/监听线程启动前调用) + void setLocalPort(long port); + // 设置原始帧日志回调(转发给 udpComm,可为空) void setCommLogger(ICommLogger* logger); diff --git a/src/pPowerManger/PowerManger.cpp b/src/pPowerManger/PowerManger.cpp index 5a5e031..34be737 100644 --- a/src/pPowerManger/PowerManger.cpp +++ b/src/pPowerManger/PowerManger.cpp @@ -51,6 +51,8 @@ PowerManger::PowerManger() // CCU 地址默认值,可在 moos 文件中用 ccuhost/ccuport 覆盖 m_ccuHost = CCUHOST; m_ccuPort = CCUPORT; + // 本地输入端口(接收 CCU 数据)默认值,可在 moos 文件中用 iport 覆盖 + m_localPort = IPORT; m_pmCurrentState = {}; m_currentDisBreakerState = {}; @@ -69,7 +71,7 @@ PowerManger::PowerManger() //初始化下位机通信 m_lowerCommManager = new LowerCommManager(this,IPORT,m_ccuPort,m_ccuHost); - m_lowerCommManager->initUdpComm(); + // UDP 端口 bind 延迟到 OnStartUp 读取配置后进行,保证 iport/ccuhost/ccuport 配置生效 //初始化上位机通信 m_upperCommManager = new UpperCommManager(this); @@ -195,6 +197,7 @@ bool PowerManger::OnStartUp() std::string logPath; // 日志文件路径,不配置则默认当前目录 pPowerManger.log std::string ccuHost; // CCU 地址,不配置则沿用默认值 long ccuPort = 0; // CCU 端口,不配置则沿用默认值 + long localPort = 0; // 本地输入端口(CCU->pPowerManger),不配置则沿用默认值 STRING_LIST::iterator p; for(p=sParams.begin(); p!=sParams.end(); p++) { @@ -226,6 +229,10 @@ bool PowerManger::OnStartUp() ccuPort = atoi(value.c_str()); handled = true; } + else if(param == "iport") { + localPort = atoi(value.c_str()); + handled = true; + } if(!handled) reportUnhandledConfigWarning(orig); @@ -251,6 +258,17 @@ bool PowerManger::OnStartUp() m_lowerCommManager->setCcuAddress(m_ccuHost, 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 双向落库) m_lowerCommManager->setCommLogger(m_db); // 数据库就绪后再启动下位机监听线程 diff --git a/src/pPowerManger/PowerManger.h b/src/pPowerManger/PowerManger.h index aa45bf4..c8087b5 100644 --- a/src/pPowerManger/PowerManger.h +++ b/src/pPowerManger/PowerManger.h @@ -125,6 +125,7 @@ class PowerManger : public AppCastingMOOSApp bool isDeviceCmdEmpty() const; //===========================CONFIG======================================== long m_lPort; + long m_localPort; // 本地输入端口(CCU->pPowerManger),moos 配置项 iport std::string m_ccuHost; // CCU 地址,moos 配置项 ccuhost long m_ccuPort; // CCU 端口,moos 配置项 ccuport unsigned int m_nReceiveBufferSizeKB; diff --git a/src/pPowerManger/pPowerManger.moos b/src/pPowerManger/pPowerManger.moos index 8a8f717..a0d1e83 100644 --- a/src/pPowerManger/pPowerManger.moos +++ b/src/pPowerManger/pPowerManger.moos @@ -11,4 +11,6 @@ ProcessConfig = pPowerManger // CCU 地址;不配置则默认 127.0.0.1:7000 // ccuhost = 127.0.0.1 // ccuport = 7000 + // 本地输入端口(接收 CCU 数据);不配置则默认 5001 + // iport = 5001 } \ No newline at end of file diff --git a/test/pccu/pccuTest.cpp b/test/pccu/pccuTest.cpp index eb5a5c7..c50f31b 100644 --- a/test/pccu/pccuTest.cpp +++ b/test/pccu/pccuTest.cpp @@ -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() { - BmsStatusValue v; + uint8_t addr = 0; - // 0x10010000:14 da 17 70 00 c9 31 01(实车抓包) - // 总电压 0x14DA=5338 -> 533.8V;SOC 0x00C9=201 -> 20.1% - // 故障码 0x31=49;状态位 0x01 + // 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(decodeCanBmsFrame(0x10010000, d0, 8, v)); - CHECK(v.totalVoltage == 533.8); - CHECK(v.soc == 20.1); - CHECK(v.faultCode == 0x31); - CHECK(v.statusBits == 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); - // 文档示例帧:14 d9 => 533.7V - const uint8_t d0doc[8] = {0x14,0xD9,0x17,0x70,0x00,0xC9,0x31,0x01}; - CHECK(decodeCanBmsFrame(0x10010000, d0doc, 8, v)); - CHECK(v.totalVoltage == 533.7); + // 回路电流:充电 -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); - // 0x10010005:07 d0 00 00 0e a8 00 00 - // 最大电流限制 0x07D0=2000 -> 200.0A;总电流 00 00 00 -> 0.0A - // 最高单体电压 0x0EA8=3752 -> 3.752V - const uint8_t d5[8] = {0x07,0xD0,0x00,0x00,0x0E,0xA8,0x00,0x00}; - CHECK(decodeCanBmsFrame(0x10010005, d5, 8, v)); - CHECK(v.maxCurrentLimit == 200.0); - CHECK(v.totalCurrent == 0.0); - CHECK(v.maxCellVoltage == 3.752); + // 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))); - // 总电流负数:s16 补码 FF 06 = -250 -> -25.0A(xlsx 负数示例) - const uint8_t d5n[8] = {0x07,0xD0,0xFF,0x06,0x0E,0xA8,0x00,0x00}; - CHECK(decodeCanBmsFrame(0x10010005, d5n, 8, v)); - CHECK(v.totalCurrent == -25.0); + // 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); - // 0x10010006:07 d0 07 d0 14 da 00 00 -> 总电压校验 0x14DA=533.8V - const uint8_t d6[8] = {0x07,0xD0,0x07,0xD0,0x14,0xDA,0x00,0x00}; - CHECK(decodeCanBmsFrame(0x10010006, d6, 8, v)); - CHECK(v.totalVoltageChk == 533.8); + // 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); - // 非 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}; - CHECK(!decodeCanBmsFrame(0x10010035, dx, 8, v)); - CHECK(!decodeCanBmsFrame(0x10010000, nullptr, 0, v)); - CHECK(isCanBmsId(0x10010000) && isCanBmsId(0x10010005) && isCanBmsId(0x10010006)); - CHECK(!isCanBmsId(0x10010001)); + 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)); } //--------------------------------------------------------------------------