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