5 Commits
Author SHA1 Message Date
zjk 873a1a9b09 新增 pMotor 推进电机操作程序:经 pCanBridge(CAN1) 与电机控制器交互
build-test-deploy / ci (push) Failing after 32m37s
- 协议模块(protocol/MotorCan):12 条 CAN ID、指令帧 0x18EF2010 编码、
  状态帧(转速/母线电压/温度)与支路一/二故障报警帧解码,故障位按协议
  逐位映射中文描述(docs/推进电机通信协议20260321.docx)
- 主程序(MOOS 应用):订阅 pCanBridge 透传 CAN_0x* 解码电机状态;
  500ms 周期下发指令帧(CAN_TX_0x18EF2010,m_sSrcAux=CAN1),
  可选 red_channel 冗余下发 0x18EF2011;复位位保持 reset_hold_sec
- 网页(18081):按协议指令帧字段独立下发(使能命令 Byte0 bit0 /
  复位命令 Byte0 bit1 / 期望转速 Byte2/3,无联动按钮);
  WebSocket 1Hz 推送转速/电压/温度/故障告警/链路统计
- 配置接入:missions/h100.moos 增加 pMotor 配置块(can_channel=CAN1)
- 测试:pmotorTest 92 项(指令帧字节级对照协议示例、解码/故障表)
- 部署:scripts/build-board.sh 增加 pMotor 产物拷贝 + pMotor.service;
  .gitignore 补 bin/pMotor、bin/pmotorTest
- 联调验证:MOOSDB+pCanBridge+假CANET 全链路,下行帧 01 00 E8 03
  与协议文档示例一致,复位位 3s 保持行为正确
2026-09-02 10:50:28 +08:00
zjk 67e732045a pCanBridge 扩展多通道支持:每通道独立TCP链路 + 上下行对称通道标注(m_sSrcAux)
pCanBridge:
- 配置改为 channel = <通道名>,<IP>,<工作端口> 重复行(兼容旧 can_host/can_port 单通道回退),
  每通道一条独立 CanEndpoint(TCP Client) + 接收线程,对应 CANET 的 CAN0=4001..CAN7=4008
- 上行 CAN_0x%08X 不变,m_sSrcAux=通道名区分来源通道
- 下行按 m_sSrcAux 路由到对应通道;回退规则:default_channel > 单通道 > 多通道告警丢弃
- buildReport 逐通道统计(连接/收发帧数/错误/重连)

pCCU:
- 新增 bcu_can_channel 配置(默认 CAN0),BCU 断路器下行 Post 前 SetSourceAux 指定目标通道,
  key 保持 CAN_TX_0x%08X(与上行通道标注对称,key 格式不变)

配置/文档:pCanBridge.moos、CanBridge_Info、pCCU.moos、missions/h100.moos、
test/pccu/pccu_it.moos 同步新格式。

验证:伪造 CANET Server 冒烟通过(多通道建链/上行解析发布/SQLite channel 列/
下行通道路由与回退丢弃/srcAux 经 MOOSDB 往返);
pccu 集成测试 PASSED(BCU 断路器下行 3 帧经新路由全部正确);
pccuTest 183/183、PowerMangerTests 64/64 通过。
2026-09-01 21:23:52 +08:00
zjk 90caacf9a2 pCCU 锂电池页新增 BCU 断路器控制(0x10XX81FF 经 pCanBridge 下行 CAN 总线)
- CanBms: bcuCtrlCanId(0x10XX81FF) + buildBcuRelayCtrlData(02 00 总正 总负 FF*4)
- SystemData: BcuCtrlCmd 下行队列(Web线程入队->Iterate/MOOS线程出队Notify) + 下发统计
- CCU: /api/bcu_ctrl(addr/action 或 pos/neg, 校验地址01h~36h) + Iterate 经 MOOS CAN_TX_0x* 下发
- pCanBridge 变双向透传: 订阅 CAN_TX_0x*,CanEndpoint::sendFrame(13字节CANET帧,TCP写,互斥保护)
- 网页: 每BCU节点断路器闭合/断开按钮(confirm确认) + 下发统计卡片
- 测试: pccuTest 控制帧编码用例(183项);集成测试新增假CANET服务器验证完整下行链路(闭合/断开/非法地址)
2026-09-01 15:23:30 +08:00
zjk 0639b5904d pCCU 新增真实CCU配电桥接(rcu链路原帧透传+网页显示)并适配板卡端口拓扑
- protocol/DisProtocol: 配电协议8条消息(指令0x0001~4/反馈0x0005~8, Sum8),复用pmSysvariable结构体+static_assert核对
- ChecksumPolicy 新增 Sum8;Frame 支持 1 字节校验和
- MessageRegistry 支持 (id+帧总长) 二维查找:配电指令与PM操控指令共用id按帧长区分(帧头length字段不可信)
- LinkManager: 按实际帧长分发 + sendRaw原帧透传 + 非本协议帧头落库节流告警;UdpEndpoint sendTo 加锁
- comm/RcuLinkManager: 真实CCU链路;core/DisBridge: 配电桥接器(反馈转pPowerManger+解码显示,指令转真实CCU,0x0004仅显示不转发)
- 网页新增"真实CCU"页签全字段展示;SnapshotBuilder realCcu 节 + rcu 链路描述
- h100.moos: 端口腾挪(真实CCU固定上报5001归pCCU,pPowerManger iport=5002),rcu_remote=192.168.0.140:7000
- 测试: pccuTest 新增配电/Sum8/长度分发/RCU注册表用例(176项);集成测试新增桥接字节一致+不误转发用例
2026-09-01 15:06:11 +08:00
zjk 5b8f0b1421 锂电池改用 BCU-MBMS CAN 协议按节点解析 + bcu_node 解析数据落库 + 板卡对时脚本
- pCCU/CanBms:按《04KT38电池BCU-MBMS通信(CAN)定义》重写解码,
  BmsStatus 单状态模型改为 BcuNodeStatus 多节点模型(0x10XX00YY,
  节点地址 01~36h),支持 0x0000 电压/电流/SOC/告警码、0x0001 单体
  电压、0x0002 单体温度、0x0003 继电器、0x0006 绝缘/端口电压、
  0x0010 告警位(附录1 中文码表)六类报文
- 平均单体温度偏移修正:协议文档 BYTE5 写"偏移0"有误,实测固件与
  最高/最低一致均带 -40℃ 偏移(实车 0x10020002 原始 68/69 减 40 后
  为 28/29℃,落在最低28~最高30区间内,按文档直读则超出物理范围)
- pCCU/DbStore:新增 bcu_node 解析数据表,锂电池 BMS 报文每帧落一行
  节点合成状态(原 comm_log 仅原始帧,BMS/CAN 帧此前不落 pCCU 库),
  buildReport 增加 BMS 记录数
- 快照/网页:BCU 按节点分组展示(告警位解析中文含义、数据 age),
  PM 状态报文锂电池/应急电池卡片
- pPowerManger:新增 iport 本地输入端口配置(UDP bind 延迟到
  OnStartUp 读取配置后执行,保证 iport/ccuhost/ccuport 生效),
  各 mission 文件补充注释
- test:CAN BCU 解码用例按新协议/新偏移更新(实车抓包 + 文档示例值),
  133 项全部通过
- docs:删除旧 BMS 协议(xlsx/20230324docx),归档 04KT38 BCU-MBMS
  CAN 定义、配电控制器通信协议 20260824、配电系统 CAN 通讯协议
- scripts:新增 sync-board-time.sh,223 板卡(RK3588)时间校正
  (本机为基准 + RTT 折半补偿对时,尽力写 RTC,支持 status/--time)
2026-09-01 12:58:33 +08:00
71 changed files with 5266 additions and 326 deletions
+2
View File
@@ -252,6 +252,7 @@ bin/pPowerMangerHost
bin/pCCU
bin/pccuTest
bin/pCanBridge
bin/pMotor
# CI test reports
reports/
@@ -260,6 +261,7 @@ reports/
bin/PowerMangerTests
bin/udpFeeder
bin/fullFeeder
bin/pmotorTest
# ============================================================
# 数据库与日志文件(全局忽略,一律不入库)
+2
View File
@@ -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.
+53 -12
View File
@@ -1,6 +1,6 @@
// MOOS file
// 板卡 (RK3588 / Ubuntu 20.04) 专用 mission 配置
// 五个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU / pCanBridge)
// 六个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU / pCanBridge / pMotor)
// 共用本文件。
// 由 scripts/deploy.sh 推送到板卡 /root/work/h100/missions/h100.moos
@@ -20,6 +20,10 @@ ProcessConfig = pPowerManger
// CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000
ccuhost = 127.0.0.1
ccuport = 7000
// 本地输入端口(接收 CCU 数据);默认 5001。
// 5001 让给 pCCU 的 rcu 链路接收真实CCU(192.168.0.140) 固定上报的
// 配电反馈,故本进程接收端口改为 5002(pCCU 转发的反馈与状态报文发往此处)
iport = 5002
}
ProcessConfig = pPowerMangerHost
@@ -46,11 +50,24 @@ ProcessConfig = pCCU
fc_remote_port = 7000
// PM 链路(监听 pPowerManger 指令;向 pPowerManger 周期发送 PM 状态)
// pPowerManger 与 pCCU 同机部署,pPowerManger 绑定 0.0.0.0:5001,
// 故 PM 状态发送目标用回环地址即可送达。
// pPowerManger 与 pCCU 同机部署;pPowerManger 接收端口为 5002(iport),
// 故 PM 状态发送目标用 127.0.0.1:5002。
pm_local_port = 7000
pm_remote_ip = 127.0.0.1
pm_remote_port = 5001
pm_remote_port = 5002
//======== 真实CCU 桥接链路(配电协议) ========
// 真实CCU(192.168.0.140) 设备侧固定上报到 192.168.0.223:5001
// (与 pPowerManger 默认 CCU 配置一致:CCUPORT=7000 为其监听口,
// 上报目标为控制主机 5001),设备侧不可修改。
// 故 pCCU 的 rcu 链路直接绑定 5001 接收其反馈:
// 配电反馈 0x0005~0x0008:原帧转发 pPowerManger(127.0.0.1:5002) + 网页显示
// 配电指令 0x0001~0x0004:转发目标为真实CCU 监听口 192.168.0.140:7000
// 其余帧(旧协议 0x20 0x20 状态帧、无效帧)落库忽略
rcu_enable = true
rcu_local_port = 5001
rcu_remote_ip = 192.168.0.140
rcu_remote_port = 7000
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
@@ -67,13 +84,37 @@ ProcessConfig = pCanBridge
AppTick = 4
CommsTick = 4
// CAN->MOOSDB 透传:USBCAN-8E-U/CANET(TCP Server 模式)
// 工作端口:CAN0=4001,CAN1=4002,...,CAN7=4008
can_host = 192.168.0.222
can_port = 4001
// 通道名(写入消息 m_sSrcAux 辅助字段)
can_channel_name = CAN0
// CAN<->MOOSDB 多通道透传:CANET(TCP Server 模式)
// 每通道一条独立 TCP 连接;工作端口:CAN0=4001,CAN1=4002,...,CAN7=4008
channel = CAN0,192.168.0.222,4001
channel = CAN1,192.168.0.222,4002
// channel = CAN1,192.168.0.222,4002
// CAN 帧 SQLite 落库路径(can_frame 表)
dbpath = /root/work/h100/data/pCanBridge_data.db
// CAN_TX 下行默认通道(pCCU 未指定 m_sSrcAux 时使用;可省略)
default_channel = CAN0
// CAN 帧 SQLite 落库路径(can_frame 表,channel 列区分通道)
dbpath = /root/work/h100/data/pCanBridge_data.db
}
ProcessConfig = pMotor
{
AppTick = 4
CommsTick = 4
// 推进电机控制器:CAN1 通道(经 pCanBridge 透传,docs/推进电机通信协议20260321.docx)
// 下行 500ms 周期指令帧 0x18EF2010(网页 /api/motor_cmd 控制启停/复位/转速),
// 上行订阅 CAN_0x* 解码故障/转速/母线电压/温度,网页 18081 展示
can_channel = CAN1
// 冗余 CAN 通道(配置后指令帧同内容发 0x18EF2011;留空=不发送)
// red_channel = CAN2
cmd_period_ms = 500
reset_hold_sec = 5
web_port = 18081
web_enable = true
logpath = /root/work/h100/data/pMotor.log
}
+2
View File
@@ -5,4 +5,6 @@ ProcessConfig = pPowerManger
// CCU 地址;不配置则默认 127.0.0.1:7000
// ccuhost = 127.0.0.1
// ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
}
+28 -4
View File
@@ -7,7 +7,7 @@
#
# 用法:
# ./scripts/build-board.sh # 同步+编译+部署(不重启)
# ./scripts/build-board.sh --start # 编译后重启全部服务(含 pCanBridge)
# ./scripts/build-board.sh --start # 编译后重启全部服务(含 pCanBridge/pMotor)
# ./scripts/build-board.sh --clean # 板卡上重新 cmake 再编译
# ./scripts/build-board.sh --jobs 4 # 指定并行度(默认板卡 nproc)
#
@@ -68,7 +68,7 @@ DO_CLEAN=0
CCU_HOST=""
CCU_PORT=""
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge)
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge pMotor)
info() { printf '\033[1;36m[build-board]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[build-board]\033[0m %s\n' "$*"; }
@@ -211,12 +211,13 @@ ${SSH} "${SSH_TARGET}" "mkdir -p ${BOARD_BIN} && \
cp -f ${BOARD_SRC}/bin/pPowerMangerHost ${BOARD_BIN}/pPowerMangerHost && \
cp -f ${BOARD_SRC}/bin/pCCU ${BOARD_BIN}/pCCU && \
cp -f ${BOARD_SRC}/bin/pCanBridge ${BOARD_BIN}/pCanBridge && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost ${BOARD_BIN}/pCCU ${BOARD_BIN}/pCanBridge" \
cp -f ${BOARD_SRC}/bin/pMotor ${BOARD_BIN}/pMotor && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost ${BOARD_BIN}/pCCU ${BOARD_BIN}/pCanBridge ${BOARD_BIN}/pMotor" \
|| { err "部署产物失败"; exit 1; }
ok "产物已部署"
#-------------------------------------------------------------------
# 4.5 安装/刷新 pCanBridge systemd unit(幂等,其它 4 个 unit 已装)
# 4.5 安装/刷新 pCanBridge systemd unit(幂等,其它 5 个 unit 已装)
#-------------------------------------------------------------------
info "安装 pCanBridge.service ..."
${SSH} "${SSH_TARGET}" "cat > /etc/systemd/system/pCanBridge.service" <<EOF
@@ -238,6 +239,29 @@ EOF
${SSH} "${SSH_TARGET}" "systemctl daemon-reload && systemctl reset-failed pCanBridge 2>/dev/null || true"
ok "pCanBridge.service 已安装"
#-------------------------------------------------------------------
# 4.6 安装/刷新 pMotor systemd unit(幂等)
#-------------------------------------------------------------------
info "安装 pMotor.service ..."
${SSH} "${SSH_TARGET}" "cat > /etc/systemd/system/pMotor.service" <<EOF
[Unit]
Description=pMotor Propulsion Motor for H100 Power Manager
After=moosdb.service pCanBridge.service
Requires=moosdb.service
[Service]
Type=simple
WorkingDirectory=${BOARD_BIN}
ExecStart=${BOARD_BIN}/pMotor --alias=pMotor /root/work/h100/missions/h100.moos
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
${SSH} "${SSH_TARGET}" "systemctl daemon-reload && systemctl reset-failed pMotor 2>/dev/null || true"
ok "pMotor.service 已安装"
echo ""
ok "产物: ${BOARD_BIN}/pPowerManger (aarch64)"
${SSH} "${SSH_TARGET}" "file ${BOARD_BIN}/pPowerManger | cut -c1-60"
+176
View File
@@ -0,0 +1,176 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/sync-board-time.sh
# DESC: 校正 223 板卡(RK3588,默认 192.168.0.223)的系统时间:以
# 本机时间(或 --time 指定时间)为基准,经 SSH 设置板卡系统时
# 钟,并尽力写入硬件时钟 (RTC)。板卡网络隔离无 NTP,用本脚本对时。
#
# 用法:
# ./scripts/sync-board-time.sh # 用本机时间校正板卡
# ./scripts/sync-board-time.sh status # 只查看时间与偏差
# ./scripts/sync-board-time.sh --time "2026-09-01 12:00:00"
# # 用指定时间校正(本地时区)
#
# 参数:
# --host <ip> 目标主机(默认 192.168.0.223)
# --user <user> 登录用户(默认 root)
# --time <str> 手动指定基准时间(date 可解析的格式,按本地时区解释)
# --keep-ntp 不执行 timedatectl set-ntp false
# --no-rtc 不写硬件时钟(跳过 hwclock --systohc)
# -h, --help 显示帮助
#
# 说明:
# - 需要 root 登录与免密 SSH(deploy.sh setup-ssh)。
# - 对时经 SSH 往返时延 (RTT) 折半补偿,精度约 ±0.1s(受网络抖动影响)。
# - 板卡若无可用 RTC,hwclock 失败仅告警;重启后时间可能回跳。
# - 对时会调整系统时钟,运行中的服务日志时间戳会跳变,属正常现象。
#=======================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOST="192.168.0.223"
USER="root"
SET_TIME=""
KEEP_NTP=0
NO_RTC=0
info() { printf '\033[1;36m[time]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[time]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[time]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[time]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
#-------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
--host) [ $# -ge 2 ] || { err "--host 需要参数"; exit 1; }; HOST="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--time) [ $# -ge 2 ] || { err "--time 需要参数"; exit 1; }; SET_TIME="$2"; shift 2 ;;
--keep-ntp) KEEP_NTP=1; shift ;;
--no-rtc) NO_RTC=1; shift ;;
status|help|-h|--help) ACTION="$1"; shift ;;
"") shift ;;
*) err "未知参数: $1(见 help)"; exit 1 ;;
esac
done
[ -n "${ACTION}" ] || ACTION="sync"
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
#-------------------------------------------------------------------
# 读取板卡时间并测 RTT
# 输出: "<epoch>|<板卡可读时间> <t0> <t1>"(t0/t1 为本机采样时刻)
#-------------------------------------------------------------------
read_board_time() {
local t0 t1 out
t0="$(date +%s.%N)"
out="$(${SSH} "${SSH_TARGET}" 'date "+%s.%N|%F %T %Z"' 2>/dev/null)" || {
err "无法免密 SSH 到 ${SSH_TARGET}(先执行: ./scripts/deploy.sh setup-ssh)" >&2
return 1
}
t1="$(date +%s.%N)"
printf '%s %.3f %.3f\n' "${out}" "${t0}" "${t1}"
}
# 从 read_board_time 输出解析: set -- <out> 后取 $1/$2/$3
parse_board_time() {
B_EPOCH="${1%%|*}"
B_DATE="${1#*|}"
T0="$2"
T1="$3"
MID="$(awk -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t0 + (t1-t0)/2}')"
DIFF="$(awk -v b="${B_EPOCH}" -v m="${MID}" 'BEGIN{printf "%+.3f", b - m}')"
}
#-------------------------------------------------------------------
# 命令: status
#-------------------------------------------------------------------
cmd_status() {
local out
out="$(read_board_time)" || return 1
parse_board_time ${out}
local rtt
rtt="$(awk -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t1-t0}')"
printf '本机时间: %s (RTT %.3fs)\n' "$(date -d "@${MID}" '+%F %T')" "${rtt}"
printf '板卡时间: %s\n' "${B_DATE}"
printf '偏差: 板卡比本机快 %s 秒\n' "${DIFF}"
}
#-------------------------------------------------------------------
# 命令: sync(默认)
#-------------------------------------------------------------------
cmd_sync() {
local out target board_reply
# 前置检查:SSH 连通 + 板卡 root 权限
out="$(read_board_time)" || return 1
parse_board_time ${out}
if ! ${SSH} "${SSH_TARGET}" '[ "$(id -u)" = 0 ]' 2>/dev/null; then
err "板卡登录用户需要 root 权限(当前 --user ${USER})"
return 1
fi
ok "SSH 连接正常: ${SSH_TARGET}"
info "当前偏差: 板卡比本机快 ${DIFF} 秒"
# 关闭 NTP 自动同步(板卡网络隔离本就无 NTP 源,防止干扰手动对时)
if [ "${KEEP_NTP}" = "0" ]; then
info "关闭板卡 NTP 自动同步 (timedatectl set-ntp false) ..."
${SSH} "${SSH_TARGET}" "command -v timedatectl >/dev/null 2>&1 && timedatectl set-ntp false || true"
fi
# 计算目标时间:手动指定值直接用;否则取本机时间 + RTT/2 补偿
if [ -n "${SET_TIME}" ]; then
target="$(date -d "${SET_TIME}" '+%s.%N' 2>/dev/null)" || {
err "无法解析时间: ${SET_TIME}"; return 1;
}
info "基准时间(手动): ${SET_TIME}"
else
local t2
t2="$(date +%s.%N)"
target="$(awk -v t2="${t2}" -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t2 + (t1-t0)/2}')"
info "基准时间(本机 + RTT/2 补偿)"
fi
info "设置板卡系统时间 -> $(date -d "@${target}" '+%F %T') ..."
board_reply="$(${SSH} "${SSH_TARGET}" "date -s @${target} 2>&1")" || {
err "date -s 失败: ${board_reply}"
return 1
}
ok "板卡返回: ${board_reply}"
# 写硬件时钟(无 RTC 时仅告警)
if [ "${NO_RTC}" = "0" ]; then
info "同步硬件时钟 (hwclock --systohc) ..."
if ${SSH} "${SSH_TARGET}" "hwclock --systohc" 2>/dev/null; then
ok "RTC 已同步"
else
warn "hwclock 失败(板卡可能无 RTC 或驱动未加载),重启后时间可能回跳"
fi
fi
# 校验
out="$(read_board_time)" || return 1
parse_board_time ${out}
info "校正后偏差: 板卡比本机快 ${DIFF} 秒"
ok "板卡时间: ${B_DATE}"
}
#-------------------------------------------------------------------
# 命令入口
#-------------------------------------------------------------------
case "${ACTION}" in
sync) cmd_sync ;;
status) cmd_status ;;
help|-h|--help) usage ;;
*) usage ;;
esac
+1
View File
@@ -22,6 +22,7 @@ add_subdirectory(pPMtest)
add_subdirectory(pPowerMangerHost)
add_subdirectory(pCCU)
add_subdirectory(pCanBridge)
add_subdirectory(pMotor)
##############################################################################
# END of CMakeLists.txt
##############################################################################
+186 -17
View File
@@ -5,6 +5,7 @@
#include "protocol/CanBms.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
using namespace ccu;
using namespace std;
@@ -17,8 +18,10 @@ CCU::CCU() {}
CCU::~CCU() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
if (m_coord) { delete m_coord; m_coord = nullptr; }
if (m_bridge) { delete m_bridge; m_bridge = nullptr; }
if (m_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; }
if (m_rcuLink) { m_rcuLink->stop(); delete m_rcuLink; m_rcuLink = nullptr; }
if (m_db) { m_db->close(); delete m_db; m_db = nullptr; }
if (m_snap) { delete m_snap; m_snap = nullptr; }
if (m_sysData) { delete m_sysData; m_sysData = nullptr; }
@@ -49,8 +52,13 @@ bool CCU::OnStartUp() {
else if (param == "pm_local_port") m_pmLocalPort = atol(value.c_str());
else if (param == "pm_remote_ip") m_pmHost = value;
else if (param == "pm_remote_port") m_pmPort = atol(value.c_str());
else if (param == "rcu_enable") m_rcuEnable = (tolower(value) == "true" || value == "1");
else if (param == "rcu_local_port") m_rcuLocalPort = atol(value.c_str());
else if (param == "rcu_remote_ip") m_rcuHost = value;
else if (param == "rcu_remote_port") m_rcuPort = atol(value.c_str());
else if (param == "dbpath") m_dbPath = value;
else if (param == "logpath") m_logPath = value;
else if (param == "bcu_can_channel") m_bcuCanChannel = value;
else if (param == "web_port") m_webPort = atoi(value.c_str());
else if (param == "web_enable") {
m_webEnable = (tolower(value) == "true" || value == "1");
@@ -81,6 +89,9 @@ bool CCU::OnStartUp() {
// 系统数据快照
m_sysData = new SystemData();
// 配电桥接器(须在链路 start 前完成依赖注入,避免接收线程空指针)
m_bridge = new DisBridge();
// FC 链路
m_fcLink = new FcLinkManager(m_fcLocalPort, m_fcHost, m_fcPort);
m_fcLink->setLogSink(m_db);
@@ -90,19 +101,40 @@ bool CCU::OnStartUp() {
m_fcLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) {
// 原始帧已通过 LogSink 落库;如需 Web 实时原始报文可在此扩展
});
if (!m_fcLink->start())
LOG_F(ERROR, "FC link start failed");
// PM 链路
m_pmLink = new PmLinkManager(m_pmLocalPort, m_pmHost, m_pmPort);
m_pmLink->setLogSink(m_db);
m_pmLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
// 配电指令(0x0001~0x0004,Sum8 短帧)优先桥接转发真实CCU;
// 注册表按帧总长区分配电指令与 PM 操控/参数指令(共用 id)
if (m_bridge && m_bridge->onPmFrame(msg, frame)) return;
handlePmMessage(msg, frame);
});
m_pmLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
});
// 真实 CCU 链路(配电桥接,rcu_enable 开启时创建)
if (m_rcuEnable) {
m_rcuLink = new RcuLinkManager(m_rcuLocalPort, m_rcuHost, m_rcuPort);
m_rcuLink->setLogSink(m_db);
m_rcuLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
handleRcuMessage(msg, frame);
});
m_rcuLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
});
}
// 注入桥接依赖(m_rcuLink 可为空:未启用桥接时相关入口自动失效)
m_bridge->setup(m_pmLink, m_rcuLink, m_sysData);
// 启动链路
if (!m_fcLink->start())
LOG_F(ERROR, "FC link start failed");
if (!m_pmLink->start())
LOG_F(ERROR, "PM link start failed");
if (m_rcuLink && !m_rcuLink->start())
LOG_F(ERROR, "RCU link start failed on port %ld", m_rcuLocalPort);
// 锂电池:CAN 总线(BMS 协议),数据经 pCanBridge 以 CAN_0x* 消息透传,
// 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。
@@ -112,7 +144,7 @@ bool CCU::OnStartUp() {
m_coord->setup(m_sysData, m_fcLink, m_pmLink);
// 快照构建器(链路就绪后创建)
m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_db);
m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_rcuLink, m_db);
// 网页
if (m_webEnable) {
@@ -128,10 +160,20 @@ bool CCU::OnStartUp() {
}
}
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort);
if (m_rcuLink) {
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"rcu_link local=%ld -> %s:%ld (配电桥接), "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort,
m_rcuLocalPort, m_rcuHost.c_str(), m_rcuPort);
} else {
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"rcu_link disabled, "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort);
}
return true;
}
@@ -155,6 +197,7 @@ void CCU::registerVariables() {
// 运行状态变量
Register("CCU_FC_LINK_STATE", 0);
Register("CCU_PM_LINK_STATE", 0);
Register("CCU_RCU_LINK_STATE", 0);
}
//---------------------------------------------------------
@@ -197,20 +240,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);
}
}
}
@@ -230,6 +279,9 @@ bool CCU::Iterate() {
m_lastStatusTx = now;
}
// BCU 断路器控制指令下发(网页 -> 队列 -> MOOS CAN_TX_*)
sendPendingBcuCtrl();
// 电源协调器:协调算法占位
if (m_coord)
m_coord->tick(now);
@@ -275,6 +327,8 @@ void CCU::handleFcMessage(Message* msg, const std::vector<uint8_t>& frame) {
void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return;
// 配电指令已在入口由 DisBridge 处理(见 OnStartUp 的 onMessage 接线),
// 到达此处的均为 PM 协议消息(Sum32 校验)
switch (msg->id()) {
case 0x0001: { // PM 操控指令 -> 转发给 FC
PmControlValue c;
@@ -334,6 +388,19 @@ void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
}
}
//---------------------------------------------------------
// handleRcuMessage:处理真实 CCU 链路收到的消息(配电桥接)
//
// 配电反馈(0x0005~0x0008)与真实CCU状态报文(0x0004)由 DisBridge
// 处理(原帧转发 pPowerManger / 仅解码显示);其余(如配电指令
// 回显)仅记录,不转发。
void CCU::handleRcuMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return;
if (m_bridge && m_bridge->onRcuFrame(msg, frame)) return;
LOG_F(WARNING, "[RCU] unhandled msg id 0x%04X, len=%zu", msg->id(), frame.size());
}
//---------------------------------------------------------
// sendPmStatus:整合最新 FC 状态,编码 PM 状态报文发送
@@ -441,16 +508,99 @@ std::string CCU::buildSnapshot() {
}
//---------------------------------------------------------
// handleApi:/api/logs
// handleApi:/api/logs、/api/bcu_ctrl
std::string CCU::handleApi(const std::string& uri, const std::string& query) {
if (uri == "/api/logs") {
if (m_snap) return m_snap->buildLogs(50);
return "[]";
}
if (uri == "/api/bcu_ctrl") {
return handleBcuCtrlApi(query);
}
return "";
}
//---------------------------------------------------------
// handleBcuCtrlApi:BCU 断路器控制指令下发
//
// GET /api/bcu_ctrl?addr=5&action=1 action: 1 闭合 / 0 断开(总正+总负)
// GET /api/bcu_ctrl?addr=5&pos=1&neg=0 也可单独指定总正/总负
// Web 线程不可直接 Notify,指令入队后由 Iterate(MOOS 线程)下发。
std::string CCU::handleBcuCtrlApi(const std::string& query) {
if (!m_sysData) return "{\"ok\":false,\"error\":\"not ready\"}";
// 简易 query 解析(k=v&k=v)
auto getParam = [&query](const char* key, int& out) -> bool {
std::string k = std::string(key) + "=";
size_t p = query.find(k);
if (p == std::string::npos) return false;
size_t e = query.find('&', p);
std::string v = query.substr(p + k.size(),
(e == std::string::npos) ? std::string::npos
: e - p - k.size());
out = atoi(v.c_str());
return true;
};
int addr = 0, action = -1, pos = -1, neg = -1;
getParam("addr", addr);
getParam("action", action);
getParam("pos", pos);
getParam("neg", neg);
if (addr < kBcuAddrMin || addr > kBcuAddrMax)
return "{\"ok\":false,\"error\":\"bad addr\"}";
if (pos < 0) pos = action; // 未单独指定时跟随 action
if (neg < 0) neg = action;
if (action < 0 && (pos < 0 || neg < 0))
return "{\"ok\":false,\"error\":\"missing action\"}";
if (action > 1 || pos > 1 || neg > 1)
return "{\"ok\":false,\"error\":\"bad value\"}";
BcuCtrlCmd c;
c.addr = static_cast<uint8_t>(addr);
c.pos = static_cast<uint8_t>(pos);
c.neg = static_cast<uint8_t>(neg);
if (!m_sysData->pushBcuCtrlCmd(c))
return "{\"ok\":false,\"error\":\"queue full\"}";
LOG_F(INFO, "[BCU/CAN] 断路器指令入队: 节点%d 总正=%d 总负=%d", addr, pos, neg);
char buf[96];
std::snprintf(buf, sizeof(buf),
"{\"ok\":true,\"addr\":%d,\"pos\":%d,\"neg\":%d}", addr, pos, neg);
return buf;
}
//---------------------------------------------------------
// sendPendingBcuCtrl:出队控制指令,编码 0x10XX81FF 经 MOOS 下发
// 链路:MOOS CAN_TX_0x*(m_sSrcAux=目标通道) -> pCanBridge 订阅
// -> 按通道路由 CanEndpoint(TCP) -> CANET -> CAN 总线
void CCU::sendPendingBcuCtrl() {
if (!m_sysData) return;
BcuCtrlCmd c;
while (m_sysData->popBcuCtrlCmd(c)) {
uint8_t data[8];
buildBcuRelayCtrlData(c.pos, c.neg, data);
uint32_t canId = bcuCtrlCanId(c.addr);
char key[32];
std::snprintf(key, sizeof(key), "CAN_TX_0x%08X", canId);
// 二进制构造同 pCanBridge 上行发布(MOOS_BINARY_STRING)
CMOOSMsg msg(MOOS_NOTIFY, key,
static_cast<unsigned int>(sizeof(data)), data);
// 目标通道经 m_sSrcAux 指定(与上行 CAN_0x* 的通道标注对称)
msg.SetSourceAux(m_bcuCanChannel);
bool ok = m_Comms.Post(msg);
m_sysData->noteBcuCtrlSent(c, ok);
LOG_F(INFO, "[BCU/CAN] 断路器指令 0x%08X 节点%d 总正=%d 总负=%d 通道=%s %s",
canId, c.addr, c.pos, c.neg, m_bcuCanChannel.c_str(),
ok ? "已投递MOOS" : "投递失败");
}
}
//---------------------------------------------------------
// buildReport
@@ -470,15 +620,34 @@ bool CCU::buildReport() {
<< " tx:" << m_pmLink->txCount()
<< " err:" << m_pmLink->errorCount() << "\n";
}
if (m_rcuLink) {
m_msgs << "RCU 链路 local:" << m_rcuLink->localPort()
<< " -> " << m_rcuLink->rcuHost() << ":" << m_rcuLink->rcuPort()
<< " rx:" << m_rcuLink->rxCount()
<< " tx:" << m_rcuLink->txCount()
<< " err:" << m_rcuLink->errorCount() << "\n";
if (m_sysData) {
const RealCcuState& st = m_sysData->realCcu();
m_msgs << "配电桥接: 反馈转发 " << st.fwdFbOk << " 成功 / " << st.fwdFbErr
<< " 失败, 指令转发 " << st.fwdCmdOk << " 成功 / " << st.fwdCmdErr
<< " 失败\n";
}
}
if (m_sysData) {
m_msgs << "锂电池: BMS/CAN (经 pCanBridge CAN_0x* 订阅)\n";
m_msgs << "CAN 帧接收计数:" << m_sysData->canFrameCount() << "\n";
m_msgs << "BMS 报文接收计数:" << m_sysData->bmsStatusCount() << "\n";
m_msgs << "FC 状态接收次数:" << m_sysData->fcStatusCount() << "\n";
m_msgs << "PM 指令接收次数:" << m_sysData->pmControlCount() << "\n";
const BcuCtrlStat& bc = m_sysData->bcuCtrlStat();
m_msgs << "断路器指令下发:" << bc.sentCount << " 成功 / " << bc.errCount
<< " 失败 (最近: 节点" << static_cast<int>(bc.last.addr)
<< " 总正=" << static_cast<int>(bc.last.pos)
<< " 总负=" << static_cast<int>(bc.last.neg) << ")\n";
}
if (m_db) {
m_msgs << "数据库记录数:" << m_db->count() << "\n";
m_msgs << "数据库BMS记录数:" << m_db->countBcu() << "\n";
}
return true;
}
+23 -1
View File
@@ -5,9 +5,11 @@
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "comm/FcLinkManager.h"
#include "comm/PmLinkManager.h"
#include "comm/RcuLinkManager.h"
#include "core/SystemData.h"
#include "core/SnapshotBuilder.h"
#include "core/PowerCoordinator.h"
#include "core/DisBridge.h"
#include "store/DbStore.h"
#include "web/WebServer.h"
@@ -20,10 +22,13 @@ namespace ccu {
// 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger
// 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池
// 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003)
// 收 PM 配电指令(0x0001~0x0004,Sum8短帧) -> DisBridge 原帧转发真实CCU(rcu 链路)
// 收真实CCU 配电反馈(0x0005~0x0008) -> DisBridge 原帧转发 pPowerManger + 解码显示
// 收真实CCU 状态报文(0x0004) -> 仅解码显示,不转发(避免状态冲突)
// 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文
// (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照
// (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用)
// 收发全部帧落库 SQLite;网页周期推送 JSON 快照。
// 收发全部帧落库 SQLite(锂电池 BMS 解析数据另存 bcu_node 表);网页周期推送 JSON 快照。
//============================================================================
class CCU : public AppCastingMOOSApp {
@@ -43,6 +48,7 @@ private:
// 链路收帧处理
void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame);
void handlePmMessage(Message* msg, const std::vector<uint8_t>& frame);
void handleRcuMessage(Message* msg, const std::vector<uint8_t>& frame);
// MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布)
void handleCanMessage(CMOOSMsg& msg);
@@ -54,6 +60,11 @@ private:
std::string buildSnapshot();
std::string handleApi(const std::string& uri, const std::string& query);
// BCU 断路器控制:/api/bcu_ctrl 解析与下发(经 MOOS CAN_TX_* -> pCanBridge)
std::string handleBcuCtrlApi(const std::string& query);
// 出队并下发一条 BCU 控制指令(Iterate 调用,MOOS 线程内 Notify 安全)
void sendPendingBcuCtrl();
// 配置
long m_fcLocalPort = 6000;
std::string m_fcHost = "192.168.1.162";
@@ -63,11 +74,20 @@ private:
std::string m_pmHost = "192.168.0.140";
long m_pmPort = 5001;
// 真实 CCU 桥接链路(配电协议透传)
bool m_rcuEnable = false; // 默认关闭,.moos 中开启
long m_rcuLocalPort = 7100;
std::string m_rcuHost = "192.168.0.200"; // 占位地址,.moos 中改为实际值
long m_rcuPort = 7000;
std::string m_dbPath = "pccu_data.db";
std::string m_logPath = "pCCU.log";
int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开
bool m_webEnable = true;
// BCU 断路器控制帧下发的目标 CAN 通道(经 pCanBridge 的 m_sSrcAux 路由)
std::string m_bcuCanChannel = "CAN0";
// CAN BMS 日志节流(避免 60帧/s 刷爆日志)
double m_lastBmsLog = 0;
@@ -78,6 +98,8 @@ private:
// 组件
FcLinkManager* m_fcLink = nullptr;
PmLinkManager* m_pmLink = nullptr;
RcuLinkManager* m_rcuLink = nullptr;
DisBridge* m_bridge = nullptr;
SystemData* m_sysData = nullptr;
DbStore* m_db = nullptr;
SnapshotBuilder* m_snap = nullptr;
+11 -1
View File
@@ -19,7 +19,9 @@ void showSynopsis() {
blk(" It receives FC status, integrates & forwards to pPowerManger,");
blk(" maps pPowerManger commands to FC operations, receives CAN ");
blk(" bus BMS battery frames via pCanBridge (CAN_0x* MOOS msgs), ");
blk(" stores data in SQLite and provides a web monitoring page. ");
blk(" bridges power-distribution (配电) frames between pPowerManger");
blk(" and a real CCU device (rcu link), stores data in SQLite and ");
blk(" provides a web monitoring page. ");
blk(" ");
}
@@ -68,6 +70,10 @@ void showExampleConfigAndExit() {
blk(" pm_local_port = 7000 // PM 指令接收端口 ");
blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP ");
blk(" pm_remote_port = 5001 // 控制主机接收端口 ");
blk(" rcu_enable = true // 是否启用真实CCU配电桥接 ");
blk(" rcu_local_port = 7100 // 真实CCU 数据接收端口 ");
blk(" rcu_remote_ip = 192.168.0.200 // 真实CCU IP(占位,按实际修改) ");
blk(" rcu_remote_port= 7000 // 真实CCU 接收端口 ");
blk(" // 锂电池 BMS 走 CAN 总线(经 pCanBridge 的 CAN_0x* 透传) ");
blk(" dbpath = pccu_data.db // 数据库路径 ");
blk(" logpath = pCCU.log // 日志路径 ");
@@ -91,11 +97,15 @@ void showInterfaceAndExit() {
blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(锂电池 BMS) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CAN_TX_0x%08X = BCU 断路器控制帧 0x10XX81FF(二进制 ");
blk(" 数据域,经 pCanBridge 下行到 CAN 总线) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" ");
exit(0);
}
+3 -1
View File
@@ -1,6 +1,6 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pCCU
# 复合管控器网关:FC <-> pPowerManger
# 复合管控器网关:FC <-> pPowerManger,配电桥接 <-> 真实CCU
#--------------------------------------------------------
if (${WIN32})
@@ -24,6 +24,7 @@ SET(CCU_PROTOCOL_SRC
protocol/MessageRegistry.cpp
protocol/FcProtocol.cpp
protocol/PmProtocol.cpp
protocol/DisProtocol.cpp
protocol/CanBms.cpp
)
@@ -36,6 +37,7 @@ SET(CCU_CORE_SRC
core/SnapshotBuilder.cpp
core/PowerCoordinator.cpp
core/CoordFsm.cpp
core/DisBridge.cpp
)
SET(CCU_STORE_SRC
+31 -2
View File
@@ -54,17 +54,46 @@ bool LinkManager::sendMessageTo(uint16_t id, const void* value,
return true;
}
bool LinkManager::sendRaw(const std::vector<uint8_t>& frame) {
if (frame.empty()) return false;
const std::string& host = defaultRemoteHost();
long port = defaultRemotePort();
if (host.empty() || port <= 0) return false;
if (!m_endpoint.sendTo(host, port, frame)) {
LOG_F(ERROR, "[%s] send raw frame (%zu B) to %s:%ld failed",
m_linkName.c_str(), frame.size(), host.c_str(), port);
return false;
}
++m_txCount;
uint16_t id = 0;
Frame::parseId(frame, id);
if (m_logSink) m_logSink->onRawFrame(1, id, m_linkName, frame, true);
if (m_onRawFrame) m_onRawFrame(1, frame, true);
return true;
}
bool LinkManager::handleIncoming(const std::vector<uint8_t>& frame) {
if (frame.size() < FRAME_HEADER_LEN) return false;
uint16_t id = 0;
if (!Frame::parseId(frame, id)) {
LOG_F(WARNING, "[%s] bad frame header", m_linkName.c_str());
// 非本协议帧(如真实CCU 混发的旧协议 0x20 0x20 状态帧、无效帧):
// 原样落库便于排查,告警按 10s 节流避免刷屏
++m_errorCount;
if (m_logSink) m_logSink->onRawFrame(0, 0, m_linkName, frame, false);
double now = MOOSTime(false);
if (now - m_lastBadHdrLog >= 10.0) {
m_lastBadHdrLog = now;
LOG_F(WARNING, "[%s] 非本协议帧头(%02X %02X, %zu B),已忽略(10s节流)",
m_linkName.c_str(), frame[0], frame[1], frame.size());
}
return false;
}
Message* msg = m_registry.find(id);
// 优先按 id + 实际帧总长精确匹配(配电指令 0x0001~0x0004 与 PM 指令
// 共用 id,仅帧长不同;帧头 length 字段不可信,须用实际收到的字节数),
// 未命中回退该 id 的第一条(保持旧行为:decode 失败报错)
Message* msg = m_registry.find(id, frame.size());
if (!msg) {
LOG_F(WARNING, "[%s] unknown msg id 0x%04X, len=%zu",
m_linkName.c_str(), id, frame.size());
+8 -1
View File
@@ -48,7 +48,9 @@ public:
MessageRegistry& registry() { return m_registry; }
const MessageRegistry& registry() const { return m_registry; }
// 收帧回调:msg 为注册表中命中的消息对象(可调用 decode 解码),frame 为原始完整帧
// 收帧回调:msg 为注册表中命中的消息对象(可调用 decode 解码),frame 为原始完整帧。
// 注意:同 id 不同长度的消息(如配电指令与 PM 指令)按帧总长精确区分,
// 回调携带的 msg 即精确命中的对象;长度未命中时回退该 id 的第一条。
void setOnMessage(std::function<void(Message* msg, const std::vector<uint8_t>& frame)> cb) {
m_onMessage = std::move(cb);
}
@@ -68,6 +70,10 @@ public:
bool sendMessageTo(uint16_t id, const void* value,
const std::string& host, long port);
// 原样发送一帧到默认远端(桥接透传用,不做编码);
// 消息 id 从帧头解析,收发计数与落库口径与 sendMessage 一致
bool sendRaw(const std::vector<uint8_t>& frame);
// 处理一帧收到的原始数据(供接收线程调用,也可测试时手动喂入)
bool handleIncoming(const std::vector<uint8_t>& frame);
@@ -102,6 +108,7 @@ private:
ILinkLogSink* m_logSink = nullptr;
double m_lastRxTime = 0;
double m_lastBadHdrLog = 0; // 非本协议帧头告警节流
unsigned long m_rxCount = 0;
unsigned long m_txCount = 0;
unsigned long m_errorCount = 0;
+8
View File
@@ -3,6 +3,7 @@
#include "LinkManager.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
@@ -11,6 +12,12 @@ namespace ccu {
//
// - 本地监听:控制主机指令端口(默认 7000)
// - 发送目标:控制主机(默认 192.168.0.140:5001)
//
// 注册的消息:
// - PM 协议 0x0001~0x0004(Sum32)
// - 配电指令 0x0001~0x0004(Sum8,13~21B 短帧):pPowerManger 下发的
// 配电指令与其操控/参数指令共用 id,仅帧长不同,由注册表按
// (id+帧长) 区分,DisBridge 据此原桥接转发真实CCU
// 端口/地址均可由 .moos 配置覆盖。
//============================================================================
@@ -20,6 +27,7 @@ public:
const std::string& pmHost, long pmPort)
: LinkManager("pm", localPort), m_pmHost(pmHost), m_pmPort(pmPort) {
registerPmMessages(registry());
registerDisCmdMessages(registry());
}
void setPmAddress(const std::string& host, long port) {
+51
View File
@@ -0,0 +1,51 @@
#ifndef PCCU_RCU_LINK_MANAGER_H
#define PCCU_RCU_LINK_MANAGER_H
#include "LinkManager.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
//============================================================================
// RcuLinkManager:面向真实 CCU 设备的链路(配电桥接)。
//
// - 本地监听:rcu_local_port(默认 7100),接收真实CCU 上报的数据
// - 发送目标:真实CCU(默认占位 192.168.0.200:7000,.moos 可配)
//
// 注册的消息:
// - 配电反馈 0x0005~0x0008(Sum8):真实CCU 上报的配电状态,
// 由 DisBridge 原帧转发给 pPowerManger 并解码显示
// - 配电指令 0x0001~0x0004(Sum8):真实CCU 的指令回显仅识别,不转发
// - PM 消息 0x0001~0x0004(Sum32):真实CCU 状态报文 0x0004 仅解码
// 显示,不转发(避免与 pCCU 自身模拟的状态报文冲突)
//============================================================================
class RcuLinkManager : public LinkManager {
public:
RcuLinkManager(long localPort,
const std::string& rcuHost, long rcuPort)
: LinkManager("rcu", localPort), m_rcuHost(rcuHost), m_rcuPort(rcuPort) {
registerDisFbMessages(registry());
registerDisCmdMessages(registry());
registerPmMessages(registry());
}
void setRcuAddress(const std::string& host, long port) {
m_rcuHost = host; m_rcuPort = port;
}
const std::string& rcuHost() const { return m_rcuHost; }
long rcuPort() const { return m_rcuPort; }
protected:
std::string defaultRemoteHost() const override { return m_rcuHost; }
long defaultRemotePort() const override { return m_rcuPort; }
private:
std::string m_rcuHost;
long m_rcuPort;
};
} // namespace ccu
#endif // PCCU_RCU_LINK_MANAGER_H
+1
View File
@@ -36,6 +36,7 @@ bool UdpEndpoint::init(long localPort) {
bool UdpEndpoint::sendTo(const std::string& host, long port, const std::vector<uint8_t>& data) {
if (!m_socket || data.empty()) return false;
try {
std::lock_guard<std::mutex> lock(m_sendMutex);
m_socket->iSendMessageTo(const_cast<uint8_t*>(data.data()),
static_cast<int>(data.size()),
port, host);
+4
View File
@@ -4,6 +4,7 @@
#define UNIX
#include "MOOS/libMOOS/Comms/XPCUdpSocket.h"
#include <cstdint>
#include <mutex>
#include <string>
#include <vector>
@@ -17,6 +18,8 @@ namespace ccu {
// - 向指定远端 (host:port) 发送数据报
// - 阻塞接收数据报
// 不关心协议内容,仅负责字节的收发。
// 发送可能来自多个线程(MOOS 主线程 + 各链路接收线程的桥接转发),
// sendTo 内部加锁串行化。
//============================================================================
class UdpEndpoint {
@@ -39,6 +42,7 @@ public:
private:
XPCUdpSocket* m_socket = nullptr;
long m_localPort = 0;
std::mutex m_sendMutex; // 串行化并发 sendTo
};
} // namespace ccu
+3 -3
View File
@@ -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;
}
+3 -3
View File
@@ -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) {}
};
//---- 状态机 ---------------------------------------------------------------
+173
View File
@@ -0,0 +1,173 @@
#include "DisBridge.h"
#include "../comm/LinkManager.h"
#include "../protocol/DisProtocol.h"
#include "SystemData.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "../pPowerManger/logc/loguru.hpp"
namespace ccu {
void DisBridge::setup(LinkManager* pmLink, LinkManager* rcuLink, SystemData* sys) {
m_pmLink = pmLink;
m_rcuLink = rcuLink;
m_sys = sys;
}
//--------------------------------------------------------------------------
// PM 链路入口:pPowerManger -> 真实CCU(配电指令)
//--------------------------------------------------------------------------
bool DisBridge::onPmFrame(Message* msg, const std::vector<uint8_t>& frame) {
if (!isDisCmdMessage(msg)) return false;
forwardCmdToRcu(msg, frame);
return true;
}
void DisBridge::forwardCmdToRcu(Message* msg, const std::vector<uint8_t>& frame) {
if (!m_rcuLink || !m_sys) return;
// 原帧透传给真实CCU(帧字节不变,Sum8 校验和保持有效)
bool ok = m_rcuLink->sendRaw(frame);
// 取副本 -> 更新 -> 写回(跨线程共享快照的既有模式)
RealCcuState st = m_sys->realCcu();
st.cmdCount++;
if (ok) st.fwdCmdOk++; else st.fwdCmdErr++;
// 解码缓存最近指令(仅用于展示;解码失败不影响透传结果)
if (msg) {
bool decoded = false;
switch (msg->id()) {
case DIS_HV_CMD_ID: {
DisHighVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvCmd);
break;
}
case DIS_HVA_CMD_ID: {
DisHighAVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvaCmd);
break;
}
case DIS_HVB_CMD_ID: {
DisHighBVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvbCmd);
break;
}
case DIS_LV_CMD_ID: {
DisLowBusCmdMessage m;
decoded = m.decode(frame, st.lastLvCmd);
break;
}
default:
break;
}
if (decoded) {
st.cmdValid = true;
st.lastCmdRx = MOOSTime(false);
} else {
LOG_F(WARNING, "[桥接] 配电指令 0x%04X 解码失败(透传不受影响)", msg->id());
}
}
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 配电指令 0x%04X (%zu B) -> 真实CCU %s", msg ? msg->id() : 0,
frame.size(), ok ? "成功" : "失败");
}
//--------------------------------------------------------------------------
// RCU 链路入口:真实CCU -> pPowerManger(配电反馈)+ 状态报文显示
//--------------------------------------------------------------------------
bool DisBridge::onRcuFrame(Message* msg, const std::vector<uint8_t>& frame) {
if (isDisFbMessage(msg)) {
forwardFbToPm(msg, frame);
return true;
}
if (!isDisCmdMessage(msg) && msg && msg->id() == 0x0004) {
// 真实CCU 状态报文:仅解码显示,不转发(避免与 pCCU 模拟状态冲突)
storeRealCcuStatus(frame);
return true;
}
return false; // 配电指令回显等:不处理,交上层记录
}
void DisBridge::forwardFbToPm(Message* msg, const std::vector<uint8_t>& frame) {
if (!m_pmLink || !m_sys) return;
// 原帧透传给 pPowerManger
bool ok = m_pmLink->sendRaw(frame);
RealCcuState st = m_sys->realCcu();
st.fbCount++;
if (ok) st.fwdFbOk++; else st.fwdFbErr++;
// 解码缓存四条母线最新状态
if (msg) {
bool decoded = false;
switch (msg->id()) {
case DIS_HV_FB_ID: {
DisHighVolBusFbMessage m;
decoded = m.decode(frame, st.hvBus);
if (decoded) st.hvLastRx = MOOSTime(false);
break;
}
case DIS_HVA_FB_ID: {
DisHighAVolBusFbMessage m;
decoded = m.decode(frame, st.hvaBus);
if (decoded) st.hvaLastRx = MOOSTime(false);
break;
}
case DIS_HVB_FB_ID: {
DisHighBVolBusFbMessage m;
decoded = m.decode(frame, st.hvbBus);
if (decoded) st.hvbLastRx = MOOSTime(false);
break;
}
case DIS_LV_FB_ID: {
DisLowBusFbMessage m;
decoded = m.decode(frame, st.lvBus);
if (decoded) st.lvLastRx = MOOSTime(false);
break;
}
default:
break;
}
if (decoded) {
switch (msg->id()) {
case DIS_HV_FB_ID: st.hvValid = true; break;
case DIS_HVA_FB_ID: st.hvaValid = true; break;
case DIS_HVB_FB_ID: st.hvbValid = true; break;
case DIS_LV_FB_ID: st.lvValid = true; break;
default: break;
}
} else {
LOG_F(WARNING, "[桥接] 配电反馈 0x%04X 解码失败(透传不受影响)", msg->id());
}
}
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 配电反馈 0x%04X (%zu B) -> pPowerManger %s", msg ? msg->id() : 0,
frame.size(), ok ? "成功" : "失败");
}
void DisBridge::storeRealCcuStatus(const std::vector<uint8_t>& frame) {
if (!m_sys) return;
PmStatusMessage m;
PmStatusValue v;
if (!m.decode(frame, v)) {
LOG_F(WARNING, "[桥接] 真实CCU 状态报文 0x0004 解码失败");
return;
}
RealCcuState st = m_sys->realCcu();
st.ccuStatus = v;
st.ccuStatusValid = true;
st.ccuStatusLastRx = MOOSTime(false);
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 真实CCU 状态报文: mode=%d status=%d heartbeat=%d (仅显示)",
v.fc_mode, v.fc_status, v.heartbeat);
}
} // namespace ccu
+55
View File
@@ -0,0 +1,55 @@
#ifndef PCCU_DIS_BRIDGE_H
#define PCCU_DIS_BRIDGE_H
#include <cstdint>
#include <vector>
#include "../protocol/Message.h"
namespace ccu {
class LinkManager;
class SystemData;
//============================================================================
// DisBridge:配电协议桥接器(pPowerManger <-> 真实CCU)。
//
// pCCU 居中桥接,原帧透传(不做解析重编码,校验和无需重算):
// pPowerManger --配电指令 0x0001~0x0004(Sum8,短帧)--> pCCU --原帧--> 真实CCU
// 真实CCU --配电反馈 0x0005~0x0008(Sum8)------> pCCU --原帧--> pPowerManger
//
// 同时把配电反馈(及真实CCU 状态报文 0x0004)解码写入 SystemData
// 供网页显示与统计;真实CCU 的 0x0004 仅显示不转发,避免与 pCCU
// 自身模拟的状态报文冲突。
//
// 调用约定:onPmFrame 在 PM 链路接收线程回调,onRcuFrame 在 RCU
// 链路接收线程回调;返回 true 表示该帧已被桥接处理,上层不再处理。
//============================================================================
class DisBridge {
public:
// 注入依赖(在 CCU::OnStartUp 中链路创建完成后调用)
void setup(LinkManager* pmLink, LinkManager* rcuLink, SystemData* sys);
// PM 链路收帧入口:命中配电指令则原帧转发真实CCU,返回是否已处理
bool onPmFrame(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收帧入口:命中配电反馈则原帧转发 pPowerManger 并解码
// 显示;真实CCU 状态报文 0x0004 仅解码显示。返回是否已处理
bool onRcuFrame(Message* msg, const std::vector<uint8_t>& frame);
private:
// PM 链路收到配电指令:原帧转发 + 解码缓存
void forwardCmdToRcu(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收到配电反馈:原帧转发 + 解码缓存
void forwardFbToPm(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收到真实CCU 状态报文:仅解码缓存(显示)
void storeRealCcuStatus(const std::vector<uint8_t>& frame);
LinkManager* m_pmLink = nullptr;
LinkManager* m_rcuLink = nullptr;
SystemData* m_sys = nullptr;
};
} // namespace ccu
#endif // PCCU_DIS_BRIDGE_H
+1 -1
View File
@@ -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() : 控制主机最新操控指令
//
+360 -25
View File
@@ -1,9 +1,11 @@
#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>
#include <utility>
namespace ccu {
@@ -17,10 +19,12 @@ void put(JsonVal& j, const char* k, uint32_t v){ j[k] = JsonVal(static_cast<Json
void putI(JsonVal& j, const char* k, int16_t v) { j[k] = JsonVal(static_cast<int>(v)); }
//--------------------------------------------------------------------------
// 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。
// 方向是相对 pCCU 而言:收=对方→CCU,发=CCU→对方。
// 消息描述:根据链路 + 消息 ID + 帧长 + 方向(0=收 1=发),给出中文名与
// "来源→去向"。方向是相对 pCCU 而言:收=对方→CCU,发=CCU→对方。
// length 用于区分同 id 消息:配电指令 0x0001~0x0004 为 13~21B 短帧,
// 与 PM 操控/参数指令(28/45B)共用 id。
//--------------------------------------------------------------------------
std::string describeMessageName(const std::string& link, uint16_t id) {
std::string describeMessageName(const std::string& link, uint16_t id, int length) {
if (link == "fc") {
switch (id) {
case 0x0001: return "FC控制指令";
@@ -29,10 +33,30 @@ std::string describeMessageName(const std::string& link, uint16_t id) {
}
} else if (link == "pm") {
switch (id) {
case 0x0001: return "PM操控指令";
case 0x0002: return "PM参数设定";
case 0x0003: return "PM参数反馈";
case 0x0004: return "PM状态报文";
case 0x0001:
return (length > 0 && length <= 21) ? "PM配电指令(高压母线)" : "PM操控指令";
case 0x0002:
return (length > 0 && length <= 21) ? "PM配电指令(高压母线A)" : "PM参数设定";
case 0x0003:
return (length > 0 && length <= 21) ? "PM配电指令(低压主母线)" : "PM参数反馈";
case 0x0004:
return (length > 0 && length <= 21) ? "PM配电指令(仪表汇流排)" : "PM状态报文";
case 0x0005: return "PM配电反馈(高压母线)";
case 0x0006: return "PM配电反馈(高压母线A)";
case 0x0007: return "PM配电反馈(低压主母线)";
case 0x0008: return "PM配电反馈(仪表汇流排)";
default: break;
}
} else if (link == "rcu") {
switch (id) {
case 0x0001: return "配电指令(高压母线)";
case 0x0002: return "配电指令(高压母线A)";
case 0x0003: return "配电指令(低压主母线)";
case 0x0004: return "配电指令(仪表汇流排)";
case 0x0005: return "配电反馈(高压母线)";
case 0x0006: return "配电反馈(高压母线A)";
case 0x0007: return "配电反馈(低压主母线)";
case 0x0008: return "配电反馈(仪表汇流排)";
default: break;
}
}
@@ -44,6 +68,7 @@ std::string describeMessageName(const std::string& link, uint16_t id) {
std::string describeDirection(const std::string& link, int direction) {
std::string peer = "PM";
if (link == "fc") peer = "FC";
else if (link == "rcu") peer = "真实CCU";
return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU");
}
@@ -56,7 +81,7 @@ JsonVal commLogToJson(const CommLogRow& r) {
e["msgId"] = r.msgId;
e["checksumOk"] = r.checksumOk;
e["hex"] = r.hex;
e["name"] = describeMessageName(r.link, r.msgId);
e["name"] = describeMessageName(r.link, r.msgId, r.length);
e["dirText"] = describeDirection(r.link, r.direction);
return e;
}
@@ -209,19 +234,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;
}
@@ -337,11 +415,247 @@ JsonVal linkJson(const LinkManager* lm) {
return j;
}
//--------------------------------------------------------------------------
// 真实CCU 配电桥接 -> JSON(rcu 节)
// 断路器/开关量按原始值下发(0x55 闭合 / 0xAA 断开 / 0x5A 故障),
// 由前端渲染;age 为距最后收到该类帧的秒数(-1 表示从未收到)。
//--------------------------------------------------------------------------
JsonVal ageJson(bool valid, double lastRx, double now) {
JsonVal j(Json::objectValue);
j["valid"] = JsonVal(valid);
j["age"] = JsonVal(valid ? (now - lastRx) : -1.0);
return j;
}
// 断路器组( 名称 -> 原始值 )
JsonVal disBreakersJson(const std::vector<std::pair<const char*, uint8_t>>& items) {
JsonVal j(Json::objectValue);
for (const auto& it : items) put(j, it.first, it.second);
return j;
}
JsonVal hvBusJson(const disHighVolBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"fuelCell", v.fuelCellCircuitBreaker},
{"powerLithiumBattery", v.powerLithiumBatteryCircuitBreaker},
{"propulsionMotor", v.propulsionMotorCircuitBreaker},
{"lithiumBatteryGroupInstrument", v.lithiumBatteryGroupInstrumentCircuitBreaker},
{"dcDc5Module", v.dcDc5ModuleCircuitBreaker},
{"bowHighVoltageDistributionBox", v.bowHighVoltageDistributionBoxCircuitBreaker},
{"sternFTDevice45", v.sternFTDevice45CircuitBreaker},
{"fbReserved", v.fbReservedCircuitBreaker},
{"sternRudderSwitch1", v.sternRudderSwitch1CircuitBreaker},
{"sternRudderSwitch2", v.sternRudderSwitch2CircuitBreaker},
{"tyzReserved", v.tyzReservedCircuitBreaker},
{"reserved", v.reservedCircuitBreaker},
});
put(j, "dcDcFaultWord1", v.dcDcFaultWord1);
put(j, "dcDcFaultWord2", v.dcDcFaultWord2);
put(j, "powerBusInsulationStatus", v.powerBusInsulationStatus);
put(j, "aBusInsulationStatus", v.aBusInsulationStatus);
put(j, "meterPowerLossSignal", v.meterPowerLossSignal);
put(j, "emergencyPowerLossSignal", v.emergencyPowerLossSignal);
put(j, "dcDcTemperature", v.dcDcTemperature);
put(j, "powerBusVoltage", v.powerBusVoltage);
put(j, "dcDc5ModuleCurrent", v.dcDc5ModuleCurrent);
put(j, "powerBusCurrent", v.powerBusCurrent);
put(j, "busbarAVoltage", v.busbarAVoltage);
put(j, "propulsionMotorControlBoxCurrent", v.propulsionMotorControlBoxCurrent);
put(j, "busbarACurrent", v.busbarACurrent);
put(j, "lithiumBatteryGroupMeterCurrent", v.lithiumBatteryGroupMeterCurrent);
put(j, "bowHighVoltageDistributionBoxCurrent", v.bowHighVoltageDistributionBoxCurrent);
put(j, "sternFTDevice45Current", v.sternFTDevice45Current);
put(j, "tyzReservedSwitchCurrent", v.tyzReservedSwitchCurrent);
put(j, "sternRudderSwitch1Current", v.sternRudderSwitch1Current);
put(j, "reservedCurrent1", v.reservedCurrent1);
put(j, "sternRudderSwitch2Current", v.sternRudderSwitch2Current);
put(j, "reservedCurrent2", v.reservedCurrent2);
put(j, "fbReservedSwitchCurrent", v.fbReservedSwitchCurrent);
put(j, "reservedCurrent3", v.reservedCurrent3);
put(j, "coolWaterPressure", v.coolWaterPressure);
return j;
}
JsonVal hvaBusJson(const disHighAVolBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"bowFTDevice123", v.bowFTDevice123CircuitBreaker},
{"actuator", v.actuatorCircuitBreaker},
{"mastSteeringGearControlBox", v.mastSteeringGearControlBoxCircuitBreaker},
{"xcz", v.xczCircuitBreaker},
{"bowRudderControlBox", v.bowRudderControlBoxCircuitBreaker},
{"openWaterCoverStartCylinder", v.openWaterCoverStartCylinderCircuitBreaker},
});
put(j, "instrumentPowerFailureSignal", v.instrumentPowerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "waterIngressionAlarm", v.waterIngressionAlarm);
put(j, "busbarBVoltage", v.busbarBVoltage);
put(j, "busbarBCurrent", v.busbarBCurrent);
put(j, "bowFTDevice123Current", v.bowFTDevice123Current);
put(j, "actuatorCurrent", v.actuatorCurrent);
put(j, "mastSteeringGearControlBoxCurrent", v.mastSteeringGearControlBoxCurrent);
put(j, "xczCurrent", v.xczCurrent);
put(j, "bowRudderControlBoxCurrent", v.bowRudderControlBoxCurrent);
put(j, "openWaterCoverStartCylinderCurrent", v.openWaterCoverStartCylinderCurrent);
return j;
}
JsonVal hvbBusJson(const disLowMainBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"lithiumBatteryGroupInstrument", v.lithiumBatteryGroupInstrumentCircuitBreaker},
{"bowLowVoltageDistributionBox", v.bowLowVoltageDistributionBoxCircuitBreaker},
{"unit4InstrumentDC48V", v.unit4InstrumentDC48VCircuitBreaker},
{"dcC1DCDistributionPanel", v.dcC1DCDistributionPanelCircuitBreaker},
{"reserved1", v.reservedCircuitBreaker1},
{"reserved2", v.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup2", v.emergencyLithiumBatteryGroup2CircuitBreaker},
});
put(j, "instrumentPowerFailureSignal", v.instrumentPowerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "instrumentBusbarInsulationLow", v.instrumentBusbarInsulationLow);
put(j, "instrumentBusbarVoltage", v.instrumentBusbarVoltage);
put(j, "bowLowVoltageDistributionBoxCurrent", v.bowLowVoltageDistributionBoxCurrent);
put(j, "dcC1InstrumentDC48VCurrent", v.dcC1InstrumentDC48VCurrent);
put(j, "reservedCurrent1", v.reservedCurrent1);
put(j, "emergencyLithiumBatteryGroup2Current", v.emergencyLithiumBatteryGroup2Current);
put(j, "lithiumBatteryGroupInstrumentCurrent", v.lithiumBatteryGroupInstrumentCurrent);
put(j, "unit4InstrumentDC48VCurrent", v.unit4InstrumentDC48VCurrent);
put(j, "reservedCurrent2", v.reservedCurrent2);
put(j, "emergency2BusbarVoltage", v.emergency2BusbarVoltage);
put(j, "compositeEnergyEmergencyDC48VCurrent", v.compositeEnergyManagementSystemEmergencyDC48VCurrent);
put(j, "fuelCellSecurityEmergencyDC48VCurrent", v.fuelCellSecuritySystemEmergencyDC48VCurrent);
put(j, "plcControlPowerCurrent", v.plcControlPowerCurrent);
return j;
}
JsonVal lvBusJson(const disLowBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"unit1", v.unit1CircuitBreaker},
{"unit2", v.unit2CircuitBreaker},
{"unit3", v.unit3CircuitBreaker},
{"unit5", v.unit5CircuitBreaker},
{"bowPZDevice", v.bowPZDeviceCircuitBreaker},
{"reserved1", v.reservedCircuitBreaker1},
{"reserved2", v.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup1", v.emergencyLithiumBatteryGroup1CircuitBreaker},
});
put(j, "powerFailureSignal", v.powerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "waterIngressionAlarm", v.waterIngressionAlarm);
put(j, "instrumentBusbarVoltage", v.instrumentBusbarVoltage);
put(j, "instrumentBusbarCurrent", v.instrumentBusbarCurrent);
put(j, "unit1Current", v.unit1Current);
put(j, "unit2Current", v.unit2Current);
put(j, "unit3Current", v.unit3Current);
put(j, "unit5Current", v.unit5Current);
put(j, "bowPZDeviceCurrent", v.bowPZDeviceCurrent);
put(j, "emergencyLithiumBatteryGroup1Current", v.emergencyLithiumBatteryGroup1Current);
put(j, "emergency1BusbarVoltage", v.emergency1BusbarVoltage);
return j;
}
JsonVal realCcuJson(const RealCcuState& st, double now) {
JsonVal j(Json::objectValue);
// 桥接统计
JsonVal fwd(Json::objectValue);
fwd["fbCount"] = JsonVal(static_cast<Json::UInt>(st.fbCount));
fwd["fwdFbOk"] = JsonVal(static_cast<Json::UInt>(st.fwdFbOk));
fwd["fwdFbErr"] = JsonVal(static_cast<Json::UInt>(st.fwdFbErr));
fwd["cmdCount"] = JsonVal(static_cast<Json::UInt>(st.cmdCount));
fwd["fwdCmdOk"] = JsonVal(static_cast<Json::UInt>(st.fwdCmdOk));
fwd["fwdCmdErr"] = JsonVal(static_cast<Json::UInt>(st.fwdCmdErr));
j["forward"] = fwd;
// 四条母线最新反馈(全字段)+ 新鲜度
j["hvAge"] = ageJson(st.hvValid, st.hvLastRx, now);
j["hvaAge"] = ageJson(st.hvaValid, st.hvaLastRx, now);
j["hvbAge"] = ageJson(st.hvbValid, st.hvbLastRx, now);
j["lvAge"] = ageJson(st.lvValid, st.lvLastRx, now);
j["hv"] = st.hvValid ? hvBusJson(st.hvBus) : JsonVal(Json::objectValue);
j["hva"] = st.hvaValid ? hvaBusJson(st.hvaBus) : JsonVal(Json::objectValue);
j["hvb"] = st.hvbValid ? hvbBusJson(st.hvbBus) : JsonVal(Json::objectValue);
j["lv"] = st.lvValid ? lvBusJson(st.lvBus) : JsonVal(Json::objectValue);
// 最近配电指令(pPowerManger -> 真实CCU)
JsonVal lastCmd(Json::objectValue);
lastCmd["valid"] = JsonVal(st.cmdValid);
lastCmd["age"] = JsonVal(st.cmdValid ? (now - st.lastCmdRx) : -1.0);
JsonVal cmds(Json::objectValue);
cmds["hv"] = disBreakersJson({
{"fuelCell", st.lastHvCmd.fuelCellCircuitBreaker},
{"powerLithiumBattery", st.lastHvCmd.powerLithiumBatteryCircuitBreaker},
{"propulsionMotor", st.lastHvCmd.propulsionMotorCircuitBreaker},
{"lithiumBatteryGroupInstrument", st.lastHvCmd.lithiumBatteryGroupInstrumentCircuitBreaker},
{"dcDc5Module", st.lastHvCmd.dcDc5ModuleCircuitBreaker},
{"bowHighVoltageDistributionBox", st.lastHvCmd.bowHighVoltageDistributionBoxCircuitBreaker},
{"sternFTDevice45", st.lastHvCmd.sternFTDevice45CircuitBreaker},
{"fbReserved", st.lastHvCmd.fbReservedCircuitBreaker},
{"sternRudderSwitch1", st.lastHvCmd.sternRudderSwitch1CircuitBreaker},
{"sternRudderSwitch2", st.lastHvCmd.sternRudderSwitch2CircuitBreaker},
{"tyzReserved", st.lastHvCmd.tyzReservedCircuitBreaker},
{"reserved", st.lastHvCmd.reservedCircuitBreaker},
{"dcdc5ModuleStart", st.lastHvCmd.dcdc5Module},
{"coolingSystemStart", st.lastHvCmd.coolingSystem},
});
cmds["hva"] = disBreakersJson({
{"bowFTDevice123", st.lastHvaCmd.bowFTDevice123CircuitBreaker},
{"actuator", st.lastHvaCmd.actuatorCircuitBreaker},
{"mastSteeringGearControlBox", st.lastHvaCmd.mastSteeringGearControlBoxCircuitBreaker},
{"xcz", st.lastHvaCmd.xczCircuitBreaker},
{"bowRudderControlBox", st.lastHvaCmd.bowRudderControlBoxCircuitBreaker},
{"openWaterCoverStartCylinder", st.lastHvaCmd.openWaterCoverStartCylinderCircuitBreaker},
});
cmds["hvb"] = disBreakersJson({
{"lithiumBatteryGroupInstrument", st.lastHvbCmd.lithiumBatteryGroupInstrumentCircuitBreaker},
{"bowLowVoltageDistributionBox", st.lastHvbCmd.bowLowVoltageDistributionBoxCircuitBreaker},
{"unit4InstrumentDC48V", st.lastHvbCmd.unit4InstrumentDC48VCircuitBreaker},
{"dcC1DCDistributionPanel", st.lastHvbCmd.dcC1DCDistributionPanelCircuitBreaker},
{"reserved1", st.lastHvbCmd.reservedCircuitBreaker1},
{"reserved2", st.lastHvbCmd.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup2", st.lastHvbCmd.emergencyLithiumBatteryGroup2CircuitBreaker},
});
cmds["lv"] = disBreakersJson({
{"unit1", st.lastLvCmd.unit1CircuitBreaker},
{"unit2", st.lastLvCmd.unit2CircuitBreaker},
{"unit3", st.lastLvCmd.unit3CircuitBreaker},
{"unit5", st.lastLvCmd.unit5CircuitBreaker},
{"bowPZDevice", st.lastLvCmd.bowPZDeviceCircuitBreaker},
{"reserved1", st.lastLvCmd.reservedCircuitBreaker1},
{"reserved2", st.lastLvCmd.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup1", st.lastLvCmd.emergencyLithiumBatteryGroup1CircuitBreaker},
});
lastCmd["cmds"] = cmds;
j["lastCmd"] = lastCmd;
// 真实CCU 状态报文 0x0004(仅显示)
JsonVal ccu(Json::objectValue);
ccu["valid"] = JsonVal(st.ccuStatusValid);
ccu["age"] = JsonVal(st.ccuStatusValid ? (now - st.ccuStatusLastRx) : -1.0);
if (st.ccuStatusValid) {
put(ccu, "fc_mode", st.ccuStatus.fc_mode);
put(ccu, "fc_status", st.ccuStatus.fc_status);
put(ccu, "fc_fault_level", st.ccuStatus.fc_fault_level);
put(ccu, "generation_power", st.ccuStatus.generation_power);
put(ccu, "dyn_soc", st.ccuStatus.dyn_soc);
put(ccu, "ins_soc", st.ccuStatus.ins_soc);
put(ccu, "heartbeat", st.ccuStatus.heartbeat);
put(ccu, "emergency_cmd", st.ccuStatus.emergency_cmd);
}
j["ccuStatus"] = ccu;
return j;
}
} // namespace
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
DbStore* db)
: m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {}
LinkManager* rcu, DbStore* db)
: m_sys(sys), m_fc(fc), m_pm(pm), m_rcu(rcu), m_db(db) {}
std::string SnapshotBuilder::build() const {
JsonVal root(Json::objectValue);
@@ -353,14 +667,32 @@ 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()));
root["pmStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmStatusCount()));
root["bmsStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->bmsStatusCount()));
root["canFrameCount"] = JsonVal(static_cast<Json::UInt>(m_sys->canFrameCount()));
// 真实CCU 配电桥接数据(RCU 链路)
root["realCcu"] = realCcuJson(m_sys->realCcu(), MOOSTime(false));
// BCU 断路器控制下发统计(网页 -> 0x10XX81FF -> pCanBridge)
{
BcuCtrlStat bc = m_sys->bcuCtrlStat();
JsonVal jc(Json::objectValue);
jc["sentCount"] = JsonVal(static_cast<Json::UInt>(bc.sentCount));
jc["errCount"] = JsonVal(static_cast<Json::UInt>(bc.errCount));
jc["valid"] = JsonVal(bc.lastTime > 0);
jc["age"] = JsonVal(bc.lastTime > 0 ? (MOOSTime(false) - bc.lastTime) : -1.0);
put(jc, "addr", bc.last.addr);
put(jc, "pos", bc.last.pos);
put(jc, "neg", bc.last.neg);
jc["lastOk"] = JsonVal(bc.lastOk);
root["bcuCtrl"] = jc;
}
} else {
root["fc"] = JsonVal(Json::objectValue);
root["pmCmd"] = JsonVal(Json::objectValue);
@@ -368,12 +700,15 @@ 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);
root["realCcu"] = JsonVal(Json::objectValue);
root["bcuCtrl"] = JsonVal(Json::objectValue);
}
JsonVal links(Json::objectValue);
links["fc"] = linkJson(m_fc);
links["pm"] = linkJson(m_pm);
if (m_rcu) links["rcu"] = linkJson(m_rcu);
root["links"] = links;
// 最近原始帧(默认 20 条)
+5 -3
View File
@@ -12,13 +12,14 @@ class DbStore;
//============================================================================
// SnapshotBuilder:构建网页推送的 JSON 快照。
//
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN)、两条 UDP 链路统计、
// 最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN + 真实CCU 配电桥接)、
// 三条 UDP 链路统计、最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
//============================================================================
class SnapshotBuilder {
public:
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db);
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
LinkManager* rcu, DbStore* db);
// 生成完整快照 JSON
std::string build() const;
@@ -30,6 +31,7 @@ private:
SystemData* m_sys;
LinkManager* m_fc;
LinkManager* m_pm;
LinkManager* m_rcu;
DbStore* m_db;
};
+128 -9
View File
@@ -3,22 +3,86 @@
#include <mutex>
#include <atomic>
#include <vector>
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/CanBms.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
//============================================================================
// RealCcuState:真实 CCU 设备的桥接数据缓存(配电协议)。
//
// 数据来源:RCU 链路(真实CCU -> pCCU),由 DisBridge 在转发的同时
// 解码写入;网页快照读取展示。各 lastRx 时间由写入方更新(MOOSTime),
// 供在线判定与新鲜度展示。
//============================================================================
struct RealCcuState {
// 四条配电母线最新反馈(valid 表示收到过对应反馈帧)
disHighVolBusState hvBus{}; bool hvValid = false;
disHighAVolBusState hvaBus{}; bool hvaValid = false;
disLowMainBusState hvbBus{}; bool hvbValid = false;
disLowBusState lvBus{}; bool lvValid = false;
double hvLastRx = 0, hvaLastRx = 0, hvbLastRx = 0, lvLastRx = 0;
// 最近收到的配电指令(pPowerManger 下发、已转发真实CCU,用于展示)
disHighVolBusCmd lastHvCmd{};
disHighAVolBusCmd lastHvaCmd{};
disLowMainBusCmd lastHvbCmd{};
disLowBusCmd lastLvCmd{};
bool cmdValid = false;
double lastCmdRx = 0;
// 真实CCU 状态报文 0x0004(仅解码显示,不转发)
PmStatusValue ccuStatus{};
bool ccuStatusValid = false;
double ccuStatusLastRx = 0;
// 桥接统计
unsigned long fbCount = 0; // 收到配电反馈帧数
unsigned long fwdFbOk = 0; // 反馈转发 pPowerManger 成功帧数
unsigned long fwdFbErr = 0; // 反馈转发失败帧数
unsigned long cmdCount = 0; // 收到配电指令帧数
unsigned long fwdCmdOk = 0; // 指令转发真实CCU 成功帧数
unsigned long fwdCmdErr = 0; // 指令转发失败帧数
};
//============================================================================
// SystemData:跨线程共享的最新状态快照。
//
// - 接收线程(FC 链路)写 FcStatus 快照
// - MOOS 主线程(OnNewMail)写 BMS 快照(CAN 经 pCanBridge 透传)
// - MOOS 主线程(OnNewMail)写 BCU 节点快照(CAN 经 pCanBridge 透传)
// - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送
// - 网页线程读快照展示
// 通过互斥锁保护读写。
//============================================================================
//============================================================================
// BcuCtrlCmd:BCU 断路器(总正/总负继电器)控制指令。
//
// 链路:网页 /api/bcu_ctrl(Web 线程)-> 入队 -> CCU::Iterate(MOOS 线程)
// 出队编码 0x10XX81FF -> MOOS CAN_TX_0x* -> pCanBridge 下行 CAN 总线。
// 队列化是为了让 MOOS Notify 只发生在 MOOS 线程(Web 线程不可直接 Notify)。
//============================================================================
struct BcuCtrlCmd {
uint8_t addr = 0; // BCU 节点地址 1~54
uint8_t pos = 0; // 总正继电器 1 闭合 / 0 断开
uint8_t neg = 0; // 总负继电器 1 闭合 / 0 断开
};
// BCU 断路器控制的下发统计(网页展示)
struct BcuCtrlStat {
unsigned long sentCount = 0; // 已下发帧数(成功投递 MOOS)
unsigned long errCount = 0; // 投递失败帧数
BcuCtrlCmd last{};
bool lastOk = false;
double lastTime = 0; // 最近下发时刻(MOOSTime)
};
class SystemData {
public:
// 更新/获取燃料电池最新状态
@@ -101,17 +165,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);
@@ -128,6 +202,48 @@ public:
return m_canFrameCount;
}
//-------- 真实 CCU 桥接数据(配电协议,RCU 链路) --------
// 整体更新真实CCU 快照(DisBridge 在接收线程先取副本->修改->写回)
void updateRealCcu(const RealCcuState& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_realCcu = v;
}
RealCcuState realCcu() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_realCcu;
}
//-------- BCU 断路器控制(网页 -> MOOS 下行队列) --------
// 入队一条控制指令(Web API 线程调用);队列上限 16,满则丢弃并返回 false
bool pushBcuCtrlCmd(const BcuCtrlCmd& c) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_bcuCtrlQueue.size() >= 16) return false;
m_bcuCtrlQueue.push_back(c);
return true;
}
// 出队一条控制指令(MOOS 主循环 Iterate 调用);空队列返回 false
bool popBcuCtrlCmd(BcuCtrlCmd& out) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_bcuCtrlQueue.empty()) return false;
out = m_bcuCtrlQueue.front();
m_bcuCtrlQueue.erase(m_bcuCtrlQueue.begin());
return true;
}
// 记录一次下发结果(Iterate 调用,供网页统计展示)
void noteBcuCtrlSent(const BcuCtrlCmd& c, bool ok) {
std::lock_guard<std::mutex> lock(m_mutex);
if (ok) m_bcuCtrlStat.sentCount++; else m_bcuCtrlStat.errCount++;
m_bcuCtrlStat.last = c;
m_bcuCtrlStat.lastOk = ok;
m_bcuCtrlStat.lastTime = MOOSTime(false);
}
BcuCtrlStat bcuCtrlStat() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_bcuCtrlStat;
}
private:
mutable std::mutex m_mutex;
FcStatusValue m_fcStatus;
@@ -136,7 +252,10 @@ private:
FcControlValue m_fcControl;
PmStatusValue m_pmStatus;
PmParamSetFbValue m_pmParamFb;
BmsStatusValue m_bmsStatus;
BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1
RealCcuState m_realCcu;
std::vector<BcuCtrlCmd> m_bcuCtrlQueue;
BcuCtrlStat m_bcuCtrlStat;
unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0;
+19
View File
@@ -4,7 +4,13 @@
// 链路拓扑:
// 燃料电池控制器(FC) 192.168.1.162:7000(UDP)
// 控制主机(pPowerManger) 192.168.0.140:5001(UDP)
// 真实CCU设备(rcu) 192.168.0.200:7000(UDP,配电桥接,占位地址需按实际修改)
// 锂电池 BMS CAN 总线(经 pCanBridge 订阅 CAN_0x* 透传)
//
// 配电桥接(rcu_enable = true 时启用):
// pPowerManger 下发的配电指令(0x0001~0x0004) 经 pCCU 原帧转发真实CCU;
// 真实CCU 上报的配电反馈(0x0005~0x0008) 经 pCCU 原帧转发 pPowerManger,
// 同时解码显示在网页"真实CCU"页签;真实CCU 状态报文(0x0004)仅显示不转发。
//============================================================================
ProcessConfig = pCCU
@@ -26,10 +32,23 @@ ProcessConfig = pCCU
pm_remote_ip = 192.168.0.140
pm_remote_port = 5001
//======== 真实CCU 桥接链路(配电协议) ========
// 是否启用桥接(关闭时 pCCU 行为与纯模拟版一致)
rcu_enable = true
// 本机接收真实CCU 数据的端口
rcu_local_port = 7100
// 真实CCU 地址(转发配电指令目标;占位地址,按实际设备修改)
rcu_remote_ip = 192.168.0.200
rcu_remote_port = 7000
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
// 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。
//======== BCU 断路器控制下发 ========
// 下行 CAN_TX_0x* 消息的目标通道(经 m_sSrcAux 由 pCanBridge 路由)
bcu_can_channel = CAN0
//======== 存储与日志 ========
// 数据库路径(SQLite)
dbpath = pccu_data.db
+124 -22
View File
@@ -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;
+153 -41
View File
@@ -6,58 +6,170 @@
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 下发指令(继电器控制,经 MOOS CAN_TX_0x* 下行)
//
// 【下发指令 0x10XX81FF】数据域 8 字节(BYTE1~8 = byte0~7):
// byte0 = 0x02(索引号) byte1 = 0x00(索引号)
// byte2 = 总正继电器控制(1 闭合 / 0 断开)
// byte3 = 总负继电器控制(1 闭合 / 0 断开)
// byte4~7 = Reserve(填充 0xFF)
//
// 【上报报文】均为扩展帧,字段大端,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 断路器(总正/总负继电器)下行控制
//--------------------------------------------------------------------------
// BCU 节点继电器控制指令 CAN ID(0x10XX81FF,XX=节点地址 01h~36h)
inline uint32_t bcuCtrlCanId(uint8_t addr) {
return 0x10000000u | (static_cast<uint32_t>(addr) << 16) | 0x81FFu;
}
// 编码继电器控制数据域(8 字节,经 MOOS CAN_TX_0x%08X 下发,pCanBridge 发送):
// [0]=0x02 [1]=0x00 [2]=总正(1闭合/0断开) [3]=总负 [4..7]=0xFF
// pos/neg 非 0 值均视为闭合(1)。
inline void buildBcuRelayCtrlData(int pos, int neg, uint8_t out[8]) {
out[0] = 0x02;
out[1] = 0x00;
out[2] = pos ? 0x01 : 0x00;
out[3] = neg ? 0x01 : 0x00;
out[4] = 0xFF;
out[5] = 0xFF;
out[6] = 0xFF;
out[7] = 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
+7
View File
@@ -12,6 +12,7 @@ namespace ccu {
//
// 协议联调阶段校验和的实现可能调整,因此对每条消息独立配置策略:
// - None : 无校验和字段(如 FC 状态帧文档未列校验行)
// - Sum8 : uint8 字节和(配电协议,取字节和低 8 位)
// - Sum32 : uint32 字节和,从域起始符到校验和之前所有数据的字节和
// - Sum16 : uint16 字节和(预留,备用)
// 新增策略只需在此类扩展,不影响其它代码。
@@ -19,6 +20,7 @@ namespace ccu {
enum class ChecksumType {
None, // 无校验
Sum8, // uint8 字节和(配电协议)
Sum32, // uint32 字节和
Sum16, // uint16 字节和
};
@@ -32,6 +34,7 @@ public:
// 校验和字段占用的字节数;None 返回 0
size_t size() const {
switch (m_type) {
case ChecksumType::Sum8: return 1;
case ChecksumType::Sum32: return 4;
case ChecksumType::Sum16: return 2;
case ChecksumType::None: return 0;
@@ -60,6 +63,10 @@ public:
uint32_t actual = 0;
size_t offset = frame.size() - cs;
if (m_type == ChecksumType::Sum8) {
actual = static_cast<uint32_t>(frame[offset]);
return (expect & 0xFF) == actual;
}
if (m_type == ChecksumType::Sum32) {
actual = static_cast<uint32_t>(frame[offset]) |
(static_cast<uint32_t>(frame[offset + 1]) << 8) |
+155
View File
@@ -0,0 +1,155 @@
#include "DisProtocol.h"
#include "Frame.h"
#include "MessageRegistry.h"
#include <cstring>
namespace ccu {
//============================================================================
// 结构体尺寸核对:pPowerManger 按同结构裸解析收帧,布局必须与文档一致;
// 若 pmSysvariable.h 结构调整导致尺寸变化,此处编译期报错提醒同步。
//============================================================================
static_assert(sizeof(disHighVolBusCmd) == 14, "disHighVolBusCmd 尺寸与协议不符");
static_assert(sizeof(disHighAVolBusCmd) == 6, "disHighAVolBusCmd 尺寸与协议不符");
static_assert(sizeof(disLowMainBusCmd) == 7, "disLowMainBusCmd 尺寸与协议不符");
static_assert(sizeof(disLowBusCmd) == 8, "disLowBusCmd 尺寸与协议不符");
static_assert(sizeof(disHighVolBusState) == 53, "disHighVolBusState 尺寸与协议不符");
static_assert(sizeof(disHighAVolBusState) == 25, "disHighAVolBusState 尺寸与协议不符");
static_assert(sizeof(disLowMainBusState) == 34, "disLowMainBusState 尺寸与协议不符");
static_assert(sizeof(disLowBusState) == 29, "disLowBusState 尺寸与协议不符");
//============================================================================
// 通用编解码:配电结构体为 #pragma pack(1) 的 POD,与线上字节布局一致
//(pPowerManger 亦按同结构裸解析),因此直接按字节拷贝。
//============================================================================
namespace {
std::vector<uint8_t> encodePacked(uint16_t id, const void* obj, size_t size,
const ChecksumPolicy& policy) {
std::vector<uint8_t> p(size, 0);
std::memcpy(p.data(), obj, size);
return Frame::build(id, p, policy);
}
bool decodePacked(const std::vector<uint8_t>& frame, uint16_t id,
const ChecksumPolicy& policy, size_t size, void* obj) {
// 注意:不依赖帧头 length 字段(pPowerManger 发送的配电指令该字段
// 与总长不符),validateFrame 只要实际帧长足够即放行,校验和兜底。
if (!Message::validateFrame(frame, id, policy,
FRAME_HEADER_LEN + size + policy.size()))
return false;
std::memcpy(obj, frame.data() + FRAME_HEADER_LEN, size);
return true;
}
} // namespace
//--------------------------------------------------------------------------
// 指令消息编解码
//--------------------------------------------------------------------------
std::vector<uint8_t> DisHighVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighVolBusCmd), m_checksum);
}
bool DisHighVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusCmd), obj);
}
std::vector<uint8_t> DisHighAVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighAVolBusCmd), m_checksum);
}
bool DisHighAVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusCmd), obj);
}
std::vector<uint8_t> DisHighBVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowMainBusCmd), m_checksum);
}
bool DisHighBVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusCmd), obj);
}
std::vector<uint8_t> DisLowBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowBusCmd), m_checksum);
}
bool DisLowBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowBusCmd), obj);
}
//--------------------------------------------------------------------------
// 反馈消息编解码
//--------------------------------------------------------------------------
std::vector<uint8_t> DisHighVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighVolBusState), m_checksum);
}
bool DisHighVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusState), obj);
}
std::vector<uint8_t> DisHighAVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighAVolBusState), m_checksum);
}
bool DisHighAVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusState), obj);
}
std::vector<uint8_t> DisHighBVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowMainBusState), m_checksum);
}
bool DisHighBVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusState), obj);
}
std::vector<uint8_t> DisLowBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowBusState), m_checksum);
}
bool DisLowBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowBusState), obj);
}
//--------------------------------------------------------------------------
// 消息类型判断(供桥接器/上层分发使用)
//--------------------------------------------------------------------------
bool isDisMessage(const Message* msg) {
return isDisCmdMessage(msg) || isDisFbMessage(msg);
}
bool isDisCmdMessage(const Message* msg) {
if (!msg) return false;
return dynamic_cast<const DisHighVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighAVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighBVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisLowBusCmdMessage*>(msg) != nullptr;
}
bool isDisFbMessage(const Message* msg) {
if (!msg) return false;
return dynamic_cast<const DisHighVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighAVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighBVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisLowBusFbMessage*>(msg) != nullptr;
}
//--------------------------------------------------------------------------
// 注册
//--------------------------------------------------------------------------
void registerDisCmdMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new DisHighVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighAVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighBVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisLowBusCmdMessage()));
}
void registerDisFbMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new DisHighVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighAVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighBVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisLowBusFbMessage()));
}
} // namespace ccu
+115
View File
@@ -0,0 +1,115 @@
#ifndef PCCU_DIS_PROTOCOL_H
#define PCCU_DIS_PROTOCOL_H
#include <cstdint>
#include <vector>
#include "Message.h"
#include "pmSysvariable.h"
namespace ccu {
//============================================================================
// 配电协议(配电控制器 <-> 复合管控器,经 CCU 在 PM 链路与真实 CCU 链路间桥接)
// 依据:docs/配电控制器与复合管控器的通信协议20260824.docx
// 数据结构复用 src/pPowerManger/pmSysvariable.h(pPowerManger 按同结构
// 裸解析收帧,复用可保证两侧 sizeof/字段布局完全一致)。
//
// 帧格式:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + uint8 校验和
// 校验和 = 从域起始符到校验和之前所有数据的字节和低 8 位(Sum8)。
//
// 注意(重要):
// 配电指令 0x0001~0x0004 与 PM 协议操控/参数指令共用 id,仅能靠
// 帧总长区分(配电指令 13~21B,PM 指令 28/45B)。且 pPowerManger 发送
// 配电指令时帧头 length 字段并非总长(见 udpComm.cpp TODO),
// 分发必须用实际帧长(见 MessageRegistry::find(id, totalLength))。
//
// 八条消息(均 Sum8 校验):
// 指令(pPowerManger -> CCU -> 真实CCU) payload 总长
// 0x0001 高压母线配电指令 disHighVolBusCmd 14B 21B
// 0x0002 高压母线A配电指令 disHighAVolBusCmd 6B 13B
// 0x0003 低压主母线配电指令 disLowMainBusCmd 7B 14B
// 0x0004 仪表汇流排配电指令 disLowBusCmd 8B 15B
// 反馈(真实CCU -> CCU -> pPowerManger)
// 0x0005 高压母线配电反馈 disHighVolBusState 53B 60B
// 0x0006 高压母线A配电反馈 disHighAVolBusState 25B 32B
// 0x0007 低压主母线配电反馈 disLowMainBusState 34B 41B
// 0x0008 仪表汇流排配电反馈 disLowBusState 29B 36B
// (payload/总长由 static_assert 在编译期核对,见 DisProtocol.cpp)
//============================================================================
enum : uint16_t {
DIS_HV_CMD_ID = 0x0001, // 高压母线配电指令
DIS_HVA_CMD_ID = 0x0002, // 高压母线A配电指令
DIS_HVB_CMD_ID = 0x0003, // 低压主母线配电指令
DIS_LV_CMD_ID = 0x0004, // 仪表汇流排配电指令
DIS_HV_FB_ID = 0x0005, // 高压母线配电反馈
DIS_HVA_FB_ID = 0x0006, // 高压母线A配电反馈
DIS_HVB_FB_ID = 0x0007, // 低压主母线配电反馈
DIS_LV_FB_ID = 0x0008, // 仪表汇流排配电反馈
};
// 断路器/开关类指令与状态值
enum : uint8_t {
DIS_BREAKER_ON = 0x55, // 闭合
DIS_BREAKER_OFF = 0xAA, // 断开
DIS_BREAKER_FLT = 0x5A, // 故障脱扣
};
// 判断是否配电协议消息(指令或反馈),桥接器据此识别
bool isDisMessage(const Message* msg);
bool isDisCmdMessage(const Message* msg);
bool isDisFbMessage(const Message* msg);
//--------------------------------------------------------------------------
// 指令消息(pPowerManger 下发,pCCU 原帧转发给真实CCU)
//--------------------------------------------------------------------------
#define PCCU_DIS_MSG_CLASS(clsName, msgId, msgName, dataT) \
class clsName : public Message { \
public: \
uint16_t id() const override { return msgId; } \
const char* name() const override { return msgName; } \
const ChecksumPolicy& checksum() const override { return m_checksum; } \
size_t payloadLength() const override { return sizeof(dataT); } \
std::vector<uint8_t> encode(const void* obj) const override; \
bool decode(const std::vector<uint8_t>& frame, void* obj) const override; \
std::vector<uint8_t> encode(const dataT& v) const { return encode(&v); } \
bool decode(const std::vector<uint8_t>& frame, dataT& v) const { \
return decode(frame, static_cast<void*>(&v)); \
} \
private: \
ChecksumPolicy m_checksum{ChecksumType::Sum8}; \
}
// 高压母线配电指令(0x0001)
PCCU_DIS_MSG_CLASS(DisHighVolBusCmdMessage, DIS_HV_CMD_ID, "dis_hv_cmd", disHighVolBusCmd);
// 高压母线A配电指令(0x0002)
PCCU_DIS_MSG_CLASS(DisHighAVolBusCmdMessage, DIS_HVA_CMD_ID, "dis_hva_cmd", disHighAVolBusCmd);
// 低压主母线配电指令(0x0003)
PCCU_DIS_MSG_CLASS(DisHighBVolBusCmdMessage, DIS_HVB_CMD_ID, "dis_hvb_cmd", disLowMainBusCmd);
// 仪表汇流排配电指令(0x0004)
PCCU_DIS_MSG_CLASS(DisLowBusCmdMessage, DIS_LV_CMD_ID, "dis_lv_cmd", disLowBusCmd);
//--------------------------------------------------------------------------
// 反馈消息(真实CCU 上报,pCCU 原帧转发给 pPowerManger 并解码显示)
//--------------------------------------------------------------------------
// 高压母线配电反馈(0x0005)
PCCU_DIS_MSG_CLASS(DisHighVolBusFbMessage, DIS_HV_FB_ID, "dis_hv_fb", disHighVolBusState);
// 高压母线A配电反馈(0x0006)
PCCU_DIS_MSG_CLASS(DisHighAVolBusFbMessage, DIS_HVA_FB_ID, "dis_hva_fb", disHighAVolBusState);
// 低压主母线配电反馈(0x0007)
PCCU_DIS_MSG_CLASS(DisHighBVolBusFbMessage, DIS_HVB_FB_ID, "dis_hvb_fb", disLowMainBusState);
// 仪表汇流排配电反馈(0x0008)
PCCU_DIS_MSG_CLASS(DisLowBusFbMessage, DIS_LV_FB_ID, "dis_lv_fb", disLowBusState);
#undef PCCU_DIS_MSG_CLASS
// 注册配电指令消息(PM 链路用:识别 pPowerManger 下发的配电指令)
void registerDisCmdMessages(class MessageRegistry& reg);
// 注册配电反馈消息(RCU 链路用:识别真实CCU 上报的配电反馈)
void registerDisFbMessages(class MessageRegistry& reg);
} // namespace ccu
#endif // PCCU_DIS_PROTOCOL_H
+3 -2
View File
@@ -41,8 +41,9 @@ std::vector<uint8_t> Frame::build(
std::vector<uint8_t> body(frame.begin(), frame.end() - csSize);
uint32_t sum = checksum.compute(body);
size_t off = frame.size() - csSize;
if (csSize == 4) FieldCodec::putU32(frame, off, sum);
else FieldCodec::putU16(frame, off, static_cast<uint16_t>(sum));
if (csSize == 4) FieldCodec::putU32(frame, off, sum);
else if (csSize == 1) frame[off] = static_cast<uint8_t>(sum & 0xFF);
else FieldCodec::putU16(frame, off, static_cast<uint16_t>(sum));
}
return frame;
}
+16 -3
View File
@@ -5,8 +5,12 @@ namespace ccu {
bool MessageRegistry::registerMessage(std::unique_ptr<Message> msg) {
if (!msg) return false;
uint16_t id = msg->id();
if (m_msgs.count(id) != 0) return false; // id 冲突
m_msgs[id] = std::move(msg);
// 同 id 且同总长的重复注册视为冲突;同 id 不同总长为合法
//(配电指令与 PM 指令共用 id,仅帧长不同)
for (auto it = m_msgs.lower_bound(id); it != m_msgs.upper_bound(id); ++it) {
if (it->second->totalLength() == msg->totalLength()) return false;
}
m_msgs.insert(std::make_pair(id, std::move(msg)));
return true;
}
@@ -15,6 +19,15 @@ Message* MessageRegistry::find(uint16_t id) const {
return (it != m_msgs.end()) ? it->second.get() : nullptr;
}
Message* MessageRegistry::find(uint16_t id, size_t totalLength) const {
Message* first = nullptr;
for (auto it = m_msgs.lower_bound(id); it != m_msgs.upper_bound(id); ++it) {
if (it->second->totalLength() == totalLength) return it->second.get();
if (!first) first = it->second.get();
}
return first; // 总长未命中时回退该 id 的第一条
}
size_t MessageRegistry::size() const {
return m_msgs.size();
}
@@ -26,4 +39,4 @@ std::vector<Message*> MessageRegistry::all() const {
return out;
}
} // namespace ccu
} // namespace ccu
+15 -6
View File
@@ -2,6 +2,7 @@
#define PCCU_MESSAGE_REGISTRY_H
#include <cstdint>
#include <cstddef>
#include <map>
#include <memory>
#include <vector>
@@ -12,31 +13,39 @@ namespace ccu {
//============================================================================
// MessageRegistry:消息注册表与收发分发中心。
//
// 每条链路(FC 链路 / PM 链路)持有各自的注册表实例。
// 每条链路(FC 链路 / PM 链路 / RCU 链路)持有各自的注册表实例。
// 由于 FC 协议与 PM 协议的域标识符存在重叠(0x0001/0x0002),
// 分属不同注册表可完全隔离,互不干扰。
//
// 同 id 多消息:配电指令 0x0001~0x0004 与 PM 协议操控/参数指令共用 id,
// 仅帧总长不同,因此注册表允许同 id 多条消息,并支持按总长精确查找:
// find(id, totalLength) 精确匹配(收帧分发用,未命中回退 find(id))
// find(id) 返回该 id 的第一条(兼容旧接口)
//
// 协议新增/调整消息时只需 register() 对应 Message 实现,上层无需改动。
//============================================================================
class MessageRegistry {
public:
// 注册一条消息;返回是否成功(id 冲突时返回 false)
// 注册一条消息;返回是否成功(同 id 且同总长的重复注册返回 false)
bool registerMessage(std::unique_ptr<Message> msg);
// 按 id 查找消息;未注册返回 nullptr
// 按 id 查找消息(同 id 多条时返回第一条);未注册返回 nullptr
Message* find(uint16_t id) const;
// 按 id + 帧总长精确查找;未命中时回退为 find(id)
Message* find(uint16_t id, size_t totalLength) const;
// 已注册消息数量
size_t size() const;
// 所有已注册消息(按 id 升序)
// 所有已注册消息(按 id 升序,同 id 按注册顺序)
std::vector<Message*> all() const;
private:
std::map<uint16_t, std::unique_ptr<Message>> m_msgs;
std::multimap<uint16_t, std::unique_ptr<Message>> m_msgs;
};
} // namespace ccu
#endif // PCCU_MESSAGE_REGISTRY_H
#endif // PCCU_MESSAGE_REGISTRY_H
+121 -1
View File
@@ -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
+11
View File
@@ -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;
};
+355 -31
View File
@@ -65,6 +65,14 @@ 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;}
.btn{padding:3px 10px;margin:0 4px 4px 0;border:1px solid var(--line);border-radius:6px;background:#222b3d;color:var(--fg);font-size:11px;cursor:pointer;}
.btn:hover{border-color:#3b4a63;}
.btn.on{background:#1f6feb;border-color:#1f6feb;color:#fff;font-weight:600;}
.btn.off{background:#3d2226;border-color:#f85149;color:#f85149;}
.btn:disabled{opacity:.4;cursor:not-allowed;}
</style>
</head>
<body>
@@ -83,6 +91,7 @@ th{color:var(--dim);font-weight:500;}
</div>
<div class="tab-group"><span class="tg-label">系统</span>
<button class="tab" data-tab="links">链路状态</button>
<button class="tab" data-tab="realCcu">真实CCU</button>
<button class="tab" data-tab="raw">原始报文</button>
</div>
</div>
@@ -92,11 +101,12 @@ th{color:var(--dim);font-weight:500;}
const $ = id => document.getElementById(id);
const pagesEl = $('pages');
const statusEl = $('status');
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','raw'];
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','realCcu','raw'];
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 +138,33 @@ 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(typeof a !== 'number' || isNaN(a)) return '—';
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 +330,14 @@ 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)+
if(l.rcu){
h+=row('<span><span class="status-dot '+dot(online(l.rcu.lastRx))+'"></span>RCU 链路 (真实CCU)</span>',
'收:'+l.rcu.rx+' 发:'+l.rcu.tx+' 误:'+l.rcu.err);
}
// 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 +407,96 @@ function pmFbCard(d){
return card('设定结果', h);
}
//------------------ 锂电池 BMS(CAN) 卡片 ------------------
//------------------ BCU 断路器控制(下行 0x10XX81FF) ------------------
// pos/neg: 1 闭合 / 0 断开(总正+总负同帧下发,经 pCanBridge 写 USBCAN)
function bcuCtrl(addr, pos, neg, label){
if(!confirm('确认向 BCU 节点 '+addr+' 下发「'+label+'」指令?\n该指令将直接控制电池簇总正/总负继电器。')) return;
fetch('/api/bcu_ctrl?addr='+addr+'&pos='+pos+'&neg='+neg)
.then(function(r){ return r.json(); })
.then(function(j){
if(j.ok){
alert('已下发: 节点'+j.addr+' 总正='+(j.pos?'闭合':'断开')+' 总负='+(j.neg?'闭合':'断开')+
'\n继电器状态以 0x10'+pad2(j.addr)+'0003 上报刷新为准');
}else{
alert('下发失败: '+(j.error||'未知错误'));
}
})
.catch(function(){ alert('网络错误,下发未完成'); });
}
// 断路器控制下发统计卡片(最近一次指令与成败计数)
function bcuCtrlStatCard(s){
let c='';
const v = s && s.valid;
c+=row('累计下发', (s && s.sentCount || 0) + ' 成功 / ' + (s && s.errCount || 0) + ' 失败');
if(v){
c+=row('最近指令', '节点'+s.addr+' 总正='+(s.pos?'闭合':'断开')+' 总负='+(s.neg?'闭合':'断开')
+ ' ' + (s.lastOk ? '<span class="fc-ok">已投递</span>' : '<span class="fc-bad">投递失败</span>'));
c+=row('下发时间', ageCell(s.age, true));
}else{
c+=row('最近指令', '无');
}
c+=sub('链路: 网页 → pCCU(0x10XX81FF) → MOOS CAN_TX_* → pCanBridge → USBCAN → BCU');
return card('断路器控制下发', c);
}
//------------------ 锂电池 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));
// 断路器控制(0x10XX81FF 下发,总正+总负继电器同帧控制)
h+=sub('断路器控制 (0x10'+pad2(n.addr)+'81FF)');
h+=row('操作',
'<button class="btn on" onclick="bcuCtrl('+n.addr+',1,1,\'断路器闭合\')">断路器闭合</button>'+
'<button class="btn off" onclick="bcuCtrl('+n.addr+',0,0,\'断路器断开\')">断路器断开</button>');
// 绝缘/端口电压(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){
@@ -430,6 +538,214 @@ function pmStatusCard(d){
return card('整合后状态', h);
}
//------------------ 真实 CCU(配电桥接) 卡片 ------------------
// 数据来源:RCU 链路。配电反馈(0x0005~0x0008)由 pCCU 原帧转发
// pPowerManger 并在此全字段展示;配电指令(0x0001~0x0004)由 pPowerManger
// 经 pCCU 原帧转发真实CCU。断路器/开关量:0x55 闭合 0xAA 断开 0x5A 故障
function brkText(v){
if(v===0x55) return '<span class="fc-ok">闭合</span>';
if(v===0xAA) return '断开';
if(v===0x5A) return '<span class="fc-bad">故障脱扣</span>';
return hex(v);
}
// 双值信号(0x55 正常 / 0xAA 异常)
function sigText(v){
if(v===0x55) return '<span class="fc-ok">正常</span>';
if(v===0xAA) return '<span class="fc-bad">异常</span>';
return hex(v);
}
function insStatText(v){ return v===0x55 ? '<span class="fc-ok">正常</span>'
: (v===0xAA ? '<span class="fc-bad">绝缘低</span>' : hex(v)); }
// age:距最后收到该类帧的秒数;valid:是否收到过(未收到显示"从未收到")
function ageCell(age, valid){
if(!valid || typeof age !== 'number' || isNaN(age))
return '<span style="color:var(--dim)">从未收到</span>';
const online = age >= 0 && age < 5;
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">') + ageText(age)
+ (online ? ' (在线)' : ' (超时)') + '</span>';
}
function fwdLine(okN, errN){
return okN + ' 成功' + (errN ? ' <span class="fc-bad">'+errN+' 失败</span>' : '');
}
// 母线卡片通用:breakers 子表 + 信号/电气量行(无数据时只显示概要行)
function busCard(title, bus, age, rows){
let h='';
const b = bus && bus.breakers ? bus.breakers : {};
const keys = Object.keys(bNamesDis).filter(function(k){ return b[k] !== undefined; });
if(!keys.length){
h+=sub('断路器状态');
h+=row('状态','暂无数据');
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
h+=sub('断路器状态');
for(const k of keys) h+=row(bNamesDis[k], brkText(b[k]));
for(const r of rows) h+=row(r[0], r[1]);
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
// 母线断路器中文名表(busCard 共用)
const bNamesDis = {
fuelCell:'燃料电池断路器', powerLithiumBattery:'动力锂电池断路器',
propulsionMotor:'推进电机断路器', lithiumBatteryGroupInstrument:'锂电池组(仪表)断路器',
dcDc5Module:'DC/DC5模块断路器', bowHighVoltageDistributionBox:'艏部高压配电箱断路器',
sternFTDevice45:'艉部FT装置4/5断路器', fbReserved:'FB预留断路器',
sternRudderSwitch1:'艉舵开关1断路器', sternRudderSwitch2:'艉舵开关2断路器',
tyzReserved:'TYZ预留断路器', reserved:'预留断路器',
bowFTDevice123:'艏部FT装置1/2/3断路器', actuator:'启闭机构断路器',
mastSteeringGearControlBox:'桅杆舵机控制箱断路器', xcz:'XCZ断路器',
bowRudderControlBox:'艏舵控制箱断路器', openWaterCoverStartCylinder:'敞水盖启动电缸断路器',
bowLowVoltageDistributionBox:'艏部低压配电箱断路器', unit4InstrumentDC48V:'UNIT4仪表DC48V断路器',
dcC1DCDistributionPanel:'DC-C1直流配电板断路器', reserved1:'预留断路器1', reserved2:'预留断路器2',
emergencyLithiumBatteryGroup2:'应急锂电池组2断路器',
unit1:'UNIT1断路器', unit2:'UNIT2断路器', unit3:'UNIT3断路器', unit5:'UNIT5断路器',
bowPZDevice:'艏部PZ装置断路器', emergencyLithiumBatteryGroup1:'应急锂电池组1断路器',
dcdc5ModuleStart:'DC/DC5模块启动', coolingSystemStart:'冷却系统启动'
};
function realCcuPage(d){
if(!d) return '<div class="card"><h2>真实CCU</h2><div class="row"><span class="k">数据状态</span><span class="v">等待数据...</span></div></div>';
let h='<div class="page-hdr"><span class="raw-id">RCU</span> <span class="raw-name">真实CCU 配电桥接</span> '+
'<span class="badge ok">原帧透传</span></div><div class="grid">';
// 桥接概览卡片
{
let c='';
const fwd = d.forward || {};
c+=row('配电反馈收帧', fwd.fbCount || 0);
c+=row('反馈转发PM', fwdLine(fwd.fwdFbOk || 0, fwd.fwdFbErr || 0));
c+=row('配电指令收帧', fwd.cmdCount || 0);
c+=row('指令转发真实CCU', fwdLine(fwd.fwdCmdOk || 0, fwd.fwdCmdErr || 0));
const ccu = d.ccuStatus || {};
c+=sub('真实CCU 状态报文(仅显示,不转发)');
c+=row('在线', ageCell(ccu.age, ccu.valid));
if(ccu.valid){
c+=row('运行模式', modeText(ccu.fc_mode));
c+=row('运行状态', statusText(ccu.fc_status));
c+=row('系统故障等级', faultLvlBadge(ccu.fc_fault_level));
c+=row('系统发电功率', (ccu.generation_power*0.01).toFixed(2)+' kW');
c+=row('仪表/动力SOC', ccu.ins_soc+'% / '+ccu.dyn_soc+'%');
c+=row('通信心跳', ccu.heartbeat);
}
h+=card('桥接概览', c);
}
// 高压母线(0x0005 反馈 / 0x0001 指令)
{
const hv = d.hv || {};
const rows = [
['DC/DC5故障字低', hex(hv.dcDcFaultWord1)],
['DC/DC5故障字高', hex(hv.dcDcFaultWord2)],
['动力母排绝缘状态', insStatText(hv.powerBusInsulationStatus)],
['A汇流排绝缘状态', insStatText(hv.aBusInsulationStatus)],
['仪表电源状态', sigText(hv.meterPowerLossSignal)],
['应急电源状态', sigText(hv.emergencyPowerLossSignal)],
['DC/DC温度', hv.dcDcTemperature],
['动力汇流排电压', hv.powerBusVoltage],
['DC/DC5模块电流', hv.dcDc5ModuleCurrent],
['动力汇流排电流', hv.powerBusCurrent],
['A汇流排电压', hv.busbarAVoltage],
['推进电机控制箱电流', hv.propulsionMotorControlBoxCurrent],
['A汇流排电流', hv.busbarACurrent],
['锂电池组(仪表)电流', hv.lithiumBatteryGroupMeterCurrent],
['艏部高压配电箱电流', hv.bowHighVoltageDistributionBoxCurrent],
['艉部FT装置4/5电流', hv.sternFTDevice45Current],
['TYZ预留开关电流', hv.tyzReservedSwitchCurrent],
['艉舵开关1电流', hv.sternRudderSwitch1Current],
['预留电流1', hv.reservedCurrent1],
['艉舵开关2电流', hv.sternRudderSwitch2Current],
['预留电流2', hv.reservedCurrent2],
['FB预留电流', hv.fbReservedSwitchCurrent],
['预留电流3', hv.reservedCurrent3],
['冷却水压力', hv.coolWaterPressure],
];
h+=busCard('高压母线 (0x0005 反馈 / 0x0001 指令)', hv, d.hvAge, rows);
}
// 高压母线A(0x0006 反馈 / 0x0002 指令)
{
const b = d.hva || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['汇流排B电压', b.busbarBVoltage],
['汇流排B电流', b.busbarBCurrent],
['艏部FT装置1/2/3电流', b.bowFTDevice123Current],
['启闭机构电流', b.actuatorCurrent],
['桅杆舵机控制箱电流', b.mastSteeringGearControlBoxCurrent],
['XCZ电流', b.xczCurrent],
['艏舵控制箱电流', b.bowRudderControlBoxCurrent],
['敞水盖启动电缸电流', b.openWaterCoverStartCylinderCurrent],
];
h+=busCard('高压母线A (0x0006 反馈 / 0x0002 指令)', b, d.hvaAge, rows);
}
// 低压主母线(0x0007 反馈 / 0x0003 指令)
{
const b = d.hvb || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['仪表汇流排绝缘', insStatText(b.instrumentBusbarInsulationLow)],
['仪表母排电压', b.instrumentBusbarVoltage],
['艏部低压配电箱电流', b.bowLowVoltageDistributionBoxCurrent],
['DC-C1仪表DC48V电流', b.dcC1InstrumentDC48VCurrent],
['预留电流1', b.reservedCurrent1],
['应急锂电池组2电流', b.emergencyLithiumBatteryGroup2Current],
['锂电池组(仪表)电流', b.lithiumBatteryGroupInstrumentCurrent],
['UNIT4仪表DC48V电流', b.unit4InstrumentDC48VCurrent],
['预留电流2', b.reservedCurrent2],
['应急2汇流排电压', b.emergency2BusbarVoltage],
['复合能源应急DC48V电流', b.compositeEnergyEmergencyDC48VCurrent],
['燃料电池安全应急DC48V电流', b.fuelCellSecurityEmergencyDC48VCurrent],
['PLC控制电源电流', b.plcControlPowerCurrent],
];
h+=busCard('低压主母线 (0x0007 反馈 / 0x0003 指令)', b, d.hvbAge, rows);
}
// 仪表汇流排(0x0008 反馈 / 0x0004 指令)
{
const b = d.lv || {};
const rows = [
['仪表电源失电信号', sigText(b.powerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['仪表母排电压', b.instrumentBusbarVoltage],
['仪表母排电流', b.instrumentBusbarCurrent],
['UNIT1电流', b.unit1Current],
['UNIT2电流', b.unit2Current],
['UNIT3电流', b.unit3Current],
['UNIT5电流', b.unit5Current],
['艏部PZ装置电流', b.bowPZDeviceCurrent],
['应急锂电池组1电流', b.emergencyLithiumBatteryGroup1Current],
['应急1汇流排电压', b.emergency1BusbarVoltage],
];
h+=busCard('仪表汇流排 (0x0008 反馈 / 0x0004 指令)', b, d.lvAge, rows);
}
// 最近下发指令卡片
{
const lc = d.lastCmd || {};
let c='';
c+=row('最后指令时间', ageCell(lc.age, lc.valid));
const cmds = lc.cmds || {};
const groups = [['hv','高压母线'],['hva','高压母线A'],['hvb','低压主母线'],['lv','仪表汇流排']];
for(const g of groups){
const gb = cmds[g[0]] || {};
const keys = Object.keys(gb);
if(!keys.length) continue;
c+=sub(g[1]+' (0x'+({hv:'0001',hva:'0002',hvb:'0003',lv:'0004'})[g[0]]+')');
for(const k of keys) c+=row(bNamesDis[k]||k, brkText(gb[k]));
}
h+=card('最近下发的配电指令 (PM→真实CCU)', c);
}
h+='</div>';
return h;
}
//------------------ 标签页头部 ------------------
function pageHeader(id, name, dir){
const idHex = '0x'+id.toString(16).toUpperCase().padStart(4,'0');
@@ -482,19 +798,27 @@ 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)+
bcuCtrlStatCard(snap.bcuCtrl)+
'<div class="card"><h2>报文来源 (CAN ID)</h2>'+
row('0x10010000','总电压 / SOC / 故障码 / 状态位')+
row('0x10010005','最大电流限制 / 总电流 / 最高单体电压')+
row('0x10010006','总电压(校验)')+
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'</div></div>',
row('0x10XX0000','累加电压 / 回路电流 / SOC / 告警码 / 自检状态')+
row('0x10XX0001','最高/最低/平均单体电压及编号')+
row('0x10XX0002','最高/最低/平均单体温度及编号')+
row('0x10XX0003','正/负极继电器状态')+
row('0x10XX0006','正/负极绝缘阻抗 / 端口电压 / 继电器外侧电压')+
row('0x10XX0010','告警位(附录1,按位解析显示告警含义)')+
row('0x10XX81FF','断路器控制下发(MBMS→BCU,总正/总负继电器,总正+总负同帧)')+
row('XX','BCU 节点地址 01h~36h,同一节点的报文归入同一卡片')+
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+
row('下行通道','pCCU 发布 CAN_TX_0x* → pCanBridge → USBCAN')+'</div></div>',
fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>',
// 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据
pmStatus: pageHeader(0x0004,'PM状态报文','CCU→PM')+'<div class="grid">'+
pmStatusCard(snap.pmStatus)+batteryCard(snap.pmStatus)+emergCard(snap.pmStatus)+'</div>',
pmFb: pageHeader(0x0003,'PM参数反馈','CCU→PM')+'<div class="grid">'+pmFbCard(snap.pmFb)+'</div>',
links: '<div class="grid">'+linkCard(snap)+'</div>',
realCcu: realCcuPage(snap.realCcu),
raw: rawCard(snap.logs)
};
+203 -41
View File
@@ -14,11 +14,14 @@ namespace canbridge {
CanBridge::CanBridge() {}
CanBridge::~CanBridge() {
if (m_endpoint) {
m_endpoint->stop();
delete m_endpoint;
m_endpoint = nullptr;
for (size_t i = 0; i < m_channels.size(); ++i) {
if (m_channels[i].endpoint) {
m_channels[i].endpoint->stop();
delete m_channels[i].endpoint;
m_channels[i].endpoint = nullptr;
}
}
m_channels.clear();
if (m_db) {
m_db->close();
delete m_db;
@@ -27,7 +30,11 @@ CanBridge::~CanBridge() {
}
//---------------------------------------------------------
// OnStartUp:读取配置并启动 CAN 链路
// OnStartUp:读取配置并启动各通道 CAN 链路
//
// 通道配置(可重复多行):
// channel = <通道名>,<转换器IP>,<工作端口>
// 兼容旧单通道配置:can_host / can_port / can_channel_name
bool CanBridge::OnStartUp() {
AppCastingMOOSApp::OnStartUp();
@@ -37,6 +44,11 @@ bool CanBridge::OnStartUp() {
if (!m_MissionReader.GetConfiguration(GetAppName(), sParams))
reportConfigWarning("No config block found for " + GetAppName());
// 旧单通道配置缓存(存在 can_host 时回退使用)
bool hasLegacy = false;
string legacyHost, legacyName = "CAN0";
long legacyPort = 0;
STRING_LIST::iterator p;
for (p = sParams.begin(); p != sParams.end(); p++) {
string orig = *p;
@@ -45,19 +57,55 @@ bool CanBridge::OnStartUp() {
string value = stripBlankEnds(line);
bool handled = true;
if (param == "can_host") m_canHost = value;
else if (param == "can_port") m_canPort = atol(value.c_str());
else if (param == "can_channel_name") m_channelName = value;
else if (param == "dbpath") m_dbPath = value;
if (param == "channel") {
// channel = <通道名>,<IP>,<端口>
string name = stripBlankEnds(biteStringX(value, ','));
string host = stripBlankEnds(biteStringX(value, ','));
string port = stripBlankEnds(value);
ChannelCfg cfg;
cfg.name = name;
cfg.host = host;
cfg.port = atol(port.c_str());
if (cfg.name.empty() || cfg.host.empty() || cfg.port <= 0) {
reportConfigWarning("bad channel line (want name,host,port): " + orig);
} else {
Channel ch;
ch.cfg = cfg;
m_channels.push_back(ch);
}
}
else if (param == "default_channel") m_defaultChannel = value;
else if (param == "dbpath") m_dbPath = value;
// 旧单通道配置(向后兼容)
else if (param == "can_host") { legacyHost = value; hasLegacy = true; }
else if (param == "can_port") { legacyPort = atol(value.c_str()); hasLegacy = true; }
else if (param == "can_channel_name") { legacyName = value; }
else handled = false;
if (!handled)
reportUnhandledConfigWarning(orig);
}
// 无 channel 行但有旧配置 -> 构造单通道
if (m_channels.empty() && hasLegacy &&
!legacyHost.empty() && legacyPort > 0) {
ChannelCfg cfg;
cfg.name = legacyName;
cfg.host = legacyHost;
cfg.port = legacyPort;
Channel ch;
ch.cfg = cfg;
m_channels.push_back(ch);
reportConfigWarning("legacy can_host/can_port config used; "
"prefer 'channel = name,host,port' lines");
}
registerVariables();
// 数据库(CAN 帧落库)
if (m_channels.empty())
reportRunWarning("no CAN channel configured");
// 数据库(CAN 帧落库,channel 列区分通道)
m_db = new CanDbStore(m_dbPath);
if (!m_db->open()) {
reportRunWarning("CAN db open failed: " + m_dbPath + " (" + m_db->lastError() + ")");
@@ -65,19 +113,26 @@ bool CanBridge::OnStartUp() {
m_db = nullptr;
}
// CAN 链路(TCP Client,收帧回调在接收线程中触发)
m_endpoint = new CanEndpoint();
m_endpoint->configure(m_canHost, m_canPort);
m_endpoint->setFrameCallback(
[this](uint8_t fi, uint32_t id, const uint8_t* data, int dlc) {
handleFrame(fi, id, data, dlc);
});
if (!m_endpoint->start()) {
reportRunWarning("CAN endpoint start failed: " + m_canHost);
// 各通道 CAN 链路(TCP Client,收帧回调在各自接收线程中触发)
for (size_t i = 0; i < m_channels.size(); ++i) {
Channel& ch = m_channels[i];
ch.endpoint = new CanEndpoint();
ch.endpoint->configure(ch.cfg.host, ch.cfg.port);
const string name = ch.cfg.name; // 按通道捕获
ch.endpoint->setFrameCallback(
[this, name](uint8_t fi, uint32_t id, const uint8_t* data, int dlc) {
handleFrame(name, fi, id, data, dlc);
});
if (!ch.endpoint->start())
reportRunWarning("CAN endpoint start failed: " + ch.cfg.name +
" (" + ch.cfg.host + ")");
}
cout << "pCanBridge started: " << m_canHost << ":" << m_canPort
<< " (" << m_channelName << "), db: " << m_dbPath << endl;
cout << "pCanBridge started with " << m_channels.size() << " channel(s):";
for (size_t i = 0; i < m_channels.size(); ++i)
cout << " " << m_channels[i].cfg.name << "="
<< m_channels[i].cfg.host << ":" << m_channels[i].cfg.port;
cout << ", db: " << m_dbPath << endl;
return true;
}
@@ -90,25 +145,120 @@ bool CanBridge::OnConnectToServer() {
}
//---------------------------------------------------------
// registerVariables:纯发布应用,无需注册订阅变量
// registerVariables:订阅下行指令(CAN_TX_0x*,pCCU 发布的
// BCU 断路器控制帧等,通道经 m_sSrcAux 指定)。
void CanBridge::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// 通配订阅必须用三参数重载(变量模式 + 来源模式)
Register("CAN_TX_0x*", "*", 0);
}
//---------------------------------------------------------
// OnNewMail:当前无订阅指令,预留
// OnNewMail:下行指令 CAN_TX_0x%08X(二进制数据域)
bool CanBridge::OnNewMail(MOOSMSG_LIST &NewMail) {
AppCastingMOOSApp::OnNewMail(NewMail);
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
(void)msg;
if (msg.m_sKey.rfind("CAN_TX_0x", 0) == 0)
handleTxMessage(msg);
}
return true;
}
//---------------------------------------------------------
// findChannel:按通道名查找运行通道
CanBridge::Channel* CanBridge::findChannel(const std::string& name) {
for (size_t i = 0; i < m_channels.size(); ++i) {
if (m_channels[i].cfg.name == name && m_channels[i].endpoint)
return &m_channels[i];
}
return nullptr;
}
//---------------------------------------------------------
// resolveTxChannel:下行目标通道解析(回退规则)
// 1) srcAux 非空:命中通道则用之,否则告警丢弃
// 2) srcAux 为空:default_channel 命中则用之
// 3) 仍无:仅单通道时用该通道,多通道告警丢弃
CanBridge::Channel* CanBridge::resolveTxChannel(const std::string& srcAux) {
if (!srcAux.empty()) {
Channel* ch = findChannel(srcAux);
if (!ch)
reportRunWarning("CAN_TX unknown channel: '" + srcAux + "'");
return ch;
}
if (!m_defaultChannel.empty()) {
Channel* ch = findChannel(m_defaultChannel);
if (ch) return ch;
}
if (m_channels.size() == 1)
return m_channels[0].endpoint ? &m_channels[0] : nullptr;
reportRunWarning("CAN_TX no channel (empty srcAux) with " +
to_string(m_channels.size()) + " channels, dropped");
return nullptr;
}
//---------------------------------------------------------
// handleTxMessage:MOOS 下行消息 -> CANET 13 字节帧 -> 对应通道
//
// m_sKey = "CAN_TX_0x%08X"(CAN ID)
// m_sVal = 二进制数据域(dlc<=8)
// m_sSrcAux = 目标通道名(空时按回退规则)
// 帧信息:id>0x7FF 视为扩展帧(bit7=1),DLC=数据字节数。
void CanBridge::handleTxMessage(CMOOSMsg& msg) {
++m_txCount;
uint32_t id = 0;
// "CAN_TX_0x" 前缀长 9 字符(注意不是 8)
int sr = std::sscanf(msg.m_sKey.c_str() + 9, "%x", &id);
if (sr != 1) {
++m_txDropCount;
reportRunWarning("CAN_TX bad key: " + msg.m_sKey);
return;
}
if (!msg.IsBinary() || msg.GetBinaryDataSize() <= 0 ||
msg.GetBinaryDataSize() > 8) {
++m_txDropCount;
reportRunWarning("CAN_TX bad data size: " + msg.m_sKey);
return;
}
Channel* ch = resolveTxChannel(msg.GetSourceAux());
if (!ch) {
++m_txDropCount;
return;
}
const unsigned char* d = msg.GetBinaryData();
if (!d) {
++m_txDropCount;
return;
}
int dlc = static_cast<int>(msg.GetBinaryDataSize());
uint8_t frameInfo = static_cast<uint8_t>(((id > 0x7FF) ? 0x80 : 0x00) | dlc);
if (ch->endpoint->sendFrame(frameInfo, id, d, dlc)) {
char hex[32] = {0};
for (int i = 0; i < dlc && i < 8; ++i)
std::snprintf(hex + i * 2, 3, "%02X", d[i]);
std::cout << "[CAN-TX " << ch->cfg.name << "] 0x" << std::hex << id
<< std::dec << " dlc=" << dlc << " data=" << hex << std::endl;
} else {
reportRunWarning("CAN_TX send failed on " + ch->cfg.name + ": " +
msg.m_sKey +
(ch->endpoint->isConnected() ? "" : " (未连接)"));
}
}
//---------------------------------------------------------
// Iterate:周期任务
@@ -121,12 +271,12 @@ bool CanBridge::Iterate() {
//---------------------------------------------------------
// handleFrame:CAN 帧 -> CMOOSMsg(二进制) -> MOOSDB
//
// 在 CanEndpoint 接收线程中调用;m_Comms::Post 线程安全。
// 在各通道 CanEndpoint 接收线程中调用;m_Comms::Post 线程安全。
void CanBridge::handleFrame(uint8_t frameInfo, uint32_t id,
const uint8_t* data, int dlc) {
void CanBridge::handleFrame(const std::string& channel, uint8_t frameInfo,
uint32_t id, const uint8_t* data, int dlc) {
// 落库(WAL + 预处理语句,~60帧/秒无压力)
if (m_db) m_db->onFrame(m_channelName, frameInfo, id, data, dlc);
if (m_db) m_db->onFrame(channel, frameInfo, id, data, dlc);
char key[32];
std::snprintf(key, sizeof(key), "CAN_0x%08X", id);
@@ -135,29 +285,41 @@ void CanBridge::handleFrame(uint8_t frameInfo, uint32_t id,
CMOOSMsg msg(MOOS_NOTIFY, key,
static_cast<unsigned int>(dlc),
const_cast<uint8_t*>(data));
msg.SetSourceAux(m_channelName); // 通道 -> m_sSrcAux
msg.SetSourceAux(channel); // 通道 -> m_sSrcAux
msg.SetDoubleAux(static_cast<double>(frameInfo)); // FF/RTR/DLC -> m_dfVal2
m_Comms.Post(msg);
++m_notifyCount;
}
//---------------------------------------------------------
// buildReport:AppCasting 报告
// buildReport:AppCasting 报告(逐通道)
bool CanBridge::buildReport() {
m_msgs << "============================================" << "\n";
m_msgs << "pCanBridge CAN->MOOSDB 透传" << "\n";
m_msgs << "pCanBridge CAN<->MOOSDB 多通道双向透传" << "\n";
m_msgs << "============================================" << "\n";
if (m_endpoint) {
m_msgs << "目标: " << m_endpoint->host() << ":"
<< m_endpoint->port() << " (" << m_channelName << ")" << "\n";
m_msgs << "连接状态: " << (m_endpoint->isConnected() ? "已连接" : "断开") << "\n";
m_msgs << "收帧数: " << m_endpoint->frameCount() << "\n";
m_msgs << "发布数: " << m_notifyCount << "\n";
m_msgs << "错误数: " << m_endpoint->errorCount() << "\n";
m_msgs << "重连次数: " << m_endpoint->reconnectCount() << "\n";
m_msgs << "最近收帧: " << m_endpoint->lastFrameTime() << "\n";
m_msgs << "通道数: " << m_channels.size() << "\n";
if (!m_defaultChannel.empty())
m_msgs << "默认通道: " << m_defaultChannel << "\n";
m_msgs << "MOOS下发数: " << m_txCount << " (丢弃 " << m_txDropCount << ")\n";
for (size_t i = 0; i < m_channels.size(); ++i) {
const Channel& ch = m_channels[i];
m_msgs << "--------------------------------------------" << "\n";
m_msgs << "[" << ch.cfg.name << "] " << ch.cfg.host << ":"
<< ch.cfg.port << "\n";
if (ch.endpoint) {
m_msgs << " 连接状态: "
<< (ch.endpoint->isConnected() ? "已连接" : "断开") << "\n";
m_msgs << " 收帧数: " << ch.endpoint->frameCount() << "\n";
m_msgs << " 下行帧数: " << ch.endpoint->txCount()
<< " (错误 " << ch.endpoint->txErrorCount() << ")\n";
m_msgs << " 错误数: " << ch.endpoint->errorCount() << "\n";
m_msgs << " 重连次数: " << ch.endpoint->reconnectCount() << "\n";
m_msgs << " 最近收帧: " << ch.endpoint->lastFrameTime() << "\n";
}
}
m_msgs << "============================================" << "\n";
if (m_db) {
m_msgs << "数据库: " << m_dbPath
<< (m_db->isOpen() ? " (已打开)" : " (未打开)") << "\n";
+61 -17
View File
@@ -6,26 +6,48 @@
#include "CanEndpoint.h"
#include "CanDbStore.h"
#include <atomic>
#include <string>
#include <vector>
namespace canbridge {
//============================================================================
// CanBridge:CAN -> MOOSDB 透传桥(MOOS 应用外壳)。
// CanBridge:CAN <-> MOOSDB 多通道双向透传桥(MOOS 应用外壳)。
//
// 数据流:
// USBCAN-8E-U (TCP Server, 默认 192.168.0.222:4001 = CAN0)
// 通道配置(.moos 中可重复多行):
// channel = <通道名>,<转换器IP>,<工作端口>
// 如 channel = CAN0,192.168.0.222,4001(CANET: CAN0=4001..CAN7=4008)
// 每通道一条独立 TCP 连接 + 独立接收线程。
//
// 数据流(上行,每通道一条 TCP 连接):
// CANET 工作端口 (TCP Server)
// -> CanEndpoint(TCP Client,13 字节切帧)
// -> handleFrame() 解析
// -> Notify 到 MOOSDB
//
// MOOS 消息映射:
// m_sKey = "CAN_0x%08X" CAN ID(完整 4 字节,标准/扩展帧同格式)
// m_sVal = 二进制 data(dlc 字节,MOOS_BINARY_STRING)
// m_sSrcAux = 通道名(默认 "CAN0",配置项 can_channel_name)
// m_dfVal2 = 原始帧信息字节 byte0(bit7 FF / bit6 RTR / bit3~0 DLC)
// 数据流(下行,BCU 断路器/继电器控制等):
// MOOS "CAN_TX_0x%08X"(二进制数据域,dlc<=8)
// -> OnNewMail 解析 CAN ID 与数据
// -> 按消息 m_sSrcAux(目标通道名)路由到对应通道的 CanEndpoint
// -> CanEndpoint::sendFrame(该通道 TCP 连接写出 13 字节帧)
// -> CANET -> CAN 总线
//
// MOOS 消息映射(上行/下行对称,通道一律走 m_sSrcAux):
// 上行 m_sKey = "CAN_0x%08X" CAN ID(完整 4 字节,标准/扩展帧同格式)
// 上行 m_sVal = 二进制 data(dlc 字节,MOOS_BINARY_STRING)
// 上行 m_sSrcAux = 通道名(如 "CAN0")
// 上行 m_dfVal2 = 原始帧信息字节 byte0(bit7 FF / bit6 RTR / bit3~0 DLC)
// 下行 m_sKey = "CAN_TX_0x%08X",m_sVal = 二进制 data,m_sSrcAux = 目标通道名
// m_nID 不使用(MOOS 内部消息序号)
//
// 每帧同时落库 SQLite(can_frame 表,配置项 dbpath)。
// 下行通道回退规则(m_sSrcAux 为空时):
// 1) 配置了 default_channel 且命中 -> 用该通道
// 2) 仅配置了单通道 -> 用该通道
// 3) 多通道且无 default_channel -> 告警丢弃
// m_sSrcAux 非空但未命中任何通道 -> 告警丢弃
//
// 每帧(上行)同时落库 SQLite(can_frame 表,配置项 dbpath);
// 下行帧仅日志记录(can_frame 表无方向列,保持旧库兼容)。
//============================================================================
class CanBridge : public AppCastingMOOSApp {
@@ -42,19 +64,41 @@ protected:
void registerVariables();
private:
// 通道配置(.moos 重复行 channel = name,host,port)
struct ChannelCfg {
std::string name;
std::string host;
long port = 0;
};
// 运行中的通道(配置 + 独立 TCP 连接)
struct Channel {
ChannelCfg cfg;
CanEndpoint* endpoint = nullptr;
};
// 收帧回调(CanEndpoint 接收线程调用)
void handleFrame(uint8_t frameInfo, uint32_t id, const uint8_t* data, int dlc);
void handleFrame(const std::string& channel, uint8_t frameInfo,
uint32_t id, const uint8_t* data, int dlc);
// 下行指令处理(OnNewMail 调用):CAN_TX_0x%08X -> 按通道路由发送
void handleTxMessage(CMOOSMsg& msg);
// 按通道名查找运行通道;未命中返回 nullptr
Channel* findChannel(const std::string& name);
// 下行目标通道解析(含回退规则);返回通道指针,nullptr 表示丢弃
Channel* resolveTxChannel(const std::string& srcAux);
// 配置
std::string m_canHost = "192.168.0.222";
long m_canPort = 4001;
std::string m_channelName = "CAN0"; // 写入 m_sSrcAux
std::string m_dbPath = "pCanBridge_data.db"; // SQLite 路径(moos 配置项 dbpath)
std::vector<Channel> m_channels;
std::string m_defaultChannel; // default_channel(可空)
std::string m_dbPath = "pCanBridge_data.db"; // SQLite 路径(moos 配置项 dbpath)
// 组件
CanEndpoint* m_endpoint = nullptr;
CanDbStore* m_db = nullptr;
std::atomic<unsigned long> m_notifyCount{0};
CanDbStore* m_db = nullptr;
std::atomic<unsigned long> m_txCount{0}; // MOOS 下行接收总数
std::atomic<unsigned long> m_txDropCount{0}; // 下行因通道解析失败丢弃数
};
} // namespace canbridge
+16 -11
View File
@@ -14,11 +14,12 @@ using namespace std;
void showSynopsis() {
blk("SYNOPSIS: ");
blk("------------------------------------ ");
blk(" The pCanBridge application bridges CAN bus frames from a ");
blk(" USBCAN-8E-U / CANET Ethernet-CAN converter (TCP Server mode) ");
blk(" into the MOOSDB. Each received CAN frame is parsed from the ");
blk(" The pCanBridge application bridges CAN bus frames between ");
blk(" multi-channel CANET Ethernet-CAN converters (TCP Server mode) ");
blk(" and the MOOSDB. Each channel uses its own TCP connection ");
blk(" (CANET working port). Every CAN frame is parsed from the ");
blk(" 13-byte CANET wire format and transparently published as a ");
blk(" binary MOOS message. ");
blk(" binary MOOS message (channel tagged via m_sSrcAux). ");
blk(" ");
}
@@ -61,12 +62,13 @@ void showExampleConfigAndExit() {
blk(" AppTick = 4 ");
blk(" CommsTick = 4 ");
blk(" ");
blk(" // USBCAN-8E-U/CANET 目标地址(TCP Server 模式) ");
blk(" can_host = 192.168.0.222 // 转换器 IP ");
blk(" can_port = 4001 // 工作端口: CAN0=4001 CAN1=4002.. ");
blk(" // CANET 通道(可重复多行):通道名,IP,工作端口 ");
blk(" // 工作端口: CAN0=4001 CAN1=4002 ... CAN7=4008 ");
blk(" channel = CAN0,192.168.0.222,4001 ");
blk(" channel = CAN1,192.168.0.222,4002 ");
blk(" ");
blk(" // 通道名(写入消息的 m_sSrcAux 辅助字段) ");
blk(" can_channel_name = CAN0 ");
blk(" // CAN_TX 下行默认通道(m_sSrcAux 为空时使用,可省略) ");
blk(" default_channel = CAN0 ");
blk(" ");
blk(" // CAN 帧 SQLite 落库路径(can_frame 表) ");
blk(" dbpath = pCanBridge_data.db ");
@@ -85,14 +87,17 @@ void showInterfaceAndExit() {
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" (none: publish-only application) ");
blk(" CAN_TX_0x%08X = 下行 CAN 帧(每帧一条二进制消息) ");
blk(" m_sVal: 二进制 data(dlc<=8 字节, MOOS_BINARY_STRING) ");
blk(" m_sSrcAux: 目标通道名(如 CAN0;空时按回退规则: ");
blk(" default_channel > 单通道 > 多通道告警丢弃) ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CAN_0x%08X = CAN 帧(每帧一条二进制消息) ");
blk(" 变量名: CAN ID 完整 4 字节十六进制, 如 CAN_0x10010001 ");
blk(" m_sVal: 二进制 data(dlc 字节, MOOS_BINARY_STRING) ");
blk(" m_sSrcAux: 通道名(默认 CAN0) ");
blk(" m_sSrcAux: 通道名(如 CAN0) ");
blk(" m_dfVal2: 原始帧信息字节 byte0 ");
blk(" bit7=FF(1扩展/0标准) bit6=RTR(1远程/0数据) ");
blk(" bit3~0=DLC ");
+43 -1
View File
@@ -12,7 +12,6 @@
#include <iostream>
#include <chrono>
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
namespace canbridge {
namespace {
@@ -128,6 +127,49 @@ void CanEndpoint::closeSocket() {
}
}
//----------------------------------------------------------------------
// 下行发送:组装 13 字节 CANET 帧并写出
// 接收线程可能并发断线重连(m_fd 被关闭),用 m_txMutex 串行化:
// - 持锁期间检查 m_connected,避免对已关闭 fd 发送
// - 接收线程的 closeSocket 不持此锁,若恰逢发送中出现 EPIPE 等
// 错误按发送失败计,断线由接收线程统一检出
//----------------------------------------------------------------------
bool CanEndpoint::sendFrame(uint8_t frameInfo, uint32_t id,
const uint8_t* data, int dlc) {
if (dlc < 0 || dlc > 8) return false;
uint8_t frame[kFrameLen] = {0};
frame[0] = frameInfo;
frame[1] = static_cast<uint8_t>((id >> 24) & 0xFF);
frame[2] = static_cast<uint8_t>((id >> 16) & 0xFF);
frame[3] = static_cast<uint8_t>((id >> 8) & 0xFF);
frame[4] = static_cast<uint8_t>(id & 0xFF);
if (data && dlc > 0)
std::memcpy(frame + 5, data, static_cast<size_t>(dlc));
std::lock_guard<std::mutex> lock(m_txMutex);
if (!m_connected || m_fd < 0) {
++m_txErrorCount;
std::cerr << "[CanEndpoint] send failed: not connected" << std::endl;
return false;
}
size_t sent = 0;
while (sent < kFrameLen) {
ssize_t n = ::send(m_fd, frame + sent, kFrameLen - sent, MSG_NOSIGNAL);
if (n > 0) {
sent += static_cast<size_t>(n);
continue;
}
if (n < 0 && (errno == EINTR)) continue;
++m_txErrorCount;
std::cerr << "[CanEndpoint] send error: "
<< std::strerror(errno) << std::endl;
return false;
}
++m_txCount;
return true;
}
//----------------------------------------------------------------------
// 切帧:缓冲 >= 13 字节即解析并回调
//----------------------------------------------------------------------
+11
View File
@@ -4,6 +4,7 @@
#include <cstdint>
#include <string>
#include <vector>
#include <mutex>
#include <atomic>
#include <thread>
#include <functional>
@@ -48,8 +49,15 @@ public:
bool isConnected() const { return m_connected; }
// 下行发送一帧 CAN(MOOS->CAN 方向,须已连接)。
// frameInfo: bit7 FF(1扩展/0标准) bit6 RTR bit3~0 DLC;
// data 为 dlc(0~8) 字节数据域。返回是否成功写出。
bool sendFrame(uint8_t frameInfo, uint32_t id, const uint8_t* data, int dlc);
// 统计
unsigned long frameCount() const { return m_frameCount; }
unsigned long txCount() const { return m_txCount; }
unsigned long txErrorCount() const { return m_txErrorCount; }
unsigned long errorCount() const { return m_errorCount; }
unsigned long reconnectCount() const { return m_reconnectCount; }
double lastFrameTime() const { return m_lastFrameTime; }
@@ -70,6 +78,7 @@ private:
std::atomic<bool> m_running{false};
std::atomic<bool> m_connected{false};
std::thread m_thread;
std::mutex m_txMutex; // 下行 send 与断线 close 的并发保护
CanFrameCallback m_onFrame;
@@ -77,6 +86,8 @@ private:
std::vector<uint8_t> m_buf;
std::atomic<unsigned long> m_frameCount{0};
std::atomic<unsigned long> m_txCount{0};
std::atomic<unsigned long> m_txErrorCount{0};
std::atomic<unsigned long> m_errorCount{0};
std::atomic<unsigned long> m_reconnectCount{0};
std::atomic<double> m_lastFrameTime{0.0};
+21 -16
View File
@@ -1,12 +1,14 @@
//============================================================================
// pCanBridge 配置示例
// pCanBridge 配置示例(多通道)
//
// 拓扑:
// USBCAN-8E-U(CANET 系列,TCP Server 模式)
// 192.168.0.222:4001 = CAN0 工作端口(CAN1=4002 ... CAN7=4008)
// pCanBridge 以 TCP Client 连接目标,收帧后透传 MOOSDB:
// 变量名 CAN_0x%08X = CAN ID;m_sVal = 二进制 data;
// m_sSrcAux = 通道名;m_dfVal2 = 原始帧信息字节(FF/RTR/DLC)
// 拓扑(双向透传,每通道一条独立 TCP 连接):
// 上行:CANET 工作端口(TCP Server)-> pCanBridge(TCP Client)
// -> MOOS "CAN_0x%08X"(二进制 data,m_sSrcAux=通道名)
// 下行:MOOS "CAN_TX_0x%08X"(二进制 data,m_sSrcAux=目标通道,
// 如 BCU 断路器控制帧 0x10XX81FF,由 pCCU 发布)
// -> pCanBridge 按通道路由 -> CANET -> CAN 总线
// 变量名 CAN_0x%08X = CAN ID;m_sVal = 二进制 data;
// m_sSrcAux = 通道名;上行 m_dfVal2 = 原始帧信息字节(FF/RTR/DLC)
//============================================================================
ProcessConfig = pCanBridge
@@ -14,17 +16,20 @@ ProcessConfig = pCanBridge
AppTick = 4
CommsTick = 4
//======== 目标 USBCAN-8E-U 地址(TCP Server 模式) ========
// 转换器 IP
can_host = 192.168.0.222
// 工作端口:CAN0=4001,CAN1=4002,...,CAN7=4008
can_port = 4001
//======== 通道配置(可重复多行) ========
// 格式:channel = <通道名>,<转换器IP>,<工作端口>
// CANET 工作端口:CAN0=4001,CAN1=4002,...,CAN7=4008
channel = CAN0,192.168.0.222,4001
// channel = CAN1,192.168.0.222,4002
// channel = CAN2,192.168.0.222,4003
// ...
//======== 通道标识 ========
// 写入消息 m_sSrcAux 辅助字段,用于下游区分通道
can_channel_name = CAN0
//======== 下行默认通道(可空) ========
// CAN_TX 消息 m_sSrcAux 为空时:配置了本项则用它;
// 未配置且仅单通道则用该通道;多通道则告警丢弃。
default_channel = CAN0
//======== 存储 ========
// CAN 帧 SQLite 落库路径(can_frame 表)
// CAN 帧 SQLite 落库路径(can_frame 表,channel 列区分通道)
dbpath = pCanBridge_data.db
}
+46
View File
@@ -0,0 +1,46 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pMotor
# 推进电机操作程序:网页控制 <-> pCanBridge(CAN1) <-> 电机控制器
#--------------------------------------------------------
if (${WIN32})
SET(SYSTEM_LIBS wsock32)
else (${WIN32})
SET(SYSTEM_LIBS m pthread)
endif (${WIN32})
# 复用仓库内 pPowerManger 的公共源码(相对路径引用,避免重复维护)
SET(PM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../pPowerManger)
SET(SHARED_SRC
${PM_DIR}/logc/loguru.cpp
${PM_DIR}/httpserver/mongoose.c
)
SET(SRC
${SHARED_SRC}
protocol/MotorCan.cpp
web/WebServer.cpp
Motor.cpp
Motor_Info.cpp
main.cpp
)
ADD_EXECUTABLE(pMotor ${SRC})
TARGET_INCLUDE_DIRECTORIES(pMotor PRIVATE
${PM_DIR}
${PM_DIR}/httpserver
${PM_DIR}/logc
)
TARGET_LINK_LIBRARIES(pMotor
${MOOS_LIBRARIES}
apputil
mbutil
m
pthread
jsoncpp
dl
${SYSTEM_LIBS}
)
+467
View File
@@ -0,0 +1,467 @@
#include "Motor.h"
#include "MBUtils.h"
#include "../pPowerManger/logc/loguru.hpp"
#include "json/json.h"
#include <cstdio>
#include <sstream>
using namespace motor;
using namespace std;
//---------------------------------------------------------
// Constructor / Destructor
Motor::Motor() {}
Motor::~Motor() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
}
//---------------------------------------------------------
// OnStartUp:读取配置并初始化各组件
bool Motor::OnStartUp() {
AppCastingMOOSApp::OnStartUp();
STRING_LIST sParams;
m_MissionReader.EnableVerbatimQuoting(false);
if (!m_MissionReader.GetConfiguration(GetAppName(), sParams))
reportConfigWarning("No config block found for " + GetAppName());
STRING_LIST::iterator p;
for (p = sParams.begin(); p != sParams.end(); p++) {
string orig = *p;
string line = *p;
string param = stripBlankEnds(tolower(biteStringX(line, '=')));
string value = stripBlankEnds(line);
bool handled = true;
if (param == "can_channel") m_canChannel = value;
else if (param == "red_channel") m_redChannel = value;
else if (param == "cmd_period_ms") m_cmdPeriodMs = atoi(value.c_str());
else if (param == "reset_hold_sec") m_resetHoldSec = atof(value.c_str());
else if (param == "web_port") m_webPort = atoi(value.c_str());
else if (param == "web_enable") m_webEnable = (tolower(value) == "true" || value == "1");
else if (param == "logpath") m_logPath = value;
else handled = false;
if (!handled)
reportUnhandledConfigWarning(orig);
}
if (m_cmdPeriodMs < 50) m_cmdPeriodMs = 50;
registerVariables();
// 日志文件
if (m_logPath.empty()) m_logPath = "pMotor.log";
loguru::add_file(m_logPath.c_str(), loguru::Append, loguru::Verbosity_MAX);
LOG_F(INFO, "pMotor log path: %s", m_logPath.c_str());
// 网页
if (m_webEnable) {
m_web = new WebServer();
m_web->setOnOpen([this]() { return buildSnapshot(); });
m_web->setApiHandler([this](const std::string& uri, const std::string& query) {
return handleApi(uri, query);
});
if (!m_web->start(m_webPort)) {
LOG_F(ERROR, "web server start failed on port %d", m_webPort);
delete m_web;
m_web = nullptr;
}
}
LOG_F(INFO, "pMotor started: motor CAN via pCanBridge channel=%s (red_channel=%s), "
"cmd period=%dms, web=%s",
m_canChannel.c_str(), m_redChannel.c_str(), m_cmdPeriodMs,
m_web ? ("port " + to_string(m_webPort)).c_str() : "disabled");
return true;
}
//---------------------------------------------------------
// OnConnectToServer
bool Motor::OnConnectToServer() {
registerVariables();
return true;
}
//---------------------------------------------------------
// registerVariables:订阅 pCanBridge 透传的全部 CAN 帧
void Motor::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// 通配订阅必须用三参数重载(变量模式 + 来源模式)
Register("CAN_0x*", "*", 0);
}
//---------------------------------------------------------
// OnNewMail
bool Motor::OnNewMail(MOOSMSG_LIST &NewMail) {
AppCastingMOOSApp::OnNewMail(NewMail);
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
if (msg.m_sKey.rfind("CAN_0x", 0) == 0)
handleCanMessage(msg);
}
return true;
}
//---------------------------------------------------------
// handleCanMessage:解码 pCanBridge 透传的 CAN 帧
//
// 消息格式(pCanBridge 约定):
// m_sKey = "CAN_0x%08X"(CAN ID,扩展帧)
// m_sVal = 二进制数据域(8 字节)
// m_dfVal2 = 原始帧信息字节(bit7 FF / bit6 RTR / bit3~0 DLC)
void Motor::handleCanMessage(CMOOSMsg& msg) {
// 从变量名解析 CAN ID(跳过前缀 "CAN_0x" 共 6 字符)
uint32_t canId = 0;
if (std::sscanf(msg.m_sKey.c_str() + 6, "%x", &canId) != 1) return;
if (!isCanMotorId(canId)) return;
if (!(msg.IsBinary() && msg.GetBinaryDataSize() >= 8)) return;
const unsigned char* d = msg.GetBinaryData();
if (!d) return;
// 日志值在锁内取副本,避免与 Web 线程快照读取竞争
uint32_t rx = 0;
int spd = 0;
int bus = 0;
size_t f1 = 0, f2 = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!decodeMotorFrame(canId, d, static_cast<int>(msg.GetBinaryDataSize()), m_state))
return;
m_state.rxCount++;
double now = MOOSTime(false);
switch (canId) {
case MOTOR_CAN_FAULT1: case MOTOR_CAN_FAULT1_RED:
m_state.branch1.lastRxTime = now; break;
case MOTOR_CAN_FAULT2: case MOTOR_CAN_FAULT2_RED:
m_state.branch2.lastRxTime = now; break;
case MOTOR_CAN_STATE3: case MOTOR_CAN_STATE3_RED:
m_state.state3LastRx = now; break;
case MOTOR_CAN_STATE4: case MOTOR_CAN_STATE4_RED:
m_state.state4LastRx = now; break;
default: break;
}
rx = m_state.rxCount;
spd = m_state.feedbackSpeed;
bus = m_state.busVoltage;
f1 = m_state.branch1.faults.size();
f2 = m_state.branch2.faults.size();
}
// 节流日志:1s 一条
double now = MOOSTime();
if (now - m_lastRxLog >= 1.0) {
m_lastRxLog = now;
LOG_F(INFO, "[Motor/CAN] rx=%u speed=%d rpm bus=%dV faults(b1=%zu,b2=%zu)",
rx, spd, bus, f1, f2);
}
}
//---------------------------------------------------------
// Iterate:周期任务(AppTick 次/秒)
bool Motor::Iterate() {
AppCastingMOOSApp::Iterate();
double now = MOOSTime();
// 网页指令出队并应用
processCmdQueue();
// 周期发送指令帧(500ms,协议要求)
sendCmdFrame(now);
// 网页快照 1Hz 推送
if (m_web) {
static double lastWebPush = 0;
if (now - lastWebPush >= 1.0) {
lastWebPush = now;
m_web->broadcast(buildSnapshot());
}
}
AppCastingMOOSApp::PostReport();
return true;
}
//---------------------------------------------------------
// handleApi:/api/motor_cmd
std::string Motor::handleApi(const std::string& uri, const std::string& query) {
if (uri == "/api/motor_cmd")
return handleMotorCmdApi(query);
return "";
}
//---------------------------------------------------------
// handleMotorCmdApi:电机控制指令入队(按协议指令帧字段独立下发,无联动)
//
// GET /api/motor_cmd?action=enable 使能命令=1(Byte0 bit0)
// GET /api/motor_cmd?action=disable 使能命令=0,期望转速清0(Byte0 bit0,协议"停机"示例)
// GET /api/motor_cmd?action=reset 复位命令=1(Byte0 bit1,仅故障态有效)
// GET /api/motor_cmd?action=speed&speed=1000 设定期望转速(Byte2/3,s16)
// Web 线程不可直接 Notify,指令入队后由 Iterate(MOOS 线程)应用。
std::string Motor::handleMotorCmdApi(const std::string& query) {
// 简易 query 解析(k=v&k=v)
auto getParam = [&query](const char* key, std::string& out) -> bool {
std::string k = std::string(key) + "=";
size_t p = query.find(k);
if (p == std::string::npos) return false;
size_t e = query.find('&', p);
out = query.substr(p + k.size(),
(e == std::string::npos) ? std::string::npos
: e - p - k.size());
return true;
};
std::string action, speedStr;
getParam("action", action);
getParam("speed", speedStr);
MotorCmd c;
if (action == "enable") {
c.action = MOTOR_ACT_ENABLE;
} else if (action == "disable") {
c.action = MOTOR_ACT_DISABLE;
} else if (action == "reset") {
c.action = MOTOR_ACT_RESET;
} else if (action == "speed") {
c.action = MOTOR_ACT_SPEED;
c.speed = static_cast<int16_t>(atoi(speedStr.c_str()));
} else {
return "{\"ok\":false,\"error\":\"bad action (enable/disable/reset/speed)\"}";
}
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_cmdQueue.size() >= 16)
return "{\"ok\":false,\"error\":\"queue full\"}";
m_cmdQueue.push_back(c);
m_lastCmd = c;
m_lastCmdValid = true;
m_lastCmdTime = MOOSTime(false);
}
LOG_F(INFO, "[Motor/CAN] 指令入队: action=%s speed=%d", action.c_str(), c.speed);
char buf[96];
std::snprintf(buf, sizeof(buf),
"{\"ok\":true,\"action\":\"%s\",\"speed\":%d}", action.c_str(), c.speed);
return buf;
}
//---------------------------------------------------------
// processCmdQueue:出队网页指令并更新指令状态
void Motor::processCmdQueue() {
MotorCmd c;
double now = MOOSTime();
{
std::lock_guard<std::mutex> lock(m_mutex);
while (!m_cmdQueue.empty()) {
c = m_cmdQueue.front();
m_cmdQueue.erase(m_cmdQueue.begin());
switch (c.action) {
case MOTOR_ACT_ENABLE:
m_enable = true;
break;
case MOTOR_ACT_DISABLE:
m_enable = false;
m_speed = 0;
m_resetUntil = 0;
break;
case MOTOR_ACT_SPEED:
m_speed = c.speed;
break;
case MOTOR_ACT_RESET:
m_resetUntil = now + m_resetHoldSec;
break;
default:
break;
}
LOG_F(INFO, "[Motor/CAN] 指令应用: action=%d speed=%d enable=%d",
c.action, m_speed, m_enable ? 1 : 0);
}
}
}
//---------------------------------------------------------
// sendCmdFrame:按周期编码并发送指令帧
// 主通道 0x18EF2010;配置 red_channel 时同帧发 0x18EF2011 到冗余通道
void Motor::sendCmdFrame(double now) {
double lastTx = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (now - m_lastTx < m_cmdPeriodMs / 1000.0)
return;
lastTx = m_lastTx;
}
if (now - lastTx < m_cmdPeriodMs / 1000.0)
return;
uint8_t data[8];
{
std::lock_guard<std::mutex> lock(m_mutex);
bool reset = (now < m_resetUntil);
buildMotorCmdData(m_enable, reset, m_enable ? m_speed : 0, data);
m_lastTx = now;
}
bool ok = postCanFrame(MOTOR_CAN_CMD, data, m_canChannel);
int okCount = ok ? 1 : 0, errCount = ok ? 0 : 1;
if (!m_redChannel.empty()) {
bool okRed = postCanFrame(MOTOR_CAN_CMD_RED, data, m_redChannel);
okCount += okRed ? 1 : 0;
errCount += okRed ? 0 : 1;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_txCount += static_cast<unsigned long>(okCount);
m_txErr += static_cast<unsigned long>(errCount);
}
}
//---------------------------------------------------------
// postCanFrame:MOOS CAN_TX_0x%08X(二进制数据域,m_sSrcAux=目标通道)
// 链路:MOOS -> pCanBridge 订阅 -> 按通道路由 CanEndpoint(TCP)
// -> CANET -> CAN 总线
bool Motor::postCanFrame(uint32_t canId, const uint8_t data[8], const std::string& channel) {
char key[32];
std::snprintf(key, sizeof(key), "CAN_TX_0x%08X", canId);
CMOOSMsg msg(MOOS_NOTIFY, key, 8u, data);
msg.SetSourceAux(channel);
bool ok = m_Comms.Post(msg);
if (!ok)
LOG_F(ERROR, "[Motor/CAN] 指令帧 0x%08X 投递 MOOS 失败 (通道=%s)", canId, channel.c_str());
return ok;
}
//---------------------------------------------------------
// buildSnapshot:网页 JSON 快照(jsoncpp)
namespace {
void putI(Json::Value& j, const char* k, int v) { j[k] = Json::Value(v); }
void putU(Json::Value& j, const char* k, unsigned long v) { j[k] = Json::Value(static_cast<Json::UInt64>(v)); }
void putD(Json::Value& j, const char* k, double v) { j[k] = Json::Value(v); }
// age:距最后收到该帧的秒数;valid=false 时前端显示"从未收到"
void putAge(Json::Value& j, double lastRx, double now, bool valid) {
j["valid"] = valid ? 1 : 0;
j["age"] = valid ? (now - lastRx) : -1;
}
Json::Value branchToJson(const MotorBranchState& br, double now) {
Json::Value j(Json::objectValue);
putAge(j, br.lastRxTime, now, br.valid);
Json::Value words(Json::arrayValue);
for (int i = 0; i < 4; ++i) words.append(static_cast<int>(br.word[i]));
j["words"] = words;
Json::Value faults(Json::arrayValue);
for (size_t i = 0; i < br.faults.size(); ++i) faults.append(br.faults[i]);
j["faults"] = faults;
Json::Value alarms(Json::arrayValue);
for (size_t i = 0; i < br.alarms.size(); ++i) alarms.append(br.alarms[i]);
j["alarms"] = alarms;
return j;
}
} // namespace
std::string Motor::buildSnapshot() {
Json::Value root(Json::objectValue);
double now = MOOSTime(false);
{
std::lock_guard<std::mutex> lock(m_mutex);
// 通信链路
Json::Value link(Json::objectValue);
link["channel"] = m_canChannel;
link["redChannel"] = m_redChannel;
link["periodMs"] = m_cmdPeriodMs;
putU(link, "txCount", m_txCount);
putU(link, "txErr", m_txErr);
putU(link, "rxCount", m_state.rxCount);
link["lastTxValid"] = (m_lastTx > 0) ? 1 : 0;
putD(link, "lastTxAge", m_lastTx > 0 ? (now - m_lastTx) : -1);
root["link"] = link;
// 指令状态
Json::Value cmd(Json::objectValue);
putI(cmd, "enable", m_enable ? 1 : 0);
putI(cmd, "speed", m_speed);
putI(cmd, "resetActive", (now < m_resetUntil) ? 1 : 0);
cmd["lastCmdValid"] = m_lastCmdValid ? 1 : 0;
if (m_lastCmdValid) {
static const char* kActionName[] = {"", "enable", "disable", "speed", "reset"};
int idx = (m_lastCmd.action >= 1 && m_lastCmd.action <= 4) ? m_lastCmd.action : 0;
cmd["lastCmdAction"] = kActionName[idx];
putI(cmd, "lastCmdSpeed", m_lastCmd.speed);
putD(cmd, "lastCmdAge", m_lastCmdTime > 0 ? (now - m_lastCmdTime) : -1);
}
root["cmd"] = cmd;
// 状态帧3:转速 / 母线电压
Json::Value s3(Json::objectValue);
putAge(s3, m_state.state3LastRx, now, m_state.state3Valid);
putI(s3, "speed", m_state.feedbackSpeed);
putI(s3, "voltage", m_state.busVoltage);
root["state3"] = s3;
// 状态帧4:温度
Json::Value s4(Json::objectValue);
putAge(s4, m_state.state4LastRx, now, m_state.state4Valid);
putI(s4, "tA", m_state.tempA);
putI(s4, "tB", m_state.tempB);
putI(s4, "tC", m_state.tempC);
putI(s4, "tInv", m_state.tempInv);
putI(s4, "tFilter", m_state.tempFilter);
putI(s4, "tAir", m_state.tempAir);
root["state4"] = s4;
// 故障/报警
root["fault1"] = branchToJson(m_state.branch1, now);
root["fault2"] = branchToJson(m_state.branch2, now);
}
Json::FastWriter writer;
return writer.write(root);
}
//---------------------------------------------------------
// buildReport
bool Motor::buildReport() {
m_msgs << "============================================" << "\n";
m_msgs << "pMotor 推进电机操作程序" << "\n";
m_msgs << "============================================" << "\n";
m_msgs << "CAN 通道: " << m_canChannel
<< (m_redChannel.empty() ? "" : (" 冗余: " + m_redChannel)).c_str()
<< " 指令周期: " << m_cmdPeriodMs << "ms" << "\n";
m_msgs << "指令帧下发: " << m_txCount << " 成功 / " << m_txErr << " 失败" << "\n";
{
std::lock_guard<std::mutex> lock(m_mutex);
m_msgs << "电机报文接收: " << m_state.rxCount << "\n";
m_msgs << "指令状态: enable=" << (m_enable ? 1 : 0)
<< " speed=" << m_speed
<< " reset=" << (MOOSTime(false) < m_resetUntil ? 1 : 0) << "\n";
m_msgs << "反馈转速: " << m_state.feedbackSpeed
<< " rpm 母线电压: " << m_state.busVoltage << "\n";
m_msgs << "支路一故障: " << m_state.branch1.faults.size()
<< " 条, 支路二故障: " << m_state.branch2.faults.size() << " 条" << "\n";
}
return true;
}
+96
View File
@@ -0,0 +1,96 @@
#ifndef PMOTOR_MOTOR_H
#define PMOTOR_MOTOR_H
#define UNIX
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "protocol/MotorCan.h"
#include "web/WebServer.h"
#include <mutex>
namespace motor {
//============================================================================
// Motor:pMotor 程序主类(MOOS 应用外壳)——推进电机操作程序。
//
// 数据流:
// 上行:电机控制器 -> CAN1 -> CANET -> pCanBridge -> MOOS CAN_0x*
// -> pMotor 订阅解码(故障/转速/母线电压/温度)-> SystemData 合成快照
// 下行:网页 /api/motor_cmd(启停/复位/设定转速)
// -> 指令入队 -> Iterate(MOOS 线程)按 500ms 周期编码指令帧
// -> MOOS CAN_TX_0x18EF2010(m_sSrcAux=CAN1)-> pCanBridge
// -> CANET(CAN1) -> 电机控制器
// 网页:WebSocket 周期推送 JSON 快照(1Hz)。
//
// 协议定义见 docs/推进电机通信协议20260321.docx。
//============================================================================
class Motor : public AppCastingMOOSApp {
public:
Motor();
~Motor();
protected:
bool OnNewMail(MOOSMSG_LIST &NewMail);
bool Iterate();
bool OnConnectToServer();
bool OnStartUp();
bool buildReport();
void registerVariables();
private:
// MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布)
void handleCanMessage(CMOOSMsg& msg);
// 网页
std::string buildSnapshot();
std::string handleApi(const std::string& uri, const std::string& query);
// 控制指令:/api/motor_cmd 解析入队(Web 线程)
std::string handleMotorCmdApi(const std::string& query);
// 出队并应用控制指令(Iterate 调用)
void processCmdQueue();
// 周期(默认 500ms)发送指令帧 0x18EF2010(+可选冗余 0x18EF2011)
void sendCmdFrame(double now);
// 经 MOOS CAN_TX_0x%08X 发布一帧二进制数据(m_sSrcAux=通道名)
bool postCanFrame(uint32_t canId, const uint8_t data[8], const std::string& channel);
// 配置
std::string m_canChannel = "CAN1"; // 指令帧下行 / 帧上报来源通道
std::string m_redChannel; // 冗余 CAN 通道(空=不发送冗余帧)
int m_cmdPeriodMs = 500; // 指令帧周期
double m_resetHoldSec = 5.0; // 复位位保持时长(秒)
int m_webPort = 18081;
bool m_webEnable = true;
std::string m_logPath = "pMotor.log";
// 指令状态(processCmdQueue 修改,sendCmdFrame 读取;m_mutex 保护)
mutable std::mutex m_mutex;
bool m_enable = false;
int16_t m_speed = 0;
double m_resetUntil = 0; // 复位位有效的截止时间(MOOSTime)
// 指令队列(Web 线程入队,Iterate 出队)
std::vector<MotorCmd> m_cmdQueue;
// 最近一次网页指令(供网页展示)
MotorCmd m_lastCmd;
bool m_lastCmdValid = false;
double m_lastCmdTime = 0;
// 下行统计
unsigned long m_txCount = 0;
unsigned long m_txErr = 0;
double m_lastTx = 0;
// CAN 帧接收日志节流
double m_lastRxLog = 0;
// 电机状态(OnNewMail/Iterate 均在 MOOS 线程访问;buildSnapshot 经 m_mutex 读取)
MotorState m_state;
// 组件
WebServer* m_web = nullptr;
};
} // namespace motor
#endif // PMOTOR_MOTOR_H
+103
View File
@@ -0,0 +1,103 @@
/****************************************************************/
/* NAME: Motor_Info */
/* FILE: Motor_Info.cpp */
/****************************************************************/
#include <cstdlib>
#include <iostream>
#include "Motor_Info.h"
#include "ColorParse.h"
#include "ReleaseInfo.h"
using namespace std;
void showSynopsis() {
blk("SYNOPSIS: ");
blk("------------------------------------ ");
blk(" The pMotor application is the propulsion motor operation ");
blk(" program. It talks to the motor controller over CAN (via ");
blk(" pCanBridge pass-through, default channel CAN1): ");
blk(" subscribes motor state/fault frames (CAN_0x*), sends the ");
blk(" 500ms periodic command frame 0x18EF2010 (enable/reset/speed) ");
blk(" via MOOS CAN_TX_0x*, and provides a web page for control ");
blk(" and monitoring. Protocol: ");
blk(" docs/推进电机通信协议20260321.docx. ");
blk(" ");
}
void showHelpAndExit() {
blk(" ");
blu("=============================================================== ");
blu("Usage: pMotor file.moos [OPTIONS] ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("Options: ");
mag(" --alias","=<ProcessName> ");
blk(" Launch pMotor with the given process name ");
blk(" rather than pMotor. ");
mag(" --example, -e ");
blk(" Display example MOOS configuration block. ");
mag(" --help, -h ");
blk(" Display this help message. ");
mag(" --interface, -i ");
blk(" Display MOOS publications and subscriptions. ");
mag(" --version,-v ");
blk(" Display the release version of pMotor. ");
blk(" ");
blk("Note: If argv[2] does not otherwise match a known option, ");
blk(" then it will be interpreted as a run alias. This is ");
blk(" to support pAntler launching conventions. ");
blk(" ");
exit(0);
}
void showExampleConfigAndExit() {
blk(" ");
blu("=============================================================== ");
blu("pMotor Example MOOS Configuration ");
blu("=============================================================== ");
blk(" ");
blk("ProcessConfig = pMotor ");
blk("{ ");
blk(" AppTick = 4 ");
blk(" CommsTick = 4 ");
blk(" ");
blk(" can_channel = CAN1 // 电机 CAN 通道(pCanBridge 配置名) ");
blk(" red_channel = // 冗余 CAN 通道(空=不发冗余帧) ");
blk(" cmd_period_ms = 500 // 指令帧周期(协议 500ms) ");
blk(" reset_hold_sec= 5 // 复位位保持时长(秒) ");
blk(" web_port = 18081 // 网页端口(避开8080/8090/18080) ");
blk(" web_enable = true // 是否启用网页 ");
blk(" logpath = pMotor.log // 日志路径 ");
blk("} ");
blk(" ");
exit(0);
}
void showInterfaceAndExit() {
blk(" ");
blu("=============================================================== ");
blu("pMotor INTERFACE ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(电机状态/故障) ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CAN_TX_0x18EF2010 = 主控指令帧(二进制 8 字节数据域, ");
blk(" m_sSrcAux=CAN1,经 pCanBridge 下行) ");
blk(" CAN_TX_0x18EF2011 = 指令帧冗余(配置 red_channel 时发送) ");
blk(" ");
exit(0);
}
void showReleaseInfoAndExit() {
showReleaseInfo("pMotor", "gpl");
exit(0);
}
+15
View File
@@ -0,0 +1,15 @@
/****************************************************************/
/* NAME: Motor_Info */
/* FILE: Motor_Info.h */
/****************************************************************/
#ifndef PMOTOR_INFO_HEADER
#define PMOTOR_INFO_HEADER
void showSynopsis();
void showHelpAndExit();
void showExampleConfigAndExit();
void showInterfaceAndExit();
void showReleaseInfoAndExit();
#endif
+54
View File
@@ -0,0 +1,54 @@
/************************************************************/
/* NAME: pMotor */
/* FILE: main.cpp */
/************************************************************/
#include <string>
#include "MBUtils.h"
#include "ColorParse.h"
#include "Motor.h"
#include "Motor_Info.h"
#include "../pPowerManger/logc/loguru.hpp"
using namespace std;
int main(int argc, char *argv[])
{
loguru::init(argc, argv);
string mission_file;
string run_command = argv[0];
// 默认以程序文件名(去掉路径)作为进程名,保证与 .moos 中 ProcessConfig 匹配
{
size_t slash = run_command.find_last_of('/');
if (slash != string::npos) run_command = run_command.substr(slash + 1);
}
for (int i = 1; i < argc; i++) {
string argi = argv[i];
if ((argi == "-v") || (argi == "--version") || (argi == "-version"))
showReleaseInfoAndExit();
else if ((argi == "-e") || (argi == "--example") || (argi == "-example"))
showExampleConfigAndExit();
else if ((argi == "-h") || (argi == "--help") || (argi == "-help"))
showHelpAndExit();
else if ((argi == "-i") || (argi == "--interface"))
showInterfaceAndExit();
else if (strEnds(argi, ".moos") || strEnds(argi, ".moos++"))
mission_file = argv[i];
else if (strBegins(argi, "--alias="))
run_command = argi.substr(8);
else if (i == 2)
run_command = argi;
}
if (mission_file == "")
showHelpAndExit();
LOG_F(INFO, "pMotor launching as %s", run_command.c_str());
motor::Motor Motor;
Motor.Run(run_command.c_str(), mission_file.c_str());
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
// MOOS file
// pMotor 推进电机操作程序 配置示例
//
// 功能:经 pCanBridge(CAN1 通道)与推进电机控制器交互
// 下行:网页 /api/motor_cmd(启停/复位/转速)-> 500ms 周期指令帧 0x18EF2010
// 上行:订阅 pCanBridge 透传的 CAN_0x*(故障/转速/母线电压/温度)-> 网页展示
//
// 协议定义:docs/推进电机通信协议20260321.docx
ServerHost = localhost
ServerPort = 9000
Community = h100
ProcessConfig = pMotor
{
AppTick = 4
CommsTick = 4
// 电机 CAN 通道(须与 pCanBridge 的 channel 配置名一致)
can_channel = CAN1
// 冗余 CAN 通道(配置后指令帧同内容发 0x18EF2011 到该通道;空=不发送)
// red_channel = CAN2
// 指令帧周期 ms(协议 500ms)
cmd_period_ms = 500
// 复位位保持时长(秒):下发复位后 reset bit 保持高电平数秒,
// 仅故障状态下电机执行复位
reset_hold_sec = 5
// 网页(避开 pCCU 8080 / pPowerManger 8090 / pPowerMangerHost 18080)
web_port = 18081
web_enable = true
logpath = pMotor.log
}
+219
View File
@@ -0,0 +1,219 @@
#include "MotorCan.h"
#include <cstring>
namespace motor {
const char* kMotorWordName[4] = {
"故障字1 (Byte0|Byte1)",
"故障字2 (Byte2|Byte3)",
"故障字3 (Byte4|Byte5)",
"报警字 (Byte6|Byte7)",
};
namespace {
// 故障/报警位描述(bit -> 中文描述),按字定义
struct FaultDesc {
uint16_t bit;
const char* text;
};
//--------------------------------------------------------------------------
// 故障字1(Byte0|Byte1):支路一 / 支路二 仅 bit7 不同
//--------------------------------------------------------------------------
const FaultDesc kW0Branch1[] = {
{0, "CPLD自检故障标志"},
{1, "温度继电器过温故障"},
{2, "A相绕组过温故障"},
{3, "B相绕组过温故障"},
{4, "C相绕组过温故障"},
{5, "逆变组件底板过温故障"},
{6, "输出滤波器过温故障"},
{7, "筒内空气过温故障"},
{8, "超速故障"},
{9, "紧急停机故障"},
{11, "协调通信丢失故障"},
{12, "协调故障停机"},
};
const FaultDesc kW0Branch2[] = {
{0, "CPLD自检故障标志"},
{1, "温度继电器过温故障"},
{2, "A相绕组过温故障"},
{3, "B相绕组过温故障"},
{4, "C相绕组过温故障"},
{5, "逆变组件底板过温故障"},
{6, "输出滤波器过温故障"},
// bit7 支路二为预留
{8, "超速故障"},
{9, "紧急停机故障"},
{11, "协调通信丢失故障"},
{12, "协调故障停机"},
};
//--------------------------------------------------------------------------
// 故障字2(Byte2|Byte3)
//--------------------------------------------------------------------------
const FaultDesc kW1[] = {
{0, "逆变A1相驱动1(上管)故障"},
{1, "逆变A1相驱动2(下管)故障"},
{4, "逆变B1相驱动3(上管)故障"},
{5, "逆变B1相驱动4(下管)故障"},
{8, "逆变C1相驱动5(上管)故障"},
{9, "逆变C1相驱动6(下管)故障"},
{12, "硬件总故障"},
{13, "驱动总故障"},
{14, "采样总故障"},
{15, "24V欠压故障"},
};
//--------------------------------------------------------------------------
// 故障字3(Byte4|Byte5)
//--------------------------------------------------------------------------
const FaultDesc kW2[] = {
{0, "A相软件过流故障"},
{1, "B相软件过流故障"},
{2, "C相软件过流故障"},
{3, "母线电压过压故障"},
{4, "母线电压欠压故障"},
{5, "输出缺相故障"},
{6, "预充电故障"},
{8, "A1相硬件过流故障"},
{9, "C1相硬件过流故障"},
{15, "母线过压硬件故障"},
};
//--------------------------------------------------------------------------
// 报警字(Byte6|Byte7)
//--------------------------------------------------------------------------
const FaultDesc kW3[] = {
{0, "直流母线1电压偏高报警"},
{1, "直流母线1电压偏低报警"},
{2, "A相绕组温度偏高报警"},
{3, "B相绕组温度偏高报警"},
{4, "C相绕组温度偏高报警"},
{5, "逆变组件底板温度偏高报警"},
{6, "输出滤波器温度偏高报警"},
{7, "筒内空气温度偏高报警"},
{8, "遥控CAN通信中断报警"},
{9, "485通信中断报警"},
{10, "协调CAN通信中断报警"},
{11, "参数设置失败报警"},
{12, "flash读取失败"},
{13, "直流母线2电压偏高报警"},
{14, "直流母线2电压偏低报警"},
{15, "协调光纤报警"},
};
const FaultDesc* tableOf(int branch, int wordIdx, size_t& count) {
switch (wordIdx) {
case 0:
if (branch == 2) { count = sizeof(kW0Branch2) / sizeof(kW0Branch2[0]); return kW0Branch2; }
count = sizeof(kW0Branch1) / sizeof(kW0Branch1[0]); return kW0Branch1;
case 1:
count = sizeof(kW1) / sizeof(kW1[0]); return kW1;
case 2:
count = sizeof(kW2) / sizeof(kW2[0]); return kW2;
case 3:
count = sizeof(kW3) / sizeof(kW3[0]); return kW3;
default:
count = 0; return nullptr;
}
}
// 16 位字(Intel:低字节在前)
uint16_t wordOf(const uint8_t* d, int byteIdx) {
return static_cast<uint16_t>(d[byteIdx] | (static_cast<uint16_t>(d[byteIdx + 1]) << 8));
}
// 重建某支路的故障/报警描述列表(word 更新后调用)
void rebuildBranchTexts(MotorBranchState& br) {
br.faults.clear();
br.alarms.clear();
for (int w = 0; w < 3; ++w) {
std::vector<std::string> tmp;
motorFaultTexts(br.branch, w, br.word[w], tmp);
br.faults.insert(br.faults.end(), tmp.begin(), tmp.end());
}
motorFaultTexts(br.branch, 3, br.word[3], br.alarms);
}
} // namespace
//--------------------------------------------------------------------------
// motorFaultTexts
int motorFaultTexts(int branch, int wordIdx, uint16_t bits,
std::vector<std::string>& out) {
out.clear();
if (branch < 1 || branch > 2) return 0;
size_t count = 0;
const FaultDesc* table = tableOf(branch, wordIdx, count);
if (!table) return 0;
for (size_t i = 0; i < count; ++i) {
if (bits & (1u << table[i].bit))
out.push_back(table[i].text);
}
return static_cast<int>(out.size());
}
//--------------------------------------------------------------------------
// buildMotorCmdData:指令帧数据域编码
// byte0 = enable(bit0) | reset(bit1)
// byte2/3 = speed s16(Intel 低字节在前)
void buildMotorCmdData(bool enable, bool reset, int16_t speed, uint8_t out[8]) {
std::memset(out, 0, 8);
out[0] = static_cast<uint8_t>((enable ? 0x01u : 0u) | (reset ? 0x02u : 0u));
uint16_t sp = static_cast<uint16_t>(speed);
out[2] = static_cast<uint8_t>(sp & 0xFFu);
out[3] = static_cast<uint8_t>((sp >> 8) & 0xFFu);
}
//--------------------------------------------------------------------------
// decodeMotorFrame:电机上行报文解析(部分更新合并进 st)
bool decodeMotorFrame(uint32_t canId, const uint8_t* data, int dlc, MotorState& st) {
if (!data || dlc < 8) return false;
switch (canId) {
case MOTOR_CAN_FAULT1:
case MOTOR_CAN_FAULT1_RED:
case MOTOR_CAN_FAULT2:
case MOTOR_CAN_FAULT2_RED: {
bool isBranch2 = (canId == MOTOR_CAN_FAULT2 || canId == MOTOR_CAN_FAULT2_RED);
MotorBranchState& br = isBranch2 ? st.branch2 : st.branch1;
br.branch = isBranch2 ? 2 : 1;
br.word[0] = wordOf(data, 0);
br.word[1] = wordOf(data, 2);
br.word[2] = wordOf(data, 4);
br.word[3] = wordOf(data, 6);
rebuildBranchTexts(br);
br.valid = true;
return true;
}
case MOTOR_CAN_STATE3:
case MOTOR_CAN_STATE3_RED: {
uint16_t sp = wordOf(data, 0);
st.feedbackSpeed = static_cast<int16_t>(sp);
st.busVoltage = wordOf(data, 2);
st.state3Valid = true;
return true;
}
case MOTOR_CAN_STATE4:
case MOTOR_CAN_STATE4_RED: {
st.tempA = static_cast<int8_t>(data[0]);
st.tempB = static_cast<int8_t>(data[1]);
st.tempC = static_cast<int8_t>(data[2]);
st.tempInv = static_cast<int8_t>(data[3]);
st.tempFilter = static_cast<int8_t>(data[4]);
st.tempAir = static_cast<int8_t>(data[5]);
st.state4Valid = true;
return true;
}
default:
return false;
}
}
} // namespace motor
+154
View File
@@ -0,0 +1,154 @@
#ifndef PMOTOR_CAN_H
#define PMOTOR_CAN_H
#include <cstdint>
#include <string>
#include <vector>
namespace motor {
//============================================================================
// 推进电机控制器 CAN 通信协议(docs/推进电机通信协议20260321.docx)
//
// 数据来源:pCanBridge 透传的 MOOS 消息
// 变量名 CAN_0x%08X = CAN ID(29 位扩展帧),m_sVal = 二进制数据域(8 字节),
// m_dfVal2 = 原始帧信息字节。本模块只负责按报文 ID 解析/编码数据域字段。
//
// 物理/链路层:CAN2.0B 扩展帧,250kbps(默认),数据域 8 字节,
// 多字节数值采用 Intel 编码(低字节在前,高字节在后)。
//
// 【报文清单】
// 下行(pMotor -> 电机控制器,周期 500ms):
// 0x18EF2010 主控指令帧(冗余 CAN 0x18EF2011)
// 0x18EF2110 主控限制帧(冗余 CAN 0x18EF2111,全预留,暂不发送)
// 上行(电机控制器 -> pMotor):
// 0x18EF1020 支路一故障报警状态帧1(200ms,冗余 CAN 0x18EF1021)
// 0x18EF1120 支路二故障报警状态帧2(200ms,冗余 CAN 0x18EF1121)
// 0x18EF1220 状态帧3:反馈转速/母线电压(100ms,冗余 0x18EF1221)
// 0x18EF1320 状态帧4:温度(100ms,冗余 0x18EF1321)
//
// 【指令帧 0x18EF2010】数据域 8 字节:
// byte0 bit0 = 使能命令(0 无效 1 有效,IGBT 开关;需先预充后使能)
// byte0 bit1 = 复位命令(0 无效 1 有效,仅故障状态下有效,故障支路复位)
// byte0 bit2 = 支路一通信状态(0 正常 1 中断)
// byte0 bit3 = 支路二通信状态(0 正常 1 中断)
// byte0 bit4~7 / byte1 / byte4~7 = 预留(发 0)
// byte2/3 = (转速模式)期望转速 s16(偏移 0,分辨率 1,Intel)
// 文档示例:启动并给定 1000 转速 -> 01 00 E8 03 00 00 00 00
// 停机 -> 00 00 00 00 00 00 00 00
// 复位 -> 02 00 00 00 00 00 00 00
//
// 【状态帧3 0x18EF1220】byte0/1 反馈转速 s16(Intel),byte2/3 母线电压 u16(Intel)
//
// 【状态帧4 0x18EF1320】byte0~5 依次为 A/B/C 相绕组、逆变组件底板、
// 输出滤波器、筒内空气温度,均为 s8(偏移 0,分辨率 1)
//
// 【故障帧 0x18EF1020/0x18EF1120】4 个 16 位字(byte0|1、byte2|3、byte4|5、byte6|7),
// 字内 bit 定义见 MotorCan.cpp 中描述表;前两帧仅支路一/支路二
// byte0|1 的 bit7 不同(支路一=筒内空气过温故障,支路二=预留)。
//============================================================================
// CAN ID 定义(29 位扩展帧)
enum MotorCanId : uint32_t {
MOTOR_CAN_CMD = 0x18EF2010u, // 主控指令帧(pMotor -> 电机)
MOTOR_CAN_CMD_RED = 0x18EF2011u, // 主控指令帧 冗余 CAN
MOTOR_CAN_LIMIT = 0x18EF2110u, // 主控限制帧(全预留,暂不使用)
MOTOR_CAN_LIMIT_RED = 0x18EF2111u, // 主控限制帧 冗余 CAN
MOTOR_CAN_FAULT1 = 0x18EF1020u, // 支路一故障报警状态帧1
MOTOR_CAN_FAULT1_RED = 0x18EF1021u, // 支路一 冗余 CAN
MOTOR_CAN_FAULT2 = 0x18EF1120u, // 支路二故障报警状态帧2
MOTOR_CAN_FAULT2_RED = 0x18EF1121u, // 支路二 冗余 CAN
MOTOR_CAN_STATE3 = 0x18EF1220u, // 状态帧3(转速/母线电压)
MOTOR_CAN_STATE3_RED = 0x18EF1221u, // 状态帧3 冗余 CAN
MOTOR_CAN_STATE4 = 0x18EF1320u, // 状态帧4(温度)
MOTOR_CAN_STATE4_RED = 0x18EF1321u, // 状态帧4 冗余 CAN
};
// 指令动作(网页 /api/motor_cmd -> 队列 -> Iterate 应用)
// 与协议指令帧字段一一对应,独立下发、无联动:
enum MotorAction {
MOTOR_ACT_ENABLE = 1, // 使能命令=1(Byte0 bit0,保持当前期望转速)
MOTOR_ACT_DISABLE = 2, // 使能命令=0(Byte0 bit0,协议"停机"示例:期望转速清 0)
MOTOR_ACT_SPEED = 3, // 期望转速(Byte2/3,speed 参数,使能状态保持不变)
MOTOR_ACT_RESET = 4, // 复位命令=1(Byte0 bit1,保持高 resetHold 秒,仅故障态有效)
};
// 网页下发的控制指令
struct MotorCmd {
int action = 0; // MotorAction
int16_t speed = 0; // MOTOR_ACT_SPEED / MOTOR_ACT_START 有效
};
// 单条电机上行消息的新鲜度 + 原始数据
struct MotorBranchState {
int branch = 0; // 1 / 2
uint16_t word[4] = {0, 0, 0, 0}; // 4 个 16 位字(故障1/故障2/故障3/报警)
std::vector<std::string> faults; // 当前置位的故障描述(word0~2)
std::vector<std::string> alarms; // 当前置位的报警描述(word3)
bool valid = false; // 是否收到过该支路故障帧
double lastRxTime = 0; // 最近收到时刻(MOOSTime,调用方设置)
};
// 电机最新合成状态(各帧部分更新合并)
struct MotorState {
// 状态帧3 0x18EF1220
int16_t feedbackSpeed = 0; // 反馈转速
uint16_t busVoltage = 0; // 母线电压
bool state3Valid = false;
double state3LastRx = 0;
// 状态帧4 0x18EF1320(s8,偏移 0)
int8_t tempA = 0, tempB = 0, tempC = 0; // A/B/C 相绕组温度
int8_t tempInv = 0, tempFilter = 0, tempAir = 0; // 逆变底板/滤波器/筒内空气
bool state4Valid = false;
double state4LastRx = 0;
// 故障/报警
MotorBranchState branch1;
MotorBranchState branch2;
// 计数
uint32_t rxCount = 0; // 命中电机报文的帧计数
};
// 是否为电机协议定义的 CAN ID(含冗余 CAN 变体)
inline bool isCanMotorId(uint32_t canId) {
switch (canId) {
case MOTOR_CAN_CMD:
case MOTOR_CAN_CMD_RED:
case MOTOR_CAN_LIMIT:
case MOTOR_CAN_LIMIT_RED:
case MOTOR_CAN_FAULT1:
case MOTOR_CAN_FAULT1_RED:
case MOTOR_CAN_FAULT2:
case MOTOR_CAN_FAULT2_RED:
case MOTOR_CAN_STATE3:
case MOTOR_CAN_STATE3_RED:
case MOTOR_CAN_STATE4:
case MOTOR_CAN_STATE4_RED:
return true;
default:
return false;
}
}
// 故障/报警字索引(帧内):0=byte0|1 1=byte2|3 2=byte4|5 3=byte6|7(报警)
extern const char* kMotorWordName[4];
// 按支路(1/2)与字索引查故障/报警描述表,把 bits 中置位的描述填入 out;
// 返回描述条数。wordIdx 非法或无置位返回 0。
int motorFaultTexts(int branch, int wordIdx, uint16_t bits,
std::vector<std::string>& out);
// 编码指令帧数据域(8 字节,经 MOOS CAN_TX_0x%08X 下发 pCanBridge 发送):
// [0] = enable(bit0) | reset(bit1)(支路通信状态位恒 0)
// [1] = 0 [2]/[3] = speed s16 Intel(停用时调用方传 0) [4..7] = 0
void buildMotorCmdData(bool enable, bool reset, int16_t speed, uint8_t out[8]);
// 解析一帧 CAN 数据域;命中电机上行报文返回 true 并部分更新 st:
// 故障帧 -> 对应支路 word[4] + 故障/报警描述
// 状态帧3 -> feedbackSpeed / busVoltage
// 状态帧4 -> 6 路温度
// 非电机报文或 data/dlc 非法返回 false。lastRxTime 由调用方设置。
bool decodeMotorFrame(uint32_t canId, const uint8_t* data, int dlc, MotorState& st);
} // namespace motor
#endif // PMOTOR_CAN_H
+144
View File
@@ -0,0 +1,144 @@
#include "WebServer.h"
#include "pages/index.h"
#include <iostream>
namespace motor {
//---------------------------------------------------------
// 事件处理:HTTP 路由 + WebSocket 收发
void WebServer::handleEvent(struct mg_connection* c, int ev, void* ev_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message* hm = (struct mg_http_message*)ev_data;
// WebSocket 升级
if (mg_match(hm->uri, mg_str("/ws"), NULL)) {
mg_ws_upgrade(c, hm, NULL);
return;
}
// /api/* 接口:交给宿主程序处理,返回 JSON
std::string path(hm->uri.buf, hm->uri.len);
if (path.rfind("/api/", 0) == 0) {
std::string body;
if (m_apiHandler) {
std::string query(hm->query.buf, hm->query.len);
body = m_apiHandler(path, query);
}
if (!body.empty()) {
mg_http_reply(c, 200, "Content-Type: application/json\r\n",
"%.*s", (int)body.size(), body.c_str());
} else {
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
}
return;
}
// 电机控制页面
if (path == "/" || path == "/index.html") {
mg_http_reply(c, 200, "Content-Type: text/html; charset=utf-8\r\n",
"%.*s", (int)INDEX_HTML.size(), INDEX_HTML.c_str());
return;
}
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
} else if (ev == MG_EV_WS_OPEN) {
// Web 线程内:先构建欢迎快照,再登记连接
std::string welcome;
if (m_onOpen) welcome = m_onOpen();
m_wsConnections.push_back(c);
if (!welcome.empty()) {
mg_ws_send(c, welcome.c_str(), welcome.size(), WEBSOCKET_OP_TEXT);
}
} else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) {
for (auto it = m_wsConnections.begin(); it != m_wsConnections.end(); ++it) {
if (*it == c) {
m_wsConnections.erase(it);
break;
}
}
} else if (ev == MG_EV_WS_MSG) {
// 忽略客户端消息(控制走 /api/*)
}
}
WebServer::WebServer() {
mg_mgr_init(&m_mgr);
}
WebServer::~WebServer() {
stop();
mg_mgr_free(&m_mgr);
}
void WebServer::setOnOpen(std::function<std::string()> handler) {
m_onOpen = std::move(handler);
}
void WebServer::setApiHandler(std::function<std::string(const std::string&, const std::string&)> handler) {
m_apiHandler = std::move(handler);
}
bool WebServer::start(int port) {
if (m_running) return true;
m_port = port;
std::string addr = "http://0.0.0.0:" + std::to_string(port);
mg_http_listen(&m_mgr, addr.c_str(), [](mg_connection* c, int ev, void* ev_data) {
WebServer* server = static_cast<WebServer*>(c->fn_data);
if (server) server->handleEvent(c, ev, ev_data);
}, this);
m_running = true;
m_thread = std::thread([this]() { serverThreadFunc(); });
std::cout << "pMotor web UI listening on " << addr << std::endl;
return true;
}
void WebServer::stop() {
if (!m_running) return;
m_running = false;
if (m_thread.joinable()) m_thread.join();
// Web 线程已退出,此处触碰连接列表安全
for (auto c : m_wsConnections) {
mg_ws_send(c, "", 0, WEBSOCKET_OP_CLOSE);
}
m_wsConnections.clear();
}
bool WebServer::serverThreadCB(void* pParam) {
WebServer* pThis = static_cast<WebServer*>(pParam);
return pThis->serverThreadFunc();
}
bool WebServer::serverThreadFunc() {
while (m_running) {
mg_mgr_poll(&m_mgr, 200);
// 仅在本线程(poll 间隙)发送,避免跨线程操作 mongoose 连接
flushPending();
}
return true;
}
void WebServer::flushPending() {
std::string text;
{
std::lock_guard<std::mutex> lock(m_pendingMutex);
text = std::move(m_pending);
m_pending.clear();
}
if (text.empty()) return;
for (auto c : m_wsConnections) {
mg_ws_send(c, text.c_str(), text.size(), WEBSOCKET_OP_TEXT);
}
}
void WebServer::broadcast(const std::string& text) {
// 只投递到队列,实际发送在 Web 线程 flushPending() 中完成
std::lock_guard<std::mutex> lock(m_pendingMutex);
m_pending = text;
}
} // namespace motor
+66
View File
@@ -0,0 +1,66 @@
#ifndef PMOTOR_WEB_SERVER_H
#define PMOTOR_WEB_SERVER_H
#define UNIX
#include "mongoose.h"
#include <string>
#include <functional>
#include <vector>
#include <mutex>
#include <atomic>
#include <thread>
namespace motor {
//============================================================================
// WebServer:HTTP + WebSocket 控制/监控服务器(基于 mongoose)。
//
// 路由:
// / -> 内嵌电机控制页面
// /ws -> WebSocket,推送 JSON 快照(由 Motor 周期调用 broadcast)
// /api/* -> 控制指令接口(由 Motor 处理,返回 JSON)
// 采用与仓库内 pCCU/WebServer、pPowerMangerHost/WebServer 一致的回调模式。
//============================================================================
class WebServer {
public:
WebServer();
~WebServer();
bool start(int port);
void stop();
bool isRunning() const { return m_running; }
// 向所有 WebSocket 客户端广播文本
// 线程安全:mongoose 非线程安全,本接口只把文本放入待发队列,
// 由 Web 线程在 mg_mgr_poll 返回后统一发送(禁止跨线程直接 mg_ws_send)
void broadcast(const std::string& text);
// 新客户端连接时调用,返回初始快照
void setOnOpen(std::function<std::string()> handler);
// 处理 /api/* 请求,返回 JSON 响应体(空串表示 404)
void setApiHandler(std::function<std::string(const std::string& uri, const std::string& query)> handler);
private:
static bool serverThreadCB(void* pParam);
bool serverThreadFunc();
void handleEvent(struct mg_connection* c, int ev, void* ev_data);
// Web 线程内:把待发队列中的快照推给所有 WS 客户端(仅本线程触碰连接列表)
void flushPending();
struct mg_mgr m_mgr;
int m_port = 0;
std::atomic<bool> m_running{false};
std::thread m_thread;
std::mutex m_pendingMutex; // 保护 m_pending
std::string m_pending; // 最新待广播快照(新快照覆盖旧快照)
std::vector<mg_connection*> m_wsConnections; // 仅 Web 线程访问
std::function<std::string()> m_onOpen;
std::function<std::string(const std::string&, const std::string&)> m_apiHandler;
};
} // namespace motor
#endif // PMOTOR_WEB_SERVER_H
+271
View File
@@ -0,0 +1,271 @@
#ifndef PMOTOR_PAGE_INDEX_H
#define PMOTOR_PAGE_INDEX_H
#include <string>
//============================================================================
// pMotor 推进电机控制页面(纯前端,内嵌 HTML)。
//
// - 通过 WebSocket /ws 接收服务器推送的 JSON 快照(每秒)
// - 控制:按协议指令帧字段独立下发(无联动按钮)
// 使能命令(Byte0 bit0) / 复位命令(Byte0 bit1) / 期望转速(Byte2/3)
// (fetch /api/motor_cmd -> pMotor 入队 -> MOOS CAN_TX -> pCanBridge -> CAN1)
// - 展示:反馈转速/母线电压、温度、支路一/支路二故障与报警(按位解析中文)
// - 协议定义:docs/推进电机通信协议20260321.docx
//============================================================================
namespace motor {
const std::string INDEX_HTML = R"HTML(<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pMotor 推进电机控制</title>
<style>
:root{--bg:#0f1420;--card:#1a2130;--line:#2a3447;--fg:#e6edf3;--dim:#8b98ab;--ok:#2ea043;--warn:#d29922;--bad:#f85149;}
*{margin:0;padding:0;box-sizing:border-box;}
body{background:var(--bg);color:var(--fg);font-family:'Segoe UI',system-ui,sans-serif;font-size:13px;padding:16px;}
h1{font-size:18px;margin-bottom:12px;display:flex;align-items:center;gap:10px;}
h2{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;border-bottom:1px solid var(--line);padding-bottom:6px;}
.badge{font-size:11px;padding:2px 8px;border-radius:10px;background:var(--card);border:1px solid var(--line);}
.badge.ok{color:var(--ok);border-color:var(--ok);}
.badge.warn{color:var(--warn);border-color:var(--warn);}
.badge.err{color:var(--bad);border-color:var(--bad);}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:12px;}
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px;}
.row{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px dashed #222c3d;}
.row:last-child{border-bottom:none;}
.row .k{color:var(--dim);}
.row .v{font-family:Consolas,monospace;color:var(--fg);text-align:right;}
.sub{font-size:11px;color:#79c0ff;margin:10px 0 2px;border-bottom:1px solid #22304a;padding-bottom:2px;}
.sub:first-child{margin-top:0;}
.btn{padding:6px 14px;margin:0 8px 8px 0;border:1px solid var(--line);border-radius:6px;background:#222b3d;color:var(--fg);font-size:12px;cursor:pointer;}
.btn:hover{border-color:#3b4a63;}
.btn.on{background:#1f6feb;border-color:#1f6feb;color:#fff;font-weight:600;}
.btn.off{background:#3d2226;border-color:#f85149;color:#f85149;}
.btn.warn{background:#3a2e12;border-color:var(--warn);color:var(--warn);}
.btn:disabled{opacity:.4;cursor:not-allowed;}
input[type=number]{width:110px;background:#0f1420;border:1px solid var(--line);border-radius:6px;color:var(--fg);font-family:Consolas,monospace;padding:5px 8px;font-size:13px;}
input[type=number]:focus{outline:none;border-color:#1f6feb;}
.fc-bad{color:#f85149;font-weight:700;background:rgba(248,81,73,.12);padding:1px 6px;border-radius:4px;display:inline-block;margin:2px 4px 2px 0;}
.fc-warn{color:#d29922;font-weight:600;background:rgba(210,153,34,.10);padding:1px 6px;border-radius:4px;display:inline-block;margin:2px 4px 2px 0;}
.fc-ok{color:#3fb950;font-weight:600;}
#status{color:var(--dim);font-size:12px;}
.state-big{font-size:22px;font-family:Consolas,monospace;}
</style>
</head>
<body>
<h1>pMotor 推进电机控制 <span id="status" class="badge">连接中...</span></h1>
<div class="grid" id="pages">加载中...</div>
<script>
const $ = id => document.getElementById(id);
const pagesEl = $('pages');
const statusEl = $('status');
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 card(title, html){ return '<div class="card"><h2>'+title+'</h2>'+html+'</div>'; }
function ageText(a){
if(typeof a !== 'number' || a < 0) return '从未收到';
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)+' 分前';
}
function ageCell(d){
if(!d || !d.valid) return '<span style="color:var(--dim)">从未收到</span>';
const online = d.age < 3;
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">')
+ ageText(d.age) + (online ? ' (在线)' : ' (超时)') + '</span>';
}
//------------------ 控制指令(/api/motor_cmd) ------------------
// 链路: 网页 fetch -> pMotor 入队 -> Iterate 周期编码 0x18EF2010
// -> MOOS CAN_TX_0x18EF2010 -> pCanBridge -> CAN1 -> 电机控制器
function sendCmd(action, speed, confirmMsg){
if(confirmMsg && !confirm(confirmMsg)) return;
let url = '/api/motor_cmd?action='+action;
if(speed !== undefined && speed !== null) url += '&speed='+speed;
fetch(url)
.then(function(r){ return r.json(); })
.then(function(j){
if(j.ok) alert('指令已下发: '+j.action+(j.action==='speed' ? ' '+(j.speed||0)+' rpm' : ''));
else alert('下发失败: '+(j.error||'未知错误'));
})
.catch(function(){ alert('网络错误,指令未下发'); });
}
function motorEnable(){
sendCmd('enable', null, '确认启动推进电机(使能=1,IGBT 开管)?\n电机将按当前期望转速运行。');
}
function motorStop(){
sendCmd('disable', null, '确认停机(使能=0,期望转速清零)?');
}
function motorReset(){
sendCmd('reset', null, '确认下发故障复位指令?\n复位命令仅在故障状态下有效,故障支路将执行复位。');
}
function readSpeed(){
const el = $('speedInput');
const v = parseInt(el.value, 10);
if(isNaN(v)){ alert('请输入有效整数转速'); return null; }
if(v < -32767 || v > 32767){ alert('转速范围 -32767 ~ 32767'); return null; }
return v;
}
function motorSetSpeed(){
const v = readSpeed(); if(v === null) return;
sendCmd('speed', v, '确认设定期望转速 '+v+' rpm?\n(对应指令帧 Byte2/3,使能状态保持不变)');
}
//------------------ 控制卡片 ------------------
function cmdCard(s){
const c = s || {};
const cmd = c.cmd || {};
const en = cmd.enable;
let h = '';
h += row('使能状态', en
? '<span class="fc-ok">已使能(运行)</span>'
: '<span style="color:var(--dim)">未使能(停机)</span>');
h += row('期望转速', (cmd.speed||0) + ' rpm');
h += row('复位命令', cmd.resetActive ? '<span class="fc-warn">生效中(保持数秒)</span>' : '无效');
if(cmd.lastCmdValid){
const names = {enable:'使能命令=1', disable:'使能命令=0(停机)', reset:'复位命令=1', speed:'期望转速'};
h += row('最近指令', (names[cmd.lastCmdAction]||cmd.lastCmdAction)
+ (cmd.lastCmdAction==='speed' ? ' ('+(cmd.lastCmdSpeed||0)+' rpm)' : '')
+ ' · ' + ageText(cmd.lastCmdAge));
} else {
h += row('最近指令', '<span style="color:var(--dim)">无</span>');
}
h += '<div class="sub">使能命令 (Byte0 bit0)</div>' +
'<div>' +
'<button class="btn on" onclick="motorEnable()">使能 (1)</button>' +
'<button class="btn off" onclick="motorStop()">去使能 (0)</button></div>' +
'<div class="sub">复位命令 (Byte0 bit1,仅故障状态下有效)</div>' +
'<div>' +
'<button class="btn warn" onclick="motorReset()">复位</button></div>' +
'<div class="sub">期望转速 (Byte2/3,s16,单位 rpm)</div>' +
'<div>' +
'<input type="number" id="speedInput" value="1000" min="-32767" max="32767" step="100" title="期望转速 (rpm)">' +
'<span style="color:var(--dim);margin-left:6px;">rpm</span> ' +
'<button class="btn" onclick="motorSetSpeed()">设定</button></div>' +
'<div class="sub" style="margin-top:8px;">指令帧 0x18EF2010 周期 ' + ((c.link&&c.link.periodMs)||500) +
'ms 持续发送(协议要求);停机=使能0且转速清0(协议示例 00 00 00 00 00 00 00 00)</div>';
return card('电机指令 (下行 0x18EF2010)', h);
}
//------------------ 状态帧3:转速 / 母线电压 ------------------
function state3Card(d){
if(!d) return '';
let h = '';
h += row('反馈转速', '<span class="state-big">'+(d.valid?(d.speed||0):'—')+'</span> rpm');
h += row('母线电压', (d.valid?(d.voltage||0):'—') + (d.valid?' V':''));
h += row('最后更新', ageCell(d));
h += sub('来源 0x18EF1220(100ms,冗余 0x18EF1221)');
return card('运行状态', h);
}
//------------------ 状态帧4:温度 ------------------
function state4Card(d){
if(!d) return '';
const v = d.valid;
let h = '';
const temps = [
['A相绕组温度', d.tA], ['B相绕组温度', d.tB], ['C相绕组温度', d.tC],
['逆变组件底板温度', d.tInv], ['输出滤波器温度', d.tFilter], ['筒内空气温度', d.tAir],
];
for(const t of temps){
h += row(t[0], v ? (t[1]|0) + ' ℃' : '—');
}
h += row('最后更新', ageCell(d));
h += sub('来源 0x18EF1320(100ms,冗余 0x18EF1321,Byte0~5 各 1 字节 s8)');
return card('温度', h);
}
//------------------ 支路故障/报警卡片 ------------------
function branchCard(title, d){
if(!d) return '';
const words = d.words || [];
let h = '';
const faults = d.faults || [];
const alarms = d.alarms || [];
if(faults.length){
h += row('故障(' + faults.length + '条)', faults.map(function(f){ return '<span class="fc-bad">'+f+'</span>'; }).join(''));
} else {
h += row('故障', d.valid ? '<span class="fc-ok">无</span>' : '—');
}
if(alarms.length){
h += row('报警(' + alarms.length + '条)', alarms.map(function(a){ return '<span class="fc-warn">'+a+'</span>'; }).join(''));
} else {
h += row('报警', d.valid ? '<span class="fc-ok">无</span>' : '—');
}
h += row('故障字原始值', words.map(hex16).join(' '));
h += row('最后更新', ageCell(d));
return card(title, h);
}
//------------------ 通信链路卡片 ------------------
function linkCard(d){
const l = (d && d.link) || {};
let h = '';
h += row('下行通道 (CAN1)', l.channel || 'CAN1');
h += row('冗余通道', l.redChannel ? l.redChannel : '<span style="color:var(--dim)">未配置</span>');
h += row('指令帧周期', (l.periodMs||0) + ' ms');
h += row('指令帧累计', (l.txCount||0) + ' 成功 / ' + (l.txErr||0) + ' 失败');
h += row('最近下发', ageCell({valid:!!l.lastTxValid, age:l.lastTxAge}));
h += row('电机报文接收', (l.rxCount||0) + ' 帧');
h += sub('链路: 电机 <-> CAN1 <-> CANET <-> pCanBridge <-> MOOS <-> pMotor');
return card('通信链路 (pCanBridge)', h);
}
//------------------ 协议参考卡片 ------------------
function protoCard(){
let h = '';
h += row('0x18EF2010', '主控指令帧(下行 500ms,冗余 0x18EF2011)');
h += row('0x18EF2110', '主控限制帧(下行,全预留,暂不使用)');
h += row('0x18EF1020', '支路一故障报警状态帧1(上行 200ms,冗余 0x18EF1021)');
h += row('0x18EF1120', '支路二故障报警状态帧2(上行 200ms,冗余 0x18EF1121)');
h += row('0x18EF1220', '状态帧3:反馈转速/母线电压(上行 100ms,冗余 0x18EF1221)');
h += row('0x18EF1320', '状态帧4:温度(上行 100ms,冗余 0x18EF1321)');
h += row('帧格式', 'CAN2.0B 扩展帧 29 位 ID,数据域 8 字节,Intel 字节序');
h += row('波特率', '250 kbps(默认,按客户可定制 125~1000)');
return card('报文定义 (推进电机通信协议 20260321)', h);
}
function render(snap){
let html = '';
html += cmdCard(snap);
html += state3Card(snap.state3);
html += state4Card(snap.state4);
html += branchCard('支路一故障/报警 (0x18EF1020)', snap.fault1);
html += branchCard('支路二故障/报警 (0x18EF1120)', snap.fault2);
html += linkCard(snap);
html += protoCard();
pagesEl.innerHTML = html;
// 保留转速输入值(render 会重建 DOM)
const prev = window.__lastSpeed;
if(prev !== undefined){ const el = $('speedInput'); if(el) el.value = prev; }
}
function connect(){
const proto = location.protocol==='https:'?'wss':'ws';
const ws = new WebSocket(proto+'://'+location.host+'/ws');
ws.onopen = ()=>{ statusEl.textContent='已连接'; statusEl.className='badge ok'; };
ws.onclose = ()=>{ statusEl.textContent='已断开,重连中...'; statusEl.className='badge err'; setTimeout(connect, 2000); };
ws.onerror = ()=>{ ws.close(); };
ws.onmessage = (ev)=>{
try{
const snap = JSON.parse(ev.data);
const el = $('speedInput');
if(el && el === document.activeElement) window.__lastSpeed = el.value;
render(snap);
}catch(e){}
};
}
connect();
</script>
</body>
</html>
)HTML";
} // namespace motor
#endif // PMOTOR_PAGE_INDEX_H
+10
View File
@@ -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;
+3
View File
@@ -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);
+19 -1
View File
@@ -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);
// 数据库就绪后再启动下位机监听线程
+2 -1
View File
@@ -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;
@@ -167,7 +168,7 @@ class PowerManger : public AppCastingMOOSApp
double GetPublicLastIterateTime() { return GetLastIterateTime(); }
int GetPublicIterateCount() { return GetIterateCount(); }
string GetServerHost() { return m_sServerHost; }
int GetServerPort() { return m_lServerPort; }
int GetServerPort() { return m_lServerPort; }
public:
//故障注入
void injectFault(unsigned type,unsigned int faultCode);
+2
View File
@@ -11,4 +11,6 @@ ProcessConfig = pPowerManger
// CCU 地址;不配置则默认 127.0.0.1:7000
// ccuhost = 127.0.0.1
// ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
}
+21
View File
@@ -137,6 +137,7 @@ add_executable(pccuTest
${CCU_DIR}/protocol/MessageRegistry.cpp
${CCU_DIR}/protocol/FcProtocol.cpp
${CCU_DIR}/protocol/PmProtocol.cpp
${CCU_DIR}/protocol/DisProtocol.cpp
${CCU_DIR}/protocol/CanBms.cpp
${CCU_DIR}/comm/UdpEndpoint.cpp
${CCU_DIR}/comm/LinkManager.cpp
@@ -160,3 +161,23 @@ target_link_libraries(pccuTest
pthread
dl
)
################################################pmotorTest(pMotor 单元测试)####
# pMotor 推进电机 CAN 协议编解码单元测试(独立可执行,无 gtest 依赖)
set(MOTOR_DIR ${CMAKE_SOURCE_DIR}/src/pMotor)
add_executable(pmotorTest
pmotor/pmotorTest.cpp
${MOTOR_DIR}/protocol/MotorCan.cpp
)
target_include_directories(pmotorTest PRIVATE
${MOTOR_DIR}
)
set_target_properties(pmotorTest PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
)
target_link_libraries(pmotorTest
m
pthread
)
+244 -35
View File
@@ -15,10 +15,12 @@
#include "../../src/pCCU/protocol/MessageRegistry.h"
#include "../../src/pCCU/protocol/FcProtocol.h"
#include "../../src/pCCU/protocol/PmProtocol.h"
#include "../../src/pCCU/protocol/DisProtocol.h"
#include "../../src/pCCU/protocol/CanBms.h"
#include "../../src/pCCU/comm/LinkManager.h"
#include "../../src/pCCU/comm/FcLinkManager.h"
#include "../../src/pCCU/comm/PmLinkManager.h"
#include "../../src/pCCU/comm/RcuLinkManager.h"
#include "../../src/pCCU/store/DbStore.h"
using namespace ccu;
@@ -289,50 +291,252 @@ 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));
}
//--------------------------------------------------------------------------
// 9. 配电协议帧长度与 Sum8 校验
static void testDisFrameLengths() {
DisHighVolBusCmdMessage hvCmd; CHECK(hvCmd.totalLength() == 21); // 6+14+1
DisHighAVolBusCmdMessage hvaCmd; CHECK(hvaCmd.totalLength() == 13); // 6+6+1
DisHighBVolBusCmdMessage hvbCmd; CHECK(hvbCmd.totalLength() == 14); // 6+7+1
DisLowBusCmdMessage lvCmd; CHECK(lvCmd.totalLength() == 15); // 6+8+1
DisHighVolBusFbMessage hvFb; CHECK(hvFb.totalLength() == 60); // 6+53+1
DisHighAVolBusFbMessage hvaFb; CHECK(hvaFb.totalLength() == 32); // 6+25+1
DisHighBVolBusFbMessage hvbFb; CHECK(hvbFb.totalLength() == 41); // 6+34+1
DisLowBusFbMessage lvFb; CHECK(lvFb.totalLength() == 36); // 6+29+1
}
//--------------------------------------------------------------------------
// 10. 配电指令/反馈编解码往返(Sum8 校验,篡改检测)
static void testDisRoundTrip() {
// 指令:高压母线(含启动类字节)
disHighVolBusCmd hv;
hv.fuelCellCircuitBreaker = DIS_BREAKER_ON;
hv.powerLithiumBatteryCircuitBreaker = DIS_BREAKER_OFF;
hv.dcdc5Module = 1; hv.coolingSystem = 0;
DisHighVolBusCmdMessage hvMsg;
std::vector<uint8_t> hf = hvMsg.encode(hv);
CHECK(hf.size() == 21);
// Sum8 校验和位于帧尾 1 字节
unsigned sum = 0;
for (size_t i = 0; i + 1 < hf.size(); ++i) sum += hf[i];
CHECK(hf.back() == (sum & 0xFF));
disHighVolBusCmd hvOut;
CHECK(hvMsg.decode(hf, static_cast<void*>(&hvOut)));
CHECK(hvOut.fuelCellCircuitBreaker == DIS_BREAKER_ON);
CHECK(hvOut.powerLithiumBatteryCircuitBreaker == DIS_BREAKER_OFF);
CHECK(hvOut.dcdc5Module == 1 && hvOut.coolingSystem == 0);
// 篡改数据域 -> Sum8 失败
std::vector<uint8_t> bad = hf; bad[8] ^= 0xFF;
CHECK(!hvMsg.decode(bad, static_cast<void*>(&hvOut)));
// 指令:低压主母线 / 仪表汇流排
disLowMainBusCmd hvb; hvb.dcC1DCDistributionPanelCircuitBreaker = DIS_BREAKER_ON;
DisHighBVolBusCmdMessage hvbMsg;
std::vector<uint8_t> bf = hvbMsg.encode(hvb);
CHECK(bf.size() == 14);
disLowMainBusCmd hvbOut;
CHECK(hvbMsg.decode(bf, static_cast<void*>(&hvbOut)));
CHECK(hvbOut.dcC1DCDistributionPanelCircuitBreaker == DIS_BREAKER_ON);
disLowBusCmd lv; lv.unit2CircuitBreaker = DIS_BREAKER_ON;
DisLowBusCmdMessage lvMsg;
std::vector<uint8_t> lf = lvMsg.encode(lv);
CHECK(lf.size() == 15);
disLowBusCmd lvOut;
CHECK(lvMsg.decode(lf, static_cast<void*>(&lvOut)));
CHECK(lvOut.unit2CircuitBreaker == DIS_BREAKER_ON);
// 反馈:高压母线A(断路器+u16 电气量抽查)
disHighAVolBusState hva;
hva.bowFTDevice123CircuitBreaker = DIS_BREAKER_ON;
hva.busbarBVoltage = 0x1234; hva.bowFTDevice123Current = 567;
DisHighAVolBusFbMessage hvaMsg;
std::vector<uint8_t> af = hvaMsg.encode(hva);
CHECK(af.size() == 32);
disHighAVolBusState hvaOut;
CHECK(hvaMsg.decode(af, static_cast<void*>(&hvaOut)));
CHECK(hvaOut.bowFTDevice123CircuitBreaker == DIS_BREAKER_ON);
CHECK(hvaOut.busbarBVoltage == 0x1234);
CHECK(hvaOut.bowFTDevice123Current == 567);
CHECK(isDisCmdMessage(&hvMsg) && isDisFbMessage(&hvaMsg));
CHECK(!isDisMessage(nullptr));
}
//--------------------------------------------------------------------------
// 11. 注册表 (id+帧长) 二维分发:配电指令与 PM 指令共用 id 不串扰
static void testRegistryLengthDispatch() {
MessageRegistry reg;
registerPmMessages(reg); // 0x0001(45B) 0x0002(28B) 0x0003(12B) 0x0004(248B)
registerDisCmdMessages(reg); // 0x0001(21B) 0x0002(13B) 0x0003(14B) 0x0004(15B)
CHECK(reg.size() == 8);
// 精确长度命中
CHECK(reg.find(0x0001, 45)->name() == std::string("pm_control"));
CHECK(reg.find(0x0001, 21)->name() == std::string("dis_hv_cmd"));
CHECK(reg.find(0x0002, 28)->name() == std::string("pm_param_set"));
CHECK(reg.find(0x0002, 13)->name() == std::string("dis_hva_cmd"));
CHECK(reg.find(0x0004, 248)->name() == std::string("pm_status"));
CHECK(reg.find(0x0004, 15)->name() == std::string("dis_lv_cmd"));
// LinkManager 收帧分发:配电指令短帧 -> dis 消息;PM 长帧 -> pm 消息
LinkManager lm("test_pm", 0);
registerPmMessages(lm.registry());
registerDisCmdMessages(lm.registry());
std::string gotName;
lm.setOnMessage([&](Message* m, const std::vector<uint8_t>&) {
gotName = m ? m->name() : "";
});
disHighVolBusCmd hv; hv.fuelCellCircuitBreaker = DIS_BREAKER_ON;
DisHighVolBusCmdMessage hvMsg;
CHECK(lm.handleIncoming(hvMsg.encode(hv)));
CHECK(gotName == "dis_hv_cmd");
PmControlValue pc; pc.mode = 2;
PmControlMessage pcMsg;
CHECK(lm.handleIncoming(pcMsg.encode(pc)));
CHECK(gotName == "pm_control");
}
//--------------------------------------------------------------------------
// 12. RCU 链路注册表:配电反馈 + 真实CCU 状态报文可识别
static void testRcuRegistry() {
RcuLinkManager rcu(0, "127.0.0.1", 1);
// 反馈命中
disHighVolBusState hv; hv.powerBusVoltage = 0x0FA0;
DisHighVolBusFbMessage hvMsg;
std::vector<uint8_t> f = hvMsg.encode(hv);
std::string gotName;
rcu.setOnMessage([&](Message* m, const std::vector<uint8_t>&) {
gotName = m ? m->name() : "";
});
CHECK(rcu.handleIncoming(f));
CHECK(gotName == "dis_hv_fb");
// 真实CCU 状态报文(PM 状态消息复用)
PmStatusValue st; st.heartbeat = 9;
PmStatusMessage stMsg;
CHECK(rcu.handleIncoming(stMsg.encode(st)));
CHECK(gotName == "pm_status");
}
//--------------------------------------------------------------------------
// 13. BCU 断路器控制帧(0x10XX81FF 下行)
static void testBcuCtrlFrame() {
// CAN ID:0x10 XX 81 FF,XX=节点地址
CHECK(bcuCtrlCanId(1) == 0x100181FFu);
CHECK(bcuCtrlCanId(54) == 0x103681FFu);
// 数据域:02 00 总正 总负 FF*4
uint8_t d[8];
buildBcuRelayCtrlData(1, 1, d);
CHECK(d[0] == 0x02 && d[1] == 0x00);
CHECK(d[2] == 0x01 && d[3] == 0x01);
CHECK(d[4] == 0xFF && d[5] == 0xFF && d[6] == 0xFF && d[7] == 0xFF);
buildBcuRelayCtrlData(0, 0, d);
CHECK(d[2] == 0x00 && d[3] == 0x00);
buildBcuRelayCtrlData(1, 0, d);
CHECK(d[2] == 0x01 && d[3] == 0x00);
}
//--------------------------------------------------------------------------
@@ -346,6 +550,11 @@ int main() {
testLinkDispatch();
testDbStore();
testCanBmsDecode();
testDisFrameLengths();
testDisRoundTrip();
testRegistryLengthDispatch();
testRcuRegistry();
testBcuCtrlFrame();
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
+254 -14
View File
@@ -6,6 +6,10 @@
测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002]
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003]
测试脚本(真实CCU模拟) --16004--> pCCU(rcu_local) [配电反馈 0x0005~0x0008]
pCCU --16003--> 测试脚本 [配电反馈原帧转发 pPowerManger]
测试脚本(PM模拟) --16002--> pCCU(pm_local) [配电指令 0x0001~0x0004 短帧]
pCCU --16005--> 测试脚本 [配电指令原帧转发真实CCU]
锂电池已切换为 CAN 总线(BMS 协议,经 pCanBridge 透传 CAN_0x* 消息),
不在本脚本 UDP 拓扑内:BMS 解码由 pccuTest 单元测试覆盖
@@ -15,8 +19,11 @@
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
4. SQLite 记录了收/发原始帧
5. Web 页面可访问
4. 配电反馈(0x0005~0x0008)原帧转发到 16003(字节一致)
5. 配电指令(0x0001~0x0004 短帧)原帧转发到 16005(字节一致);
PM 操控指令(45B, 同id)不转发到真实CCU(按帧长区分)
6. SQLite 记录了收/发原始帧(含 link='rcu')
7. Web 页面可访问
"""
import socket
import struct
@@ -26,6 +33,7 @@ import subprocess
import os
import signal
import sys
import json
import urllib.request
# ---- 配置 ----
@@ -33,10 +41,14 @@ FC_STATUS_PORT = 16000
FC_CTRL_RECV_PORT = 16001
PM_CTRL_PORT = 16002
PM_STATUS_RECV_PORT = 16003
RCU_FB_PORT = 16004 # pCCU rcu 链路本地端口(收配电反馈)
RCU_CMD_RECV_PORT = 16005 # 模拟真实CCU 接收转发的配电指令
CANET_PORT = 16006 # 模拟 USBCAN-8E-U TCP Server(捕获下行 CAN 帧)
WEB_PORT = 18081
DB_PATH = "/tmp/pccu_it_test.db"
MOOSDB_EXE = "/usr/local/bin/MOOSDB"
PCCU_EXE = "/home/zjk/project/H100PowerManger/bin/pCCU"
CANBRIDGE_EXE = "/home/zjk/project/H100PowerManger/bin/pCanBridge"
MISSION = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pccu_it.moos")
# 帧头
@@ -53,6 +65,13 @@ def frame(msg_id, payload, checksum=True):
return body
def dis_frame(msg_id, payload):
"""配电协议帧:0x40 0x40 + id(2B) + length(2B) + payload + Sum8 校验"""
total = 6 + len(payload) + 1
body = START + struct.pack("<HH", msg_id, total) + payload
return body + bytes([sum(body) & 0xFF])
def fc_status_frame():
# payload 144B,仅填关键字段,其余为0
p = bytearray(144)
@@ -98,6 +117,142 @@ def pm_paramset_frame():
return frame(0x0002, bytes(p))
#------------------ 配电协议帧(真实CCU桥接) ------------------
def dis_hv_fb_frame():
"""0x0005 高压母线配电反馈(disHighVolBusState 53B:19字节+17×u16)"""
p = bytearray(53)
p[0] = 0x55 # 燃料电池断路器 闭合
p[1] = 0xAA # 动力锂电池断路器 断开
struct.pack_into("<H", p, 19, 0x0FA0) # 动力汇流排电压
struct.pack_into("<H", p, 21, 123) # DC/DC5模块电流
struct.pack_into("<H", p, 25, 0x0F90) # A汇流排电压
return dis_frame(0x0005, bytes(p))
def dis_hva_fb_frame():
"""0x0006 高压母线A配电反馈(disHighAVolBusState 25B:9字节+8×u16)"""
p = bytearray(25)
p[0] = 0x55 # 艏部FT装置1/2/3断路器 闭合
struct.pack_into("<H", p, 9, 0x0F88) # 汇流排B电压
struct.pack_into("<H", p, 11, 321) # 汇流排B电流
return dis_frame(0x0006, bytes(p))
def dis_hvb_fb_frame():
"""0x0007 低压主母线配电反馈(disLowMainBusState 34B:10字节+12×u16)"""
p = bytearray(34)
p[0] = 0x55 # 锂电池组(仪表)断路器 闭合
struct.pack_into("<H", p, 10, 0x0F70) # 仪表母排电压
struct.pack_into("<H", p, 12, 654) # 艏部低压配电箱电流
return dis_frame(0x0007, bytes(p))
def dis_lv_fb_frame():
"""0x0008 仪表汇流排配电反馈(disLowBusState 29B:11字节+9×u16)"""
p = bytearray(29)
p[0] = 0x55 # UNIT1断路器 闭合
struct.pack_into("<H", p, 11, 0x0F60) # 仪表母排电压
struct.pack_into("<H", p, 13, 111) # UNIT1电流
return dis_frame(0x0008, bytes(p))
def dis_hv_cmd_frame():
"""0x0001 高压母线配电指令(disHighVolBusCmd 14B,pPowerManger 下发)"""
p = bytearray(14)
p[0] = 0x55 # 燃料电池断路器 闭合
p[1] = 0xAA # 动力锂电池断路器 断开
p[12] = 1 # DC/DC5模块启动
return dis_frame(0x0001, bytes(p))
def dis_hva_cmd_frame():
"""0x0002 高压母线A配电指令(disHighAVolBusCmd 6B)"""
p = bytearray(6)
p[0] = 0x55
return dis_frame(0x0002, bytes(p))
def dis_hvb_cmd_frame():
"""0x0003 低压主母线配电指令(disLowMainBusCmd 7B)"""
p = bytearray(7)
p[3] = 0x55 # DC-C1直流配电板断路器
return dis_frame(0x0003, bytes(p))
def dis_lv_cmd_frame():
"""0x0004 仪表汇流排配电指令(disLowBusCmd 8B)"""
p = bytearray(8)
p[0] = 0x55 # UNIT1断路器
return dis_frame(0x0004, bytes(p))
#------------------ BCU 断路器控制下行测试 ------------------
class CanetServer(threading.Thread):
"""最小 CANET TCP Server 模拟:接受 pCanBridge 连接,捕获下行 13 字节帧"""
def __init__(self, port):
super().__init__(daemon=True)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(("127.0.0.1", port))
self.sock.listen(2)
self.frames = []
self.lock = threading.Lock()
self.stop_flag = False
def run(self):
self.sock.settimeout(1.0)
conn = None
while not self.stop_flag:
try:
if conn is None:
conn, _ = self.sock.accept()
conn.settimeout(0.5)
buf = b""
while not self.stop_flag:
try:
d = conn.recv(4096)
except socket.timeout:
break
if not d:
conn.close()
conn = None
break
buf += d
while len(buf) >= 13:
with self.lock:
self.frames.append(buf[:13])
buf = buf[13:]
except socket.timeout:
continue
except OSError:
break
def snapshot(self):
with self.lock:
return list(self.frames)
def bcu_ctrl_expect(addr, pos, neg):
"""期望的下行 13 字节 CANET 帧:扩展帧 DLC8 + 0x10XX81FF + 02 00 pos neg FF*4"""
return bytes([0x80 | 8,
(0x10000000 | (addr << 16) | 0x81FF) >> 24 & 0xFF,
(0x10000000 | (addr << 16) | 0x81FF) >> 16 & 0xFF,
(0x10000000 | (addr << 16) | 0x81FF) >> 8 & 0xFF,
(0x10000000 | (addr << 16) | 0x81FF) & 0xFF,
0x02, 0x00, 0x01 if pos else 0x00, 0x01 if neg else 0x00,
0xFF, 0xFF, 0xFF, 0xFF])
def api_get(path):
try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}{path}", timeout=3)
return json.loads(resp.read().decode())
except Exception as e:
return {"ok": False, "error": str(e)}
def recv_loop(sock, label, results, msg_ids, timeout):
"""持续接收指定 msg_id 的帧,记录到 results"""
sock.settimeout(timeout)
@@ -118,11 +273,13 @@ def recv_loop(sock, label, results, msg_ids, timeout):
def main():
# 清理
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log"):
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log",
"/tmp/pccu_it_can.db", "/tmp/pccu_it_can.db-wal", "/tmp/pccu_it_can.db-shm"):
if os.path.exists(f):
os.remove(f)
procs = []
canet = CanetServer(CANET_PORT)
try:
# 1. 启动 MOOSDB
moosdb = subprocess.Popen([MOOSDB_EXE, "--moos", "--port", "9000"],
@@ -130,27 +287,38 @@ def main():
procs.append(moosdb)
time.sleep(1.0)
# 2. 启动 pCCU
# 2. 启动 pCCU + pCanBridge(pCanBridge 连接模拟 USBCAN,用于下行测试)
pccu = subprocess.Popen([PCCU_EXE, MISSION],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
procs.append(pccu)
time.sleep(2.0)
canet.start()
pcan = subprocess.Popen([CANBRIDGE_EXE, MISSION],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
procs.append(pcan)
time.sleep(2.5)
if pccu.poll() is not None:
print("FAIL: pCCU exited early, code", pccu.returncode)
return 1
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003)
# 3. 启动接收监听(FC 控制转发 16001,PM 状态/配电反馈 16003,
# 转发真实CCU 的配电指令 16005)
fc_ctl_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fc_ctl_recv.bind(("127.0.0.1", FC_CTRL_RECV_PORT))
pm_status_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
pm_status_recv.bind(("127.0.0.1", PM_STATUS_RECV_PORT))
rcu_cmd_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rcu_cmd_recv.bind(("127.0.0.1", RCU_CMD_RECV_PORT))
fc_ctl_results, pm_status_results = [], []
fc_ctl_results, pm_status_results, rcu_cmd_results = [], [], []
t1 = threading.Thread(target=recv_loop,
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 12))
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 14))
t2 = threading.Thread(target=recv_loop,
args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12))
t1.start(); t2.start()
args=(pm_status_recv, "pm-status", pm_status_results,
{0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008}, 14))
t3 = threading.Thread(target=recv_loop,
args=(rcu_cmd_recv, "rcu-cmd", rcu_cmd_results,
{0x0001, 0x0002, 0x0003, 0x0004}, 14))
t1.start(); t2.start(); t3.start()
# 4. 发送 FC 状态(周期模拟)
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -164,7 +332,21 @@ def main():
time.sleep(0.5)
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT))
t1.join(); t2.join()
# 6. 发送配电反馈(真实CCU -> pCCU -> pPowerManger,应原帧转发到 16003)
rcu_fb_frames = [dis_hv_fb_frame(), dis_hva_fb_frame(),
dis_hvb_fb_frame(), dis_lv_fb_frame()]
for f in rcu_fb_frames:
pm_send.sendto(f, ("127.0.0.1", RCU_FB_PORT))
time.sleep(0.2)
# 7. 发送配电指令(pPowerManger -> pCCU -> 真实CCU,应原帧转发到 16005)
dis_cmd_frames = [dis_hv_cmd_frame(), dis_hva_cmd_frame(),
dis_hvb_cmd_frame(), dis_lv_cmd_frame()]
for f in dis_cmd_frames:
pm_send.sendto(f, ("127.0.0.1", PM_CTRL_PORT))
time.sleep(0.2)
t1.join(); t2.join(); t3.join()
ok = True
@@ -208,16 +390,73 @@ def main():
else:
ok = False
# 校验 4:数据库有记录(FC/PM 链路收发)
# 校验 4:配电反馈原帧转发到 pPowerManger(字节一致)
for i, f in enumerate(rcu_fb_frames):
got = [d for d in pm_status_results if d == f]
mid = struct.unpack_from("<H", f, 2)[0]
print(f"[配电反馈 0x{mid:04X} 转发] {'OK' if got else 'MISSING'} (字节一致)")
ok = ok and bool(got)
# 校验 5:配电指令原帧转发到真实CCU(字节一致)
for f in dis_cmd_frames:
got = [d for d in rcu_cmd_results if d == f]
mid = struct.unpack_from("<H", f, 2)[0]
print(f"[配电指令 0x{mid:04X} 转发] {'OK' if got else 'MISSING'} (字节一致)")
ok = ok and bool(got)
# 校验 6:PM 操控指令(0x0001, 45B)不得转发到真实CCU(按帧长区分)
pm_ctl_leak = [d for d in rcu_cmd_results if len(d) == 45]
print(f"[PM操控指令不桥接] 收到 {len(pm_ctl_leak)} 帧 (期望 0)")
ok = ok and not pm_ctl_leak
# 校验 7:数据库有记录(FC/PM 链路收发 + rcu 链路桥接)
time.sleep(1.0)
db_count = -1
db_count, db_rcu = -1, -1
if os.path.exists(DB_PATH):
import sqlite3
conn = sqlite3.connect(DB_PATH)
db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0]
db_rcu = conn.execute(
"SELECT COUNT(*) FROM comm_log WHERE link='rcu'").fetchone()[0]
conn.close()
print(f"[数据库] comm_log 记录数 = {db_count} (期望>=6)")
print(f"[数据库] comm_log 记录数 = {db_count} (期望>=6), "
f"rcu 链路记录数 = {db_rcu} (期望>=8)")
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
ok = ok and (db_rcu >= 8) # 4条配电反馈(收) + 4条配电指令(发)
# 校验 8:BCU 断路器控制下行(网页API -> pCCU -> MOOS -> pCanBridge -> 模拟USBCAN)
# 闭合(addr=5, 总正+总负=1)
j = api_get("/api/bcu_ctrl?addr=5&pos=1&neg=1")
print(f"[断路器闭合 API] {j}")
ok = ok and j.get("ok") is True
# action 语法(addr=6, action=1 等价 pos=neg=1)
j = api_get("/api/bcu_ctrl?addr=6&action=1")
print(f"[断路器闭合 API(action)] {j}")
ok = ok and j.get("ok") is True
# 非法地址
j = api_get("/api/bcu_ctrl?addr=99&action=1")
print(f"[非法地址 API] {j}")
ok = ok and j.get("ok") is False
# 断开(addr=5, 总正+总负=0)
j = api_get("/api/bcu_ctrl?addr=5&pos=0&neg=0")
print(f"[断路器断开 API] {j}")
ok = ok and j.get("ok") is True
time.sleep(3.0)
frames = canet.snapshot()
print(f"[下行帧捕获] {len(frames)} 帧")
exp_close5 = bcu_ctrl_expect(5, 1, 1)
exp_close6 = bcu_ctrl_expect(6, 1, 1)
exp_open5 = bcu_ctrl_expect(5, 0, 0)
hit5 = any(f == exp_close5 for f in frames)
hit6 = any(f == exp_close6 for f in frames)
hit5o = any(f == exp_open5 for f in frames)
leak99 = any(f[1] == 0x63 and f[2] == 0x00 for f in frames) # addr=99 未下发
print(f"[断路器闭合下行(addr=5)] {'OK' if hit5 else 'MISSING'}")
print(f"[断路器闭合下行(addr=6)] {'OK' if hit6 else 'MISSING'}")
print(f"[断路器断开下行(addr=5)] {'OK' if hit5o else 'MISSING'}")
print(f"[非法地址未下发(addr=99)] {'OK' if not leak99 else 'LEAK'}")
ok = ok and hit5 and hit6 and hit5o and not leak99
web_ok = False
try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)
@@ -232,6 +471,7 @@ def main():
return 0 if ok else 1
finally:
canet.stop_flag = True
for p in reversed(procs):
try:
p.terminate()
+17
View File
@@ -16,6 +16,12 @@ ProcessConfig = pCCU
pm_remote_ip = 127.0.0.1
pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文)
// 真实CCU 桥接链路(配电协议透传)
rcu_enable = true
rcu_local_port = 16004 // 监听真实CCU 配电反馈(测试发送此端口)
rcu_remote_ip = 127.0.0.1
rcu_remote_port = 16005 // 真实CCU(测试监听此端口收转发的配电指令)
// 锂电池:CAN 总线 BMS 协议,经 pCanBridge 的 CAN_0x* 消息透传
// (集成测试不覆盖 CAN 链路,BMS 解码由 pccuTest 单元测试覆盖)
@@ -24,4 +30,15 @@ ProcessConfig = pCCU
web_port = 18081 // 避开 pPowerMangerHost(18080)
web_enable = true
}
ProcessConfig = pCanBridge
{
AppTick = 4
CommsTick = 4
// 测试用 CANET TCP Server(本脚本模拟,捕获 pCanBridge 下行帧)
channel = CAN0,127.0.0.1,16006
default_channel = CAN0
dbpath = /tmp/pccu_it_can.db
}
+255
View File
@@ -0,0 +1,255 @@
//============================================================================
// pmotorTest:pMotor 推进电机 CAN 协议编解码单元测试
// 无第三方测试库,独立可执行。参照 test/pccu/pccuTest.cpp 的轻量风格。
//
// 协议依据:docs/推进电机通信协议20260321.docx
//============================================================================
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "../../src/pMotor/protocol/MotorCan.h"
using namespace motor;
static int g_fail = 0;
static int g_pass = 0;
#define CHECK(cond) do { \
if (cond) { ++g_pass; } \
else { ++g_fail; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
} while (0)
#define CHECK_STREQ(a, b) do { \
if (std::string(a) == std::string(b)) { ++g_pass; } \
else { ++g_fail; std::printf("FAIL %s:%d \"%s\" != \"%s\"\n", __FILE__, __LINE__, std::string(a).c_str(), std::string(b).c_str()); } \
} while (0)
//--------------------------------------------------------------------------
// 1. 指令帧编码(对照文档示例)
static void testCmdEncode() {
uint8_t d[8];
// 文档示例:启动并给定 1000 转速 -> 01 00 E8 03 00 00 00 00
buildMotorCmdData(true, false, 1000, d);
uint8_t expect1[] = {0x01, 0x00, 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00};
CHECK(std::memcmp(d, expect1, 8) == 0);
// 文档示例:停机 -> 00 00 00 00 00 00 00 00
buildMotorCmdData(false, false, 0, d);
uint8_t expect0[] = {0, 0, 0, 0, 0, 0, 0, 0};
CHECK(std::memcmp(d, expect0, 8) == 0);
// 文档示例:复位 -> 02 00 00 00 00 00 00 00
buildMotorCmdData(false, true, 0, d);
uint8_t expect2[] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
CHECK(std::memcmp(d, expect2, 8) == 0);
// 使能+复位 同时有效 -> bit0+bit1 = 0x03
buildMotorCmdData(true, true, 0, d);
CHECK(d[0] == 0x03);
// 负转速 s16 Intel:-100 = 0xFF9C -> byte2=0x9C byte3=0xFF
buildMotorCmdData(true, false, -100, d);
CHECK(d[0] == 0x01);
CHECK(d[2] == 0x9C && d[3] == 0xFF);
// 边界转速:32767 / -32768
buildMotorCmdData(true, false, 32767, d);
CHECK(d[2] == 0xFF && d[3] == 0x7F);
buildMotorCmdData(true, false, -32768, d);
CHECK(d[2] == 0x00 && d[3] == 0x80);
// 支路通信状态位(bit2/bit3)恒 0(pMotor 不反馈通信状态)
buildMotorCmdData(true, false, 1, d);
CHECK((d[0] & 0x0C) == 0);
}
//--------------------------------------------------------------------------
// 2. CAN ID 识别
static void testCanId() {
// 12 条电机 ID 全部命中
const uint32_t ids[] = {
MOTOR_CAN_CMD, MOTOR_CAN_CMD_RED,
MOTOR_CAN_LIMIT, MOTOR_CAN_LIMIT_RED,
MOTOR_CAN_FAULT1, MOTOR_CAN_FAULT1_RED,
MOTOR_CAN_FAULT2, MOTOR_CAN_FAULT2_RED,
MOTOR_CAN_STATE3, MOTOR_CAN_STATE3_RED,
MOTOR_CAN_STATE4, MOTOR_CAN_STATE4_RED,
};
for (size_t i = 0; i < sizeof(ids) / sizeof(ids[0]); ++i)
CHECK(isCanMotorId(ids[i]));
// 冗余 CAN 与主 CAN 仅末字节差 1
CHECK(MOTOR_CAN_CMD_RED == MOTOR_CAN_CMD + 1);
CHECK(MOTOR_CAN_FAULT1_RED == MOTOR_CAN_FAULT1 + 1);
CHECK(MOTOR_CAN_FAULT2_RED == MOTOR_CAN_FAULT2 + 1);
CHECK(MOTOR_CAN_STATE3_RED == MOTOR_CAN_STATE3 + 1);
CHECK(MOTOR_CAN_STATE4_RED == MOTOR_CAN_STATE4 + 1);
// 其他协议报文不命中(BCU 电池 / 配电)
CHECK(!isCanMotorId(0x10010000)); // BCU 概要
CHECK(!isCanMotorId(0x103681FF)); // BCU 继电器控制
CHECK(!isCanMotorId(0x1401A501)); // 配电控制指令
CHECK(!isCanMotorId(0x18EF2012)); // 电机协议未定义的 ID
}
//--------------------------------------------------------------------------
// 3. 状态帧3 解码(0x18EF1220:反馈转速/母线电压)
static void testState3Decode() {
MotorState st;
uint8_t d[8] = {0xE8, 0x03, 0xF4, 0x01, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3, d, 8, st));
CHECK(st.state3Valid);
CHECK(st.feedbackSpeed == 1000); // 0x03E8 Intel
CHECK(st.busVoltage == 500); // 0x01F4 Intel
// 负转速
uint8_t d2[8] = {0x9C, 0xFF, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3, d2, 8, st));
CHECK(st.feedbackSpeed == -100);
// 冗余 CAN 同样可解码
MotorState st2;
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3_RED, d, 8, st2));
CHECK(st2.feedbackSpeed == 1000 && st2.busVoltage == 500);
// 非法 dlc
MotorState st3;
CHECK(!decodeMotorFrame(MOTOR_CAN_STATE3, d, 4, st3));
CHECK(!decodeMotorFrame(MOTOR_CAN_STATE3, nullptr, 8, st3));
}
//--------------------------------------------------------------------------
// 4. 状态帧4 解码(0x18EF1320:温度)
static void testState4Decode() {
MotorState st;
uint8_t d[8] = {26, 44, 62, 79, 90, 107, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE4, d, 8, st));
CHECK(st.state4Valid);
CHECK(st.tempA == 26 && st.tempB == 44 && st.tempC == 62);
CHECK(st.tempInv == 79 && st.tempFilter == 90 && st.tempAir == 107);
// 冗余 CAN
MotorState st2;
CHECK(decodeMotorFrame(MOTOR_CAN_STATE4_RED, d, 8, st2));
CHECK(st2.tempA == 26);
}
//--------------------------------------------------------------------------
// 5. 故障帧解码(0x18EF1020 支路一 / 0x18EF1120 支路二)
static void testFaultDecode() {
MotorState st;
// 支路一:word0 置 bit8(超速) + bit9(紧急停机) -> byte0|byte1<<8 = 0x0300;
// word3 置 bit8(遥控CAN通信中断报警) -> 0x0100
uint8_t d1[8] = {0x00, 0x03, 0, 0, 0, 0, 0x00, 0x01};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d1, 8, st));
CHECK(st.branch1.valid);
CHECK(st.branch1.word[0] == 0x0300);
CHECK(st.branch1.word[3] == 0x0100);
CHECK(st.branch1.faults.size() == 2);
CHECK_STREQ(st.branch1.faults[0], "超速故障");
CHECK_STREQ(st.branch1.faults[1], "紧急停机故障");
CHECK(st.branch1.alarms.size() == 1);
CHECK_STREQ(st.branch1.alarms[0], "遥控CAN通信中断报警");
// 支路一 word0 bit7 = 筒内空气过温故障
uint8_t d2[8] = {0x80, 0x00, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d2, 8, st));
CHECK(st.branch1.faults.size() == 1);
CHECK_STREQ(st.branch1.faults[0], "筒内空气过温故障");
// 支路二:同 bit7 为预留,不应产生故障描述
uint8_t d3[8] = {0x80, 0x00, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT2, d3, 8, st));
CHECK(st.branch2.valid);
CHECK(st.branch2.faults.empty());
CHECK(st.branch2.alarms.empty());
// 支路二 word1:bit0(逆变A1相驱动1上管) + word2 bit3(母线电压过压)
uint8_t d4[8] = {0, 0, 0x01, 0x00, 0x08, 0x00, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT2, d4, 8, st));
CHECK(st.branch2.word[1] == 0x0001);
CHECK(st.branch2.word[2] == 0x0008);
CHECK(st.branch2.faults.size() == 2);
CHECK_STREQ(st.branch2.faults[0], "逆变A1相驱动1(上管)故障");
CHECK_STREQ(st.branch2.faults[1], "母线电压过压故障");
// 冗余 CAN 同样可解码且归入对应支路
MotorState st2;
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1_RED, d1, 8, st2));
CHECK(st2.branch1.valid && !st2.branch2.valid);
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT2_RED, d4, 8, st2));
CHECK(st2.branch2.valid && st2.branch2.faults.size() == 2);
}
//--------------------------------------------------------------------------
// 6. 故障描述查表
static void testFaultTable() {
std::vector<std::string> out;
// word1 bit12/13/14/15
CHECK(motorFaultTexts(1, 1, (1u << 12) | (1u << 13) | (1u << 14) | (1u << 15), out) == 4);
CHECK_STREQ(out[0], "硬件总故障");
CHECK_STREQ(out[1], "驱动总故障");
CHECK_STREQ(out[2], "采样总故障");
CHECK_STREQ(out[3], "24V欠压故障");
// word3 全位 16 条
CHECK(motorFaultTexts(1, 3, 0xFFFF, out) == 16);
CHECK_STREQ(out[0], "直流母线1电压偏高报警");
CHECK_STREQ(out[15], "协调光纤报警");
// 无置位
CHECK(motorFaultTexts(1, 0, 0, out) == 0);
// 非法参数
CHECK(motorFaultTexts(0, 0, 1, out) == 0);
CHECK(motorFaultTexts(3, 0, 1, out) == 0);
CHECK(motorFaultTexts(1, 9, 1, out) == 0);
}
//--------------------------------------------------------------------------
// 7. 部分更新合并语义(故障帧重复上报,故障清除后描述随之清除)
static void testPartialUpdate() {
MotorState st;
uint8_t d1[8] = {0x01, 0x00, 0, 0, 0, 0, 0, 0}; // 支路一 CPLD自检故障 (bit0)
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d1, 8, st));
CHECK(st.branch1.faults.size() == 1);
CHECK_STREQ(st.branch1.faults[0], "CPLD自检故障标志");
// 复位后故障字清零 -> 描述清空
uint8_t d2[8] = {0, 0, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_FAULT1, d2, 8, st));
CHECK(st.branch1.faults.empty());
// 状态帧不影响支路有效标志
uint8_t d3[8] = {1, 0, 0, 0, 0, 0, 0, 0};
CHECK(decodeMotorFrame(MOTOR_CAN_STATE3, d3, 8, st));
CHECK(st.feedbackSpeed == 1);
CHECK(st.branch1.valid); // 仍为 true(部分更新语义)
}
int main() {
testCmdEncode();
testCanId();
testState3Decode();
testState4Decode();
testFaultDecode();
testFaultTable();
testPartialUpdate();
std::printf("\n==== pmotorTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}