Compare commits
5
Commits
2590282aed
...
88bed850ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88bed850ee | ||
|
|
ee8819c610 | ||
|
|
e9a85c21fc | ||
|
|
b547772709 | ||
|
|
7e4d08808a |
+14
-16
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
@@ -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
@@ -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"
|
||||
|
||||
+21
-12
@@ -3,14 +3,16 @@
|
||||
# FILE: scripts/clean-data.sh
|
||||
# DESC: 清理目标板卡 (RK3588 / Ubuntu 22.04) 上的数据库与日志文件。
|
||||
# 默认清理板卡运行目录 /root/work/h100/data/ 下的:
|
||||
# power_data.db (+ -wal/-shm) 主机功率数据库
|
||||
# feedback_data.db 反馈落盘数据库
|
||||
# pPowerManger.log 主机运行日志
|
||||
# 默认先停止 4 个服务再清理(避免 SQLite WAL 仍在写入)。
|
||||
# power_data.db (+ -wal/-shm) 主机功率数据库
|
||||
# feedback_data.db 反馈落盘数据库
|
||||
# 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 ""
|
||||
|
||||
+22
-42
@@ -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
|
||||
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 "停止服务失败"
|
||||
}
|
||||
|
||||
|
||||
+16
-8
@@ -3,10 +3,12 @@
|
||||
# FILE: scripts/fetch-data.sh
|
||||
# DESC: 从目标板卡 (RK3588 / Ubuntu 22.04) 下载数据库与日志到本地。
|
||||
# 默认抓取板卡运行目录 /root/work/h100/data/ 下的:
|
||||
# power_data.db (+ -wal/-shm) 主机功率数据库
|
||||
# feedback_data.db 反馈落盘数据库
|
||||
# pPowerManger.log 主机运行日志
|
||||
# 可选 --journal 一并导出四个 systemd 服务的 journal 日志。
|
||||
# power_data.db (+ -wal/-shm) 主机功率数据库
|
||||
# feedback_data.db 反馈落盘数据库
|
||||
# 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
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ ADD_SUBDIRECTORY(pPowerManger)
|
||||
add_subdirectory(pPMtest)
|
||||
add_subdirectory(pPowerMangerHost)
|
||||
add_subdirectory(pCCU)
|
||||
add_subdirectory(pCanBridge)
|
||||
##############################################################################
|
||||
# END of CMakeLists.txt
|
||||
##############################################################################
|
||||
|
||||
+81
-6
@@ -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";
|
||||
}
|
||||
@@ -406,4 +481,4 @@ bool CCU::buildReport() {
|
||||
m_msgs << "数据库记录数:" << m_db->count() << "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -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,8 +82,9 @@ private:
|
||||
DbStore* m_db = nullptr;
|
||||
SnapshotBuilder* m_snap = nullptr;
|
||||
WebServer* m_web = nullptr;
|
||||
PowerCoordinator* m_coord = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ccu
|
||||
|
||||
#endif // PCCU_CCU_H
|
||||
#endif // PCCU_CCU_H
|
||||
|
||||
@@ -16,9 +16,10 @@ void showSynopsis() {
|
||||
blk("------------------------------------ ");
|
||||
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(" 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(" ");
|
||||
}
|
||||
|
||||
@@ -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("------------------------------------ ");
|
||||
|
||||
@@ -4,4 +4,7 @@
|
||||
4.具备sqlit数据库存储功能,可以把接收和发送的数据都存储到sqlit数据库中
|
||||
5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置
|
||||
6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介
|
||||
7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署
|
||||
7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署
|
||||
8.接收动力/仪表锂电池的消息,协议见docs/锂电池协议20230324.docx;动力/仪表锂电池的ip和端口均可通过.moos文件配置
|
||||
9.锂电池需要额外操作(自检/上下电/电池切换/功率设定),pCCU根据操作按协议时序下发指令(0x0000自检/0x0001控制,1s心跳)
|
||||
10.pCCU按自身算法进行锂电池与燃料电池的协调操作,算法入口为core/PowerCoordinator.cpp的coordinationTick()(当前为占位,待完善)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -12,8 +12,8 @@ class DbStore;
|
||||
//============================================================================
|
||||
// SnapshotBuilder:构建网页推送的 JSON 快照。
|
||||
//
|
||||
// 组合 SystemData(最新状态)、两条链路统计、最近原始帧日志,
|
||||
// 序列化为 JSON 字符串(jsoncpp)。串行化逻辑集中在此处,便于维护。
|
||||
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN)、两条 UDP 链路统计、
|
||||
// 最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
|
||||
//============================================================================
|
||||
|
||||
class SnapshotBuilder {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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">'+
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user