锂电池改用 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:
@@ -6,4 +6,6 @@ ProcessConfig = pPowerManger
|
||||
CommsTick = 4
|
||||
log_level = INFO
|
||||
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.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -5,4 +5,6 @@ ProcessConfig = pPowerManger
|
||||
// CCU 地址;不配置则默认 127.0.0.1:7000
|
||||
// ccuhost = 127.0.0.1
|
||||
// ccuport = 7000
|
||||
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
|
||||
// iport = 5001
|
||||
}
|
||||
Executable
+176
@@ -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
@@ -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<int>(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<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 一条,便于联调观察
|
||||
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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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<CoordNormalState>() 回正常运行态)。
|
||||
// 可用:sys()->fcStatus().fc_fault_level、sys()->bmsStatus().faultCode 等。
|
||||
// 可用:sys()->fcStatus().fc_fault_level、sys()->bcuNode(addr).alarmBits 等。
|
||||
(void)e;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
};
|
||||
|
||||
//---- 状态机 ---------------------------------------------------------------
|
||||
|
||||
@@ -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() : 控制主机最新操控指令
|
||||
//
|
||||
|
||||
@@ -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 <sstream>
|
||||
#include <cstdio>
|
||||
@@ -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<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;
|
||||
}
|
||||
|
||||
@@ -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<Json::UInt>(m_sys->fcStatusCount()));
|
||||
root["pmControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmControlCount()));
|
||||
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["pmFb"] = JsonVal(Json::objectValue);
|
||||
root["pmStatus"] = JsonVal(Json::objectValue);
|
||||
root["bms"] = JsonVal(Json::objectValue);
|
||||
root["bms"] = JsonVal(Json::arrayValue);
|
||||
}
|
||||
|
||||
JsonVal links(Json::objectValue);
|
||||
|
||||
@@ -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<std::mutex> 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<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 {
|
||||
std::lock_guard<std::mutex> 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;
|
||||
|
||||
+124
-22
@@ -14,41 +14,143 @@ inline int16_t rdS16BE(const uint8_t* 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
|
||||
|
||||
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;
|
||||
|
||||
+124
-41
@@ -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<uint8_t>((canId >> 16) & 0xFF);
|
||||
return (addr >= kBcuAddrMin && addr <= kBcuAddrMax) ? addr : 0;
|
||||
}
|
||||
|
||||
// 从 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
|
||||
|
||||
#endif // PCCU_CAN_BMS_H
|
||||
|
||||
+121
-1
@@ -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<std::mutex> 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<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,
|
||||
uint16_t msgId, int limit) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -175,4 +283,16 @@ long long DbStore::count() const {
|
||||
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
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#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<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)
|
||||
std::vector<CommLogRow> 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;
|
||||
};
|
||||
|
||||
+93
-29
@@ -65,6 +65,9 @@ th{color:var(--dim);font-weight:500;}
|
||||
.lvl4{background:rgba(248,81,73,.38);color:#ff7b72;border:1px solid #f85149;}
|
||||
.fc-bad{color:#f85149;font-weight:700;background:rgba(248,81,73,.12);padding:1px 6px;border-radius:4px;}
|
||||
.fc-ok{color:#3fb950;}
|
||||
/* 卡片内分组小标题 */
|
||||
.sub{font-size:11px;color:#79c0ff;margin:10px 0 2px;border-bottom:1px solid #22304a;padding-bottom:2px;}
|
||||
.sub:first-child{margin-top:0;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -97,6 +100,7 @@ let activeTab = 'fcStatus';
|
||||
|
||||
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
|
||||
function row(k, v){ return '<div class="row"><span class="k">'+k+'</span><span class="v">'+v+'</span></div>'; }
|
||||
function sub(t){ return '<div class="sub">'+t+'</div>'; }
|
||||
function hex(v){ return '0x' + (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
|
||||
function hex16(v){ return '0x' + (v & 0xFFFF).toString(16).toUpperCase().padStart(4,'0'); }
|
||||
function hex32(v){ return '0x' + (v >>> 0).toString(16).toUpperCase().padStart(8,'0'); }
|
||||
@@ -128,16 +132,32 @@ function statusText(s){
|
||||
}
|
||||
function flameText(f){ return {0:'无效',1:'火焰报警',2:'探测器故障'}[f] || ('未知('+hex(f)+')'); }
|
||||
|
||||
//------------------ 锂电池 BMS(CAN) 解析 ------------------
|
||||
// 报文定义见 docs/BMS_协议字段定义.xlsx:
|
||||
// 0x10010000 总电压/SOC/故障码/状态位
|
||||
// 0x10010005 最大电流限制/总电流/最高单体电压
|
||||
// 0x10010006 总电压(校验)
|
||||
// 故障码高亮:0 显示绿色正常,非 0 显示红色
|
||||
function bmsFault(c){ return c===0 ? '<span class="fc-ok">'+c+' 正常</span>'
|
||||
: '<span class="fc-bad">'+c+'</span>'; }
|
||||
// 数据新鲜度(10s 内视为在线)
|
||||
function bmsOnline(t){ return (Date.now()/1000 - t) < 10; }
|
||||
//------------------ 锂电池 BCU 节点(CAN) 解析 ------------------
|
||||
// 报文定义见 docs/04KT38电池BCU-MBMS通信(CAN)定义.docx:
|
||||
// 0x10XX0000 累加电压/回路电流/SOC/告警码/自检状态
|
||||
// 0x10XX0001 单体电压 0x10XX0002 单体温度 0x10XX0003 继电器
|
||||
// 0x10XX0006 绝缘阻值/端口电压 0x10XX0010 告警位(附录1)
|
||||
// XX(01h~36h) 为 BCU 节点地址:同一节点的全部报文归入同一张卡片;
|
||||
// 告警含义由服务器端按附录1 解析为中文列表(alarms 字段)
|
||||
function pad2(v){ return (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
|
||||
function relayText(v){ return v===1 ? '<span class="fc-ok">闭合</span>' : '断开'; }
|
||||
// 告警码(0x10XX0000 BYTE7,协议未附码表,显示原始值)
|
||||
function bmsFault(c){ return c===0 ? '<span class="fc-ok">'+hex(c)+'</span>'
|
||||
: '<span class="fc-bad">'+hex(c)+'</span>'; }
|
||||
// 数据新鲜度:10s 内视为在线。优先用后端算好的 age(距最后收到该节点
|
||||
// 报文的秒数,不依赖浏览器与板卡时钟同步),无 age 时退回 lastRx 比对本地时钟
|
||||
function bmsOnline(n){
|
||||
if(!n) return undefined;
|
||||
if(typeof n.age === 'number') return n.age < 10;
|
||||
if(typeof n.lastRx === 'number') return (Date.now()/1000 - n.lastRx) < 10;
|
||||
return undefined;
|
||||
}
|
||||
// 最后更新时间文本(a:距最后收到报文的秒数)
|
||||
function ageText(a){
|
||||
if(a < 90) return a.toFixed(1)+' 秒前';
|
||||
if(a < 3600) return Math.floor(a/60)+' 分 '+Math.floor(a%60)+' 秒前';
|
||||
return Math.floor(a/3600)+' 时 '+Math.floor((a%3600)/60)+' 分前';
|
||||
}
|
||||
// 带解析的值:hex 码 + 括号解析
|
||||
function parsed(v, text){ return hex(v)+' ('+text+')'; }
|
||||
// 故障码高亮:0 显示绿色正常,非 0 显示红色
|
||||
@@ -303,7 +323,10 @@ function linkCard(s){
|
||||
'收:'+l.fc.rx+' 发:'+l.fc.tx+' 误:'+l.fc.err);
|
||||
h+=row('<span><span class="status-dot '+dot(online(l.pm.lastRx))+'"></span>PM 链路</span>',
|
||||
'收:'+l.pm.rx+' 发:'+l.pm.tx+' 误:'+l.pm.err);
|
||||
h+=row('<span><span class="status-dot '+dot(s.bms && s.bms.valid ? bmsOnline(s.bms.lastRx) : undefined)+
|
||||
// CAN/BMS 行:任一 BCU 节点在线即视为链路在线
|
||||
const bmsNodes = Array.isArray(s.bms) ? s.bms : [];
|
||||
const bmsAnyOnline = bmsNodes.some(function(n){ return bmsOnline(n); });
|
||||
h+=row('<span><span class="status-dot '+dot(bmsNodes.length ? bmsAnyOnline : undefined)+
|
||||
'"></span>CAN 链路 (pCanBridge)</span>', 'CAN帧:'+s.canFrameCount+' BMS帧:'+s.bmsStatusCount);
|
||||
h+=row('FC 状态接收计数', s.fcStatusCount);
|
||||
h+=row('PM 指令接收计数', s.pmControlCount);
|
||||
@@ -373,22 +396,59 @@ function pmFbCard(d){
|
||||
return card('设定结果', h);
|
||||
}
|
||||
|
||||
//------------------ 锂电池 BMS(CAN) 卡片 ------------------
|
||||
//------------------ 锂电池 BCU 节点(CAN) 卡片 ------------------
|
||||
// 每个节点(CAN ID 0x10XX00YY 中的 XX)一张卡片,
|
||||
// 同一节点的 0000/0001/0002/0003/0006/0010 各报文归并在卡片内分组展示
|
||||
function bmsCard(d){
|
||||
if(!d || !d.valid) return card('锂电池BMS (CAN)',
|
||||
const nodes = (d && d.length) ? d : [];
|
||||
if(!nodes.length) return card('锂电池BMS (CAN)',
|
||||
'<div class="row"><span class="k">数据状态</span><span class="v">等待报文...</span></div>');
|
||||
let h='';
|
||||
h+=row('总电压', d.totalVoltage.toFixed(1)+' V');
|
||||
h+=row('总电流', d.totalCurrent.toFixed(1)+' A');
|
||||
h+=row('SOC', d.soc.toFixed(1)+' %');
|
||||
h+=row('最大电流限制', d.maxCurrentLimit.toFixed(1)+' A');
|
||||
h+=row('最高单体电压', d.maxCellVoltage.toFixed(3)+' V');
|
||||
h+=row('总电压(校验帧)', d.totalVoltageChk.toFixed(1)+' V');
|
||||
h+=row('故障码', bmsFault(d.faultCode));
|
||||
h+=row('状态位', hex(d.statusBits));
|
||||
h+=row('数据状态', bmsOnline(d.lastRx)
|
||||
? '<span class="fc-ok">在线</span>' : '<span class="fc-bad">超时</span>');
|
||||
return card('锂电池BMS (CAN)', h);
|
||||
let html='';
|
||||
for(const n of nodes){
|
||||
const addrTxt = 'BCU节点 '+pad2(n.addr)+'h (#'+n.addr+')';
|
||||
let h='';
|
||||
// 概要(0x10XX0000)
|
||||
h+=sub('概要 0x10'+pad2(n.addr)+'0000');
|
||||
h+=row('累加电压', n.totalVoltage.toFixed(1)+' V');
|
||||
h+=row('回路电流', n.current.toFixed(1)+' A');
|
||||
h+=row('SOC', n.soc.toFixed(1)+' %');
|
||||
h+=row('告警码', bmsFault(n.alarmCode));
|
||||
h+=row('自检状态', hex(n.selfCheck));
|
||||
// 单体电压(0x10XX0001)
|
||||
h+=sub('单体电压 0x10'+pad2(n.addr)+'0001');
|
||||
h+=row('最高单体电压', n.maxCellVoltage.toFixed(3)+' V (#'+n.maxCellVoltageNo+')');
|
||||
h+=row('最低单体电压', n.minCellVoltage.toFixed(3)+' V (#'+n.minCellVoltageNo+')');
|
||||
h+=row('单体平均电压', n.avgCellVoltage.toFixed(3)+' V');
|
||||
// 单体温度(0x10XX0002)
|
||||
h+=sub('单体温度 0x10'+pad2(n.addr)+'0002');
|
||||
h+=row('最高单体温度', n.maxCellTemp.toFixed(0)+' ℃ (#'+n.maxCellTempNo+')');
|
||||
h+=row('最低单体温度', n.minCellTemp.toFixed(0)+' ℃ (#'+n.minCellTempNo+')');
|
||||
h+=row('单体平均温度', n.avgCellTemp.toFixed(0)+' ℃');
|
||||
// 继电器(0x10XX0003)
|
||||
h+=sub('继电器 0x10'+pad2(n.addr)+'0003');
|
||||
h+=row('正极继电器', relayText(n.posRelay));
|
||||
h+=row('负极继电器', relayText(n.negRelay));
|
||||
// 绝缘/端口电压(0x10XX0006)
|
||||
h+=sub('绝缘/端口 0x10'+pad2(n.addr)+'0006');
|
||||
h+=row('正极绝缘阻抗', n.posInsulationKohm.toFixed(0)+' kΩ');
|
||||
h+=row('负极绝缘阻抗', n.negInsulationKohm.toFixed(0)+' kΩ');
|
||||
h+=row('正负极端口电压', n.portVoltage.toFixed(1)+' V');
|
||||
h+=row('正极继电器外侧电压', n.posRelayOuterVoltage.toFixed(1)+' V');
|
||||
// 告警(0x10XX0010,附录1 解析)
|
||||
h+=sub('告警明细 0x10'+pad2(n.addr)+'0010');
|
||||
if(n.alarms && n.alarms.length){
|
||||
h+=row('告警(' + n.alarms.length + '条)',
|
||||
n.alarms.map(function(a){ return '<span class="fc-bad">'+a+'</span>'; }).join('<br>'));
|
||||
}else{
|
||||
h+=row('告警', '<span class="fc-ok">无</span>');
|
||||
}
|
||||
// 最后更新时间(判断该节点消息是否持续正常发送)
|
||||
const online = bmsOnline(n);
|
||||
h+=row('最后更新', (online ? '<span class="fc-ok">' : '<span class="fc-bad">')
|
||||
+ ageText(n.age || 0) + (online ? ' (在线)' : ' (超时)') + '</span>');
|
||||
html+=card(addrTxt, h);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function pmStatusCard(d){
|
||||
@@ -482,12 +542,16 @@ function render(snap){
|
||||
+tankCard(snap.fc)+cabinCard(snap.fc)+'</div>',
|
||||
pmCmd: pageHeader(0x0001,'PM操控指令','PM→CCU')+'<div class="grid">'+cmdCard(snap.pmCmd)+'</div>',
|
||||
pmParam: pageHeader(0x0002,'PM参数设定','PM→CCU')+'<div class="grid">'+pmParamCard(snap.pmParam)+'</div>',
|
||||
// 锂电池 BMS(CAN 总线,经 pCanBridge 透传)
|
||||
// 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传,按节点分组展示)
|
||||
bms: '<div class="grid">'+bmsCard(snap.bms)+
|
||||
'<div class="card"><h2>报文来源 (CAN ID)</h2>'+
|
||||
row('0x10010000','总电压 / SOC / 故障码 / 状态位')+
|
||||
row('0x10010005','最大电流限制 / 总电流 / 最高单体电压')+
|
||||
row('0x10010006','总电压(校验)')+
|
||||
row('0x10XX0000','累加电压 / 回路电流 / SOC / 告警码 / 自检状态')+
|
||||
row('0x10XX0001','最高/最低/平均单体电压及编号')+
|
||||
row('0x10XX0002','最高/最低/平均单体温度及编号')+
|
||||
row('0x10XX0003','正/负极继电器状态')+
|
||||
row('0x10XX0006','正/负极绝缘阻抗 / 端口电压 / 继电器外侧电压')+
|
||||
row('0x10XX0010','告警位(附录1,按位解析显示告警含义)')+
|
||||
row('XX','BCU 节点地址 01h~36h,同一节点的报文归入同一卡片')+
|
||||
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'</div></div>',
|
||||
fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>',
|
||||
// 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
// 数据库就绪后再启动下位机监听线程
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,4 +11,6 @@ ProcessConfig = pPowerManger
|
||||
// CCU 地址;不配置则默认 127.0.0.1:7000
|
||||
// ccuhost = 127.0.0.1
|
||||
// ccuport = 7000
|
||||
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
|
||||
// iport = 5001
|
||||
}
|
||||
+90
-35
@@ -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));
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user