5 Commits
Author SHA1 Message Date
zjk 88bed850ee pCCU 锂电池通信切换为 CAN 总线(BMS 协议,经 pCanBridge 透传)
build-test-deploy / ci (push) Failing after 1h9m57s
协议层:
- 新增 protocol/CanBms.{h,cpp}:按 docs/BMS_协议字段定义.xlsx 解码
  0x10010000(总电压/SOC/故障码/状态位)、0x10010005(最大电流限制/
  总电流/最高单体电压)、0x10010006(总电压校验)。
  缩放用除法保证与十进制字面量严格一致;总电流按 2 字节 s16 实现
  (xlsx 标注 3 字节与其自身示例/实车帧矛盾,已在代码注释说明)。
- 删除 UDP 锂电池协议:protocol/BatProtocol.{h,cpp}、
  comm/BatLinkManager.h(动力/仪表双链路、自检/接触器/功率指令)。

pCCU 主体:
- CCU 订阅 pCanBridge 发布的 CAN_0x* 二进制消息并解码 BMS 报文;
  SystemData 双电池槽位改为单一 BmsStatusValue + CAN 帧计数;
  PM 电池启停/功率指令改为告警忽略(BMS CAN 协议未定义控制报文)。
- 修复两个本地联调发现的阻断性 bug:
  1) MOOS 通配订阅必须用三参数 Register("CAN_0x*","*",0),
     单参数形式不做通配匹配;
  2) 变量名前缀匹配偏移(compare 少比 1 字符 / sscanf +5 指向 'x'),
     改为 rfind 前缀判断 + +6 偏移。
- PowerCoordinator/CoordFsm 剥离电池接触器/切换时序(依赖已删除的
  UDP 指令),保留 FC 联动与协调策略占位(CoordSwitchingState 改为
  通用的 CoordBusyState)。

网页:
- 动力/仪表锂电池两个标签页合并为"锂电池BMS(CAN)":总电压/总电流/
  SOC/最大电流限制/最高单体电压/校验帧/故障码高亮/在线状态,
  附报文来源说明;链路状态页改为 CAN 链路行(pCanBridge)。

配置与测试:
- pCCU.moos / missions/h100.moos 移除电池 UDP 配置;
- pccuTest:删除旧电池编解码用例,新增 testCanBmsDecode
  (向量取自实车抓包:533.8V/20.1%/49/3.752V/-25.0A)103/103 通过;
- 集成测试改为纯 FC/PM 流程,本地实测 PASSED(含 Web 页面校验)。
2026-08-31 21:42:26 +08:00
zjk ee8819c610 docs:新增 BMS 协议字段定义
CAN 总线 BMS(USBCAN-8E-U 接入)报文字段定义表,供 pCanBridge
透传数据(CAN_0x* / can_frame 表)的下游解析与联调参考。
2026-08-31 20:09:50 +08:00
zjk e9a85c21fc 脚本清理:移除交叉编译,统一板卡原生编译流程 + 数据脚本支持 pCanBridge
- 移除交叉编译脚本 build-arm64.sh / build-arm64-image.sh 及本地
  bin/arm64、build-arm64 产物目录(QEMU 模拟编译过慢)。
  板卡产物统一由 ./scripts/build-board.sh 原生编译产出。
- deploy.sh 简化为 mission + 5 个 systemd 服务安装(不再推送
  二进制),头注明确二进制来源为 build-board.sh。
- fetch-data.sh:新增 pCanBridge_data.db(+wal/shm) 下载,补齐
  遗漏的 pccu_data.db-wal/-shm,--journal 导出列表加入 pCanBridge。
- clean-data.sh:同步清理 pCanBridge 数据库三件套;修复停服命令
  漏掉 pCCU 的老问题(边写边清会产生脏数据),现按
  pCanBridge→apps→moosdb 顺序全停;--start 启动列表补齐。
- .gitignore:新增全局规则 *.db / *.db-shm / *.db-wal /
  *.db-journal / *.log,所有数据库与日志一律不入库;清理散落
  的重复条目与失效的 bin/arm64 条目。

验证:各脚本 bash -n 通过,help 输出正常,git check-ignore 命中
新规则且不影响 .moos 等配置文件。
2026-08-31 19:42:20 +08:00
zjk b547772709 新增 pCanBridge:USBCAN-8E-U CAN->MOOSDB 透传桥 + SQLite 落库
新应用 src/pCanBridge(独立 MOOS App,单通道):
- CanEndpoint:TCP Client 连接 CANET/USBCAN-8E-U 工作端口
  (目标 IP/端口可配置,默认 192.168.0.222:4001=CAN0),
  接收线程流式缓冲按 13 字节切帧,过滤无效 DLC(>8),
  断线自动重连(非阻塞 connect+超时 / SO_RCVTIMEO 读超时)。
- 帧协议(CANET-8E-U 手册 8.1):byte0=帧信息(bit7 FF/bit6 RTR/
  bit3~0 DLC),byte1~4=ID 大端,byte5~12=数据。
- MOOS 映射:变量名 CAN_0x%08X=CAN ID(不缩减),m_sVal=二进制
  data(逐帧透传不限流),m_sSrcAux=通道名(can_channel_name),
  m_dfVal2=原始帧信息字节(用于区分标准/扩展、数据/远程帧)。
- CanDbStore:每帧落库 SQLite(can_frame 表:time/channel/
  frame_info/can_id/dlc/hex,含 time、can_id 索引),参考 pCCU
  DbStore:预处理语句 + WAL + synchronous=NORMAL,路径由 dbpath
  配置;复用 pPowerManger 的 sqlite3 amalgamation。
- AppCast 输出连接状态/收帧/发布/落库统计。

配套:
- missions/h100.moos 新增 pCanBridge 配置块(板卡 dbpath 指向
  data 目录)。
- build-board.sh / deploy.sh 支持 pCanBridge:产物部署 +
  pCanBridge.service 安装,服务列表加入第 5 个服务。
- .gitignore 忽略 bin/pCanBridge 与本机默认数据库。

已验证(板卡 192.168.0.223 实测):服务 active,连接设备正常,
~60 帧/s 透传,落库 9000+ 行 integrity ok、无丢帧。
2026-08-31 19:14:59 +08:00
zjk 7e4d08808a pCCU新增锂电池通信/协调操作 + 电源协调状态机骨架(惰性)
锂电池通信(docs/锂电池协议20230324.docx):
- 协议层 BatProtocol:自检0x0000(2B)/设备控制0x0001(17B)/电池组状态0x0003(80B)/
  电池包报警0x0004(68B),uint32字节和校验、小端
- 链路层 BatLinkManager:动力/仪表锂电池独立UDP链路,本机监听与远端IP端口
  均由.moos配置(dyn_bat_*/ins_bat_*/bat_work_condition)
- SystemData/SnapshotBuilder/网页:电池数据按动力/仪表归类展示(运行状态/接触器
  电气量/电池包电压温度/报警),收发帧落库SQLite,网页新增电池标签页

操作->指令下发(PowerCoordinator):
- 操作接口:自检/上下电(母线接触器55H接通77H断开,含预充)/功率设定(0~500kW)/
  电池切换(低压->高压6步、高压->低压5步,按协议时序)
- 步骤引擎:每电池独立步骤队列,下发->状态反馈确认->超时兜底;周期1s心跳控制帧
- PM操控指令insBatCmd/dynBatCmd/dynBatPower自动映射为电池操作

电源协调状态机骨架(CoordFsm, TinyFSM):
- 事件Tick/PmControl/StepDone + 状态Normal/Switching/Fault,react均为TODO占位
- 当前未接线(无start/dispatch调用,运行行为与接入前一致),CoordFsm.hpp头部附接线指南

测试与部署:
- pccuTest新增电池协议编解码/帧长/链路分发用例(175项通过);集成测试覆盖电池
  心跳、启停映射(0x10->0x55/0x20->0x77)、状态落库;同步pccu_it.moos与ws_check
- h100.moos与pCCU.moos增加锂电池链路配置;归档锂电池协议文档
2026-08-30 14:40:27 +08:00
44 changed files with 2109 additions and 212 deletions
+14 -16
View File
@@ -241,18 +241,17 @@ CTestTestfile.cmake
_deps
# PowerManager specific log files
bin/pPowerManger.log
# PowerManager specific files
bin/ccuState.txt
*.db
# pPowerMangerHost 反馈落盘数据库
feedback_data.db
# PowerManager compiled binaries (built into bin/ by CMake)
bin/pPowerManger
bin/SQLiteTest
bin/pPMtest
bin/pPowerMangerHost
bin/pCCU
bin/pccuTest
bin/pCanBridge
# CI test reports
reports/
@@ -262,27 +261,26 @@ bin/PowerMangerTests
bin/udpFeeder
bin/fullFeeder
# SQLite runtime artifacts
# ============================================================
# 数据库与日志文件(全局忽略,一律不入库)
# *.db / *.db-* SQLite 数据库及 WAL/SHM/journal 伴生文件
# (power_data / pccu_data / pCanBridge_data /
# feedback_data / 测试库等)
# *.log 运行日志(pCCU.log / pPowerManger.log 等)
# ============================================================
*.db
*.db-shm
*.db-wal
*.db-journal
*.log
# Cross-compiled aarch64 artifacts (scripts/cross-build-local.sh)
bin/arm64/
# Board-native build dirs (build-arm64.sh / direct on-board builds)
# Board-native build dirs (build-board.sh) / 历史本机构建目录
build-arm64/
build-nosa-test/
# CMake-generated web asset table (pPowerMangerHost)
src/pPowerMangerHost/webassets_gen.h
# ============================================================
# 本机编译产物(避免误提交二进制)
# ============================================================
bin/pCCU
bin/pccuTest
# ============================================================
# MS Office 临时锁文件 / 编辑器、备份残留
# ============================================================
Binary file not shown.
Binary file not shown.
+21 -1
View File
@@ -1,6 +1,7 @@
// MOOS file
// 板卡 (RK3588 / Ubuntu 20.04) 专用 mission 配置
// 四个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU)共用本文件。
// 五个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU / pCanBridge)
// 共用本文件。
// 由 scripts/deploy.sh 推送到板卡 /root/work/h100/missions/h100.moos
ServerHost = localhost
@@ -51,9 +52,28 @@ ProcessConfig = pCCU
pm_remote_ip = 127.0.0.1
pm_remote_port = 5001
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
// 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。
dbpath = /root/work/h100/data/pccu_data.db
logpath = /root/work/h100/data/pCCU.log
web_port = 8080
web_enable = true
}
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 帧 SQLite 落库路径(can_frame 表)
dbpath = /root/work/h100/data/pCanBridge_data.db
}
-60
View File
@@ -1,60 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/build-arm64-image.sh
# DESC: 一次性构建 arm64 CI 镜像(MOOS-IvP + jsoncpp + GTest)。
# 构建产物缓存后,日常交叉编译只需 ./scripts/build-arm64.sh。
#
# 用法:
# CI_TOKEN=xxx ./scripts/build-arm64-image.sh # 构建(已存在则跳过)
# CI_TOKEN=xxx ./scripts/build-arm64-image.sh --force # 强制重新构建
#
# 环境变量:
# CI_TOKEN 内网 Gitea token(拉取 moos-ivp / googletest 镜像,兼容 GITEA_TOKEN)
#=======================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
IMAGE="h100-power-manager-ci:arm64"
GITEA_TOKEN="${CI_TOKEN:-${GITEA_TOKEN:-}}"
FORCE=0
[ "${1:-}" = "--force" ] && FORCE=1
if [ -z "${GITEA_TOKEN}" ]; then
echo "错误: 缺少内网 Gitea token。" >&2
echo " 请设置: CI_TOKEN=xxx ./scripts/build-arm64-image.sh" >&2
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "错误: 未找到 docker" >&2
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "错误: 无法连接 docker daemon(是否需要用 sudo?)" >&2
exit 1
fi
if ! docker buildx version >/dev/null 2>&1; then
echo "错误: 未找到 buildx。请安装: sudo apt-get install -y docker-buildx" >&2
exit 1
fi
if [ "${FORCE}" = "0" ] && docker image inspect "${IMAGE}" >/dev/null 2>&1; then
echo ">>> 已存在镜像 ${IMAGE},跳过构建(如需重建: ${0} --force)"
exit 0
fi
if [ "${FORCE}" = "1" ]; then
echo ">>> --force: 重新构建镜像 ${IMAGE}"
fi
CI_TOKEN="${GITEA_TOKEN}" docker buildx build \
--platform linux/arm64 \
--load \
--secret id=gitea_token,env=CI_TOKEN \
-t "${IMAGE}" -f "${PROJECT_ROOT}/ci/Dockerfile" "${PROJECT_ROOT}"
echo ">>> 镜像构建完成: ${IMAGE}"
-42
View File
@@ -1,42 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/build-arm64.sh
# DESC: 交叉编译为 RK3588 (aarch64) / Ubuntu 22.04 二进制,用法同 build.sh。
# 在 arm64 镜像(h100-power-manager-ci:arm64)内跑 cmake + make,
# 产物输出到 bin/arm64/,与本地 x86_64 的 bin/、build/ 互不影响。
#
# 用法:
# ./scripts/build-arm64.sh
#
# 前置(一次性,构建 arm64 镜像):
# CI_TOKEN=xxx ./scripts/build-arm64-image.sh
#=======================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
IMAGE="h100-power-manager-ci:arm64"
JOBS="${JOBS:-$(nproc)}"
if ! docker image inspect "${IMAGE}" >/dev/null 2>&1; then
echo "错误: 本地不存在镜像 ${IMAGE}" >&2
echo " 请先执行: CI_TOKEN=xxx ./scripts/build-arm64-image.sh" >&2
exit 1
fi
mkdir -p "${PROJECT_ROOT}/build-arm64" "${PROJECT_ROOT}/bin/arm64"
docker run --rm --platform linux/arm64 \
-u "$(id -u):$(id -g)" \
-v "${PROJECT_ROOT}":/src \
-w /src/build-arm64 \
"${IMAGE}" \
/bin/sh -c "cmake -DCMAKE_BUILD_TYPE=Release \
-DEXECUTABLE_OUTPUT_PATH=/src/bin/arm64 \
-DRUNTIME_OUTPUT_DIRECTORY=/src/bin/arm64 .. && make -j${JOBS}"
echo ""
echo ">>> 完成。产物:"
file "${PROJECT_ROOT}/bin/arm64/pPowerManger"
+27 -3
View File
@@ -7,7 +7,7 @@
#
# 用法:
# ./scripts/build-board.sh # 同步+编译+部署(不重启)
# ./scripts/build-board.sh --start # 编译后重启 3 个服务
# ./scripts/build-board.sh --start # 编译后重启全部服务(含 pCanBridge)
# ./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)
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge)
info() { printf '\033[1;36m[build-board]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[build-board]\033[0m %s\n' "$*"; }
@@ -210,10 +210,34 @@ ${SSH} "${SSH_TARGET}" "mkdir -p ${BOARD_BIN} && \
cp -f ${BOARD_SRC}/bin/pPowerManger ${BOARD_BIN}/pPowerManger && \
cp -f ${BOARD_SRC}/bin/pPowerMangerHost ${BOARD_BIN}/pPowerMangerHost && \
cp -f ${BOARD_SRC}/bin/pCCU ${BOARD_BIN}/pCCU && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost ${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" \
|| { err "部署产物失败"; exit 1; }
ok "产物已部署"
#-------------------------------------------------------------------
# 4.5 安装/刷新 pCanBridge systemd unit(幂等,其它 4 个 unit 已装)
#-------------------------------------------------------------------
info "安装 pCanBridge.service ..."
${SSH} "${SSH_TARGET}" "cat > /etc/systemd/system/pCanBridge.service" <<EOF
[Unit]
Description=pCanBridge for H100 Power Manager
After=moosdb.service
Requires=moosdb.service
[Service]
Type=simple
WorkingDirectory=${BOARD_BIN}
ExecStart=${BOARD_BIN}/pCanBridge --alias=pCanBridge /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 pCanBridge 2>/dev/null || true"
ok "pCanBridge.service 已安装"
echo ""
ok "产物: ${BOARD_BIN}/pPowerManger (aarch64)"
${SSH} "${SSH_TARGET}" "file ${BOARD_BIN}/pPowerManger | cut -c1-60"
+19 -10
View File
@@ -5,12 +5,14 @@
# 默认清理板卡运行目录 /root/work/h100/data/ 下的:
# power_data.db (+ -wal/-shm) 主机功率数据库
# feedback_data.db 反馈落盘数据库
# pPowerManger.log 主机运行日志
# 默认先停止 4 个服务再清理(避免 SQLite WAL 仍在写入)。
# pccu_data.db (+ -wal/-shm) pCCU 帧日志数据库
# pCanBridge_data.db (+ -wal/-shm) pCanBridge CAN 帧数据库
# pPowerManger.log / pCCU.log 运行日志
# 默认先停止各服务再清理(避免 SQLite WAL 仍在写入)。
#
# 用法:
# ./scripts/clean-data.sh # 停服务 -> 清理 data 目录(不重启)
# ./scripts/clean-data.sh --start # 清理后重新启动 4 个服务
# ./scripts/clean-data.sh --start # 清理后重新启动各服务
# ./scripts/clean-data.sh --no-stop # 不停服务直接清理(慎用)
# ./scripts/clean-data.sh --stale # 额外清理 bin/ 与 src/bin/ 下的旧 db/log
#
@@ -38,10 +40,16 @@ DO_START=0
DO_STOP=1
DO_STALE=0
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge)
# 板卡 data 目录下需要清理的文件(含 SQLite WAL/SHM)
DATA_FILES=(power_data.db power_data.db-wal power_data.db-shm feedback_data.db pPowerManger.log pccu_data.db pCCU.log)
DATA_FILES=(
power_data.db power_data.db-wal power_data.db-shm
feedback_data.db
pccu_data.db pccu_data.db-wal pccu_data.db-shm
pCanBridge_data.db pCanBridge_data.db-wal pCanBridge_data.db-shm
pPowerManger.log pCCU.log
)
# 历史遗留目录(旧部署位置),仅当 --stale 时清理
STALE_DIRS=("${BOARD_DIR}/bin" "${BOARD_DIR}/src/bin")
@@ -52,7 +60,7 @@ warn() { printf '\033[1;33m[clean-data]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[clean-data]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,29p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,31p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
@@ -92,7 +100,8 @@ ok "SSH 连接正常: ${SSH_TARGET}"
#-------------------------------------------------------------------
if [ "${DO_STOP}" = "1" ]; then
info "停止服务 ${SERVICES[*]} ..."
${SSH} "${SSH_TARGET}" "systemctl stop pPowerMangerHost pPowerManger moosdb 2>/dev/null; true"
# 先停应用,最后停 moosdb
${SSH} "${SSH_TARGET}" "systemctl stop pCanBridge pPowerMangerHost pPowerManger pCCU moosdb 2>/dev/null; true"
ok "服务已停止"
else
warn "--no-stop:不停服务直接清理,可能产生脏数据(SQLite 仍在写入)"
@@ -123,15 +132,15 @@ fi
# 5. 可选: 重启服务
#-------------------------------------------------------------------
if [ "${DO_START}" = "1" ]; then
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU) ..."
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU / pCanBridge) ..."
${SSH} "${SSH_TARGET}" "systemctl start moosdb && sleep 1 && \
systemctl start pPowerManger pPowerMangerHost pCCU" \
systemctl start pPowerManger pPowerMangerHost pCCU pCanBridge" \
|| { err "启动失败"; exit 1; }
ok "服务已启动"
else
echo ""
info "清理完成,服务未启动。需要重启请加 --start 或手动:"
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU"
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU pCanBridge"
fi
echo ""
+21 -41
View File
@@ -1,18 +1,20 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/deploy.sh
# DESC: 部署 pPowerManger / pPowerMangerHost / pCCU(aarch64 二进制 +
# mission)到目标板卡 (RK3588 / Ubuntu 22.04),并用四个独立
# systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU)
# 管理。只安装服务 + daemon-reload,默认不启动、不自启。
# DESC: 部署 mission + 5 个 systemd 服务(moosdb / pPowerManger /
# pPowerMangerHost / pCCU / pCanBridge)到目标板卡 (RK3588 /
# Ubuntu 22.04)。只安装服务 + daemon-reload,默认不启动、不自启。
#
# 注意:二进制由 ./scripts/build-board.sh 在板卡原生编译并部署到
# /root/work/h100/bin/(交叉编译已移除),本脚本不再推送二进制。
#
# 用法:
# ./scripts/deploy.sh setup-ssh 一次性:生成 ed25519 key + ssh-copy-id
# ./scripts/deploy.sh 默认:推送二进制 + mission + 装 4 个
# ./scripts/deploy.sh 默认:推送 mission + 装 5 个
# systemd 服务 + daemon-reload(不启动)
# ./scripts/deploy.sh --start 部署后按依赖顺序启动服务
# ./scripts/deploy.sh status 板卡 4 个服务状态
# ./scripts/deploy.sh stop 停止 4 个服务
# ./scripts/deploy.sh status 板卡各服务状态
# ./scripts/deploy.sh stop 停止各服务
# ./scripts/deploy.sh help 显示帮助
#
# 参数:
@@ -24,11 +26,12 @@
# --ccu-port <n> 覆盖 mission 中 pPowerManger 的 CCU 端口 (ccuport)
#
# 板卡布局:
# /root/work/h100/bin/ aarch64 二进制
# /root/work/h100/bin/ 二进制(由 build-board.sh 板卡编译产出)
# /root/work/h100/missions/ 板卡 mission h100.moos
# /root/work/h100/data/ 数据库存储目录 (power_data.db 等)
# /etc/systemd/system/ moosdb.service / pPowerManger.service /
# pPowerMangerHost.service / pCCU.service
# pPowerMangerHost.service / pCCU.service /
# pCanBridge.service
#=======================================================================
set -uo pipefail
@@ -42,7 +45,7 @@ DO_START=0
CCU_HOST=""
CCU_PORT=""
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge)
info() { printf '\033[1;36m[deploy]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; }
@@ -50,7 +53,7 @@ warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,31p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,35p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
@@ -99,22 +102,6 @@ cmd_setup_ssh() {
#-------------------------------------------------------------------
# 前置检查
#-------------------------------------------------------------------
check_local() {
local missing=0
for b in pPowerManger pPowerMangerHost pCCU; do
if [ ! -f "${PROJECT_ROOT}/bin/arm64/${b}" ]; then
err "缺少本地 aarch64 二进制: bin/arm64/${b}(先运行 ./scripts/build-arm64.sh)"; missing=1
elif ! file "${PROJECT_ROOT}/bin/arm64/${b}" | grep -q "aarch64"; then
err "bin/arm64/${b} 不是 aarch64 二进制"; missing=1
fi
done
if [ ! -f "${PROJECT_ROOT}/missions/h100.moos" ]; then
err "缺少板卡 mission: missions/h100.moos"; missing=1
fi
[ "${missing}" -eq 0 ] || return 1
ok "本地产物校验通过 (aarch64 + mission)"
}
check_ssh() {
if ! ${SSH} "${SSH_TARGET}" 'true' 2>/dev/null; then
err "无法免密 SSH 到 ${SSH_TARGET}"
@@ -193,19 +180,13 @@ upsert_moos_param() {
# 命令: deploy
#-------------------------------------------------------------------
cmd_deploy() {
check_local || return 1
check_ssh || return 1
[ -f "${PROJECT_ROOT}/missions/h100.moos" ] || { err "缺少板卡 mission: missions/h100.moos"; return 1; }
# 1. 推送二进制 + mission
# 1. 创建板卡目录 + 推送 mission
info "创建板卡目录 ${BOARD_DIR}/{bin,missions,data} ..."
${SSH} "${SSH_TARGET}" "mkdir -p ${BOARD_DIR}/bin ${BOARD_DIR}/missions ${BOARD_DIR}/data" || { err "创建目录失败"; return 1; }
info "推送 aarch64 二进制 ..."
${SCP} -q "${PROJECT_ROOT}/bin/arm64/pPowerManger" \
"${PROJECT_ROOT}/bin/arm64/pPowerMangerHost" \
"${PROJECT_ROOT}/bin/arm64/pCCU" \
"${SSH_TARGET}:${BOARD_DIR}/bin/" || { err "推送二进制失败"; return 1; }
info "推送 mission h100.moos ..."
local mission_src="${PROJECT_ROOT}/missions/h100.moos"
local mission_push="${mission_src}" tmp_moos=""
@@ -221,8 +202,7 @@ cmd_deploy() {
"${SSH_TARGET}:${BOARD_DIR}/missions/" || { err "推送 mission 失败"; [ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"; return 1; }
[ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"
${SSH} "${SSH_TARGET}" "chmod +x ${BOARD_DIR}/bin/pPowerManger ${BOARD_DIR}/bin/pPowerMangerHost ${BOARD_DIR}/bin/pCCU"
ok "二进制与 mission 已推送"
ok "mission 已推送(二进制由 build-board.sh 在板卡编译部署)"
# 2. 生成并安装 4 个 systemd unit
local unit
@@ -248,7 +228,7 @@ cmd_deploy() {
else
echo ""
info "部署完成。手动启动(或加 --start):"
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU"
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU pCanBridge"
echo " 浏览器: http://${HOST}:18080 (上位机模拟)"
echo " http://${HOST}:8090 (pPowerManger)"
echo " http://${HOST}:8080 (pCCU)"
@@ -259,9 +239,9 @@ cmd_deploy() {
# 命令: start / status / stop
#-------------------------------------------------------------------
cmd_start() {
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU) ..."
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU / pCanBridge) ..."
${SSH} "${SSH_TARGET}" "systemctl start moosdb && sleep 1 && \
systemctl start pPowerManger pPowerMangerHost pCCU" || { err "启动失败"; return 1; }
systemctl start pPowerManger pPowerMangerHost pCCU pCanBridge" || { err "启动失败"; return 1; }
ok "服务已启动"
cmd_status
}
@@ -271,7 +251,7 @@ cmd_status() {
}
cmd_stop() {
${SSH} "${SSH_TARGET}" "systemctl stop pPowerMangerHost pPowerManger pCCU moosdb" && \
${SSH} "${SSH_TARGET}" "systemctl stop pCanBridge pPowerMangerHost pPowerManger pCCU moosdb" && \
ok "服务已停止" || err "停止服务失败"
}
+14 -6
View File
@@ -5,8 +5,10 @@
# 默认抓取板卡运行目录 /root/work/h100/data/ 下的:
# power_data.db (+ -wal/-shm) 主机功率数据库
# feedback_data.db 反馈落盘数据库
# pPowerManger.log 主机运行日志
# 可选 --journal 一并导出四个 systemd 服务的 journal 日志。
# pccu_data.db (+ -wal/-shm) pCCU 帧日志数据库
# pCanBridge_data.db (+ -wal/-shm) pCanBridge CAN 帧数据库
# pPowerManger.log / pCCU.log 运行日志
# 可选 --journal 一并导出各 systemd 服务的 journal 日志。
#
# 用法:
# ./scripts/fetch-data.sh # 下载 db + 日志到本地 data/board-<时间>/
@@ -18,7 +20,7 @@
# --user <user> 登录用户(默认 root)
# --board-dir <dir> 板卡部署根目录(默认 /root/work/h100)
# --out-dir <dir> 本地保存目录(默认 data/board-<时间>)
# --journal 额外导出 4 个服务的 systemd journal 日志
# --journal 额外导出各服务的 systemd journal 日志
# -h, --help 显示帮助
#
# 前置: 板卡已配免密 SSH(见 scripts/deploy.sh setup-ssh)。
@@ -35,10 +37,16 @@ BOARD_DIR="/root/work/h100"
OUT_DIR=""
DO_JOURNAL=0
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge)
# 板卡 data 目录下需要下载的文件(含 SQLite WAL/SHM 以保证数据完整)
DATA_FILES=(power_data.db power_data.db-wal power_data.db-shm feedback_data.db pPowerManger.log pccu_data.db pCCU.log)
DATA_FILES=(
power_data.db power_data.db-wal power_data.db-shm
feedback_data.db
pccu_data.db pccu_data.db-wal pccu_data.db-shm
pCanBridge_data.db pCanBridge_data.db-wal pCanBridge_data.db-shm
pPowerManger.log pCCU.log
)
info() { printf '\033[1;36m[fetch-data]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[fetch-data]\033[0m %s\n' "$*"; }
@@ -46,7 +54,7 @@ warn() { printf '\033[1;33m[fetch-data]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[fetch-data]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,28p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
+1
View File
@@ -21,6 +21,7 @@ ADD_SUBDIRECTORY(pPowerManger)
add_subdirectory(pPMtest)
add_subdirectory(pPowerMangerHost)
add_subdirectory(pCCU)
add_subdirectory(pCanBridge)
##############################################################################
# END of CMakeLists.txt
##############################################################################
+80 -5
View File
@@ -2,7 +2,9 @@
#include "MBUtils.h"
#include "../pPowerManger/logc/loguru.hpp"
#include "protocol/Frame.h"
#include "protocol/CanBms.h"
#include <chrono>
#include <cstdio>
using namespace ccu;
using namespace std;
@@ -14,9 +16,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_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; }
if (m_db) { delete m_db; m_db = 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; }
}
@@ -96,11 +99,18 @@ bool CCU::OnStartUp() {
m_pmLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
handlePmMessage(msg, frame);
});
m_pmLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) {
m_pmLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
});
if (!m_pmLink->start())
LOG_F(ERROR, "PM link start failed");
// 锂电池:CAN 总线(BMS 协议),数据经 pCanBridge 以 CAN_0x* 消息透传,
// 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。
// 电源协调器(FC 联动 + 协调算法占位)
m_coord = new PowerCoordinator();
m_coord->setup(m_sysData, m_fcLink, m_pmLink);
// 快照构建器(链路就绪后创建)
m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, m_db);
@@ -118,7 +128,8 @@ bool CCU::OnStartUp() {
}
}
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld",
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);
return true;
@@ -137,7 +148,11 @@ bool CCU::OnConnectToServer() {
void CCU::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// pCCU 数据交换主要走 UDP;MOOSDB 仅登记运行状态变量
// 订阅 pCanBridge 透传的全部 CAN 帧(变量名 CAN_0x%08X,二进制数据域)。
// 注意:MOOS 通配订阅必须用三参数重载(变量模式 + 来源模式),
// 单参数 Register("CAN_0x*") 不会做通配匹配。
Register("CAN_0x*", "*", 0);
// 运行状态变量
Register("CCU_FC_LINK_STATE", 0);
Register("CCU_PM_LINK_STATE", 0);
}
@@ -150,11 +165,57 @@ bool CCU::OnNewMail(MOOSMSG_LIST &NewMail) {
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
// 预留:未来如需通过 MOOSDB 下发指令可在此处理
// pCanBridge 透传的 CAN 帧(变量名前缀 CAN_0x)
if (msg.m_sKey.rfind("CAN_0x", 0) == 0) {
handleCanMessage(msg);
continue;
}
}
return true;
}
//---------------------------------------------------------
// handleCanMessage:解码 pCanBridge 透传的 CAN 帧
//
// 消息格式(pCanBridge 约定):
// m_sKey = "CAN_0x%08X"(CAN ID,扩展帧)
// m_sVal = 二进制数据域(dlc 字节)
// m_dfVal2 = 原始帧信息字节(bit7 FF / bit6 RTR / bit3~0 DLC)
void CCU::handleCanMessage(CMOOSMsg& msg) {
if (!m_sysData) return;
// 从变量名解析 CAN ID(跳过前缀 "CAN_0x" 共 6 字符)
uint32_t canId = 0;
if (std::sscanf(msg.m_sKey.c_str() + 6, "%x", &canId) != 1) return;
m_sysData->incCanFrameCount();
// 远程帧无数据域,直接跳过
if (msg.IsBinary() && msg.GetBinaryDataSize() > 0) {
unsigned int n = msg.GetBinaryDataSize();
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);
// 节流日志: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);
}
}
}
}
//---------------------------------------------------------
// Iterate:周期任务(AppTick 次/秒)
@@ -169,6 +230,10 @@ bool CCU::Iterate() {
m_lastStatusTx = now;
}
// 电源协调器:协调算法占位
if (m_coord)
m_coord->tick(now);
// 周期(1Hz)推送网页快照
if (m_web) {
static double lastWebPush = 0;
@@ -234,6 +299,13 @@ void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
m_sysData->updateFcControl(f);
m_fcLink->sendMessage(0x0001, &f);
LOG_F(INFO, "[FC] forward control cmd=%d power=%d", f.cmd, f.outputPower);
// 锂电池启停/功率指令:BMS CAN 协议未定义控制报文,暂不支持
if (c.insBatCmd != 0 || c.dynBatCmd != 0 || c.dynBatPower != 0) {
LOG_F(WARNING, "[PM] battery cmd ignored (insBat=0x%02X dynBat=0x%02X dynPwr=%u): "
"BMS CAN 协议未定义控制报文",
c.insBatCmd, c.dynBatCmd, c.dynBatPower);
}
} else {
LOG_F(ERROR, "[PM] control decode failed");
}
@@ -399,6 +471,9 @@ bool CCU::buildReport() {
<< " err:" << m_pmLink->errorCount() << "\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";
}
+11
View File
@@ -7,6 +7,7 @@
#include "comm/PmLinkManager.h"
#include "core/SystemData.h"
#include "core/SnapshotBuilder.h"
#include "core/PowerCoordinator.h"
#include "store/DbStore.h"
#include "web/WebServer.h"
@@ -19,6 +20,9 @@ namespace ccu {
// 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger
// 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池
// 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003)
// 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文
// (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照
// (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用)
// 收发全部帧落库 SQLite;网页周期推送 JSON 快照。
//============================================================================
@@ -40,6 +44,9 @@ private:
void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame);
void handlePmMessage(Message* msg, const std::vector<uint8_t>& frame);
// MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布)
void handleCanMessage(CMOOSMsg& msg);
// 整合并发送 PM 状态报文
void sendPmStatus();
@@ -61,6 +68,9 @@ private:
int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开
bool m_webEnable = true;
// CAN BMS 日志节流(避免 60帧/s 刷爆日志)
double m_lastBmsLog = 0;
// 心跳
uint8_t m_pmHeartbeat = 0;
double m_lastStatusTx = 0;
@@ -72,6 +82,7 @@ private:
DbStore* m_db = nullptr;
SnapshotBuilder* m_snap = nullptr;
WebServer* m_web = nullptr;
PowerCoordinator* m_coord = nullptr;
};
} // namespace ccu
+7 -3
View File
@@ -17,8 +17,9 @@ void showSynopsis() {
blk(" The pCCU application is the composite controller (CCU) ");
blk(" gateway between the fuel cell (FC) system and pPowerManger. ");
blk(" It receives FC status, integrates & forwards to pPowerManger,");
blk(" forwards pPowerManger commands to FC, stores data in SQLite ");
blk(" and provides a web monitoring page. ");
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(" ");
}
@@ -67,6 +68,7 @@ void showExampleConfigAndExit() {
blk(" pm_local_port = 7000 // PM 指令接收端口 ");
blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP ");
blk(" pm_remote_port = 5001 // 控制主机接收端口 ");
blk(" // 锂电池 BMS 走 CAN 总线(经 pCanBridge 的 CAN_0x* 透传) ");
blk(" dbpath = pccu_data.db // 数据库路径 ");
blk(" logpath = pCCU.log // 日志路径 ");
blk(" web_port = 8080 // 网页端口(避开8090/18080) ");
@@ -86,7 +88,9 @@ void showInterfaceAndExit() {
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" (数据交换主要通过 UDP 链路完成,MOOSDB 交互有限) ");
blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(锂电池 BMS) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
+3
View File
@@ -5,3 +5,6 @@
5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置
6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介
7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署
8.接收动力/仪表锂电池的消息,协议见docs/锂电池协议20230324.docx;动力/仪表锂电池的ip和端口均可通过.moos文件配置
9.锂电池需要额外操作(自检/上下电/电池切换/功率设定),pCCU根据操作按协议时序下发指令(0x0000自检/0x0001控制,1s心跳)
10.pCCU按自身算法进行锂电池与燃料电池的协调操作,算法入口为core/PowerCoordinator.cpp的coordinationTick()(当前为占位,待完善)
+3
View File
@@ -24,6 +24,7 @@ SET(CCU_PROTOCOL_SRC
protocol/MessageRegistry.cpp
protocol/FcProtocol.cpp
protocol/PmProtocol.cpp
protocol/CanBms.cpp
)
SET(CCU_COMM_SRC
@@ -33,6 +34,8 @@ SET(CCU_COMM_SRC
SET(CCU_CORE_SRC
core/SnapshotBuilder.cpp
core/PowerCoordinator.cpp
core/CoordFsm.cpp
)
SET(CCU_STORE_SRC
+103
View File
@@ -0,0 +1,103 @@
#include "CoordFsm.hpp"
#include "SystemData.h"
#include "loguru.hpp"
namespace ccu {
// 静态上下文(接线时由 CoordFsm::init 注入)
PowerCoordinator* CoordFsm::m_coord = nullptr;
SystemData* CoordFsm::m_sys = nullptr;
//============================================================================
// CoordNormalState 正常运行(初始状态)
//============================================================================
void CoordNormalState::entry() {
LOG_F(INFO, "[CoordFsm] 进入状态: CoordNormalState");
}
void CoordNormalState::exit() {
LOG_F(INFO, "[CoordFsm] 离开状态: CoordNormalState");
}
void CoordNormalState::react(TickEvent const &e) {
// TODO(策略填写):周期决策入口(AppTick 频率,默认 4Hz)。
//
// 读取最新状态:
// sys()->fcStatus() 燃料电池状态
// sys()->bmsStatus() 锂电池 BMS 状态(总电压/SOC/电流/故障码,CAN)
// sys()->pmControl() 主机最新操控指令
//
// 下发操作:
// coord()->sendFcControl(FcControlValue) 直接控燃料电池
(void)e;
}
void CoordNormalState::react(PmControlEvent const &e) {
// TODO(策略填写):响应主机操控指令(边沿触发,指令到达一次触发一次)。
LOG_F(INFO, "[CoordFsm][Normal] PmControlEvent: mode=%d cmd=%d power=%d",
e.cmd.mode, e.cmd.cmd, e.cmd.outputPower);
}
void CoordNormalState::react(StepDoneEvent const &e) {
// TODO(策略填写):单步协调操作完成/超时的处理。
// e.confirmed==false 表示超时未确认;
// e.status.faultCode / statusBits 可判 BMS 故障。
LOG_F(INFO, "[CoordFsm][Normal] StepDoneEvent: confirmed=%d soc=%.1f%% u=%.1fV",
e.confirmed, e.status.soc, e.status.totalVoltage);
}
//============================================================================
// CoordBusyState 协调操作进行中
//============================================================================
void CoordBusyState::entry() {
LOG_F(INFO, "[CoordFsm] 进入状态: CoordBusyState");
}
void CoordBusyState::exit() {
LOG_F(INFO, "[CoordFsm] 离开状态: CoordBusyState");
}
void CoordBusyState::react(TickEvent const &e) {
// TODO(策略填写):操作进行中的周期监视(进度/超时统计、是否需要中止)。
// 状态可通过 sys()->bmsStatus() / sys()->fcStatus() 获取。
(void)e;
}
void CoordBusyState::react(StepDoneEvent const &e) {
// TODO(策略填写):操作步骤完成推进。
// 全部步骤完成后返回正常运行态;步骤失败可中止并进入故障态:
// if (!e.confirmed) transit<CoordFaultState>();
// else transit<CoordNormalState>();
LOG_F(INFO, "[CoordFsm][Busy] StepDoneEvent: confirmed=%d", e.confirmed);
}
//============================================================================
// CoordFaultState 故障处理
//============================================================================
void CoordFaultState::entry() {
LOG_F(INFO, "[CoordFsm] 进入状态: CoordFaultState");
}
void CoordFaultState::exit() {
LOG_F(INFO, "[CoordFsm] 离开状态: CoordFaultState");
}
void CoordFaultState::react(TickEvent const &e) {
// TODO(策略填写):故障处理与恢复判断(周期检查故障是否消除,
// 消除后 transit<CoordNormalState>() 回正常运行态)。
// 可用:sys()->fcStatus().fc_fault_level、sys()->bmsStatus().faultCode 等。
(void)e;
}
void CoordFaultState::react(StepDoneEvent const &e) {
// TODO(策略填写):故障态下操作步骤结果处理(如降级恢复的确认)。
(void)e;
}
} // namespace ccu
// 初始状态声明(TinyFSM 宏要求全局作用域 + 命名空间限定名)
FSM_INITIAL_STATE(ccu::CoordFsm, ccu::CoordNormalState)
+137
View File
@@ -0,0 +1,137 @@
#ifndef PCCU_COORD_FSM_HPP
#define PCCU_COORD_FSM_HPP
#include "../fsm/tinyfsm.hpp"
#include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/CanBms.h"
#include "PowerCoordinator.h"
namespace ccu {
class SystemData;
//============================================================================
// CoordFsm:pCCU 电源协调状态机骨架(TinyFSM,事件驱动)。
//
// 【当前状态:框架已参与编译,未接入运行流程(惰性)】
// - 无任何代码调用 CoordFsm::start() / CoordFsm::dispatch(),
// 运行行为与接入前完全一致;
// - 各状态 react() 均为 TODO 空实现,由开发者填写协调策略。
//
// 【设计约定】
// - 边沿事件走 dispatch:指令到达(PmControlEvent)、操作步骤完成(StepDoneEvent)、
// 周期节拍(TickEvent);
// - 电平状态不进事件:最新 FC/BMS 状态由状态机在 TickEvent 中直接读取
// SystemData 快照,避免周期状态报文淹没事件队列;
// - TinyFSM 非线程安全:dispatch 必须收敛到单线程(MOOS 主循环线程),
// 接收线程产生的事件一律先排队,由 tick() 在主线程统一 drain 后
// 逐个 dispatch。
//
// 【接线指南:将来启用时按以下步骤操作】
//
// 1) 事件桥(可在 PowerCoordinator 或独立 EventBus 中实现):
// a. 线程安全事件队列:pushEvent(std::function<void()>)
// b. tick() 末尾 drain 队列并逐个执行(单线程 dispatch)
//
// 2) CCU::OnStartUp:
// CoordFsm::init(m_coord, m_sysData);
// CoordFsm::start();
//
// 3) CCU::handlePmMessage 的 0x0001 分支(decode 成功后):
// CoordFsm::dispatch(PmControlEvent(c));
// (c 为已解析的 PmControlValue,按值捕获)
//
// 4) 验证:编译运行后日志出现 "[CoordFsm] 进入状态: CoordNormalState" 即接线成功。
//
// 【状态说明】
// CoordNormalState 正常运行(初始状态):周期功率分配/阈值判断、响应主机指令
// CoordBusyState 协调操作进行中:等待 StepDoneEvent 推进,完成后返回 Normal
// CoordFaultState 故障处理:告警抑制/降级运行/恢复判断
//============================================================================
//---- 事件定义(边沿触发) ------------------------------------------------
// 周期节拍:由 coordinationTick 转发(AppTick 频率,默认 4Hz)
struct TickEvent : tinyfsm::Event {
double now; // MOOSTime
TickEvent() : now(0) {}
explicit TickEvent(double t) : now(t) {}
};
// 主机操控指令到达:PM 0x0001 每次成功解析触发一次
struct PmControlEvent : tinyfsm::Event {
PmControlValue cmd;
PmControlEvent() {}
explicit PmControlEvent(const PmControlValue& c) : cmd(c) {}
};
// 协调操作步骤完成/超时:由协调动作引擎产生(预留)。
// status 携带完成时刻的 BMS 状态快照(SOC/电压/电流/故障码,可判故障)
struct StepDoneEvent : tinyfsm::Event {
bool confirmed; // true=确认完成;false=超时未确认
BmsStatusValue status; // 完成时刻 BMS 状态快照
StepDoneEvent() : confirmed(false) {}
StepDoneEvent(bool ok, const BmsStatusValue& s) : confirmed(ok), status(s) {}
};
//---- 状态机 ---------------------------------------------------------------
class CoordFsm : public tinyfsm::Fsm<CoordFsm> {
public:
// 启用时注入上下文(见接线指南)
static void init(PowerCoordinator* coord, SystemData* sys) {
m_coord = coord;
m_sys = sys;
}
static PowerCoordinator* coord() { return m_coord; }
static SystemData* sys() { return m_sys; }
// 事件处理默认实现:状态类按需覆盖;未覆盖的事件静默忽略
virtual void react(tinyfsm::Event const &) {}
virtual void react(TickEvent const &) {}
virtual void react(PmControlEvent const &) {}
virtual void react(StepDoneEvent const &) {}
// 状态进入/退出钩子(状态类覆盖以打日志/做进出动作)
virtual void entry() {}
virtual void exit() {}
private:
static PowerCoordinator* m_coord;
static SystemData* m_sys;
};
//---- 状态类 ---------------------------------------------------------------
// 正常运行(初始状态)
class CoordNormalState : public CoordFsm {
public:
void entry() override;
void exit() override;
void react(TickEvent const &e) override;
void react(PmControlEvent const &e) override;
void react(StepDoneEvent const &e) override;
};
// 协调操作进行中(提交多步协调动作后进入)
class CoordBusyState : public CoordFsm {
public:
void entry() override;
void exit() override;
void react(TickEvent const &e) override;
void react(StepDoneEvent const &e) override;
};
// 故障处理
class CoordFaultState : public CoordFsm {
public:
void entry() override;
void exit() override;
void react(TickEvent const &e) override;
void react(StepDoneEvent const &e) override;
};
} // namespace ccu
#endif // PCCU_COORD_FSM_HPP
+53
View File
@@ -0,0 +1,53 @@
#include "PowerCoordinator.h"
#include "SystemData.h"
#include "../comm/FcLinkManager.h"
#include "../comm/PmLinkManager.h"
namespace ccu {
PowerCoordinator::PowerCoordinator() {}
void PowerCoordinator::setup(SystemData* sys,
FcLinkManager* fc,
PmLinkManager* pm) {
m_sys = sys;
m_fc = fc;
m_pm = pm;
}
void PowerCoordinator::tick(double now) {
coordinationTick(now);
}
bool PowerCoordinator::sendFcControl(const FcControlValue& fcCmd) {
if (!m_fc || !m_fc->isRunning()) return false;
return m_fc->sendMessage(0x0001, &fcCmd);
}
//---------------------------------------------------------
// 协调算法占位
//
// 【占位说明】
// 本函数为锂电池(BMS/CAN)与燃料电池协调策略的入口,
// 由 CCU::Iterate 周期调用。
//
// 可用输入(通过 m_sys 读取最新状态):
// - m_sys->fcStatus() : 燃料电池最新状态
// - m_sys->bmsStatus() : 锂电池 BMS 最新状态(总电压/SOC/电流/故障码,
// CAN 经 pCanBridge 透传,protocol/CanBms.h)
// - m_sys->pmControl() : 控制主机最新操控指令
//
// 可用操作:
// - sendFcControl(FcControlValue) : 直接下发燃料电池控制指令
//
// 说明:BMS CAN 协议未定义远程控制报文(启停/接触器/功率设定),
// 电池侧控制暂不可用;协调策略围绕 FC 与 BMS 状态联动设计。
//
// TODO(开发者完善):在此实现具体的功率分配、燃料电池与
// 锂电池联合调度等协调策略。当前为空实现。
//---------------------------------------------------------
void PowerCoordinator::coordinationTick(double now) {
(void)now; // 占位:暂不执行任何协调动作
}
} // namespace ccu
+54
View File
@@ -0,0 +1,54 @@
#ifndef PCCU_POWER_COORDINATOR_H
#define PCCU_POWER_COORDINATOR_H
#include <cstdint>
#include "../protocol/FcProtocol.h"
namespace ccu {
class SystemData;
class FcLinkManager;
class PmLinkManager;
//============================================================================
// PowerCoordinator:电源协调器。
//
// 职责:
// 1. FC 控制指令下发(供协调算法联动使用)。
// 2. 协调算法占位:coordinationTick() 为锂电池(BMS/CAN)与燃料电池
// 协调策略的占位入口,由后续完善(当前为空实现)。
//
// 说明:锂电池通信已切换为 CAN 总线(BMS 协议,经 pCanBridge 透传,
// 见 protocol/CanBms.h)。BMS 报文仅含状态字段(电压/电流/SOC/故障码),
// 未定义远程控制报文,因此原 UDP 电池指令链路(自检/接触器/功率设定)
// 已移除;FC 控制仍走 UDP。
//============================================================================
class PowerCoordinator {
public:
PowerCoordinator();
// 注入依赖(均在 CCU::OnStartUp 中创建完成后调用)
void setup(SystemData* sys,
FcLinkManager* fc,
PmLinkManager* pm);
// 周期驱动:由 CCU::Iterate 周期调用(now = MOOSTime())
void tick(double now);
// 直接向燃料电池下发控制指令(供协调算法联动使用)
bool sendFcControl(const FcControlValue& fcCmd);
//---------------- 协调算法占位 ----------------
// TODO(算法完善):锂电池(BMS/CAN)与燃料电池协调策略入口。
void coordinationTick(double now);
private:
SystemData* m_sys = nullptr;
FcLinkManager* m_fc = nullptr;
PmLinkManager* m_pm = nullptr;
};
} // namespace ccu
#endif // PCCU_POWER_COORDINATOR_H
+27 -2
View File
@@ -1,5 +1,6 @@
#include "SnapshotBuilder.h"
#include "../store/DbStore.h"
#include "../protocol/CanBms.h"
#include "json/json.h"
#include <sstream>
#include <cstdio>
@@ -13,6 +14,7 @@ namespace {
void put(JsonVal& j, const char* k, uint8_t v) { j[k] = JsonVal(v); }
void put(JsonVal& j, const char* k, uint16_t v){ j[k] = JsonVal(v); }
void put(JsonVal& j, const char* k, uint32_t v){ j[k] = JsonVal(static_cast<Json::UInt>(v)); }
void putI(JsonVal& j, const char* k, int16_t v) { j[k] = JsonVal(static_cast<int>(v)); }
//--------------------------------------------------------------------------
// 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。
@@ -40,7 +42,8 @@ std::string describeMessageName(const std::string& link, uint16_t id) {
}
std::string describeDirection(const std::string& link, int direction) {
const std::string peer = (link == "fc") ? "FC" : "PM";
std::string peer = "PM";
if (link == "fc") peer = "FC";
return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU");
}
@@ -206,6 +209,22 @@ JsonVal pmFbJson(const PmParamSetFbValue& v) {
return j;
}
// 锂电池 BMS 状态(CAN,docs/BMS_协议字段定义.xlsx)-> JSON
JsonVal bmsStatusJson(const BmsStatusValue& v) {
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);
return j;
}
// PM 状态报文(整合后发送给控制主机)-> JSON(关键字段)
JsonVal pmStatusJson(const PmStatusValue& v) {
JsonVal j(Json::objectValue);
@@ -320,7 +339,8 @@ JsonVal linkJson(const LinkManager* lm) {
} // namespace
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db)
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
DbStore* db)
: m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {}
std::string SnapshotBuilder::build() const {
@@ -333,10 +353,14 @@ 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());
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()));
} else {
root["fc"] = JsonVal(Json::objectValue);
root["pmCmd"] = JsonVal(Json::objectValue);
@@ -344,6 +368,7 @@ std::string SnapshotBuilder::build() const {
root["pmParam"] = JsonVal(Json::objectValue);
root["pmFb"] = JsonVal(Json::objectValue);
root["pmStatus"] = JsonVal(Json::objectValue);
root["bms"] = JsonVal(Json::objectValue);
}
JsonVal links(Json::objectValue);
+2 -2
View File
@@ -12,8 +12,8 @@ class DbStore;
//============================================================================
// SnapshotBuilder:构建网页推送的 JSON 快照。
//
// 组合 SystemData(最新状态)、两条链路统计、最近原始帧日志,
// 序列化为 JSON 字符串(jsoncpp)。串行化逻辑集中在此处,便于维护。
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN)、两条 UDP 链路统计、
// 最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
//============================================================================
class SnapshotBuilder {
+32
View File
@@ -5,6 +5,7 @@
#include <atomic>
#include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/CanBms.h"
namespace ccu {
@@ -12,6 +13,7 @@ namespace ccu {
// SystemData:跨线程共享的最新状态快照。
//
// - 接收线程(FC 链路)写 FcStatus 快照
// - MOOS 主线程(OnNewMail)写 BMS 快照(CAN 经 pCanBridge 透传)
// - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送
// - 网页线程读快照展示
// 通过互斥锁保护读写。
@@ -99,6 +101,33 @@ public:
return m_pmParamFb;
}
//-------- 锂电池 BMS(CAN 总线,经 pCanBridge 透传) --------
// 更新最新 BMS 状态(每帧部分更新,由调用方先取出快照再合并)
void updateBmsStatus(const BmsStatusValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_bmsStatus = v;
m_bmsStatusCount++;
}
BmsStatusValue bmsStatus() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_bmsStatus;
}
unsigned long bmsStatusCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_bmsStatusCount;
}
// CAN 链路收帧计数(含非 BMS 报文,用于链路状态展示)
void incCanFrameCount() {
std::lock_guard<std::mutex> lock(m_mutex);
m_canFrameCount++;
}
unsigned long canFrameCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_canFrameCount;
}
private:
mutable std::mutex m_mutex;
FcStatusValue m_fcStatus;
@@ -107,10 +136,13 @@ private:
FcControlValue m_fcControl;
PmStatusValue m_pmStatus;
PmParamSetFbValue m_pmParamFb;
BmsStatusValue m_bmsStatus;
unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0;
unsigned long m_pmStatusCount = 0;
unsigned long m_bmsStatusCount = 0;
unsigned long m_canFrameCount = 0;
};
} // namespace ccu
+251
View File
@@ -0,0 +1,251 @@
/*
* TinyFSM - Tiny Finite State Machine Processor
*
* Copyright (c) 2012-2022 Axel Burri
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/* ---------------------------------------------------------------------
* Version: 0.3.3
*
* API documentation: see "../doc/50-API.md"
*
* The official TinyFSM website is located at:
* https://digint.ch/tinyfsm/
*
* Author:
* Axel Burri <axel@tty0.ch>
* ---------------------------------------------------------------------
*/
#ifndef TINYFSM_HPP_INCLUDED
#define TINYFSM_HPP_INCLUDED
#ifndef TINYFSM_NOSTDLIB
#include <type_traits>
#endif
// #include <iostream>
// #define DBG(str) do { std::cerr << str << std::endl; } while( false )
// DBG("*** dbg_example *** " << __PRETTY_FUNCTION__);
namespace tinyfsm
{
// --------------------------------------------------------------------------
struct Event { };
// --------------------------------------------------------------------------
#ifdef TINYFSM_NOSTDLIB
// remove dependency on standard library (silent fail!).
// useful in conjunction with -nostdlib option, e.g. if your compiler
// does not provide a standard library.
// NOTE: this silently disables all static_assert() calls below!
template<typename F, typename S>
struct is_same_fsm { static constexpr bool value = true; };
#else
// check if both fsm and state class share same fsmtype
template<typename F, typename S>
struct is_same_fsm : std::is_same< typename F::fsmtype, typename S::fsmtype > { };
#endif
template<typename S>
struct _state_instance
{
using value_type = S;
using type = _state_instance<S>;
static S value;
};
template<typename S>
typename _state_instance<S>::value_type _state_instance<S>::value;
// --------------------------------------------------------------------------
template<typename F>
class Fsm
{
public:
using fsmtype = Fsm<F>;
using state_ptr_t = F *;
static state_ptr_t current_state_ptr;
// public, leaving ability to access state instance (e.g. on reset)
template<typename S>
static constexpr S & state(void) {
static_assert(is_same_fsm<F, S>::value, "accessing state of different state machine");
return _state_instance<S>::value;
}
template<typename S>
static constexpr bool is_in_state(void) {
static_assert(is_same_fsm<F, S>::value, "accessing state of different state machine");
return current_state_ptr == &_state_instance<S>::value;
}
/// state machine functions
public:
// explicitely specialized in FSM_INITIAL_STATE macro
static void set_initial_state();
static void reset() { };
static void enter() {
current_state_ptr->entry();
}
static void start() {
set_initial_state();
enter();
}
template<typename E>
static void dispatch(E const & event) {
current_state_ptr->react(event);
}
/// state transition functions
protected:
template<typename S>
void transit(void) {
static_assert(is_same_fsm<F, S>::value, "transit to different state machine");
current_state_ptr->exit();
current_state_ptr = &_state_instance<S>::value;
current_state_ptr->entry();
}
template<typename S, typename ActionFunction>
void transit(ActionFunction action_function) {
static_assert(is_same_fsm<F, S>::value, "transit to different state machine");
current_state_ptr->exit();
// NOTE: do not send events in action_function definisions.
action_function();
current_state_ptr = &_state_instance<S>::value;
current_state_ptr->entry();
}
template<typename S, typename ActionFunction, typename ConditionFunction>
void transit(ActionFunction action_function, ConditionFunction condition_function) {
if(condition_function()) {
transit<S>(action_function);
}
}
};
template<typename F>
typename Fsm<F>::state_ptr_t Fsm<F>::current_state_ptr;
// --------------------------------------------------------------------------
template<typename... FF>
struct FsmList;
template<> struct FsmList<> {
static void set_initial_state() { }
static void reset() { }
static void enter() { }
template<typename E>
static void dispatch(E const &) { }
};
template<typename F, typename... FF>
struct FsmList<F, FF...>
{
using fsmtype = Fsm<F>;
static void set_initial_state() {
fsmtype::set_initial_state();
FsmList<FF...>::set_initial_state();
}
static void reset() {
F::reset();
FsmList<FF...>::reset();
}
static void enter() {
fsmtype::enter();
FsmList<FF...>::enter();
}
static void start() {
set_initial_state();
enter();
}
template<typename E>
static void dispatch(E const & event) {
fsmtype::template dispatch<E>(event);
FsmList<FF...>::template dispatch<E>(event);
}
};
// --------------------------------------------------------------------------
template<typename... SS> struct StateList;
template<> struct StateList<> {
static void reset() { }
};
template<typename S, typename... SS>
struct StateList<S, SS...>
{
static void reset() {
_state_instance<S>::value = S();
StateList<SS...>::reset();
}
};
// --------------------------------------------------------------------------
template<typename F>
struct MooreMachine : tinyfsm::Fsm<F>
{
virtual void entry(void) { }; /* entry actions in some states */
void exit(void) { }; /* no exit actions */
};
template<typename F>
struct MealyMachine : tinyfsm::Fsm<F>
{
// input actions are modeled in react():
// - conditional dependent of event type or payload
// - transit<>(ActionFunction)
void entry(void) { }; /* no entry actions */
void exit(void) { }; /* no exit actions */
};
} /* namespace tinyfsm */
#define FSM_INITIAL_STATE(_FSM, _STATE) \
namespace tinyfsm { \
template<> void Fsm< _FSM >::set_initial_state(void) { \
current_state_ptr = &_state_instance< _STATE >::value; \
} \
}
#endif /* TINYFSM_HPP_INCLUDED */
+7 -3
View File
@@ -2,9 +2,9 @@
// pCCU 配置示例
//
// 链路拓扑:
// 燃料电池控制器(FC) 192.168.1.162:7000
// 控制主机(pPowerManger) 192.168.0.140:5001
// pCCU 本机监听:FC 状态端口 6000、PM 指令端口 7000
// 燃料电池控制器(FC) 192.168.1.162:7000(UDP)
// 控制主机(pPowerManger) 192.168.0.140:5001(UDP)
// 锂电池 BMS CAN 总线(经 pCanBridge 订阅 CAN_0x* 透传)
//============================================================================
ProcessConfig = pCCU
@@ -26,6 +26,10 @@ ProcessConfig = pCCU
pm_remote_ip = 192.168.0.140
pm_remote_port = 5001
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
// 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。
//======== 存储与日志 ========
// 数据库路径(SQLite)
dbpath = pccu_data.db
+57
View File
@@ -0,0 +1,57 @@
#include "CanBms.h"
namespace ccu {
namespace {
// 大端 u16
inline uint16_t rdU16BE(const uint8_t* p) {
return static_cast<uint16_t>((static_cast<uint16_t>(p[0]) << 8) | p[1]);
}
// 大端 16 位有符号整数(补码)
inline int16_t rdS16BE(const uint8_t* p) {
return static_cast<int16_t>(rdU16BE(p));
}
} // namespace
bool decodeCanBmsFrame(uint32_t canId, const uint8_t* data, int dlc, BmsStatusValue& v) {
if (!data || dlc <= 0) return false;
// 缩放用除法(而非乘 0.1/0.001),保证结果与十进制字面量严格一致。
switch (canId) {
case CAN_BMS_ID_STATUS: // 0x10010000:总电压 / 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];
}
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
break;
case CAN_BMS_ID_VOLT_CHK: // 0x10010006:总电压(校验)
if (dlc >= 6) {
v.totalVoltageChk = rdU16BE(data + 4) / 10.0; // 0.1V
}
break;
default:
return false;
}
return true;
}
} // namespace ccu
+63
View File
@@ -0,0 +1,63 @@
#ifndef PCCU_CAN_BMS_H
#define PCCU_CAN_BMS_H
#include <cstdint>
namespace ccu {
//============================================================================
// CAN BMS 协议(锂电池 BMS 报文,经 USBCAN-8E-U/CANET 上 CAN 总线)
// 依据:docs/BMS_协议字段定义.xlsx
//
// 数据来源: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
//============================================================================
// 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, // 总电压(校验)
};
// 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;
}
} // namespace ccu
#endif // PCCU_CAN_BMS_H
+43 -4
View File
@@ -74,6 +74,7 @@ th{color:var(--dim);font-weight:500;}
<button class="tab active" data-tab="fcStatus">FC状态反馈</button>
<button class="tab" data-tab="pmCmd">PM操控指令</button>
<button class="tab" data-tab="pmParam">PM参数设定</button>
<button class="tab" data-tab="bms">锂电池BMS(CAN)</button>
</div>
<div class="tab-group"><span class="tg-label">发送</span>
<button class="tab" data-tab="fcCmd">FC控制指令</button>
@@ -91,7 +92,7 @@ th{color:var(--dim);font-weight:500;}
const $ = id => document.getElementById(id);
const pagesEl = $('pages');
const statusEl = $('status');
const TABS = ['fcStatus','pmCmd','pmParam','fcCmd','pmStatus','pmFb','links','raw'];
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','raw'];
let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
@@ -126,6 +127,17 @@ function statusText(s){
return m[s] || ('未知('+hex(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; }
// 带解析的值:hex 码 + 括号解析
function parsed(v, text){ return hex(v)+' ('+text+')'; }
// 故障码高亮:0 显示绿色正常,非 0 显示红色
@@ -291,6 +303,8 @@ 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)+
'"></span>CAN 链路 (pCanBridge)</span>', 'CAN帧:'+s.canFrameCount+' BMS帧:'+s.bmsStatusCount);
h+=row('FC 状态接收计数', s.fcStatusCount);
h+=row('PM 指令接收计数', s.pmControlCount);
h+=row('FC 控制转发计数', s.fcControlCount);
@@ -311,9 +325,9 @@ function cmdCard(d){
h+=row('应急允许', parsed(d.emergencyAllow, emerAllowText(d.emergencyAllow)));
h+=row('潜深深度', d.depth+' m');
h+=row('补给/排放指令', parsed(d.supplyCmd, supplyCmdText(d.supplyCmd)));
h+=row('仪表锂电池启停', parsed(d.insBatCmd, batCmdText(d.insBatCmd)));
h+=row('动力锂电池启停', parsed(d.dynBatCmd, batCmdText(d.dynBatCmd)));
h+=row('动力锂电池功率配置', d.dynBatPower+' kW');
h+=row('仪表锂电池启停', parsed(d.insBatCmd, batCmdText(d.insBatCmd))+' <span style="color:var(--warn)">(BMS/CAN暂不支持,未转发)</span>');
h+=row('动力锂电池启停', parsed(d.dynBatCmd, batCmdText(d.dynBatCmd))+' <span style="color:var(--warn)">(BMS/CAN暂不支持,未转发)</span>');
h+=row('动力锂电池功率配置', d.dynBatPower+' kW <span style="color:var(--warn)">(未转发)</span>');
h+=row('通信心跳', d.heartbeat);
h+=row('主机状态', parsed(d.hostState, hostStateText(d.hostState)));
return card('指令内容', h);
@@ -359,6 +373,24 @@ function pmFbCard(d){
return card('设定结果', h);
}
//------------------ 锂电池 BMS(CAN) 卡片 ------------------
function bmsCard(d){
if(!d || !d.valid) 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);
}
function pmStatusCard(d){
if(!d) return '';
let h='';
@@ -450,6 +482,13 @@ 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 透传)
bms: '<div class="grid">'+bmsCard(snap.bms)+
'<div class="card"><h2>报文来源 (CAN ID)</h2>'+
row('0x10010000','总电压 / SOC / 故障码 / 状态位')+
row('0x10010005','最大电流限制 / 总电流 / 最高单体电压')+
row('0x10010006','总电压(校验)')+
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+'</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">'+
+43
View File
@@ -0,0 +1,43 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pCanBridge
# CAN(USBCAN-8E-U/CANET TCP) -> MOOSDB 透传桥 + SQLite 落库
#--------------------------------------------------------
if (${WIN32})
SET(SYSTEM_LIBS wsock32)
else (${WIN32})
SET(SYSTEM_LIBS m pthread)
endif (${WIN32})
# 复用仓库内 pPowerManger 的 sqlite3 amalgamation(相对路径引用,避免重复维护)
SET(PM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../pPowerManger)
SET(SHARED_SRC
${PM_DIR}/sqlit3/sqlite3.c
)
SET(SRC
${SHARED_SRC}
CanEndpoint.cpp
CanDbStore.cpp
CanBridge.cpp
CanBridge_Info.cpp
main.cpp
)
ADD_EXECUTABLE(pCanBridge ${SRC})
TARGET_INCLUDE_DIRECTORIES(pCanBridge PRIVATE
${PM_DIR}
${PM_DIR}/sqlit3
)
TARGET_LINK_LIBRARIES(pCanBridge
${MOOS_LIBRARIES}
apputil
mbutil
m
pthread
dl
${SYSTEM_LIBS}
)
+169
View File
@@ -0,0 +1,169 @@
#include "CanBridge.h"
#include "CanBridge_Info.h"
#include "MBUtils.h"
#include "MOOS/libMOOS/Comms/MOOSMsg.h"
#include <cstdio>
using namespace std;
namespace canbridge {
//---------------------------------------------------------
// Constructor / Destructor
CanBridge::CanBridge() {}
CanBridge::~CanBridge() {
if (m_endpoint) {
m_endpoint->stop();
delete m_endpoint;
m_endpoint = nullptr;
}
if (m_db) {
m_db->close();
delete m_db;
m_db = nullptr;
}
}
//---------------------------------------------------------
// OnStartUp:读取配置并启动 CAN 链路
bool CanBridge::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_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;
else handled = false;
if (!handled)
reportUnhandledConfigWarning(orig);
}
registerVariables();
// 数据库(CAN 帧落库)
m_db = new CanDbStore(m_dbPath);
if (!m_db->open()) {
reportRunWarning("CAN db open failed: " + m_dbPath + " (" + m_db->lastError() + ")");
delete m_db;
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);
}
cout << "pCanBridge started: " << m_canHost << ":" << m_canPort
<< " (" << m_channelName << "), db: " << m_dbPath << endl;
return true;
}
//---------------------------------------------------------
// OnConnectToServer
bool CanBridge::OnConnectToServer() {
registerVariables();
return true;
}
//---------------------------------------------------------
// registerVariables:纯发布应用,无需注册订阅变量
void CanBridge::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
}
//---------------------------------------------------------
// OnNewMail:当前无订阅指令,预留
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;
}
return true;
}
//---------------------------------------------------------
// Iterate:周期任务
bool CanBridge::Iterate() {
AppCastingMOOSApp::Iterate();
AppCastingMOOSApp::PostReport();
return true;
}
//---------------------------------------------------------
// handleFrame:CAN 帧 -> CMOOSMsg(二进制) -> MOOSDB
//
// 在 CanEndpoint 接收线程中调用;m_Comms::Post 线程安全。
void CanBridge::handleFrame(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);
char key[32];
std::snprintf(key, sizeof(key), "CAN_0x%08X", id);
// 二进制构造:m_cDataType = MOOS_BINARY_STRING
CMOOSMsg msg(MOOS_NOTIFY, key,
static_cast<unsigned int>(dlc),
const_cast<uint8_t*>(data));
msg.SetSourceAux(m_channelName); // 通道 -> m_sSrcAux
msg.SetDoubleAux(static_cast<double>(frameInfo)); // FF/RTR/DLC -> m_dfVal2
m_Comms.Post(msg);
++m_notifyCount;
}
//---------------------------------------------------------
// buildReport:AppCasting 报告
bool CanBridge::buildReport() {
m_msgs << "============================================" << "\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";
}
if (m_db) {
m_msgs << "数据库: " << m_dbPath
<< (m_db->isOpen() ? " (已打开)" : " (未打开)") << "\n";
m_msgs << "落库记录数: " << m_db->count() << "\n";
}
return true;
}
} // namespace canbridge
+62
View File
@@ -0,0 +1,62 @@
#ifndef PCANBRIDGE_CAN_BRIDGE_H
#define PCANBRIDGE_CAN_BRIDGE_H
#define UNIX
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "CanEndpoint.h"
#include "CanDbStore.h"
#include <atomic>
namespace canbridge {
//============================================================================
// CanBridge:CAN -> MOOSDB 透传桥(MOOS 应用外壳)。
//
// 数据流:
// USBCAN-8E-U (TCP Server, 默认 192.168.0.222:4001 = CAN0)
// -> 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)
// m_nID 不使用(MOOS 内部消息序号)
//
// 每帧同时落库 SQLite(can_frame 表,配置项 dbpath)。
//============================================================================
class CanBridge : public AppCastingMOOSApp {
public:
CanBridge();
~CanBridge();
protected:
bool OnNewMail(MOOSMSG_LIST &NewMail);
bool Iterate();
bool OnConnectToServer();
bool OnStartUp();
bool buildReport();
void registerVariables();
private:
// 收帧回调(CanEndpoint 接收线程调用)
void handleFrame(uint8_t frameInfo, uint32_t id, const uint8_t* data, int dlc);
// 配置
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)
// 组件
CanEndpoint* m_endpoint = nullptr;
CanDbStore* m_db = nullptr;
std::atomic<unsigned long> m_notifyCount{0};
};
} // namespace canbridge
#endif // PCANBRIDGE_CAN_BRIDGE_H
+106
View File
@@ -0,0 +1,106 @@
/****************************************************************/
/* NAME: CanBridge_Info */
/* FILE: CanBridge_Info.cpp */
/****************************************************************/
#include <cstdlib>
#include <iostream>
#include "CanBridge_Info.h"
#include "ColorParse.h"
#include "ReleaseInfo.h"
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(" 13-byte CANET wire format and transparently published as a ");
blk(" binary MOOS message. ");
blk(" ");
}
void showHelpAndExit() {
blk(" ");
blu("=============================================================== ");
blu("Usage: pCanBridge file.moos [OPTIONS] ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("Options: ");
mag(" --alias","=<ProcessName> ");
blk(" Launch pCanBridge with the given process name ");
blk(" rather than pCanBridge. ");
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 pCanBridge. ");
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("pCanBridge Example MOOS Configuration ");
blu("=============================================================== ");
blk(" ");
blk("ProcessConfig = pCanBridge ");
blk("{ ");
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(" ");
blk(" // 通道名(写入消息的 m_sSrcAux 辅助字段) ");
blk(" can_channel_name = CAN0 ");
blk(" ");
blk(" // CAN 帧 SQLite 落库路径(can_frame 表) ");
blk(" dbpath = pCanBridge_data.db ");
blk("} ");
blk(" ");
exit(0);
}
void showInterfaceAndExit() {
blk(" ");
blu("=============================================================== ");
blu("pCanBridge INTERFACE ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" (none: publish-only application) ");
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_dfVal2: 原始帧信息字节 byte0 ");
blk(" bit7=FF(1扩展/0标准) bit6=RTR(1远程/0数据) ");
blk(" bit3~0=DLC ");
blk(" ");
exit(0);
}
void showReleaseInfoAndExit() {
showReleaseInfo("pCanBridge", "gpl");
exit(0);
}
+15
View File
@@ -0,0 +1,15 @@
/****************************************************************/
/* NAME: CanBridge_Info */
/* FILE: CanBridge_Info.h */
/****************************************************************/
#ifndef PCANBRIDGE_INFO_HEADER
#define PCANBRIDGE_INFO_HEADER
void showSynopsis();
void showHelpAndExit();
void showExampleConfigAndExit();
void showInterfaceAndExit();
void showReleaseInfoAndExit();
#endif
+158
View File
@@ -0,0 +1,158 @@
#include "CanDbStore.h"
#include "sqlite3.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include <cstdio>
namespace canbridge {
namespace {
void toHex(const uint8_t* data, int len, std::string& out) {
static const char* hex = "0123456789ABCDEF";
out.clear();
out.reserve(static_cast<size_t>(len) * 2);
for (int i = 0; i < len; ++i) {
out += hex[(data[i] >> 4) & 0xF];
out += hex[data[i] & 0xF];
}
}
} // namespace
CanDbStore::CanDbStore(const std::string& dbPath) : m_dbPath(dbPath) {}
CanDbStore::~CanDbStore() {
close();
}
bool CanDbStore::exec(const char* sql) {
char* err = nullptr;
int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &err);
if (rc != SQLITE_OK) {
m_lastError = err ? err : "sqlite error";
sqlite3_free(err);
return false;
}
return true;
}
bool CanDbStore::open() {
if (m_db) return true;
if (sqlite3_open(m_dbPath.c_str(), &m_db) != SQLITE_OK) {
m_lastError = m_db ? sqlite3_errmsg(m_db) : "cannot open database";
sqlite3_close(m_db);
m_db = nullptr;
return false;
}
sqlite3_busy_timeout(m_db, 2000);
exec("PRAGMA journal_mode=WAL;");
exec("PRAGMA synchronous=NORMAL;");
// can_frame:收到的每个 CAN 帧一条记录
if (!exec(
"CREATE TABLE IF NOT EXISTS can_frame ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" time REAL NOT NULL," // unix 时间(含小数秒)
" channel TEXT NOT NULL," // 通道名(CAN0~CAN7)
" frame_info INTEGER NOT NULL," // 原始帧信息字节 byte0
" can_id INTEGER NOT NULL," // CAN ID
" dlc INTEGER NOT NULL," // 数据长度 0~8
" hex TEXT NOT NULL" // 数据十六进制
");")) return false;
exec("CREATE INDEX IF NOT EXISTS idx_can_frame_time ON can_frame(time);");
exec("CREATE INDEX IF NOT EXISTS idx_can_frame_id ON can_frame(can_id);");
return prepareInsert();
}
bool CanDbStore::prepareInsert() {
const char* sql =
"INSERT INTO can_frame (time, channel, frame_info, can_id, dlc, hex) "
"VALUES (?1, ?2, ?3, ?4, ?5, ?6);";
if (sqlite3_prepare_v2(m_db, sql, -1, &m_stmtInsert, nullptr) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
return false;
}
return true;
}
void CanDbStore::close() {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_stmtInsert) {
sqlite3_finalize(m_stmtInsert);
m_stmtInsert = nullptr;
}
if (m_db) {
sqlite3_close(m_db);
m_db = nullptr;
}
}
void CanDbStore::onFrame(const std::string& channel, uint8_t frameInfo,
uint32_t canId, const uint8_t* data, int dlc) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db || !m_stmtInsert) return;
std::string hex;
if (data && dlc > 0) toHex(data, dlc, hex);
sqlite3_reset(m_stmtInsert);
sqlite3_clear_bindings(m_stmtInsert);
sqlite3_bind_double(m_stmtInsert, 1, MOOSTime(false));
sqlite3_bind_text(m_stmtInsert, 2, channel.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(m_stmtInsert, 3, frameInfo);
sqlite3_bind_int64(m_stmtInsert, 4, static_cast<sqlite3_int64>(canId));
sqlite3_bind_int(m_stmtInsert, 5, dlc);
sqlite3_bind_text(m_stmtInsert, 6, hex.c_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(m_stmtInsert) != SQLITE_DONE) {
m_lastError = sqlite3_errmsg(m_db);
}
}
std::vector<CanFrameRow> CanDbStore::queryRecent(uint32_t canId, int limit) {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<CanFrameRow> rows;
if (!m_db) return rows;
if (limit <= 0) limit = 100;
std::string sql = "SELECT id, time, channel, frame_info, can_id, dlc, hex "
"FROM can_frame";
if (canId != 0) {
sql += " WHERE can_id=" + std::to_string(canId);
}
sql += " ORDER BY id DESC LIMIT " + std::to_string(limit) + ";";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
return rows;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
CanFrameRow r;
r.id = sqlite3_column_int64(stmt, 0);
r.time = sqlite3_column_double(stmt, 1);
r.channel = sqlite3_column_text(stmt, 2)
? reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)) : "";
r.frameInfo = sqlite3_column_int(stmt, 3);
r.canId = static_cast<uint32_t>(sqlite3_column_int64(stmt, 4));
r.dlc = sqlite3_column_int(stmt, 5);
r.hex = sqlite3_column_text(stmt, 6)
? reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6)) : "";
rows.push_back(std::move(r));
}
sqlite3_finalize(stmt);
return rows;
}
long long CanDbStore::count() 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 can_frame;", -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 canbridge
+67
View File
@@ -0,0 +1,67 @@
#ifndef PCANBRIDGE_CAN_DB_STORE_H
#define PCANBRIDGE_CAN_DB_STORE_H
#include <cstdint>
#include <string>
#include <vector>
#include <mutex>
struct sqlite3;
struct sqlite3_stmt;
namespace canbridge {
//============================================================================
// CanDbStore:CAN 帧 SQLite 存储。
//
// 设计参考 src/pCCU/store/DbStore(原始帧日志模式):
// - 收到的每个 CAN 帧逐条落库(can_frame 表),
// 存原始帧信息/ID/DLC/十六进制数据,语义解析交给下游。
// - 预处理语句 + WAL + synchronous=NORMAL,满足 ~60 帧/秒持续写入。
// - 内部互斥锁:接收线程写入,Iterate 线程查询/统计。
// 复用仓库内 src/pPowerManger/sqlit3/sqlite3.c 与 sqlite3.h。
//============================================================================
struct CanFrameRow {
long long id;
double time; // unix 时间(含小数秒)
std::string channel; // 通道名(CAN0~CAN7)
int frameInfo; // 原始帧信息字节 byte0(bit7 FF / bit6 RTR / bit3~0 DLC)
uint32_t canId; // CAN ID
int dlc;
std::string hex; // 数据十六进制
};
class CanDbStore {
public:
explicit CanDbStore(const std::string& dbPath);
~CanDbStore();
bool open();
void close();
bool isOpen() const { return m_db != nullptr; }
std::string lastError() const { return m_lastError; }
// 收帧落库(CanEndpoint 接收线程调用,内部加锁)
void onFrame(const std::string& channel, uint8_t frameInfo,
uint32_t canId, const uint8_t* data, int dlc);
// 查询最近 N 条(canId=0 表示不过滤;limit<=0 表示默认 100)
std::vector<CanFrameRow> queryRecent(uint32_t canId, int limit);
long long count() const;
private:
bool exec(const char* sql);
bool prepareInsert();
std::string m_dbPath;
sqlite3* m_db = nullptr;
sqlite3_stmt* m_stmtInsert = nullptr;
mutable std::mutex m_mutex;
std::string m_lastError;
};
} // namespace canbridge
#endif // PCANBRIDGE_CAN_DB_STORE_H
+200
View File
@@ -0,0 +1,200 @@
#include "CanEndpoint.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <chrono>
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
namespace canbridge {
namespace {
// CANET 系列固定帧长
const size_t kFrameLen = 13;
// 非阻塞 connect 超时(ms)
const int kConnectTOms = 2000;
// recv 读超时(s):保证断线能及时检出、stop() 能及时退出
const int kRecvTimeoutS = 1;
// 连接失败后的重试间隔(ms)
const int kRetryInterval = 2000;
} // namespace
CanEndpoint::CanEndpoint() {
m_buf.reserve(4096);
}
CanEndpoint::~CanEndpoint() {
stop();
}
void CanEndpoint::configure(const std::string& host, long port) {
m_host = host;
m_port = port;
}
void CanEndpoint::setFrameCallback(CanFrameCallback cb) {
m_onFrame = std::move(cb);
}
bool CanEndpoint::start() {
if (m_running) return true;
if (m_host.empty() || m_port <= 0) {
std::cerr << "[CanEndpoint] invalid target: " << m_host << ":" << m_port << std::endl;
return false;
}
m_running = true;
m_thread = std::thread([this]() { threadFunc(); });
std::cout << "[CanEndpoint] started, target " << m_host << ":" << m_port << std::endl;
return true;
}
void CanEndpoint::stop() {
if (!m_running) return;
m_running = false;
if (m_thread.joinable()) m_thread.join();
closeSocket();
}
//----------------------------------------------------------------------
// 连接一次目标(非阻塞 connect + poll 超时)
//----------------------------------------------------------------------
bool CanEndpoint::connectOnce() {
m_fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (m_fd < 0) return false;
// 非阻塞 connect,避免目标不可达时线程长时间卡死
int flags = ::fcntl(m_fd, F_GETFL, 0);
::fcntl(m_fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_in addr;
std::memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(static_cast<uint16_t>(m_port));
if (::inet_pton(AF_INET, m_host.c_str(), &addr.sin_addr) != 1) {
std::cerr << "[CanEndpoint] bad host: " << m_host << std::endl;
closeSocket();
return false;
}
int rc = ::connect(m_fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
if (rc < 0 && errno != EINPROGRESS) {
closeSocket();
return false;
}
if (rc < 0) {
struct pollfd pfd;
pfd.fd = m_fd;
pfd.events = POLLOUT;
int pr = ::poll(&pfd, 1, kConnectTOms);
if (pr <= 0) {
closeSocket();
return false;
}
int err = 0;
socklen_t elen = sizeof(err);
if (::getsockopt(m_fd, SOL_SOCKET, SO_ERROR, &err, &elen) < 0 || err != 0) {
closeSocket();
return false;
}
}
// 恢复阻塞模式 + 读超时 + 禁用 Nagle
::fcntl(m_fd, F_SETFL, flags);
struct timeval tv;
tv.tv_sec = kRecvTimeoutS;
tv.tv_usec = 0;
::setsockopt(m_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
int one = 1;
::setsockopt(m_fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
m_connected = true;
++m_reconnectCount;
std::cout << "[CanEndpoint] connected to " << m_host << ":" << m_port << std::endl;
return true;
}
void CanEndpoint::closeSocket() {
m_connected = false;
if (m_fd >= 0) {
::close(m_fd);
m_fd = -1;
}
}
//----------------------------------------------------------------------
// 切帧:缓冲 >= 13 字节即解析并回调
//----------------------------------------------------------------------
void CanEndpoint::processBuffer() {
while (m_buf.size() >= kFrameLen) {
const uint8_t* f = m_buf.data();
uint8_t frameInfo = f[0];
uint8_t dlc = frameInfo & 0x0F;
uint32_t id = (static_cast<uint32_t>(f[1]) << 24) |
(static_cast<uint32_t>(f[2]) << 16) |
(static_cast<uint32_t>(f[3]) << 8) |
static_cast<uint32_t>(f[4]);
// 无效 DLC(>8)按坏帧丢弃;同时覆盖以太网心跳包(AA 00 ... 55)
if (dlc > 8) {
++m_errorCount;
m_buf.erase(m_buf.begin(), m_buf.begin() + kFrameLen);
continue;
}
if (m_onFrame) {
m_onFrame(frameInfo, id, f + 5, static_cast<int>(dlc));
}
++m_frameCount;
m_lastFrameTime = MOOSTime(false);
m_buf.erase(m_buf.begin(), m_buf.begin() + kFrameLen);
}
}
//----------------------------------------------------------------------
// 接收线程:连接 -> 读流 -> 切帧 -> 断线重连
//----------------------------------------------------------------------
void CanEndpoint::threadFunc() {
uint8_t tmp[4096];
while (m_running) {
if (!m_connected) {
if (!connectOnce()) {
++m_errorCount;
// 分片休眠,保证 stop() 能及时退出
for (int i = 0; i < kRetryInterval / 100 && m_running; ++i) {
struct timespec ts = {0, 100 * 1000 * 1000}; // 100ms
nanosleep(&ts, nullptr);
}
continue;
}
m_buf.clear();
}
ssize_t n = ::recv(m_fd, tmp, sizeof(tmp), 0);
if (n > 0) {
m_buf.insert(m_buf.end(), tmp, tmp + n);
processBuffer();
} else if (n == 0) {
// 对端关闭
std::cerr << "[CanEndpoint] connection closed by peer" << std::endl;
closeSocket();
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
// 读超时/中断:回环检查 m_running
continue;
}
std::cerr << "[CanEndpoint] recv error: " << std::strerror(errno) << std::endl;
closeSocket();
}
}
}
} // namespace canbridge
+87
View File
@@ -0,0 +1,87 @@
#ifndef PCANBRIDGE_CAN_ENDPOINT_H
#define PCANBRIDGE_CAN_ENDPOINT_H
#include <cstdint>
#include <string>
#include <vector>
#include <atomic>
#include <thread>
#include <functional>
namespace canbridge {
//============================================================================
// CanEndpoint:USBCAN-8E-U(CANET 系列)TCP 工作端口的客户端封装。
//
// 协议(CANET-8E-U 用户手册 8.1 节):TCP 为字节流,每个 CAN 帧固定 13 字节:
// byte0 帧信息:bit7=FF(1扩展/0标准) bit6=RTR(1远程/0数据) bit3~0=DLC(0~8)
// byte1~4 帧 ID(大端,标准帧 11 位有效 / 扩展帧 29 位有效)
// byte5~12 数据 8 字节(有效长度由 DLC 决定)
//
// 职责:
// - 以 TCP Client 连接目标 host:port(非阻塞 connect + 超时)
// - 接收线程阻塞读流 -> 缓冲 -> 按 13 字节切帧 -> 回调
// - 断线(recv 返回 0 / 错误)自动重连
//
// 说明:采用 POSIX socket 而非 XPCTcpSocket,便于精确控制连接超时与
// 读超时(SO_RCVTIMEO),并避免 XPC 系列的异常式错误处理。
//============================================================================
// 收帧回调:frameInfo 为原始帧信息字节;data 有效长度为 dlc 字节
using CanFrameCallback = std::function<void(uint8_t frameInfo, uint32_t id,
const uint8_t* data, int dlc)>;
class CanEndpoint {
public:
CanEndpoint();
~CanEndpoint();
// 配置目标地址(须在 start 前调用)
void configure(const std::string& host, long port);
// 收帧回调(须在 start 前调用)
void setFrameCallback(CanFrameCallback cb);
// 启动接收线程(内部自动连接/重连);返回是否成功启动线程
bool start();
void stop();
bool isConnected() const { return m_connected; }
// 统计
unsigned long frameCount() const { return m_frameCount; }
unsigned long errorCount() const { return m_errorCount; }
unsigned long reconnectCount() const { return m_reconnectCount; }
double lastFrameTime() const { return m_lastFrameTime; }
const std::string& host() const { return m_host; }
long port() const { return m_port; }
private:
void threadFunc();
bool connectOnce();
void closeSocket();
void processBuffer();
std::string m_host;
long m_port = 4001;
int m_fd = -1;
std::atomic<bool> m_running{false};
std::atomic<bool> m_connected{false};
std::thread m_thread;
CanFrameCallback m_onFrame;
// TCP 流式接收缓冲(跨 recv 保持半包/粘包状态)
std::vector<uint8_t> m_buf;
std::atomic<unsigned long> m_frameCount{0};
std::atomic<unsigned long> m_errorCount{0};
std::atomic<unsigned long> m_reconnectCount{0};
std::atomic<double> m_lastFrameTime{0.0};
};
} // namespace canbridge
#endif // PCANBRIDGE_CAN_ENDPOINT_H
+49
View File
@@ -0,0 +1,49 @@
/************************************************************/
/* NAME: pCanBridge */
/* FILE: main.cpp */
/************************************************************/
#include <string>
#include "MBUtils.h"
#include "ColorParse.h"
#include "CanBridge.h"
#include "CanBridge_Info.h"
using namespace std;
int main(int argc, char *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();
canbridge::CanBridge bridge;
bridge.Run(run_command.c_str(), mission_file.c_str());
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
//============================================================================
// 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)
//============================================================================
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
//======== 通道标识 ========
// 写入消息 m_sSrcAux 辅助字段,用于下游区分通道
can_channel_name = CAN0
//======== 存储 ========
// CAN 帧 SQLite 落库路径(can_frame 表)
dbpath = pCanBridge_data.db
}
+1
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/CanBms.cpp
${CCU_DIR}/comm/UdpEndpoint.cpp
${CCU_DIR}/comm/LinkManager.cpp
${CCU_DIR}/store/DbStore.cpp
+49
View File
@@ -15,6 +15,7 @@
#include "../../src/pCCU/protocol/MessageRegistry.h"
#include "../../src/pCCU/protocol/FcProtocol.h"
#include "../../src/pCCU/protocol/PmProtocol.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"
@@ -287,6 +288,53 @@ static void testDbStore() {
std::remove(path.c_str());
}
//--------------------------------------------------------------------------
// 9. CAN BMS 报文解码(docs/BMS_协议字段定义.xlsx,测试向量为实车抓包数据)
static void testCanBmsDecode() {
BmsStatusValue v;
// 0x10010000:14 da 17 70 00 c9 31 01(实车抓包)
// 总电压 0x14DA=5338 -> 533.8V;SOC 0x00C9=201 -> 20.1%
// 故障码 0x31=49;状态位 0x01
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);
// 文档示例帧: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);
// 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);
// 总电流负数: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);
// 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);
// 非 BMS 报文 ID 不命中
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));
}
//--------------------------------------------------------------------------
int main() {
testFrameLengths();
@@ -297,6 +345,7 @@ int main() {
testChecksum();
testLinkDispatch();
testDbStore();
testCanBmsDecode();
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
+6 -4
View File
@@ -7,6 +7,10 @@
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003]
锂电池已切换为 CAN 总线(BMS 协议,经 pCanBridge 透传 CAN_0x* 消息),
不在本脚本 UDP 拓扑内:BMS 解码由 pccuTest 单元测试覆盖
(testCanBmsDecode,向量取自实车抓包)。
校验:
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
@@ -204,7 +208,7 @@ def main():
else:
ok = False
# 校验 4:数据库有记录
# 校验 4:数据库有记录(FC/PM 链路收发)
time.sleep(1.0)
db_count = -1
if os.path.exists(DB_PATH):
@@ -212,10 +216,8 @@ def main():
conn = sqlite3.connect(DB_PATH)
db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0]
conn.close()
print(f"[数据库] comm_log 记录数 = {db_count}")
print(f"[数据库] comm_log 记录数 = {db_count} (期望>=6)")
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
# 校验 5:Web 页面
web_ok = False
try:
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)
+3
View File
@@ -16,6 +16,9 @@ ProcessConfig = pCCU
pm_remote_ip = 127.0.0.1
pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文)
// 锂电池:CAN 总线 BMS 协议,经 pCanBridge 的 CAN_0x* 消息透传
// (集成测试不覆盖 CAN 链路,BMS 解码由 pccuTest 单元测试覆盖)
dbpath = /tmp/pccu_it_test.db
logpath = /tmp/pccu_it_test.log
+5 -1
View File
@@ -82,11 +82,15 @@ def main():
print("FAIL: 快照非合法JSON:", text[:200])
return 1
ok = True
for key in ("fc", "pmCmd", "links", "fcStatusCount", "pmControlCount", "logs"):
for key in ("fc", "pmCmd", "links", "fcStatusCount", "pmControlCount", "logs",
"batDyn", "batIns", "batDynAlarm", "batInsAlarm",
"batDynStatusCount", "batInsStatusCount"):
ok = ok and key in data
ok = ok and ("batDyn" in data.get("links", {})) and ("batIns" in data.get("links", {}))
print(f"PASS: 快照 JSON 有效, 分组keys={sorted(data.keys())}")
print(f" fc.fc_status={data['fc']['fc_status']}, fc.fc_mode={data['fc']['fc_mode']}, "
f"heartbeat={data['fc']['heartbeat']}")
print(f" batDyn.soc={data['batDyn'].get('soc')}, batIns.soc={data['batIns'].get('soc')}")
print(f" links={json.dumps(data['links'], ensure_ascii=False)}")
print(f" logs条数={len(data['logs'])}, fcStatusCount={data['fcStatusCount']}")
ws.s.close()