1 Commits
Author SHA1 Message Date
CI Bot 8c9be14f5d CI test report 38c489caec 2026-09-02 13:23:47 +00:00
275 changed files with 38 additions and 370281 deletions
-10
View File
@@ -1,10 +0,0 @@
.git
.gitignore
.gitattributes
.cache
build
bin
lib
*.db
*.log
Dockerfile
-220
View File
@@ -1,220 +0,0 @@
#=======================================================================
# FILE: .gitea/workflows/ci.yml
# DESC: CI/CD for H100PowerManger
# - build/test on aarch64 (QEMU emulation of arm64 Ubuntu 20.04)
# - publish compiled binary to Gitea Releases (manual deploy)
# * tag push v* -> versioned Release
# * main push -> rolling "latest" Release (pre-release / test)
# Self-contained (no external actions). The runner is an Alpine
# container with the host Docker socket mounted; we install the
# docker CLI at runtime.
#=======================================================================
name: build-test-deploy
on:
push:
branches: [main]
tags: ['v*']
pull_request:
env:
CI_IMAGE: h100-power-manager-ci:arm64
GITEA_SERVER: https://gitea2.zhaojingkui.xyz
jobs:
ci:
runs-on: <runner_labels>
steps:
#-------------------------------------------------------------------
# Checkout via git (no external actions/checkout dependency)
#-------------------------------------------------------------------
- name: Checkout
run: |
set -euo pipefail
git init .
git remote add origin "https://zjk:${{ secrets.CI_TOKEN }}@gitea2.zhaojingkui.xyz/zjk/H100PowerManger.git"
git fetch --depth 1 origin "$GITHUB_SHA"
git checkout FETCH_HEAD
#-------------------------------------------------------------------
# Install runtime tooling inside the (Alpine) runner container
#-------------------------------------------------------------------
- name: Install tools
run: |
apk add --no-cache docker-cli docker-cli-buildx rsync openssh-client curl jq
- name: Verify docker
run: |
docker version
docker info 2>&1 | grep -Ei 'Architecture|Server Version|Operating System|Storage Driver' || true
#-------------------------------------------------------------------
# Register QEMU so arm64 images can run on the x86 host daemon
#-------------------------------------------------------------------
- name: Register QEMU for arm64
run: |
docker run --rm --privileged tonistiigi/binfmt --install arm64 \
|| docker run --rm --privileged docker.m.daocloud.io/tonistiigi/binfmt --install arm64
#-------------------------------------------------------------------
# Build the aarch64 CI image (MOOS-IvP + jsoncpp)
#-------------------------------------------------------------------
- name: Build arm64 CI image
timeout-minutes: 90
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
docker buildx version
docker buildx build --platform linux/arm64 --load \
--secret id=gitea_token,env=CI_TOKEN \
-t "$CI_IMAGE" -f ci/Dockerfile .
#-------------------------------------------------------------------
# Build project + unit test + startup smoke test (inside arm64)
# NOTE: the runner runs inside a container, so the workspace path is
# not a host path -> `-v` mounts don't work. Use `docker cp`
# (streams files through the docker socket) instead.
#-------------------------------------------------------------------
- name: Build & test inside arm64 container
timeout-minutes: 30
run: |
CID="h100-ci-test"
docker rm -f "$CID" >/dev/null 2>&1 || true
docker create --name "$CID" --platform linux/arm64 "$CI_IMAGE" /bin/sh -c 'sleep 7200' >/dev/null
docker cp "$PWD/." "$CID:/src/"
docker start "$CID" >/dev/null
# Run build + tests (capture exit code, don't fail the step here so
# we can still collect the report on failure)
docker exec \
-e GITHUB_RUN_ID="${{ github.run_id }}" \
-e GITHUB_REPOSITORY="${{ github.repository }}" \
-e GITHUB_SERVER_URL="${GITEA_SERVER}" \
"$CID" /src/ci/build-test.sh
TEST_RC=$?
mkdir -p bin reports
docker cp "$CID:/src/bin/pPowerManger" bin/pPowerManger 2>/dev/null || true
docker cp "$CID:/src/bin/pPMtest" bin/pPMtest 2>/dev/null || true
docker cp "$CID:/src/bin/SQLiteTest" bin/SQLiteTest 2>/dev/null || true
docker cp "$CID:/src/reports/." reports/ 2>/dev/null || true
docker rm -f "$CID" >/dev/null 2>&1 || true
echo "--- bin/ ---"; ls -la bin/ 2>/dev/null || true
echo "--- reports/ ---"; ls -la reports/ 2>/dev/null || true
exit "$TEST_RC"
#-------------------------------------------------------------------
# Publish visual test report (runs even if tests failed)
#-------------------------------------------------------------------
- name: Publish test report
if: always()
run: |
SUMMARY_FILE="${GITHUB_STEP_SUMMARY:-/tmp/step_summary.md}"
if [ -f reports/test-report.md ]; then
cat reports/test-report.md >> "$SUMMARY_FILE"
else
echo "## H100PowerManger CI" >> "$SUMMARY_FILE"
echo "测试报告未生成(构建阶段失败)" >> "$SUMMARY_FILE"
fi
# Persist reports to a dedicated branch (main push only)
if [ "${GITHUB_REF:-}" = "refs/heads/main" ] && [ -d reports ]; then
git config user.name "CI Bot"
git config user.email "ci@localhost"
git checkout --orphan test-reports >/dev/null 2>&1 || git checkout test-reports >/dev/null 2>&1 || true
git rm -rf . >/dev/null 2>&1 || true
cp -f reports/test-report.md reports/junit.xml . 2>/dev/null || true
git add test-report.md junit.xml
git commit -m "CI test report ${GITHUB_SHA:-}" >/dev/null 2>&1 || true
git push -f origin test-reports >/dev/null 2>&1 || echo "WARN: failed to push test-reports branch"
fi
#-------------------------------------------------------------------
# Publish the aarch64 binary to Gitea Releases (manual deployment)
# * tag push (v*) -> versioned Release (e.g. v1.0.0, stable)
# * main push -> rolling "latest" Release (pre-release/test)
# Only runs when build + tests succeeded.
#-------------------------------------------------------------------
- name: Publish release
if: success() && startsWith(github.ref, 'refs/tags/v')
env:
RELEASE_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
API="https://gitea2.zhaojingkui.xyz/api/v1/repos/${REPO}"
TAG="${GITHUB_REF#refs/tags/}"
COMMIT_SHORT="$(git rev-parse --short HEAD)"
REPORT_URL="https://gitea2.zhaojingkui.xyz/${REPO}/src/branch/test-reports/test-report.md"
BODY="pPowerManger (aarch64 / RK3588 / Ubuntu 20.04)
- Commit: \`${COMMIT_SHORT}\`
- 时间: $(date -u +'%Y-%m-%d %H:%M:%S UTC')
- 测试报告: [${REPORT_URL}](${REPORT_URL})
手动部署: 下载后 scp/rsync 到板卡:
\`\`\`
rsync -avz pPowerManger root@192.168.0.223:/root/work/moos_ws/moos-ivp-extend/bin/pPowerManger
\`\`\`"
echo ">>> 创建版本号 Release: ${TAG}"
RELEASE_JSON=$(curl -sS -X POST -H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"body\":$(jq -Rn --arg b "${BODY}" '$b'),\"prerelease\":false}" \
"${API}/releases")
echo "${RELEASE_JSON}" | jq -e .id >/dev/null || { echo "创建 Release 失败:"; echo "${RELEASE_JSON}"; exit 1; }
RELEASE_ID=$(echo "${RELEASE_JSON}" | jq -r .id)
echo ">>> 上传 pPowerManger (release id=${RELEASE_ID})"
curl -sS -X POST -H "Authorization: token ${RELEASE_TOKEN}" \
-F "attachment=@bin/pPowerManger" \
"${API}/releases/${RELEASE_ID}/assets?name=pPowerManger" \
| jq -e .id >/dev/null || { echo "上传失败"; exit 1; }
echo ">>> 发布完成: https://gitea2.zhaojingkui.xyz/${REPO}/releases/tag/${TAG}"
- name: Publish latest release
if: success() && github.ref == 'refs/heads/main'
env:
RELEASE_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
API="https://gitea2.zhaojingkui.xyz/api/v1/repos/${REPO}"
TAG="latest"
COMMIT_SHORT="$(git rev-parse --short HEAD)"
REPORT_URL="https://gitea2.zhaojingkui.xyz/${REPO}/src/branch/test-reports/test-report.md"
BODY="pPowerManger (aarch64 / RK3588 / Ubuntu 20.04) — 测试用最新构建
- Commit: \`${COMMIT_SHORT}\`
- 时间: $(date -u +'%Y-%m-%d %H:%M:%S UTC')
- 测试报告: [${REPORT_URL}](${REPORT_URL})
手动部署: 下载后 scp/rsync 到板卡:
\`\`\`
rsync -avz pPowerManger root@192.168.0.223:/root/work/moos_ws/moos-ivp-extend/bin/pPowerManger
\`\`\`"
# 1. move the "latest" tag to current commit
echo ">>> 移动 latest tag 到 ${COMMIT_SHORT}"
git tag -f "${TAG}" HEAD
git push -f origin "${TAG}" || true
# 2. delete old "latest" release (by tag), ignore if none
echo ">>> 删除旧的 latest Release (如有)"
curl -sS -X DELETE -H "Authorization: token ${RELEASE_TOKEN}" \
"${API}/releases/tags/${TAG}" || true
# 3. create new "latest" release (pre-release)
echo ">>> 创建 latest Release (pre-release)"
RELEASE_JSON=$(curl -sS -X POST -H "Authorization: token ${RELEASE_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${TAG}\",\"name\":\"latest (测试)\",\"body\":$(jq -Rn --arg b "${BODY}" '$b'),\"prerelease\":true}" \
"${API}/releases")
echo "${RELEASE_JSON}" | jq -e .id >/dev/null || { echo "创建 latest Release 失败:"; echo "${RELEASE_JSON}"; exit 1; }
RELEASE_ID=$(echo "${RELEASE_JSON}" | jq -r .id)
# 4. upload pPowerManger
echo ">>> 上传 pPowerManger (release id=${RELEASE_ID})"
curl -sS -X POST -H "Authorization: token ${RELEASE_TOKEN}" \
-F "attachment=@bin/pPowerManger" \
"${API}/releases/${RELEASE_ID}/assets?name=pPowerManger" \
| jq -e .id >/dev/null || { echo "上传失败"; exit 1; }
echo ">>> 发布完成: https://gitea2.zhaojingkui.xyz/${REPO}/releases/tag/${TAG}"
-292
View File
@@ -1,292 +0,0 @@
# ---> C++
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> MATLAB
# Windows default autosave extension
*.asv
# OSX / *nix default autosave extension
*.m~
# Compiled MEX binaries (all platforms)
*.mex*
# Packaged app and toolbox files
*.mlappinstall
*.mltbx
# Generated helpsearch folders
helpsearch*/
# Simulink code generation folders
slprj/
sccprj/
# Matlab code generation folders
codegen/
# Simulink autosave extension
*.autosave
# Simulink cache files
*.slxc
# Octave session info
octave-workspace
# ---> CMake
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
# PowerManager specific files
bin/ccuState.txt
# PowerManager compiled binaries (built into bin/ by CMake)
bin/pPowerManger
bin/SQLiteTest
bin/pPMtest
bin/pPowerMangerHost
bin/pCCU
bin/pccuTest
bin/pCanBridge
bin/pMotor
# CI test reports
reports/
# CI test binaries (built into bin/)
bin/PowerMangerTests
bin/udpFeeder
bin/fullFeeder
bin/pmotorTest
# ============================================================
# 数据库与日志文件(全局忽略,一律不入库)
# *.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
# Board-native build dirs (build-board.sh) / 历史本机构建目录
build-arm64/
build-nosa-test/
# CMake-generated web asset table (pPowerMangerHost)
src/pPowerMangerHost/webassets_gen.h
# ============================================================
# MS Office 临时锁文件 / 编辑器、备份残留
# ============================================================
~$*
*.bck
*.bak
* copy*
-99
View File
@@ -1,99 +0,0 @@
{
"files.associations": {
"list": "cpp",
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"*.tcc": "cpp",
"compare": "cpp",
"concepts": "cpp",
"cstdint": "cpp",
"deque": "cpp",
"map": "cpp",
"set": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"random": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"new": "cpp",
"numbers": "cpp",
"ostream": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
"queue": "cpp",
"csignal": "cpp",
"any": "cpp",
"bitset": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"codecvt": "cpp",
"complex": "cpp",
"condition_variable": "cpp",
"coroutine": "cpp",
"optional": "cpp",
"ratio": "cpp",
"regex": "cpp",
"source_location": "cpp",
"future": "cpp",
"mutex": "cpp",
"semaphore": "cpp",
"stop_token": "cpp",
"thread": "cpp",
"typeindex": "cpp",
"variant": "cpp",
"*.ipp": "cpp",
"cassert": "cpp",
"cerrno": "cpp",
"cfloat": "cpp",
"ciso646": "cpp",
"climits": "cpp",
"filesystem": "cpp",
"ios": "cpp",
"locale": "cpp",
"stack": "cpp",
"cstdbool": "cpp",
"version": "cpp",
"unordered_set": "cpp",
"ranges": "cpp"
},
"C_Cpp.errorSquiggles": "disabled",
"marscode.chatLanguage": "cn",
"marscode.codeCompletionPro": {
"enableCodeCompletionPro": true
},
"marscode.enableInlineCommand": true,
"idf.pythonInstallPath": "/usr/bin/python"
}
-98
View File
@@ -1,98 +0,0 @@
#=======================================================================
# FILE: moos-ivp-extend/CMakeLists.txt
# DATE: 2012/07/24
# INFO: Top-level CMakeLists.txt file for the moos-ivp-extend project
# NAME: Maintained by Mike Benjamin - Original setup by Christian Convey
# Chris Gagner, and tips borrowed from Dave Billin
#=======================================================================
CMAKE_MINIMUM_REQUIRED(VERSION 3.5)
PROJECT( IVP_EXTEND )
set (CMAKE_CXX_STANDARD 11)
#=======================================================================
# IvP 库安装前缀(MOOS 与 IvP 均安装于系统 /usr/local)
#=======================================================================
set (IVP_PREFIX "/usr/local")
# jsoncpp 头文件目录(Ubuntu 布局:/usr/include/jsoncpp/json/)
INCLUDE_DIRECTORIES(/usr/include/jsoncpp)
#=======================================================================
# Set the output directories for the binary and library files
#=======================================================================
GET_FILENAME_COMPONENT(IVP_EXTEND_BIN_DIR "${CMAKE_SOURCE_DIR}/bin" ABSOLUTE )
GET_FILENAME_COMPONENT(IVP_EXTEND_LIB_DIR "${CMAKE_SOURCE_DIR}/lib" ABSOLUTE )
SET( LIBRARY_OUTPUT_PATH "${IVP_EXTEND_LIB_DIR}" CACHE PATH "" )
SET( ARCHIVE_OUTPUT_DIRECTORY "${IVP_EXTEND_LIB_DIR}" CACHE PATH "" )
SET( LIBRARY_OUTPUT_DIRECTORY "${IVP_EXTEND_LIB_DIR}" CACHE PATH "" )
SET( EXECUTABLE_OUTPUT_PATH "${IVP_EXTEND_BIN_DIR}" CACHE PATH "" )
SET( RUNTIME_OUTPUT_DIRECTORY "${IVP_EXTEND_BIN_DIR}" CACHE PATH "" )
#=======================================================================
# Find MOOS
#=======================================================================
find_package(MOOS 10.0)
INCLUDE_DIRECTORIES(${MOOS_INCLUDE_DIRS})
#=======================================================================
# Find the "moos-ivp" base directory
#=======================================================================
# IvP 头文件与库从系统安装前缀加载
INCLUDE_DIRECTORIES(${IVP_PREFIX}/include/ivp)
FILE(GLOB IVP_INCLUDE_DIRS ${IVP_PREFIX}/include/ivp )
INCLUDE_DIRECTORIES(${IVP_INCLUDE_DIRS})
FILE(GLOB IVP_LIBRARY_DIRS ${IVP_PREFIX}/lib )
LINK_DIRECTORIES(${IVP_LIBRARY_DIRS})
#======================================================================
# Specify Compiler Flags
#======================================================================
IF( ${WIN32} )
#---------------------------------------------
# Windows Compiler Flags
#---------------------------------------------
IF(MSVC)
# Flags for Microsoft Visual Studio
SET( WALL_ON OFF CACHE BOOL
"tell me about all compiler warnings (-Wall) ")
IF(WALL_ON)
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
ENDIF(WALL_ON)
ELSE(MSVC)
# Other Windows compilers go here
ENDIF(MSVC)
ELSE( ${WIN32} )
#---------------------------------------------
# Linux and Apple Compiler Flags
#---------------------------------------------
# Force -fPIC because gcc complains when we don't use it with x86_64 code.
# Note sure why: -fPIC should only be needed for shared objects, and
# AFAIK, CMake gets that right when building shared objects. -CJC
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -g -Wdeprecated-declarations")
IF(CMAKE_COMPILER_IS_GNUCXX)
# Flags for the GNU C++ Compiler
SET( WALL_ON OFF CACHE BOOL
"tell me about all compiler warnings (-Wall) ")
IF(WALL_ON)
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall" -C++11)
ENDIF( WALL_ON)
ELSE(CMAKE_COMPILER_IS_GNUCXX)
ENDIF(CMAKE_COMPILER_IS_GNUCXX)
ENDIF( ${WIN32} )
#=======================================================================
# Add Subdirectories
#=======================================================================
ADD_SUBDIRECTORY( src )
ADD_SUBDIRECTORY( test )
-60
View File
@@ -1,60 +0,0 @@
# H100PowerManger
H100 能源管理程序
[![CI](https://gitea2.zhaojingkui.xyz/zjk/H100PowerManger/actions/workflows/ci.yml/badge.svg)](https://gitea2.zhaojingkui.xyz/zjk/H100PowerManger/actions)
## 测试报告
每次 CI 运行后自动生成测试报告:
- [最新测试报告 (Markdown)](https://gitea2.zhaojingkui.xyz/zjk/H100PowerManger/src/branch/test-reports/test-report.md)
- [JUnit XML](https://gitea2.zhaojingkui.xyz/zjk/H100PowerManger/raw/branch/test-reports/junit.xml)
## 自动化测试(PowerManger)
CI 在 aarch64 (QEMU) 环境下运行两类测试,**不改动任何 `src/` 生产代码**:
**单元测试(GoogleTest,`PowerMangerTests`,56 例)**
- `DriverTest`:10 个工况的设备表生成(STANDBY/岸基备航/水中备航/巡航/高速/上浮下潜/浮调/水下侦查/水面侦查/DJ)+ 设备状态读写 + JSON 往返
- `SystemDataTest`:4 类配电/CCU 反馈的故障检测(断路器 0x55/0xAA/0x5A 映射、绝缘低、电源失电、漏水、DCDC、继电器位提取、缩放)+ 故障码集合/等级 + 漏水位
- `FaultMapTest`:锂电池故障表(54) / 配电故障表(45) 完整性
- `UpmsgTest`:上位机/外部通信 JSON 消息解析与序列化往返
- `UdpCommTest`:UDP 校验和 / 消息头识别 / 校验解析 / 日期
- `PmSysvariableTest`:`#pragma pack(1)` 协议结构体大小/偏移 + 枚举完整性
- `SqliteTest`:用裸 sqlite3 读回校验 10 个 `insertData` 写入
**集成测试**
- 冒烟:MOOSDB + `pPowerManger` 拉起存活
- UDP 回环:`udpFeeder` 发送真实 CCU 反馈 → 校验 `power_data.db` 落行(验证 UDP→SystemData→SQLite 全链路)
本地跑测试:需要 GoogleTest(`git clone https://gitea2.zhaojingkui.xyz/zjk/googletest.git` 后 `cmake && make && make install`),然后 `./build.sh` 即可。
## 构建产物(Gitea Releases,手动部署)
CI 构建的 aarch64 二进制(RK3588 / Ubuntu 22.04,板卡实测为 Orange Pi 5 Plus 的 Jammy 系统)会发布到 Gitea Releases,供手动下载部署:
- **版本号 Release**:推送 `v*` tag(如 `v1.0.0`)时创建,正式版
→ `https://gitea2.zhaojingkui.xyz/zjk/H100PowerManger/releases/tag/v1.0.0`
- **latest Release(测试)**:每次 `main` push 时更新,标记为预发布(测试用)
→ `https://gitea2.zhaojingkui.xyz/zjk/H100PowerManger/releases/tag/latest`
### 发布流程(CI 自动执行,仅测试通过后发布)
`Publish release` / `Publish latest release` 步骤(`if: success()`,即构建 + 测试全部通过才发布)用 `${{ secrets.CI_TOKEN }}` 调 Gitea API 完成:
1. **创建 Release**:`POST /releases`
2. **删除旧 latest**(仅 latest):`DELETE /releases/tags/latest`
3. **上传附件**:`POST /releases/{id}/assets?name=pPowerManger`(multipart 字段 `attachment`)
4. **按 `github.ref` 分支处理**:
- `refs/tags/v*` → 直接为该 tag 创建版本号 Release
- `refs/heads/main` → `git tag -f latest && git push -f origin latest`,删除旧 latest Release,重建为 prerelease,再上传二进制
5. **Release body** 自动带上:commit / 时间 / 测试报告链接 / 手动部署命令
### 手动部署到板卡
从 Releases 页面下载 `pPowerManger` 后,手动推送到板卡(仅推送,不自动重启):
```bash
rsync -avz pPowerManger root@192.168.0.223:/root/work/moos_ws/moos-ivp-extend/bin/pPowerManger
```
-26
View File
@@ -1,26 +0,0 @@
@startuml PowerManagerFsm状态图
[*] --> InitState
state InitState {
[*] --> 等待复合管控消息
等待复合管控消息 --> 等待关键断路器闭合: 连接成功
等待复合管控消息 --> FaultState: 超时(10秒)
等待关键断路器闭合 --> StandbyState: 关键断路器闭合
等待关键断路器闭合 --> FaultState: 系统错误
}
state StandbyState {
[*] --> 待机
待机 --> FaultState: 检测到错误
待机 --> 处理电源命令: 收到命令
处理电源命令 --> 待机: 完成处理
}
state FaultState {
[*] --> 故障处理
}
InitState --> FaultState : FaultEvent(faultCode=1/2)
StandbyState --> FaultState : FaultEvent(faultCode)
InitState --> StandbyState : StandbyEvent
@enduml
-11
View File
@@ -1,11 +0,0 @@
//SERVERHOST = 192.168.0.223
//SERVERPORT = 9000
ProcessConfig = pPowerManger
{
AppTick = 4
CommsTick = 4
log_level = INFO
log_file = /path/to/your/logfile.log
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
}
-34
View File
@@ -1,34 +0,0 @@
#!/bin/bash
echo "开始测试pPowerManger程序启动稳定性..."
LOOP_COUNT=0
while true; do
LOOP_COUNT=$((LOOP_COUNT + 1))
echo "\n===== 第 $LOOP_COUNT 次循环 ====="
# 启动pPowerManger程序
echo "启动pPowerManger..."
./pPowerManger pPowerManger.moos &
PROCESS_ID=$!
# 等待5秒让程序启动
sleep 0.1
# 检查程序是否还在运行
if kill -0 $PROCESS_ID 2>/dev/null; then
echo "程序启动成功,PID: $PROCESS_ID"
# 等待2秒后关闭程序
sleep 2
echo "关闭程序..."
kill $PROCESS_ID
# 等待1秒让程序彻底关闭
sleep 0.1
else
echo "程序启动失败!"
echo "故障发生在第 $LOOP_COUNT 次循环"
exit 0.1
fi
done
-148
View File
@@ -1,148 +0,0 @@
#!/bin/bash
# 核心配置
CRASH_LOG_DIR="./crash_logs"
RUN_DURATION=2
GDB_TIMEOUT=$((RUN_DURATION + 2)) # 延长超时,确保GDB完全初始化
MAX_LOOP=0
# 强制捕获所有致命信号
CRASH_SIGNALS=("SIGSEGV" "SIGABRT" "SIGILL" "SIGFPE" "SIGBUS")
# 创建日志目录
mkdir -p ${CRASH_LOG_DIR}
rm -f ${CRASH_LOG_DIR}/* # 清空旧日志(可选)
# 清理进程函数
cleanup() {
pkill -9 gdb >/dev/null 2>&1
pkill -9 pPowerManger >/dev/null 2>&1
sleep 0.2
}
# 检查是否是真崩溃(从日志+系统日志双重验证)
is_real_crash() {
local log_file=$1
local loop=$2
# 1. 检查GDB日志中的崩溃关键字
if grep -qiE "segmentation fault|sigsegv|crash detected|backtrace" "${log_file}"; then
return 0
fi
# 2. 检查系统dmesg中的段错误记录(终极验证)
if dmesg | grep -i "pPowerManger.*segfault" | grep -i "loop_${loop}"; then
return 0
fi
return 1
}
# 打印系统级崩溃日志(dmesg)
print_system_crash_log() {
local loop=$1
echo -e "\033[33m===== 系统级崩溃日志(dmesg)=====\033[0m"
dmesg | grep -A 5 -B 2 "pPowerManger.*segfault" | tail -10
echo -e "\033[33m===================================\033[0m"
}
echo -e "🔍 启动pPowerManger稳定性测试(强制捕获段错误)\n"
echo "📂 崩溃日志目录: ${CRASH_LOG_DIR}"
echo "⏱ 程序运行时长: ${RUN_DURATION}秒"
echo "📌 捕获信号: ${CRASH_SIGNALS[*]}"
echo -e "========================================\n"
cleanup # 初始清理
LOOP_COUNT=0
while true; do
LOOP_COUNT=$((LOOP_COUNT + 1))
if [ ${MAX_LOOP} -ne 0 ] && [ ${LOOP_COUNT} -gt ${MAX_LOOP} ]; then
echo -e "\n✅ 测试完成(${MAX_LOOP}次循环无崩溃)"
cleanup
exit 0
fi
echo -e "===== 第 ${LOOP_COUNT} 次循环 ====="
CRASH_LOG="${CRASH_LOG_DIR}/crash_loop_${LOOP_COUNT}.log"
GDB_CMD="${CRASH_LOG_DIR}/gdb_cmds_${LOOP_COUNT}.gdb"
# 生成强化版GDB脚本:强制捕获所有信号,优先初始化信号处理
cat > ${GDB_CMD} << EOF
# 核心:GDB启动后先设置信号捕获,再启动程序
set args pPowerManger.moos
set timeout ${GDB_TIMEOUT}
set print pretty on
set print frame-arguments all
set logging file ${CRASH_LOG}.gdb_raw
set logging on # 开启GDB原始日志(双重保障)
# 强制捕获所有致命信号
EOF
# 为每个崩溃信号添加捕获规则(追加到GDB脚本)
for sig in "${CRASH_SIGNALS[@]}"; do
cat >> ${GDB_CMD} << EOF
handle ${sig} stop print pass nostop # 即使子线程崩溃也捕获
EOF
done
# 追加程序启动和崩溃处理逻辑
cat >> ${GDB_CMD} << EOF
# 延迟10ms启动程序,确保GDB完全初始化
sleep 0.01
run
# 崩溃后强制打印所有信息(无论是否主线程)
if \$exitcode != 0 || \$signal_number != 0
echo "\n====================================="
echo " CRASH DETECTED"
echo "====================================="
echo "Loop: ${LOOP_COUNT}"
echo "Exit code: \$exitcode"
echo "Signal: \$signal_name (\$signal_number)"
echo "====================================="
thread apply all bt full # 打印所有线程的调用栈(关键!)
info threads
info proc mappings # 打印内存映射(排查内存越界)
info locals
info registers
endif
set logging off
quit
EOF
# 核心:用nohup+GDB启动,避免输出丢失;同时记录进程ID
echo "启动程序(GDB强化模式)..."
nohup timeout ${GDB_TIMEOUT} gdb -batch -x ${GDB_CMD} ./pPowerManger > ${CRASH_LOG} 2>&1 &
GDB_PID=$!
wait ${GDB_PID} # 等待GDB执行完成
# 双重验证:检查是否真崩溃
if is_real_crash ${CRASH_LOG} ${LOOP_COUNT}; then
# 捕获到真崩溃
echo -e "\033[31m❌ 第 ${LOOP_COUNT} 次循环捕获到段错误!\033[0m"
echo -e "\033[31m📄 崩溃日志: ${CRASH_LOG}\033[0m"
print_system_crash_log ${LOOP_COUNT}
# 打印最关键的调用栈(所有线程)
echo -e "\033[31m===== 所有线程崩溃调用栈 =====\033[0m"
grep -A 50 "thread apply all bt full" ${CRASH_LOG} | grep -v "No stack frame"
echo -e "\033[31m==============================\033[0m"
cleanup
echo -e "\n🛑 测试终止,完整日志在 ${CRASH_LOG}"
exit 1
else
# 无崩溃(正常退出/启动失败)
if grep -qi "Program exited normally" ${CRASH_LOG}; then
echo "✅ 程序正常运行${RUN_DURATION}秒后退出"
else
echo "⚠️ 程序启动失败(非段错误),继续循环"
# 保留启动失败日志(方便排查)
mv ${CRASH_LOG} ${CRASH_LOG}.start_fail
fi
rm -f ${GDB_CMD} ${CRASH_LOG}.gdb_raw
fi
cleanup
echo -e "第 ${LOOP_COUNT} 次循环完成\n"
sleep 0.5
done
-63
View File
@@ -1,63 +0,0 @@
#!/bin/bash
INVOCATION_ABS_DIR=`pwd`
BUILD_TYPE="None"
CMD_LINE_ARGS=""
#-------------------------------------------------------------------
# Part 1: Check for and handle command-line arguments
#-------------------------------------------------------------------
for ARGI; do
if [ "${ARGI}" = "--help" -o "${ARGI}" = "-h" ] ; then
printf "%s [SWITCHES] \n" $0
printf "Switches: \n"
printf " --help, -h \n"
printf " --debug, -d \n"
printf " --release, -r \n"
printf "Notes: \n"
printf " (1) All other command line args will be passed as args \n"
printf " to \"make\" when it is eventually invoked. \n"
printf " (2) For example -k will continue making when/if a failure \n"
printf " is encountered in building one of the subdirectories. \n"
printf " (3) For example -j2 will utilize a 2nd core in the build \n"
printf " if your machine has two cores. -j4 etc for quad core. \n"
exit 0;
elif [ "${ARGI}" = "--debug" -o "${ARGI}" = "-d" ] ; then
BUILD_TYPE="Debug"
elif [ "${ARGI}" = "--release" -o "${ARGI}" = "-r" ] ; then
BUILD_TYPE="Release"
else
CMD_LINE_ARGS=$CMD_LINE_ARGS" "$ARGI
fi
done
#-------------------------------------------------------------------
# Part 2: Invoke the call to make in the build directory
#-------------------------------------------------------------------
mkdir -p build
cd build
# 修复: PATH 中包含了 Windows Anaconda(/mnt/d/Software/Anaconda),
# 其下的 GTest 是 Windows .lib, 无法被 Linux 链接器使用。
# 因此显式指定 Linux 平台的 GTest 配置, 覆盖 CMake 从 PATH 探测到的不正确路径。
CMAKE_ARGS="-DCMAKE_BUILD_TYPE=${BUILD_TYPE}"
if [ -z "${GTest_DIR}" ]; then
for GTEST_CONFIG in \
/usr/lib/x86_64-linux-gnu/cmake/GTest/GTestConfig.cmake \
/usr/lib/cmake/GTest/GTestConfig.cmake \
/usr/local/lib/cmake/GTest/GTestConfig.cmake \
"${HOME}/miniconda/lib/cmake/GTest/GTestConfig.cmake"; do
if [ -f "${GTEST_CONFIG}" ]; then
CMAKE_ARGS="${CMAKE_ARGS} -DGTest_DIR=$(dirname "${GTEST_CONFIG}")"
break
fi
done
fi
cmake ${CMAKE_ARGS} ../
make ${CMD_LINE_ARGS}
cd ${INVOCATION_ABS_DIR}
-84
View File
@@ -1,84 +0,0 @@
#=======================================================================
# FILE: ci/Dockerfile
# DESC: aarch64 (RK3588 / Ubuntu 22.04) build & test environment.
# - Ubuntu 22.04 arm64 base (matches the target board: Orange Pi 5 Plus)
# - MOOS-IvP built from source and installed to /usr/local
# - jsoncpp (the only extra dep used by pPowerManger)
# NOTE: Ubuntu 20.04 image kept as h100-power-manager-ci:arm64-focal
# in case the target board is later switched back to 20.04.
# MOOS-IvP is cloned from the internal Gitea mirror (UUV/moos-ivp)
# via a BuildKit secret (github.com is unreachable from the CN
# runner). PROJ/UTM are disabled to avoid github-based CPM fetches.
#=======================================================================
FROM arm64v8/ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
#-----------------------------------------------------------------------
# Use Aliyun mirror for apt (faster from CN) then install toolchain
#-----------------------------------------------------------------------
RUN sed -i \
-e 's|http://ports.ubuntu.com/ubuntu-ports|http://mirrors.aliyun.com/ubuntu-ports|g' \
-e 's|http://archive.ubuntu.com/ubuntu|http://mirrors.aliyun.com/ubuntu|g' \
/etc/apt/sources.list && \
apt-get update && apt-get install -y --no-install-recommends \
build-essential \
cmake \
git \
ca-certificates \
libjsoncpp-dev \
rsync \
&& rm -rf /var/lib/apt/lists/*
#-----------------------------------------------------------------------
# Clone MOOS-IvP from the internal Gitea mirror
# (token passed via BuildKit secret, not baked into the image)
#-----------------------------------------------------------------------
RUN --mount=type=secret,id=gitea_token \
set -eux; \
TOKEN="$(cat /run/secrets/gitea_token)"; \
git clone --depth 1 "https://zjk:${TOKEN}@gitea2.zhaojingkui.xyz/UUV/moos-ivp.git" /opt/moos-ivp; \
git -C /opt/moos-ivp remote set-url origin "https://gitea2.zhaojingkui.xyz/UUV/moos-ivp.git"
#-----------------------------------------------------------------------
# Build & install MOOS-IvP (bundles MOOS core + essentials + geodesy + ivp)
# build-moos.sh -m : skip GUI tools (no FLTK needed)
# build-ivp.sh -n : skip GUI apps (no FLTK needed)
# Installs libMOOS + MOOSConfig.cmake + ivp headers/libs to /usr/local,
# matching the layout expected by the project's top-level CMakeLists.txt.
#-----------------------------------------------------------------------
WORKDIR /opt/moos-ivp
RUN ./build-moos.sh -m -j"$(nproc)" && \
make -C build/MOOS/MOOSCore install && \
./build-ivp.sh -n -j"$(nproc)" && \
make -C build/ivp install && \
ldconfig
#-----------------------------------------------------------------------
# netbase provides /etc/protocols, which MOOS needs at runtime
# (getprotobyname("tcp")). The base image lacks it, so MOOSDB would throw
# XPCException "Could Not Get Protocol By Name" at startup without it.
# python3 is used by the integration DB check (check_db.py).
# Kept as a final layer so the (expensive) MOOS-IvP build stays cached.
#-----------------------------------------------------------------------
RUN apt-get update && apt-get install -y --no-install-recommends netbase python3 \
&& rm -rf /var/lib/apt/lists/*
#-----------------------------------------------------------------------
# GoogleTest (for unit tests) from the internal Gitea mirror.
# Cloned via the same BuildKit secret as MOOS-IvP; installed to /usr/local
# so `find_package(GTest)` works both inside the container and locally.
#-----------------------------------------------------------------------
RUN --mount=type=secret,id=gitea_token \
set -eux; \
TOKEN="$(cat /run/secrets/gitea_token)"; \
git clone --depth 1 "https://zjk:${TOKEN}@gitea2.zhaojingkui.xyz/zjk/googletest.git" /opt/googletest; \
mkdir -p /opt/googletest/build && cd /opt/googletest/build && \
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_GMOCK=ON .. && \
make -j"$(nproc)" && \
make install && \
ldconfig
WORKDIR /src
-319
View File
@@ -1,319 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: ci/build-test.sh
# DESC: Build the project, run unit tests + a startup smoke test, and
# generate a visual test report (markdown + junit.xml).
# Executed INSIDE the aarch64 build container (ci/Dockerfile).
#=======================================================================
set -uo pipefail
SRC_DIR="${SRC_DIR:-/src}"
cd "${SRC_DIR}"
REPORT_DIR="${SRC_DIR}/reports"
mkdir -p "${REPORT_DIR}"
ARCH="$(uname -m)"
COMMIT="$(git -C "${SRC_DIR}" rev-parse --short HEAD 2>/dev/null || echo "${GITHUB_SHA:-unknown}")"
BUILD_DATE="$(date -u +'%Y-%m-%d %H:%M:%S UTC')"
# Run link (env vars passed by the workflow via docker exec)
RUN_LINK=""
if [ -n "${GITHUB_SERVER_URL:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_RUN_ID:-}" ]; then
RUN_LINK="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
fi
#-----------------------------------------------------------------------
# Result trackers
#-----------------------------------------------------------------------
BUILD_OK=0; SQLITE_OK=0; PMTESTS_OK=0; SMOKE_OK=0; INTEG_OK=0
BUILD_TIME=0; SQLITE_TIME=0; PMTESTS_TIME=0; SMOKE_TIME=0; INTEG_TIME=0
now_ms() { date +%s; }
#-----------------------------------------------------------------------
# 1. Build
#-----------------------------------------------------------------------
echo ">>> Building (aarch64, Release)..."
rm -rf build
mkdir -p build
cd build
t0=$(now_ms)
if cmake -DCMAKE_BUILD_TYPE=Release .. > /tmp/cmake.log 2>&1 \
&& make -j2 > /tmp/make.log 2>&1; then
BUILD_OK=1
else
echo "--- build failed ---" >&2
tail -40 /tmp/make.log >&2
fi
BUILD_TIME=$(( $(now_ms) - t0 ))
cd "${SRC_DIR}"
BIN_DIR="${SRC_DIR}/bin"
if [ "${BUILD_OK}" = "1" ]; then
echo ">>> Build artifacts:"
ls -la "${BIN_DIR}"
fi
#-----------------------------------------------------------------------
# 2. Unit test (SQLiteTest)
#-----------------------------------------------------------------------
if [ "${BUILD_OK}" = "1" ]; then
echo ">>> Running unit test: SQLiteTest"
t0=$(now_ms)
if "${BIN_DIR}/SQLiteTest" > /tmp/sqlitetest.log 2>&1; then
SQLITE_OK=1
echo ">>> SQLiteTest passed"
else
echo ">>> SQLiteTest FAILED" >&2
tail -40 /tmp/sqlitetest.log >&2
fi
SQLITE_TIME=$(( $(now_ms) - t0 ))
fi
#-----------------------------------------------------------------------
# 3. PowerMangerTests (gtest) — 全部纯逻辑单测(工况设备表/故障检测/协议等)
#-----------------------------------------------------------------------
if [ "${BUILD_OK}" = "1" ]; then
echo ">>> Running PowerMangerTests (gtest)"
t0=$(now_ms)
if "${BIN_DIR}/PowerMangerTests" --gtest_output=xml:"${REPORT_DIR}/gtest-junit.xml" > /tmp/pmtests.log 2>&1; then
PMTESTS_OK=1
echo ">>> PowerMangerTests passed"
else
echo ">>> PowerMangerTests FAILED" >&2
tail -40 /tmp/pmtests.log >&2
fi
PMTESTS_TIME=$(( $(now_ms) - t0 ))
fi
#-----------------------------------------------------------------------
# 4. Startup smoke test: MOOSDB + pPowerManger liveness
#-----------------------------------------------------------------------
if [ "${BUILD_OK}" = "1" ]; then
echo ">>> Smoke test: launch MOOSDB + pPowerManger"
echo ">>> [mem-diagnostic] nproc=$(nproc), ulimit -v=$(ulimit -v), free:"
free -m 2>/dev/null | head -3 || true
MOOSDB="$(command -v MOOSDB || true)"
if [ -z "${MOOSDB}" ]; then
MOOSDB="$(ls /opt/moos-ivp/bin/MOOSDB 2>/dev/null || true)"
fi
MISSION_FILE="/tmp/smoke.moos"
cat > "${MISSION_FILE}" <<EOF
ServerHost = localhost
ServerPort = 9000
Community = h100smoke
ProcessConfig = pPowerManger
{
AppTick = 4
CommsTick = 4
log_level = INFO
log_file = /tmp/pPowerManger_smoke.log
}
EOF
cd "${BIN_DIR}"
t0=$(now_ms)
"${MOOSDB}" --moos_suicide_disable --moos_no_colour "${MISSION_FILE}" \
> /tmp/moosdb_smoke.log 2>&1 &
MOOSDB_PID=$!
sleep 3
./pPowerManger "${MISSION_FILE}" > /tmp/pm_smoke.log 2>&1 &
PM_PID=$!
# 集成测试:与冒烟窗口并行喂 UDP 反馈(在进程存活期间尽快发送)
t1=$(now_ms)
"${BIN_DIR}/udpFeeder" 127.0.0.1 5001 6 > /tmp/integ_feed.log 2>&1 &
FEEDER_PID=$!
sleep 5
if kill -0 "${PM_PID}" 2>/dev/null; then
SMOKE_OK=1
echo ">>> Smoke test PASSED (pPowerManger alive after 5s)"
else
echo ">>> Smoke test FAILED (pPowerManger exited)" >&2
fi
wait "${FEEDER_PID}" 2>/dev/null || true
# 集成校验:UDP 反馈 → SQLite 落行
if [ "${SMOKE_OK}" = "1" ]; then
if PM_DB="${BIN_DIR}/power_data.db" python3 "${SRC_DIR}/test/integration/check_db.py" > /tmp/integ_check.log 2>&1; then
INTEG_OK=1
echo ">>> Integration test PASSED (UDP->DB rows)"
else
echo ">>> Integration test FAILED" >&2
cat /tmp/integ_feed.log >&2
cat /tmp/integ_check.log >&2
echo "--- pPowerManger 日志末尾 ---" >&2
tail -15 /tmp/pm_smoke.log >&2 2>/dev/null || true
echo "--- power_data.db 表 ---" >&2
PM_DB="${BIN_DIR}/power_data.db" python3 -c "
import sqlite3,os,sys
p=os.environ['PM_DB']
print('exists:', os.path.exists(p))
if os.path.exists(p):
c=sqlite3.connect(p); cur=c.cursor()
cur.execute(\"SELECT name FROM sqlite_master WHERE type='table'\")
print('tables:', [r[0] for r in cur.fetchall()]); c.close()
" >&2 2>/dev/null || true
fi
INTEG_TIME=$(( $(now_ms) - t1 ))
fi
kill "${PM_PID}" "${MOOSDB_PID}" 2>/dev/null || true
wait "${PM_PID}" "${MOOSDB_PID}" 2>/dev/null || true
SMOKE_TIME=$(( $(now_ms) - t0 ))
cd "${SRC_DIR}"
fi
#-----------------------------------------------------------------------
# 5. Generate visual test report (markdown + junit.xml)
#-----------------------------------------------------------------------
mark() { [ "$1" = "1" ] && echo "✅ 通过" || echo "❌ 失败"; }
ALL_OK=0
[ "${BUILD_OK}" = "1" ] && [ "${SQLITE_OK}" = "1" ] && [ "${PMTESTS_OK}" = "1" ] && [ "${SMOKE_OK}" = "1" ] && ALL_OK=1
REPORT="${REPORT_DIR}/test-report.md"
: > "$REPORT"
md() { printf '%s\n' "$*" >> "$REPORT"; }
md "## H100PowerManger CI 测试报告"
md ""
md "**总体结论: $([ "${ALL_OK}" = "1" ] && echo "✅ 全部通过" || echo "❌ 存在失败")**"
md ""
md "| 步骤 | 结果 | 耗时 |"
md "| :--- | :--- | :--- |"
md "| 构建 (${ARCH}) | $(mark ${BUILD_OK}) | ${BUILD_TIME}s |"
md "| 单元测试 SQLiteTest | $(mark ${SQLITE_OK}) | ${SQLITE_TIME}s |"
md "| 单元测试 PowerMangerTests (gtest) | $(mark ${PMTESTS_OK}) | ${PMTESTS_TIME}s |"
md "| 冒烟测试 MOOSDB+pPowerManger | $(mark ${SMOKE_OK}) | ${SMOKE_TIME}s |"
md "| 集成测试 UDP反馈→SQLite | $([ "${INTEG_OK}" = "1" ] && echo "✅ 通过" || echo "⚠️ 已知问题") | ${INTEG_TIME}s |"
md ""
if [ "${ALL_OK}" != "1" ]; then
md "## 失败详情"
md ""
if [ "${BUILD_OK}" != "1" ]; then
md "### 构建 (${ARCH}) — ❌"
md ""
md '<details><summary>make.log 末尾</summary>'
md ""
md '```text'
tail -30 /tmp/make.log >> "$REPORT" 2>/dev/null || true
md '```'
md '</details>'
md ""
fi
if [ "${SQLITE_OK}" != "1" ]; then
md "### 单元测试 SQLiteTest — ❌"
md ""
md '<details><summary>SQLiteTest 输出末尾</summary>'
md ""
md '```text'
tail -30 /tmp/sqlitetest.log >> "$REPORT" 2>/dev/null || true
md '```'
md '</details>'
md ""
fi
if [ "${PMTESTS_OK}" != "1" ]; then
md "### 单元测试 PowerMangerTests — ❌"
md ""
md '<details><summary>PowerMangerTests 输出末尾</summary>'
md ""
md '```text'
tail -40 /tmp/pmtests.log >> "$REPORT" 2>/dev/null || true
md '```'
md '</details>'
md ""
fi
if [ "${SMOKE_OK}" != "1" ]; then
md "### 冒烟测试 MOOSDB+pPowerManger — ❌"
md ""
md '<details><summary>pPowerManger 日志末尾</summary>'
md ""
md '```text'
tail -20 /tmp/pm_smoke.log >> "$REPORT" 2>/dev/null || true
md '```'
md ''
md '<details><summary>MOOSDB 日志末尾</summary>'
md ""
md '```text'
tail -10 /tmp/moosdb_smoke.log >> "$REPORT" 2>/dev/null || true
md '```'
md '</details>'
md ""
fi
if [ "${INTEG_OK}" != "1" ]; then
md "### 集成测试 UDP反馈→SQLite — ⚠️ 已知问题(不阻塞 CI)"
md ""
md "检测到 pPowerManger 在接收 CCU UDP 反馈时抛出 \`std::bad_alloc\`(收到 182 字节消息但处理崩溃),未写入数据库。此问题需要排查(可能为内存/线程安全问题),**按“只测不改”原则不在本次修复**。"
md ""
md '<details><summary>feed 日志末尾</summary>'
md ""
md '```text'
tail -20 /tmp/integ_feed.log >> "$REPORT" 2>/dev/null || true
md '```'
md ''
md '<details><summary>check_db 日志末尾</summary>'
md ""
md '```text'
tail -10 /tmp/integ_check.log >> "$REPORT" 2>/dev/null || true
md '```'
md ''
md '<details><summary>pPowerManger 日志末尾(bad_alloc)</summary>'
md ""
md '```text'
tail -30 /tmp/pm_smoke.log >> "$REPORT" 2>/dev/null || true
md '```'
md '</details>'
md ""
fi
fi
md "---"
md ""
md "- 架构: \`${ARCH}\`"
md "- Commit: \`${COMMIT}\`"
md "- 时间: ${BUILD_DATE}"
if [ -n "${RUN_LINK}" ]; then
md "- 运行: [${RUN_LINK}](${RUN_LINK})"
fi
# --- JUnit XML report(汇总 gtest 单元测试 + 构建 + 冒烟 + 集成) ---
cat > "${REPORT_DIR}/junit.xml" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="H100PowerManger" tests="5" failures="$([ "${ALL_OK}" = "1" ] && echo 0 || echo 1)" time="$((BUILD_TIME+SQLITE_TIME+PMTESTS_TIME+SMOKE_TIME+INTEG_TIME))">
<testsuite name="H100PowerManger CI" tests="5" failures="$([ "${ALL_OK}" = "1" ] && echo 0 || echo 1)" time="$((BUILD_TIME+SQLITE_TIME+PMTESTS_TIME+SMOKE_TIME+INTEG_TIME))">
<testcase name="build_${ARCH}" classname="ci.build" time="${BUILD_TIME}">
$([ "${BUILD_OK}" = "1" ] || echo "<failure message=\"build failed\"/>")
</testcase>
<testcase name="SQLiteTest" classname="ci.unit" time="${SQLITE_TIME}">
$([ "${SQLITE_OK}" = "1" ] || echo "<failure message=\"SQLiteTest failed\"/>")
</testcase>
<testcase name="PowerMangerTests" classname="ci.unit" time="${PMTESTS_TIME}">
$([ "${PMTESTS_OK}" = "1" ] || echo "<failure message=\"PowerMangerTests failed\"/>")
</testcase>
<testcase name="SmokeTest_MOOSDB_pPowerManger" classname="ci.smoke" time="${SMOKE_TIME}">
$([ "${SMOKE_OK}" = "1" ] || echo "<failure message=\"smoke test failed\"/>")
</testcase>
<testcase name="IntegrationTest_UDP_To_SQLite" classname="ci.integration" time="${INTEG_TIME}">
$([ "${INTEG_OK}" = "1" ] || echo "<skipped message=\"known issue: bad_alloc on UDP feedback handling\"/>")
</testcase>
</testsuite>
</testsuites>
EOF
echo ">>> Reports generated in ${REPORT_DIR}:"
ls -la "${REPORT_DIR}"
#-----------------------------------------------------------------------
# 6. Exit status
#-----------------------------------------------------------------------
[ "${ALL_OK}" = "1" ]
-15
View File
@@ -1,15 +0,0 @@
#!/bin/bash
rm -rf build/*
rm -rf lib/*
rm -rf bin/p*
rm -f .DS_Store
rm -f missions/*/.LastOpenedMOOSLogDirectory
find . -name '.DS_Store' -print -exec rm -rfv {} \;
find . -name '*~' -print -exec rm -rfv {} \;
find . -name '#*' -print -exec rm -rfv {} \;
find . -name '*.moos++' -print -exec rm -rfv {} \;
find . -name 'MOOSLog*' -print -exec rm -rfv {} \;
-113
View File
@@ -1,113 +0,0 @@
src/pPowerManger/main.cpp:0:0: information: Too many #ifdef configurations - cppcheck only checks 12 of 78 configurations. Use --force to check all configurations. [toomanyconfigs]
^
src/pPowerManger/systemData.h:524:22: error: Array 'reserved2[1]' accessed at index 1, which is out of bounds. [arrayIndexOutOfBounds]
reserved2[i] = data.reserved2[i];
^
src/pPowerManger/systemData.h:523:23: note: Assuming that condition 'i<2' is not redundant
for(int i=0; i<2; i++) {
^
src/pPowerManger/systemData.h:524:22: note: Array index out of bounds
reserved2[i] = data.reserved2[i];
^
src/pPowerManger/systemData.h:524:42: error: Array 'data.reserved2[1]' accessed at index 1, which is out of bounds. [arrayIndexOutOfBounds]
reserved2[i] = data.reserved2[i];
^
src/pPowerManger/systemData.h:523:23: note: Assuming that condition 'i<2' is not redundant
for(int i=0; i<2; i++) {
^
src/pPowerManger/systemData.h:524:42: note: Array index out of bounds
reserved2[i] = data.reserved2[i];
^
src/pPowerManger/upmsg/uPower_pmState.h:14:9: error: Using 'memset' on struct that contains a 'std::vector'. [memsetClass]
memset(&state, 0, sizeof(state));
^
src/pPowerManger/logc/loguru.hpp:666:3: warning: Member variable 'LogScopeRAII::_verbosity' is not initialized in the constructor. [uninitMemberVar]
LogScopeRAII() : _file(nullptr) {} // No logging
^
src/pPowerManger/logc/loguru.hpp:666:3: warning: Member variable 'LogScopeRAII::_line' is not initialized in the constructor. [uninitMemberVar]
LogScopeRAII() : _file(nullptr) {} // No logging
^
src/pPowerManger/logc/loguru.hpp:666:3: warning: Member variable 'LogScopeRAII::_indent_stderr' is not initialized in the constructor. [uninitMemberVar]
LogScopeRAII() : _file(nullptr) {} // No logging
^
src/pPowerManger/logc/loguru.hpp:666:3: warning: Member variable 'LogScopeRAII::_start_time_ns' is not initialized in the constructor. [uninitMemberVar]
LogScopeRAII() : _file(nullptr) {} // No logging
^
src/pPowerManger/logc/loguru.hpp:666:3: warning: Member variable 'LogScopeRAII::_name' is not initialized in the constructor. [uninitMemberVar]
LogScopeRAII() : _file(nullptr) {} // No logging
^
src/pPowerManger/sqlit3/SQLite.h:13:5: style: Class 'SQLite' has a constructor with 1 argument that is not explicit. [noExplicitConstructor]
SQLite(const std::string& dbName);
^
src/pPowerManger/UpperCommManager.h:52:5: style: Class 'UpperCommManager' has a constructor with 1 argument that is not explicit. [noExplicitConstructor]
UpperCommManager(PowerManger* powerManager);
^
src/pPowerManger/httpserver/httpserver.h:20:5: style: Class 'HttpServer' has a constructor with 1 argument that is not explicit. [noExplicitConstructor]
HttpServer(int port = 8000);
^
src/pPowerManger/fsm/PowerManagerFsm.hpp:275:10: style: The function 'react' overrides a function in a base class but is not marked with a 'override' specifier. [missingOverride]
void react(FaultEvent const &);
^
src/pPowerManger/fsm/PowerManagerFsm.hpp:90:18: note: Virtual function in base class
virtual void react(FaultEvent const &e);
^
src/pPowerManger/fsm/PowerManagerFsm.hpp:275:10: note: Function in derived class
void react(FaultEvent const &);
^
src/pPowerManger/udpcomm/udpComm.h:54:31: style: C-style pointer casting [cstyleCast]
udpScoket->iSendMessageTo((void*) msg, size, ccuPort, ccuHost);
^
src/pPowerManger/upmsg/uPower_disSysState.h:34:42: style: C-style pointer casting [cstyleCast]
dis["HVolBusBreaker_uint16"] = *(unsigned short*)pdis1;
^
src/pPowerManger/upmsg/uPower_disSysState.h:35:42: style: C-style pointer casting [cstyleCast]
dis["HVolABusBreaker_unit16"] = *(unsigned short*)pdis2;
^
src/pPowerManger/upmsg/uPower_disSysState.h:36:42: style: C-style pointer casting [cstyleCast]
dis["HVolBBusBreaker_unit16"] = *(unsigned short*)pdis3;
^
src/pPowerManger/upmsg/uPower_disSysState.h:37:42: style: C-style pointer casting [cstyleCast]
dis["LVolBusBreaker_uint16"] = *(unsigned short*)pdis4;
^
src/pPowerManger/upmsg/uExternComm_setDeviceSwitch.h:29:20: style: Variable 'cmd.id' is reassigned a value before the old one has been used. [redundantAssignment]
cmd.id = root["id_uint8"].asUInt();
^
src/pPowerManger/upmsg/uExternComm_setDeviceSwitch.h:24:20: note: cmd.id is assigned
cmd.id = -1;
^
src/pPowerManger/upmsg/uExternComm_setDeviceSwitch.h:29:20: note: cmd.id is overwritten
cmd.id = root["id_uint8"].asUInt();
^
src/pPowerManger/upmsg/uExternComm_setOpCondition.h:34:13: style: Statements following 'return' will never be executed. [unreachableCode]
LOG_F(INFO, "上位机指令:opCondition: %d", root["state_uint8"].asUInt());
^
src/pPowerManger/udpcomm/udpComm.h:45:38: performance: Function parameter 'cmd' should be passed by const reference. [passedByValue]
bool sendCcuColCmd(const ccuColCmd cmd);
^
src/pPowerManger/fsm/PowerManagerFsm.hpp:49:27: performance: Function parameter 'eventName' should be passed by const reference. [passedByValue]
TestEvent(std::string eventName, unsigned int type,unsigned int eventId) : eventName(eventName), eventId(eventId),type(type) {};
^
src/pPowerManger/LowerCommManager.h:85:41: performance: Function parameter 'cmd' should be passed by const reference. [passedByValue]
bool sendCcuCommand(const ccuColCmd cmd);
^
src/pPowerManger/upmsg/uDevice_secdistState.h:195:61: style: Parameter 'state' can be declared as reference to const [constParameter]
std::string buildSecdistStateToJson(DeviceSecdistState &state){
^
src/pPowerManger/upmsg/uDevice_secdistState.h:435:39: style: Parameter 'driver' can be declared as reference to const [constParameter]
std::string buildMsg(DriverTable &driver){
^
src/pPowerManger/upmsg/uPlan_taskStart.h:29:20: error: Uninitialized variable: mission [uninitvar]
return mission;
^
src/pPowerManger/upmsg/uExternComm_setDeviceSwitch.h:39:16: warning: Uninitialized variable: cmd.cmd [uninitvar]
return cmd;
^
src/pPowerManger/upmsg/uExternComm_setDeviceSwitch.h:34:40: note: Assuming condition is false
if (root.isMember("cmd_uint8") && root["cmd_uint8"].isUInt()) {
^
src/pPowerManger/upmsg/uExternComm_setDeviceSwitch.h:39:16: note: Uninitialized variable: cmd.cmd
return cmd;
^
nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingInclude]
Binary file not shown.
Binary file not shown.
Binary file not shown.
-2
View File
@@ -1,2 +0,0 @@
scp root@192.168.0.223:/root/work/moos_ws/moos-ivp-extend/missions/union_test/power_data.db ./data/power_data.db
scp root@192.168.0.223:/root/work/moos_ws/moos-ivp-extend/missions/union_test/pPowerManger.log ./data/pPowerManger.log
-69
View File
@@ -1,69 +0,0 @@
VERSION ""
NS_ :
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
CAT_DEF_
CAT_
FILTER
BA_DEF_DEF_
EV_DATA_
ENVVAR_DATA_
SGTYPE_
SGTYPE_VAL_
BA_DEF_SGTYPE_
BA_SGTYPE_
SIG_TYPE_REF_
VAL_TABLE_
SIG_GROUP_
SIG_VALTYPE_
SIGTYPE_VALTYPE_
BO_TX_BU_
BA_DEF_REL_
BA_REL_
BA_DEF_DEF_REL_
BU_SG_REL_
BU_EV_REL_
BU_BO_REL_
SG_MUL_VAL_
BS_:
BU_:
BO_ 2432696577 HostToCompositeControl: 8 Vector__XXX
SG_ Reserved1 : 56|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ HostStatus : 48|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ HostHeartbeat : 40|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ PowerBatteryStartStop : 32|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterBatteryStartStop : 24|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterBatteryChargeFlag : 16|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellCommand : 8|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellModeSet : 0|8@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2432696833 CompositeControlToHost: 8 Vector__XXX
SG_ Reserved2 : 56|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ PowerBatteryTotalEnergy : 48|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterBatteryTotalEnergy : 40|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ PowerBatteryEmergency : 32|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterBatteryEmergency : 24|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellFaultLevel : 16|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellStatus : 8|8@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellSystemMode : 0|8@1+ (1,0) [0|0] "" Vector__XXX
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="H100PowerManger" tests="5" failures="0" time="1741">
<testsuite name="H100PowerManger CI" tests="5" failures="0" time="1741">
<testcase name="build_aarch64" classname="ci.build" time="1725">
</testcase>
<testcase name="SQLiteTest" classname="ci.unit" time="0">
</testcase>
<testcase name="PowerMangerTests" classname="ci.unit" time="1">
</testcase>
<testcase name="SmokeTest_MOOSDB_pPowerManger" classname="ci.smoke" time="9">
</testcase>
<testcase name="IntegrationTest_UDP_To_SQLite" classname="ci.integration" time="6">
</testcase>
</testsuite>
</testsuites>
-11
View File
@@ -1,11 +0,0 @@
To Run this mission, make sure that the following two bin
directories are in your path:
moos-ivp/ivp/bin/
moos-ivp-extend/bin/
Then launch the mission by:
pAntler alder.moos
-33
View File
@@ -1,33 +0,0 @@
//-------- FILE: alder.bhv -------------
initialize DEPLOY = false
initialize RETURN = false
//----------------------------------------------
Behavior = BHV_SimpleWaypoint
{
name = waypt_to_point
pwt = 100
condition = RETURN = false
condition = DEPLOY = true
endflag = RETURN = true
speed = 2.0 // meters per second
radius = 8.0
ptx = 100
pty = -50
}
//----------------------------------------------
Behavior = BHV_Waypoint
{
name = waypt_return
pwt = 100
condition = (RETURN = true)
condition = (DEPLOY = true)
speed = 2.0
radius = 8.0
point = 0,0
}
-159
View File
@@ -1,159 +0,0 @@
// Level 2 Configuration file: M. Benjamin
ServerHost = localhost
ServerPort = 9000
Simulator = true
Community = alder
MOOSTimeWarp = 1
LatOrigin = 43.825300
LongOrigin = -70.330400
//------------------------------------------
// Antler configuration block
ProcessConfig = ANTLER
{
MSBetweenLaunches = 200
Run = MOOSDB @ NewConsole = false
Run = uSimMarine @ NewConsole = false
Run = pNodeReporter @ NewConsole = false
Run = pMarinePID @ NewConsole = false
Run = pMarineViewer @ NewConsole = false
Run = uProcessWatch @ NewConsole = false
Run = pHelmIvP @ NewConsole = false
Run = pOdometry @ NewConsole = false
}
//------------------------------------------
// uSimMarine config block
ProcessConfig = uSimMarine
{
AppTick = 10
CommsTick = 10
START_X = 0
START_Y = 0
START_SPEED = 0
START_HEADING = 180
PREFIX = NAV
}
//------------------------------------------
// uProcessWatch config block
ProcessConfig = uProcessWatch
{
AppTick = 4
CommsTick = 4
term_report_interval = 0.8
summary_wait = 5
nowatch = uXMS*
nowatch = uPokeDB*
nowatch = uTermCommand*
watch_all = true
}
//------------------------------------------
// pHelmIvP config block
ProcessConfig = pHelmIvP
{
AppTick = 4
CommsTick = 4
Behaviors = alder.bhv
Verbose = quiet
Domain = course:0:359:360
Domain = speed:0:4:21
IVP_BEHAVIOR_DIR = ../../lib
//IVP_BEHAVIOR_DIR = /Users/mikerb/Research/moos-ivp-extend/trunk/lib
ok_skew = any
start_in_drive = false
}
//------------------------------------------
// pMarinePID config block
ProcessConfig = pMarinePID
{
AppTick = 20
CommsTick = 20
VERBOSE = true
DEPTH_CONTROL = false
// Yaw PID controller
YAW_PID_KP = 0.5
YAW_PID_KD = 0.0
YAW_PID_KI = 0.0
YAW_PID_INTEGRAL_LIMIT = 0.07
// Speed PID controller
SPEED_PID_KP = 1.0
SPEED_PID_KD = 0.0
SPEED_PID_KI = 0.0
SPEED_PID_INTEGRAL_LIMIT = 0.07
//MAXIMUMS
MAXRUDDER = 100
MAXTHRUST = 100
// A non-zero SPEED_FACTOR overrides use of SPEED_PID
// Will set DESIRED_THRUST = DESIRED_SPEED * SPEED_FACTOR
SPEED_FACTOR = 20
}
//------------------------------------------
// pMarineViewer config block
ProcessConfig = pMarineViewer
{
AppTick = 4
CommsTick = 4
TIFF_FILE = forrest19.tif
set_pan_x = -90
set_pan_y = -280
zoom = 0.65
vehicle_shape_scale = 1.5
hash_delta = 50
hash_shade = 0.4
hash_viewable = true
scope = ODOMETRY_DIST
// Appcast configuration
appcast_height = 75
appcast_width = 30
appcast_viewable = true
appcast_color_scheme = indigo
nodes_font_size = medium
procs_font_size = medium
appcast_font_size = small
BUTTON_ONE = DEPLOY # DEPLOY=true
BUTTON_ONE = MOOS_MANUAL_OVERIDE=false # RETURN=false
BUTTON_TWO = RETURN # RETURN=true
}
//------------------------------------------
// pNodeReporter config block
ProcessConfig = pNodeReporter
{
AppTick = 2
CommsTick = 2
VESSEL_TYPE = KAYAK
}
-41
View File
@@ -1,41 +0,0 @@
//-------- FILE: alder.bhv -------------
initialize DEPLOY = false
initialize RETURN = false
//----------------------------------------------
Behavior = BHV_SimpleWaypoint
{
name = waypt_to_point
pwt = 100
condition = RETURN = false
condition = DEPLOY = true
endflag = RETURN = true
speed = 2.0 // meters per second
radius = 8.0
ptx = 100
pty = -50
}
//----------------------------------------------
Behavior = BHV_SimpleWaypoint
{
name = waypt_return
pwt = 100
condition = (RETURN = true)
condition = (DEPLOY = true)
speed = 2.0
radius = 8.0
ptx = 0
pty = 0
}
//----------------------------------------------
Behavior = BHV_HSLine
{
name = hsline
time_on_leg = 20
}
-120
View File
@@ -1,120 +0,0 @@
// MOOS file
// 板卡 (RK3588 / Ubuntu 20.04) 专用 mission 配置
// 六个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU / pCanBridge / pMotor)
// 共用本文件。
// 由 scripts/deploy.sh 推送到板卡 /root/work/h100/missions/h100.moos
ServerHost = localhost
ServerPort = 9000
Community = h100
ProcessConfig = pPowerManger
{
AppTick = 4
CommsTick = 4
log_level = INFO
// 日志文件路径(目录需已存在,由 deploy.sh 创建)
logpath = /root/work/h100/data/pPowerManger.log
// 数据库存储路径(目录需已存在,由 deploy.sh 创建)
dbpath = /root/work/h100/data/power_data.db
// CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000
ccuhost = 127.0.0.1
ccuport = 7000
// 本地输入端口(接收 CCU 数据);默认 5001。
// 5001 让给 pCCU 的 rcu 链路接收真实CCU(192.168.0.140) 固定上报的
// 配电反馈,故本进程接收端口改为 5002(pCCU 转发的反馈与状态报文发往此处)
iport = 5002
}
ProcessConfig = pPowerMangerHost
{
AppTick = 4
CommsTick = 4
WEB_PORT = 18080
// 反馈落盘(SQLite),路径可配置;目录不存在会自动创建
DB_FILE = /root/work/h100/data/feedback_data.db
FB_LOG_ENABLED = true
FB_LOG_KEEP_SECONDS = 86400 // 历史保留时长(秒),24h
FB_LOG_PRUNE_INTERVAL_SECONDS = 60 // 清理检查周期(秒)
}
ProcessConfig = pCCU
{
AppTick = 4
CommsTick = 4
// FC 链路(本机监听 FC 状态;发送 FC 控制指令给燃料电池控制器)
fc_local_port = 6000
fc_remote_ip = 192.168.1.162
fc_remote_port = 7000
// PM 链路(监听 pPowerManger 指令;向 pPowerManger 周期发送 PM 状态)
// pPowerManger 与 pCCU 同机部署;pPowerManger 接收端口为 5002(iport),
// 故 PM 状态发送目标用 127.0.0.1:5002。
pm_local_port = 7000
pm_remote_ip = 127.0.0.1
pm_remote_port = 5002
//======== 真实CCU 桥接链路(配电协议) ========
// 真实CCU(192.168.0.140) 设备侧固定上报到 192.168.0.223:5001
// (与 pPowerManger 默认 CCU 配置一致:CCUPORT=7000 为其监听口,
// 上报目标为控制主机 5001),设备侧不可修改。
// 故 pCCU 的 rcu 链路直接绑定 5001 接收其反馈:
// 配电反馈 0x0005~0x0008:原帧转发 pPowerManger(127.0.0.1:5002) + 网页显示
// 配电指令 0x0001~0x0004:转发目标为真实CCU 监听口 192.168.0.140:7000
// 其余帧(旧协议 0x20 0x20 状态帧、无效帧)落库忽略
rcu_enable = true
rcu_local_port = 5001
rcu_remote_ip = 192.168.0.140
rcu_remote_port = 7000
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
// 经 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 多通道透传:CANET(TCP Server 模式)
// 每通道一条独立 TCP 连接;工作端口:CAN0=4001,CAN1=4002,...,CAN7=4008
channel = CAN0,192.168.0.222,4001
channel = CAN1,192.168.0.222,4002
// channel = CAN1,192.168.0.222,4002
// CAN_TX 下行默认通道(pCCU 未指定 m_sSrcAux 时使用;可省略)
default_channel = CAN0
// CAN 帧 SQLite 落库路径(can_frame 表,channel 列区分通道)
dbpath = /root/work/h100/data/pCanBridge_data.db
}
ProcessConfig = pMotor
{
AppTick = 4
CommsTick = 4
// 推进电机控制器:CAN1 通道(经 pCanBridge 透传,docs/推进电机通信协议20260321.docx)
// 下行 500ms 周期指令帧 0x18EF2010(网页 /api/motor_cmd 控制启停/复位/转速),
// 上行订阅 CAN_0x* 解码故障/转速/母线电压/温度,网页 18081 展示
can_channel = CAN1
// 冗余 CAN 通道(配置后指令帧同内容发 0x18EF2011;留空=不发送)
// red_channel = CAN2
cmd_period_ms = 500
reset_hold_sec = 5
web_port = 18081
web_enable = true
logpath = /root/work/h100/data/pMotor.log
}
-10
View File
@@ -1,10 +0,0 @@
ProcessConfig = pPowerManger
{
AppTick = 4
CommsTick = 4
// CCU 地址;不配置则默认 127.0.0.1:7000
// ccuhost = 127.0.0.1
// ccuport = 7000
// 本地输入端口(接收 CCU 数据);不配置则默认 5001
// iport = 5001
}
-54
View File
@@ -1,54 +0,0 @@
// MOOS file
ServerHost = localhost
ServerPort = 9000
//------------------------------------------
// Antler configuration block
ProcessConfig = ANTLER
{
MSBetweenLaunches = 200
Run = MOOSDB @ NewConsole = false
Run = pXRelay @ NewConsole = true ~ pXRelay_APPLES
Run = pXRelay @ NewConsole = true ~ pXRelay_PEARS
Run = uXMS @ NewConsole = true
}
//------------------------------------------
// First pXRelay configuration block
ProcessConfig = pXRelay_APPLES
{
AppTick = 4
CommsTick = 4
OUTGOING_VAR = APPLES
INCOMING_VAR = PEARS
}
//------------------------------------------
// Second pXRelay configuration block
ProcessConfig = pXRelay_PEARS
{
AppTick = 4
CommsTick = 4
OUTGOING_VAR = PEARS
INCOMING_VAR = APPLES
}
//------------------------------------------
// uXMS configuration block
ProcessConfig = uXMS
{
AppTick = 4
CommsTick = 4
VAR = PEARS, PEARS_ITER_HZ, PEARS_POST_HZ
VAR = APPLES, APPLES_ITER_HZ, APPLES_POST_HZ
}
-232
View File
@@ -1,232 +0,0 @@
VERSION ""
NS_ :
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
CAT_DEF_
CAT_
FILTER
BA_DEF_DEF_
EV_DATA_
ENVVAR_DATA_
SGTYPE_
SGTYPE_VAL_
BA_DEF_SGTYPE_
BA_SGTYPE_
SIG_TYPE_REF_
VAL_TABLE_
SIG_GROUP_
SIG_VALTYPE_
SIGTYPE_VALTYPE_
BO_TX_BU_
BA_DEF_REL_
BA_REL_
BA_DEF_DEF_REL_
BU_SG_REL_
BU_EV_REL_
BU_BO_REL_
SG_MUL_VAL_
BS_:
BU_:
BO_ 2483135745 CircuitBreakerControl1: 8 Vector__XXX
SG_ CoolingWaterValveControl : 26|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC5PowerControl : 24|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ ReserveBreakerControl : 22|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ TYZReserveBreakerControl : 20|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ FBReserveBreakerControl : 18|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternSwitch2BreakerControl : 16|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternSwitch1BreakerControl : 14|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternFT45BreakerControl : 12|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowPanelBreakerControl : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC5BreakerControl : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryMeterBreakerControl : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ MotorBreakerControl : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryBreakerControl : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellBreakerControl : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2483201281 CircuitBreakerControl2: 8 Vector__XXX
SG_ OpenWaterMechanismControl : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowRudderControl : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ XCBreakerControl : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ MastRudderControl : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ OpeningMechanismControl : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowFloatAdjustControl : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2483266817 CircuitBreakerControl3: 8 Vector__XXX
SG_ SternEmergencyBatteryControl : 12|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve2Control : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve1Control : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ HighVoltageControllerControl : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ U4BreakerControl : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowLowVoltageControl : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryAccessControl : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2483332353 CircuitBreakerControl4: 8 Vector__XXX
SG_ BowEmergencyBatteryControl : 14|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve4Control : 12|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve3Control : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowJettisonControl : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ U5BreakerControl : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ U3BreakerControl : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ U2BreakerControl : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ U1BreakerControl : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560950529 CircuitBreakerFeedback: 8 Vector__XXX
SG_ DCDC_Temperature : 48|8@1- (1,0) [0|0] "" Vector__XXX
SG_ EmergencyPowerLoss : 43|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterPowerLoss : 42|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ BusA_InsulationLow : 41|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ PowerBus_InsulationLow : 40|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ CoolingPump2_Running : 38|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ CoolingPump1_Running : 37|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ CoolingWaterLeak : 36|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ CoolingPump2_Fault : 35|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ CoolingPump1_Fault : 34|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_Running : 33|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_CommFault : 32|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_InternalFault : 31|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_ShortCircuit : 30|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_InputUnderVoltage : 29|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_InputOverVoltage : 28|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_UnderVoltage : 27|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_OverVoltage : 26|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_OverCurrent : 25|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC_OverTemp : 24|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ ReserveBreaker_Status : 22|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ TYZReserveBreaker_Status : 20|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ FBReserveBreaker_Status : 18|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternSwitch2Breaker_Status : 16|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternSwitch1Breaker_Status : 14|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternFT45Breaker_Status : 12|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowPanelBreaker_Status : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC5Breaker_Status : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryMeterBreaker_Status : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ MotorBreaker_Status : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryBreaker_Status : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellBreaker_Status : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560950785 PowerBusFeedback: 8 Vector__XXX
SG_ BusA_Voltage : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ PowerBus_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCDC5_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ PowerBus_Voltage : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560951041 CurrentFeedback: 8 Vector__XXX
SG_ BowPowerBox_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryMeter_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BusA_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ PropulsionMotor_Current : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560951297 SternCurrentFeedback: 8 Vector__XXX
SG_ Reserve_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternSwitch1_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ TYZReserve_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternFT45_Current : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560951553 SternAdditionalFeedback: 8 Vector__XXX
SG_ CoolingWater_Pressure : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ FBReserve_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve2_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ SternSwitch2_Current : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560951809 BowBreakerFeedback: 8 Vector__XXX
SG_ WaterLeakAlarm : 14|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyPowerLoss : 13|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterPowerLoss : 12|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ OpenWaterMechanismBreaker_Status : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowRudderBreaker_Status : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ XCZBreaker_Status : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ MastRudderBreaker_Status : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ OpeningMechanismBreaker_Status : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowFT13Breaker_Status : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560952065 BusBFeedback: 8 Vector__XXX
SG_ OpeningMechanism_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowFT13_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BusB_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BusB_Voltage : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560952321 BowMechanismFeedback: 8 Vector__XXX
SG_ OpenWaterMechanism_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowRudder_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ XCZ_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ MastRudder_Current : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560952577 MeterBreakerFeedback: 8 Vector__XXX
SG_ MeterBusInsulationLow : 18|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyPowerLoss : 17|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterPowerLoss : 16|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyBattery2Breaker_Status : 12|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve2Breaker_Status : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve1Breaker_Status : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCC1Breaker_Status : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit4MeterBreaker_Status : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowLowVoltageBoxBreaker_Status : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryMeterBreaker_Status : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560952833 MeterBusFeedback: 8 Vector__XXX
SG_ Reserve3_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ DCC1Meter_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowLowVoltageBox_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterBus_Voltage : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560953089 EmergencyBatteryFeedback: 8 Vector__XXX
SG_ Reserve4_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit4Meter_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BatteryMeter_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyBattery2_Current : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560953345 EmergencyBus2Feedback: 8 Vector__XXX
SG_ PLCPower_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ FuelCellEmergency_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ EnergySystemEmergency_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyBus2_Voltage : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560953601 UnitBreakerFeedback: 8 Vector__XXX
SG_ WaterLeakAlarm : 18|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyPowerLoss : 17|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterPowerLoss : 16|1@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyBattery1Breaker_Status : 14|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve6Breaker_Status : 12|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Reserve5Breaker_Status : 10|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowPZBreaker_Status : 8|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit5Breaker_Status : 6|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit3Breaker_Status : 4|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit2Breaker_Status : 2|2@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit1Breaker_Status : 0|2@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560953857 MeterBus2Feedback: 8 Vector__XXX
SG_ Unit2_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit1_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterBus2_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ MeterBus2_Voltage : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560954113 UnitCurrentFeedback: 8 Vector__XXX
SG_ EmergencyBattery1_Current : 48|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ BowPZ_Current : 32|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit5_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ Unit3_Current : 0|16@1+ (1,0) [0|0] "" Vector__XXX
BO_ 2560954369 EmergencyBus1Feedback: 8 Vector__XXX
SG_ ControlPower24V_Current : 16|16@1+ (1,0) [0|0] "" Vector__XXX
SG_ EmergencyBus1_Voltage : 0|16@1+ (1,0) [0|0] "" Vector__XXX
-401
View File
@@ -1,401 +0,0 @@
#!/bin/bash
if [ -d "$2$1" ]; then
echo "$2$1 already exists... quitting."
exit 1
fi
if [ -z "$1" ] ; then
echo "GenMOOSApp: usage: $0 [app-name] [prefix]"
exit 0
fi
if [ -z "$2" ] ; then
echo "GenMOOSApp: usage: $0 [app-name] [prefix]"
exit 0
fi
#if [ -z "$3" ] ; then
# $3="YOUR-NAME-HERE"
#fi
mkdir $2$1
cd $2$1
cat > CMakeLists.txt <<EOF
#--------------------------------------------------------
# The CMakeLists.txt for: $2$1
# Author(s): $3
#--------------------------------------------------------
SET(SRC
${1}.cpp
${1}_Info.cpp
main.cpp
)
ADD_EXECUTABLE($2$1 \${SRC})
TARGET_LINK_LIBRARIES($2$1
\${MOOS_LIBRARIES}
mbutil
m
pthread)
EOF
cat > ${1}.h <<EOF
/************************************************************/
/* NAME: $3 */
/* ORGN: MIT */
/* FILE: ${1}.h */
/* DATE: */
/************************************************************/
#ifndef ${1}_HEADER
#define ${1}_HEADER
#include "MOOS/libMOOS/MOOSLib.h"
class ${1} : public CMOOSApp
{
public:
${1}();
~${1}();
protected: // Standard MOOSApp functions to overload
bool OnNewMail(MOOSMSG_LIST &NewMail);
bool Iterate();
bool OnConnectToServer();
bool OnStartUp();
protected:
void RegisterVariables();
private: // Configuration variables
private: // State variables
};
#endif
EOF
cat > main.cpp <<EOF
/************************************************************/
/* NAME: $3 */
/* ORGN: MIT */
/* FILE: main.cpp */
/* DATE: */
/************************************************************/
#include <string>
#include "MBUtils.h"
#include "ColorParse.h"
#include "${1}.h"
#include "${1}_Info.h"
using namespace std;
int main(int argc, char *argv[])
{
string mission_file;
string run_command = argv[0];
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();
cout << termColor("green");
cout << "${2}${1} launching as " << run_command << endl;
cout << termColor() << endl;
${1} ${1};
${1}.Run(run_command.c_str(), mission_file.c_str());
return(0);
}
EOF
cat > $2${1}.moos <<EOF
//------------------------------------------------
// ${2}${1} config block
ProcessConfig = ${2}${1}
{
AppTick = 4
CommsTick = 4
}
EOF
cat > ${1}.cpp <<EOF
/************************************************************/
/* NAME: $3 */
/* ORGN: MIT */
/* FILE: ${1}.cpp */
/* DATE: */
/************************************************************/
#include <iterator>
#include "MBUtils.h"
#include "${1}.h"
using namespace std;
//---------------------------------------------------------
// Constructor
${1}::${1}()
{
}
//---------------------------------------------------------
// Destructor
${1}::~${1}()
{
}
//---------------------------------------------------------
// Procedure: OnNewMail
bool ${1}::OnNewMail(MOOSMSG_LIST &NewMail)
{
MOOSMSG_LIST::iterator p;
for(p=NewMail.begin(); p!=NewMail.end(); p++) {
CMOOSMsg &msg = *p;
#if 0 // Keep these around just for template
string key = msg.GetKey();
string comm = msg.GetCommunity();
double dval = msg.GetDouble();
string sval = msg.GetString();
string msrc = msg.GetSource();
double mtime = msg.GetTime();
bool mdbl = msg.IsDouble();
bool mstr = msg.IsString();
#endif
}
return(true);
}
//---------------------------------------------------------
// Procedure: OnConnectToServer
bool ${1}::OnConnectToServer()
{
RegisterVariables();
return(true);
}
//---------------------------------------------------------
// Procedure: Iterate()
// happens AppTick times per second
bool ${1}::Iterate()
{
return(true);
}
//---------------------------------------------------------
// Procedure: OnStartUp()
// happens before connection is open
bool ${1}::OnStartUp()
{
list<string> sParams;
m_MissionReader.EnableVerbatimQuoting(false);
if(m_MissionReader.GetConfiguration(GetAppName(), sParams)) {
list<string>::iterator p;
for(p=sParams.begin(); p!=sParams.end(); p++) {
string line = *p;
string param = tolower(biteStringX(line, '='));
string value = line;
if(param == "foo") {
//handled
}
else if(param == "bar") {
//handled
}
}
}
RegisterVariables();
return(true);
}
//---------------------------------------------------------
// Procedure: RegisterVariables
void ${1}::RegisterVariables()
{
// Register("FOOBAR", 0);
}
EOF
cat > ${1}_Info.h <<EOF
/****************************************************************/
/* NAME: ${3} */
/* ORGN: MIT Cambridge MA */
/* FILE: ${1}_Info.h */
/* DATE: Dec 29th 1963 */
/****************************************************************/
#ifndef ${1}_INFO_HEADER
#define ${1}_INFO_HEADER
void showSynopsis();
void showHelpAndExit();
void showExampleConfigAndExit();
void showInterfaceAndExit();
void showReleaseInfoAndExit();
#endif
EOF
cat > ${1}_Info.cpp <<EOF
/****************************************************************/
/* NAME: ${3} */
/* ORGN: MIT Cambridge MA */
/* FILE: ${1}_Info.cpp */
/* DATE: Dec 29th 1963 */
/****************************************************************/
#include <cstdlib>
#include <iostream>
#include "${1}_Info.h"
#include "ColorParse.h"
#include "ReleaseInfo.h"
using namespace std;
//----------------------------------------------------------------
// Procedure: showSynopsis
void showSynopsis()
{
blk("SYNOPSIS: ");
blk("------------------------------------ ");
blk(" The ${2}${1} application is used for ");
blk(" ");
blk(" ");
blk(" ");
blk(" ");
}
//----------------------------------------------------------------
// Procedure: showHelpAndExit
void showHelpAndExit()
{
blk(" ");
blu("=============================================================== ");
blu("Usage: ${2}${1} file.moos [OPTIONS] ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("Options: ");
mag(" --alias","=<ProcessName> ");
blk(" Launch ${2}${1} with the given process name ");
blk(" rather than ${2}${1}. ");
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 ${2}${1}. ");
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);
}
//----------------------------------------------------------------
// Procedure: showExampleConfigAndExit
void showExampleConfigAndExit()
{
blk(" ");
blu("=============================================================== ");
blu("${2}${1} Example MOOS Configuration ");
blu("=============================================================== ");
blk(" ");
blk("ProcessConfig = ${2}${1} ");
blk("{ ");
blk(" AppTick = 4 ");
blk(" CommsTick = 4 ");
blk(" ");
blk("} ");
blk(" ");
exit(0);
}
//----------------------------------------------------------------
// Procedure: showInterfaceAndExit
void showInterfaceAndExit()
{
blk(" ");
blu("=============================================================== ");
blu("${2}${1} INTERFACE ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" NODE_MESSAGE = src_node=alpha,dest_node=bravo,var_name=FOO, ");
blk(" string_val=BAR ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" Publications are determined by the node message content. ");
blk(" ");
exit(0);
}
//----------------------------------------------------------------
// Procedure: showReleaseInfoAndExit
void showReleaseInfoAndExit()
{
showReleaseInfo("${2}${1}", "gpl");
exit(0);
}
EOF
echo "$2${1} generated"
-5
View File
@@ -1,5 +0,0 @@
This directory may contain developer utility scripts.
The MyGenMOOSApp script is similar to that found in the moos-ivp/trunk/scripts/
directory. Users are encouraged to tailor this one to their own needs.
-285
View File
@@ -1,285 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/build-board.sh
# DESC: 把源码同步到目标板卡 (RK3588 / Ubuntu 22.04),在板卡原生编译
# (aarch64 直编,比本地 QEMU 模拟快约 10 倍),并把产物部署到
# /root/work/h100/bin/,可选重启 systemd 服务。
#
# 用法:
# ./scripts/build-board.sh # 同步+编译+部署(不重启)
# ./scripts/build-board.sh --start # 编译后重启全部服务(含 pCanBridge/pMotor)
# ./scripts/build-board.sh --clean # 板卡上重新 cmake 再编译
# ./scripts/build-board.sh --jobs 4 # 指定并行度(默认板卡 nproc)
#
# 参数:
# --host <ip> 目标主机(默认 192.168.0.223)
# --user <user> 登录用户(默认 root)
# --board-src <dir> 板卡源码目录(默认 /root/work/h100/src)
# --board-bin <dir> 板卡部署目录(默认 /root/work/h100/bin)
# --jobs <n> 编译并行度(默认板卡 nproc)
# --start 编译部署后重启服务
# --clean 先重新 cmake(配置变更时用)
# --ccu-host <ip> 覆盖 mission 中 pPowerManger 的 CCU 地址 (ccuhost)
# --ccu-port <n> 覆盖 mission 中 pPowerManger 的 CCU 端口 (ccuport)
# -h, --help 显示帮助
#
# 前置: 板卡已配免密 SSH,且有编译环境(cmake/g++/MOOS/jsoncpp),
# 源码已能本地编译(本脚本只负责同步+板卡编译)。
#
# !!!! 板卡网络配置(2026-08-26 联调定型,勿随意修改) !!!!
#
# 板卡与 FC 的组网为"单网卡 + /23 掩码"方案(依据《超滑FC和复合管控器
# 通讯协议-0821》1.1.1:CCU 192.168.0.140/23 <-> FC 192.168.1.162):
#
# enP3p49s0 192.168.0.223/23 (255.255.254.0) <- 唯一业务网口,接交换机
# /23 使 192.168.0.x 与 192.168.1.x 同子网直达,
# FC(192.168.1.162) 无需网关即可互通。
# NM 配置档案: "有线连接 1"(静态 manual,已持久化)
# enP4p65s0 已弃用(旧双网卡方案的 FC 专口,物理链路已断)
# 回环 lo pPowerManger(5001) <-> pCCU(7000) 同机 UDP 互通
#
# 关联端口:
# FC 状态: FC:7000 -> 板卡:6000 (pCCU 监听)
# FC 控制: 板卡:6000 -> FC:7000 (pCCU 发送)
# PM 指令: pPowerManger:5001 -> pCCU:7000 (127.0.0.1,
# 见 missions/h100.moos ccuhost/ccuport,默认值在 PowerManger.h CCUHOST)
# PM 状态: pCCU:7000 -> pPowerManger:5001 (127.0.0.1)
#
# 排障提示:
# - 若收不到 FC 状态,先在板卡上 `ethtool enP3p49s0` 看 Link detected,
# 再 `ping 192.168.1.162`;ping 不通通常是物理链路/交换机问题,
# 不要改回 /24 掩码或启用 enP4p65s0 来"修"网络。
# - 改 IP/掩码须用 `nmcli con mod "有线连接 1" ...` 持久化,
# 并保持掩码 /23。
#=======================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
HOST="192.168.0.223"
USER="root"
BOARD_SRC="/root/work/h100/src"
BOARD_BIN="/root/work/h100/bin"
JOBS=""
DO_START=0
DO_CLEAN=0
CCU_HOST=""
CCU_PORT=""
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU pCanBridge pMotor)
info() { printf '\033[1;36m[build-board]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[build-board]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[build-board]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[build-board]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,31p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
#-------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--host) [ $# -ge 2 ] || { err "--host 需要参数"; exit 1; }; HOST="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--board-src) [ $# -ge 2 ] || { err "--board-src 需要参数"; exit 1; }; BOARD_SRC="$2"; shift 2 ;;
--board-bin) [ $# -ge 2 ] || { err "--board-bin 需要参数"; exit 1; }; BOARD_BIN="$2"; shift 2 ;;
--jobs) [ $# -ge 2 ] || { err "--jobs 需要参数"; exit 1; }; JOBS="$2"; shift 2 ;;
--start) DO_START=1; shift ;;
--clean) DO_CLEAN=1; shift ;;
--ccu-host) [ $# -ge 2 ] || { err "--ccu-host 需要参数"; exit 1; }; CCU_HOST="$2"; shift 2 ;;
--ccu-port) [ $# -ge 2 ] || { err "--ccu-port 需要参数"; exit 1; }; CCU_PORT="$2"; shift 2 ;;
-h|--help) usage ;;
*) err "未知参数: $1(见 --help)"; exit 1 ;;
esac
done
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
case "${CCU_PORT}" in
""|*[!0-9]*) [ -z "${CCU_PORT}" ] || { err "--ccu-port 必须是数字端口: ${CCU_PORT}"; exit 1; } ;;
esac
#-------------------------------------------------------------------
# 1. 前置检查
#-------------------------------------------------------------------
if ! ${SSH} "${SSH_TARGET}" 'true' 2>/dev/null; then
err "无法免密 SSH 到 ${SSH_TARGET}"
err "请先配置免密登录: ssh-copy-id ${SSH_TARGET}"
exit 1
fi
ok "SSH 连接正常: ${SSH_TARGET}"
#-------------------------------------------------------------------
# mission 中插入/替换 "key = value"(键已存在则替换,否则插到第一个
# 顶层 '}' 之前,即 pPowerManger 配置块末尾)
#-------------------------------------------------------------------
upsert_moos_param() {
local f="$1" key="$2" val="$3"
if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${f}"; then
sed -i -E "s|^([[:space:]]*)${key}[[:space:]]*=.*|\1${key} = ${val}|" "${f}"
else
awk -v kv=" ${key} = ${val}" \
'!d && /^[[:space:]]*}[[:space:]]*$/ { print kv; d=1 } { print }' \
"${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}"
fi
}
#-------------------------------------------------------------------
# 2. 同步源码到板卡(排除构建产物 / git / 数据)
#-------------------------------------------------------------------
info "同步源码到板卡 ${BOARD_SRC} ..."
rsync -az --delete \
--exclude='build/' \
--exclude='build-arm64/' \
--exclude='build-native/' \
--exclude='build-nosa-test/' \
--exclude='bin/' \
--exclude='.git/' \
--exclude='.cache/' \
--exclude='.kilo/' \
--exclude='.vscode/' \
--exclude='docs/' \
--exclude='reports/' \
--exclude='*.log' \
--exclude='*.db' \
--exclude='CMakeCache.txt' \
"${PROJECT_ROOT}/" "${SSH_TARGET}:${BOARD_SRC}/" || { err "rsync 源码失败"; exit 1; }
ok "源码已同步"
if [ -f "${PROJECT_ROOT}/missions/h100.moos" ]; then
info "同步 mission h100.moos ..."
local_mission="${PROJECT_ROOT}/missions/h100.moos"
tmp_moos=""
if [ -n "${CCU_HOST}" ] || [ -n "${CCU_PORT}" ]; then
tmp_moos="$(mktemp)"
cp "${local_mission}" "${tmp_moos}"
[ -n "${CCU_HOST}" ] && upsert_moos_param "${tmp_moos}" ccuhost "${CCU_HOST}"
[ -n "${CCU_PORT}" ] && upsert_moos_param "${tmp_moos}" ccuport "${CCU_PORT}"
local_mission="${tmp_moos}"
info "CCU 地址覆盖: ccuhost=${CCU_HOST:-<保持原值>} ccuport=${CCU_PORT:-<保持原值>}"
fi
rsync -az "${local_mission}" \
"${SSH_TARGET}:/root/work/h100/missions/h100.moos" || { err "rsync mission 失败"; [ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"; exit 1; }
[ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"
ok "mission 已同步"
fi
#-------------------------------------------------------------------
# 3. 板卡原生编译
#-------------------------------------------------------------------
if [ "${DO_CLEAN}" = "1" ]; then
info "重新 cmake(--clean)..."
${SSH} "${SSH_TARGET}" "rm -rf ${BOARD_SRC}/build-native && mkdir -p ${BOARD_SRC}/build-native && \
cd ${BOARD_SRC}/build-native && cmake -DCMAKE_BUILD_TYPE=Release .." \
|| { err "cmake 失败"; exit 1; }
ok "cmake 完成"
else
${SSH} "${SSH_TARGET}" "mkdir -p ${BOARD_SRC}/build-native"
# 若首次未配置过则先 cmake
if ! ${SSH} "${SSH_TARGET}" "[ -f ${BOARD_SRC}/build-native/Makefile ]"; then
info "首次编译,先 cmake ..."
${SSH} "${SSH_TARGET}" "cd ${BOARD_SRC}/build-native && cmake -DCMAKE_BUILD_TYPE=Release .." \
|| { err "cmake 失败"; exit 1; }
ok "cmake 完成"
fi
fi
if [ -z "${JOBS}" ]; then
JOBS="$(${SSH} "${SSH_TARGET}" nproc)"
fi
info "板卡编译 (make -j${JOBS}) ..."
if ! ${SSH} "${SSH_TARGET}" "cd ${BOARD_SRC}/build-native && make -j${JOBS}"; then
err "板卡编译失败(见上方输出)"
exit 1
fi
ok "板卡编译完成"
#-------------------------------------------------------------------
# 4. 部署产物到板卡运行目录
#-------------------------------------------------------------------
info "部署产物到 ${BOARD_BIN}/ ..."
${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 && \
cp -f ${BOARD_SRC}/bin/pCanBridge ${BOARD_BIN}/pCanBridge && \
cp -f ${BOARD_SRC}/bin/pMotor ${BOARD_BIN}/pMotor && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost ${BOARD_BIN}/pCCU ${BOARD_BIN}/pCanBridge ${BOARD_BIN}/pMotor" \
|| { err "部署产物失败"; exit 1; }
ok "产物已部署"
#-------------------------------------------------------------------
# 4.5 安装/刷新 pCanBridge systemd unit(幂等,其它 5 个 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 已安装"
#-------------------------------------------------------------------
# 4.6 安装/刷新 pMotor systemd unit(幂等)
#-------------------------------------------------------------------
info "安装 pMotor.service ..."
${SSH} "${SSH_TARGET}" "cat > /etc/systemd/system/pMotor.service" <<EOF
[Unit]
Description=pMotor Propulsion Motor for H100 Power Manager
After=moosdb.service pCanBridge.service
Requires=moosdb.service
[Service]
Type=simple
WorkingDirectory=${BOARD_BIN}
ExecStart=${BOARD_BIN}/pMotor --alias=pMotor /root/work/h100/missions/h100.moos
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
${SSH} "${SSH_TARGET}" "systemctl daemon-reload && systemctl reset-failed pMotor 2>/dev/null || true"
ok "pMotor.service 已安装"
echo ""
ok "产物: ${BOARD_BIN}/pPowerManger (aarch64)"
${SSH} "${SSH_TARGET}" "file ${BOARD_BIN}/pPowerManger | cut -c1-60"
#-------------------------------------------------------------------
# 5. 可选重启服务
#-------------------------------------------------------------------
if [ "${DO_START}" = "1" ]; then
info "重启服务 ..."
${SSH} "${SSH_TARGET}" "systemctl restart ${SERVICES[*]}" \
|| { err "重启服务失败"; exit 1; }
sleep 3
ok "服务状态: $(${SSH} "${SSH_TARGET}" "systemctl is-active ${SERVICES[*]}")"
else
echo ""
info "未重启服务。需要重启请加 --start 或手动:"
echo " systemctl restart ${SERVICES[*]}"
fi
echo ""
ok "完成。"
-147
View File
@@ -1,147 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/clean-data.sh
# DESC: 清理目标板卡 (RK3588 / Ubuntu 22.04) 上的数据库与日志文件。
# 默认清理板卡运行目录 /root/work/h100/data/ 下的:
# 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 # 清理后重新启动各服务
# ./scripts/clean-data.sh --no-stop # 不停服务直接清理(慎用)
# ./scripts/clean-data.sh --stale # 额外清理 bin/ 与 src/bin/ 下的旧 db/log
#
# 参数:
# --host <ip> 目标主机(默认 192.168.0.223)
# --user <user> 登录用户(默认 root)
# --board-dir <dir> 板卡部署根目录(默认 /root/work/h100)
# --start 清理后重启服务
# --no-stop 不停服务直接清理(慎用)
# --stale 额外清理历史遗留的 bin/、src/bin/ 下旧数据库与日志
# -h, --help 显示帮助
#
# 前置: 板卡已配免密 SSH(见 scripts/deploy.sh setup-ssh)。
#=======================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
HOST="192.168.0.223"
USER="root"
BOARD_DIR="/root/work/h100"
DO_START=0
DO_STOP=1
DO_STALE=0
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
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")
info() { printf '\033[1;36m[clean-data]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[clean-data]\033[0m %s\n' "$*"; }
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,31p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
#-------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--host) [ $# -ge 2 ] || { err "--host 需要参数"; exit 1; }; HOST="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--board-dir) [ $# -ge 2 ] || { err "--board-dir 需要参数"; exit 1; }; BOARD_DIR="$2"; shift 2 ;;
--start) DO_START=1; shift ;;
--no-stop) DO_STOP=0; shift ;;
--stale) DO_STALE=1; shift ;;
-h|--help) usage ;;
*) err "未知参数: $1(见 --help)"; exit 1 ;;
esac
done
STALE_DIRS=("${BOARD_DIR}/bin" "${BOARD_DIR}/src/bin")
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
#-------------------------------------------------------------------
# 1. 前置检查
#-------------------------------------------------------------------
if ! ${SSH} "${SSH_TARGET}" 'true' 2>/dev/null; then
err "无法免密 SSH 到 ${SSH_TARGET}"
err "请先执行: ./scripts/deploy.sh setup-ssh"
exit 1
fi
ok "SSH 连接正常: ${SSH_TARGET}"
#-------------------------------------------------------------------
# 2. 停止服务(默认)
#-------------------------------------------------------------------
if [ "${DO_STOP}" = "1" ]; then
info "停止服务 ${SERVICES[*]} ..."
# 先停应用,最后停 moosdb
${SSH} "${SSH_TARGET}" "systemctl stop pCanBridge pPowerMangerHost pPowerManger pCCU moosdb 2>/dev/null; true"
ok "服务已停止"
else
warn "--no-stop:不停服务直接清理,可能产生脏数据(SQLite 仍在写入)"
fi
#-------------------------------------------------------------------
# 3. 清理 data 目录
#-------------------------------------------------------------------
BOARD_DATA="${BOARD_DIR}/data"
info "清理 ${BOARD_DATA}/ ..."
for f in "${DATA_FILES[@]}"; do
${SSH} "${SSH_TARGET}" "rm -f ${BOARD_DATA}/${f}" \
&& ok "删除 ${f}" || warn "删除 ${f} 失败(可能不存在)"
done
#-------------------------------------------------------------------
# 4. 可选: 清理历史遗留目录
#-------------------------------------------------------------------
if [ "${DO_STALE}" = "1" ]; then
for d in "${STALE_DIRS[@]}"; do
info "清理历史遗留 ${d}/ 下的 db/log ..."
${SSH} "${SSH_TARGET}" "rm -f ${d}/power_data.db ${d}/power_data.db-wal ${d}/power_data.db-shm ${d}/feedback_data.db ${d}/pPowerManger.log 2>/dev/null; true"
ok "${d} 已清理"
done
fi
#-------------------------------------------------------------------
# 5. 可选: 重启服务
#-------------------------------------------------------------------
if [ "${DO_START}" = "1" ]; then
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU / pCanBridge) ..."
${SSH} "${SSH_TARGET}" "systemctl start moosdb && sleep 1 && \
systemctl start pPowerManger pPowerMangerHost pCCU pCanBridge" \
|| { err "启动失败"; exit 1; }
ok "服务已启动"
else
echo ""
info "清理完成,服务未启动。需要重启请加 --start 或手动:"
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU pCanBridge"
fi
echo ""
ok "完成。"
-268
View File
@@ -1,268 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/deploy.sh
# 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 + 装 5 个
# systemd 服务 + daemon-reload(不启动)
# ./scripts/deploy.sh --start 部署后按依赖顺序启动服务
# ./scripts/deploy.sh status 板卡各服务状态
# ./scripts/deploy.sh stop 停止各服务
# ./scripts/deploy.sh help 显示帮助
#
# 参数:
# --host <ip> 目标主机(默认 192.168.0.223)
# --user <user> 登录用户(默认 root)
# --board-dir <dir> 板卡部署根目录(默认 /root/work/h100)
# --start 部署后立即启动服务
# --ccu-host <ip> 覆盖 mission 中 pPowerManger 的 CCU 地址 (ccuhost)
# --ccu-port <n> 覆盖 mission 中 pPowerManger 的 CCU 端口 (ccuport)
#
# 板卡布局:
# /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 /
# pCanBridge.service
#=======================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
HOST="192.168.0.223"
USER="root"
BOARD_DIR="/root/work/h100"
DO_START=0
CCU_HOST=""
CCU_PORT=""
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' "$*"; }
warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,35p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
#-------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
--host) [ $# -ge 2 ] || { err "--host 需要参数"; exit 1; }; HOST="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--board-dir)[ $# -ge 2 ] || { err "--board-dir 需要参数"; exit 1; }; BOARD_DIR="$2"; shift 2 ;;
--start) DO_START=1; shift ;;
--ccu-host) [ $# -ge 2 ] || { err "--ccu-host 需要参数"; exit 1; }; CCU_HOST="$2"; shift 2 ;;
--ccu-port) [ $# -ge 2 ] || { err "--ccu-port 需要参数"; exit 1; }; CCU_PORT="$2"; shift 2 ;;
setup-ssh|status|stop|help|-h|--help) ACTION="$1"; shift ;;
"") shift ;;
*) err "未知参数: $1(见 help)"; exit 1 ;;
esac
done
[ -n "${ACTION}" ] || ACTION="deploy"
case "${CCU_PORT}" in
""|*[!0-9]*) [ -z "${CCU_PORT}" ] || { err "--ccu-port 必须是数字端口: ${CCU_PORT}"; exit 1; } ;;
esac
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
SCP="scp -o BatchMode=yes -o ConnectTimeout=5"
#-------------------------------------------------------------------
# 命令: setup-ssh
#-------------------------------------------------------------------
cmd_setup_ssh() {
local key="$HOME/.ssh/id_ed25519"
if [ ! -f "${key}" ]; then
info "未发现 SSH key,生成 ed25519 key ..."
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
ssh-keygen -t ed25519 -N '' -f "${key}"
fi
info "上传公钥到 ${SSH_TARGET}(会提示输入密码,仅此一次)..."
ssh-copy-id -o ConnectTimeout=5 "${SSH_TARGET}"
ok "免密 SSH 已配置。验证: ssh ${SSH_TARGET}"
}
#-------------------------------------------------------------------
# 前置检查
#-------------------------------------------------------------------
check_ssh() {
if ! ${SSH} "${SSH_TARGET}" 'true' 2>/dev/null; then
err "无法免密 SSH 到 ${SSH_TARGET}"
err "请先执行: ./scripts/deploy.sh setup-ssh"
return 1
fi
ok "SSH 连接正常: ${SSH_TARGET}"
}
#-------------------------------------------------------------------
# 生成 systemd unit(含板卡绝对路径)
#-------------------------------------------------------------------
gen_moosdb_unit() {
# MOOSDB 路径在板卡探测;探测失败给出提示
local moosdb_bin
moosdb_bin="$(${SSH} "${SSH_TARGET}" 'command -v MOOSDB || true' 2>/dev/null)"
if [ -z "${moosdb_bin}" ]; then
moosdb_bin="$(command -v MOOSDB || true)"
warn "板卡未在 PATH 找到 MOOSDB,回退本机路径 ${moosdb_bin:-"(未找到)"}"
[ -n "${moosdb_bin}" ] || { err "MOOSDB 路径未知,无法生成 moosdb.service"; return 1; }
fi
ok "MOOSDB 路径: ${moosdb_bin}" >&2
cat <<EOF
[Unit]
Description=MOOSDB for H100 Power Manager
After=network.target
[Service]
Type=simple
ExecStart=${moosdb_bin} --moos_no_colour ${BOARD_DIR}/missions/h100.moos
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
}
gen_app_unit() {
local app="$1" deps=""
[ "${app}" = "moosdb" ] || deps="After=moosdb.service\nRequires=moosdb.service"
cat <<EOF
[Unit]
Description=${app} for H100 Power Manager
$(printf '%b' "${deps}")
[Service]
Type=simple
WorkingDirectory=${BOARD_DIR}/bin
ExecStart=${BOARD_DIR}/bin/${app} --alias=${app} ${BOARD_DIR}/missions/h100.moos
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
}
#-------------------------------------------------------------------
# mission 中插入/替换 "key = value"(键已存在则替换,否则插到第一个
# 顶层 '}' 之前,即 pPowerManger 配置块末尾)
#-------------------------------------------------------------------
upsert_moos_param() {
local f="$1" key="$2" val="$3"
if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${f}"; then
sed -i -E "s|^([[:space:]]*)${key}[[:space:]]*=.*|\1${key} = ${val}|" "${f}"
else
awk -v kv=" ${key} = ${val}" \
'!d && /^[[:space:]]*}[[:space:]]*$/ { print kv; d=1 } { print }' \
"${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}"
fi
}
#-------------------------------------------------------------------
# 命令: deploy
#-------------------------------------------------------------------
cmd_deploy() {
check_ssh || return 1
[ -f "${PROJECT_ROOT}/missions/h100.moos" ] || { err "缺少板卡 mission: missions/h100.moos"; return 1; }
# 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 "推送 mission h100.moos ..."
local mission_src="${PROJECT_ROOT}/missions/h100.moos"
local mission_push="${mission_src}" tmp_moos=""
if [ -n "${CCU_HOST}" ] || [ -n "${CCU_PORT}" ]; then
tmp_moos="$(mktemp)"
cp "${mission_src}" "${tmp_moos}"
[ -n "${CCU_HOST}" ] && upsert_moos_param "${tmp_moos}" ccuhost "${CCU_HOST}"
[ -n "${CCU_PORT}" ] && upsert_moos_param "${tmp_moos}" ccuport "${CCU_PORT}"
mission_push="${tmp_moos}"
info "CCU 地址覆盖: ccuhost=${CCU_HOST:-<保持原值>} ccuport=${CCU_PORT:-<保持原值>}"
fi
${SCP} -q "${mission_push}" \
"${SSH_TARGET}:${BOARD_DIR}/missions/" || { err "推送 mission 失败"; [ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"; return 1; }
[ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"
ok "mission 已推送(二进制由 build-board.sh 在板卡编译部署)"
# 2. 生成并安装 4 个 systemd unit
local unit
for s in "${SERVICES[@]}"; do
info "生成 ${s}.service ..."
if [ "${s}" = "moosdb" ]; then
unit="$(gen_moosdb_unit)" || return 1
else
unit="$(gen_app_unit "${s}")"
fi
printf '%s\n' "${unit}" | ${SSH} "${SSH_TARGET}" "cat > /etc/systemd/system/${s}.service"
ok "${s}.service 已安装"
done
# 3. daemon-reload(不 enable,不自启)
info "systemctl daemon-reload ..."
${SSH} "${SSH_TARGET}" "systemctl daemon-reload && systemctl reset-failed ${SERVICES[*]} 2>/dev/null || true"
ok "服务已安装但未启用自启(systemctl is-enabled 应输出 disabled)"
# 4. 可选启动
if [ "${DO_START}" = "1" ]; then
cmd_start
else
echo ""
info "部署完成。手动启动(或加 --start):"
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU pCanBridge"
echo " 浏览器: http://${HOST}:18080 (上位机模拟)"
echo " http://${HOST}:8090 (pPowerManger)"
echo " http://${HOST}:8080 (pCCU)"
fi
}
#-------------------------------------------------------------------
# 命令: start / status / stop
#-------------------------------------------------------------------
cmd_start() {
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU / pCanBridge) ..."
${SSH} "${SSH_TARGET}" "systemctl start moosdb && sleep 1 && \
systemctl start pPowerManger pPowerMangerHost pCCU pCanBridge" || { err "启动失败"; return 1; }
ok "服务已启动"
cmd_status
}
cmd_status() {
${SSH} "${SSH_TARGET}" "systemctl --no-pager status ${SERVICES[*]}" || true
}
cmd_stop() {
${SSH} "${SSH_TARGET}" "systemctl stop pCanBridge pPowerMangerHost pPowerManger pCCU moosdb" && \
ok "服务已停止" || err "停止服务失败"
}
#-------------------------------------------------------------------
# 命令入口
#-------------------------------------------------------------------
case "${ACTION}" in
deploy) cmd_deploy ;;
setup-ssh) cmd_setup_ssh ;;
status) cmd_status ;;
stop) cmd_stop ;;
help|-h|--help) usage ;;
*) usage ;;
esac
-133
View File
@@ -1,133 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/fetch-data.sh
# DESC: 从目标板卡 (RK3588 / Ubuntu 22.04) 下载数据库与日志到本地。
# 默认抓取板卡运行目录 /root/work/h100/data/ 下的:
# 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-<时间>/
# ./scripts/fetch-data.sh --journal # 额外导出 systemd journal
# ./scripts/fetch-data.sh --out-dir /tmp/xx # 指定本地保存目录
#
# 参数:
# --host <ip> 目标主机(默认 192.168.0.223)
# --user <user> 登录用户(默认 root)
# --board-dir <dir> 板卡部署根目录(默认 /root/work/h100)
# --out-dir <dir> 本地保存目录(默认 data/board-<时间>)
# --journal 额外导出各服务的 systemd journal 日志
# -h, --help 显示帮助
#
# 前置: 板卡已配免密 SSH(见 scripts/deploy.sh setup-ssh)。
#=======================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
HOST="192.168.0.223"
USER="root"
BOARD_DIR="/root/work/h100"
OUT_DIR=""
DO_JOURNAL=0
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
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' "$*"; }
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,28p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
#-------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--host) [ $# -ge 2 ] || { err "--host 需要参数"; exit 1; }; HOST="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--board-dir) [ $# -ge 2 ] || { err "--board-dir 需要参数"; exit 1; }; BOARD_DIR="$2"; shift 2 ;;
--out-dir) [ $# -ge 2 ] || { err "--out-dir 需要参数"; exit 1; }; OUT_DIR="$2"; shift 2 ;;
--journal) DO_JOURNAL=1; shift ;;
-h|--help) usage ;;
*) err "未知参数: $1(见 --help)"; exit 1 ;;
esac
done
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
SCP="scp -o BatchMode=yes -o ConnectTimeout=5"
#-------------------------------------------------------------------
# 1. 前置检查
#-------------------------------------------------------------------
if ! ${SSH} "${SSH_TARGET}" 'true' 2>/dev/null; then
err "无法免密 SSH 到 ${SSH_TARGET}"
err "请先执行: ./scripts/deploy.sh setup-ssh"
exit 1
fi
ok "SSH 连接正常: ${SSH_TARGET}"
if [ -z "${OUT_DIR}" ]; then
OUT_DIR="${PROJECT_ROOT}/data/board-$(date +%Y%m%d-%H%M%S)"
fi
mkdir -p "${OUT_DIR}" || { err "无法创建本地目录 ${OUT_DIR}"; exit 1; }
info "本地保存目录: ${OUT_DIR}"
#-------------------------------------------------------------------
# 2. 下载 data 目录下的数据库与日志
#-------------------------------------------------------------------
BOARD_DATA="${BOARD_DIR}/data"
missing=0
for f in "${DATA_FILES[@]}"; do
if ${SSH} "${SSH_TARGET}" "[ -f ${BOARD_DATA}/${f} ]" 2>/dev/null; then
info "下载 ${f} ..."
${SCP} -q "${SSH_TARGET}:${BOARD_DATA}/${f}" "${OUT_DIR}/" \
|| { err "下载 ${f} 失败"; exit 1; }
ok "${f}"
else
warn "板卡上不存在: ${BOARD_DATA}/${f}(跳过)"
missing=$((missing+1))
fi
done
[ "${missing}" -eq "${#DATA_FILES[@]}" ] && { err "板卡 data 目录下没有任何可下载文件"; exit 1; }
#-------------------------------------------------------------------
# 3. 可选: 导出 systemd journal
#-------------------------------------------------------------------
if [ "${DO_JOURNAL}" = "1" ]; then
for s in "${SERVICES[@]}"; do
info "导出 journal: ${s} ..."
${SSH} "${SSH_TARGET}" "journalctl --no-pager -u ${s} 2>/dev/null" > "${OUT_DIR}/${s}.journal.log" \
|| { warn "导出 ${s} journal 失败(可能无记录)"; }
ok "${s}.journal.log"
done
fi
#-------------------------------------------------------------------
# 4. 完成
#-------------------------------------------------------------------
echo ""
ok "下载完成。本地文件:"
ls -lah "${OUT_DIR}" | sed 's/^/ /'
echo ""
info "查看数据库可用: sqlite3 ${OUT_DIR}/power_data.db .tables"
-306
View File
@@ -1,306 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/run_host_sim.sh
# DESC: 运行与测试 pPowerMangerHost(模拟 pPowerManger 上位机的网页程序)
#
# 用法:
# ./scripts/run_host_sim.sh run 启动 MOOSDB + pPowerManger
# + pPowerMangerHost(前台运行,
# Ctrl-C 退出并清理)
# ./scripts/run_host_sim.sh test 自动化测试 pPowerMangerHost:
# 网页 HTTP / WebSocket 反馈 /
# 指令下发到 MOOSDB
# ./scripts/run_host_sim.sh build 仅编译需要的目标
# ./scripts/run_host_sim.sh stop 清理残留进程
# ./scripts/run_host_sim.sh help 显示帮助
#
# 环境变量(可覆盖):
# SERVER_HOST MOOSDB 地址(默认 localhost)
# SERVER_PORT MOOSDB 端口(默认 9000)
# WEB_PORT 网页监听端口(默认 18080)
# WITH_POWERMANGER run 模式是否同时启动 pPowerManger(默认 1)
#=======================================================================
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN_DIR="${ROOT}/bin"
MOOS_TEMPLATE="${ROOT}/src/pPowerMangerHost/pPowerMangerHost.moos"
SERVER_HOST="${SERVER_HOST:-localhost}"
SERVER_PORT="${SERVER_PORT:-9000}"
WEB_PORT="${WEB_PORT:-18080}"
WITH_POWERMANGER="${WITH_POWERMANGER:-1}"
MOOSDB_BIN="${MOOSDB_BIN:-$(command -v MOOSDB || true)}"
POKE_BIN="$(command -v uPokeDB || true)"
PM_BIN="${BIN_DIR}/pPowerManger"
HOST_BIN="${BIN_DIR}/pPowerMangerHost"
MOOSDB_PID=""
PM_PID=""
HOST_PID=""
WORKDIR=""
MISSION=""
info() { printf '\033[1;36m[host-sim]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[host-sim]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[host-sim]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[host-sim]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,23p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
# 生成临时 mission 文件(同时含 pPowerManger 与 pPowerMangerHost 两个配置块)
make_mission() {
WORKDIR="$(mktemp -d)"
MISSION="${WORKDIR}/host_sim.moos"
cat > "${MISSION}" <<EOF
ServerHost = ${SERVER_HOST}
ServerPort = ${SERVER_PORT}
Community = h100hostsim
ProcessConfig = pPowerManger
{
AppTick = 4
CommsTick = 4
}
ProcessConfig = pPowerMangerHost
{
AppTick = 4
CommsTick = 4
WEB_PORT = ${WEB_PORT}
}
EOF
}
kill_proc() {
[ -n "$1" ] && kill "$1" 2>/dev/null && wait "$1" 2>/dev/null
}
cleanup() {
kill_proc "${HOST_PID}"
kill_proc "${PM_PID}"
kill_proc "${MOOSDB_PID}"
[ -n "${WORKDIR}" ] && rm -rf "${WORKDIR}"
}
build_targets() {
info "编译 pPowerManger / pPowerMangerHost ..."
mkdir -p "${ROOT}/build"
cd "${ROOT}/build"
cmake -DCMAKE_BUILD_TYPE=Release "${ROOT}" >/dev/null 2>&1 || { err "cmake 失败"; return 1; }
make -j2 pPowerManger pPowerMangerHost >/dev/null 2>&1 || { err "make 失败"; return 1; }
cd "${ROOT}"
ok "编译完成"
}
check_binaries() {
local missing=0
[ -n "${MOOSDB_BIN}" ] || { err "未找到 MOOSDB(请加入 PATH)"; missing=1; }
[ -x "${HOST_BIN}" ] || { err "未找到 ${HOST_BIN}(请先运行: $0 build)"; missing=1; }
if [ "${WITH_POWERMANGER}" = "1" ]; then
[ -x "${PM_BIN}" ] || { err "未找到 ${PM_BIN}(请先运行: $0 build)"; missing=1; }
fi
return "${missing}"
}
start_moosdb() {
make_mission
info "启动 MOOSDB (${SERVER_HOST}:${SERVER_PORT}) ..."
"${MOOSDB_BIN}" --moos_no_colour "${MISSION}" > "${WORKDIR}/moosdb.log" 2>&1 &
MOOSDB_PID=$!
sleep 1
kill -0 "${MOOSDB_PID}" 2>/dev/null || { err "MOOSDB 未存活"; return 1; }
}
start_pm() {
info "启动 pPowerManger ..."
( cd "${WORKDIR}" && exec "${PM_BIN}" "${MISSION}" ) > "${WORKDIR}/pm.log" 2>&1 &
PM_PID=$!
sleep 1
kill -0 "${PM_PID}" 2>/dev/null || warn "pPowerManger 未存活(无 CCU 硬件属正常)"
}
start_host() {
info "启动 pPowerMangerHost (网页端口 ${WEB_PORT}) ..."
( cd "${WORKDIR}" && exec "${HOST_BIN}" "${MISSION}" ) > "${WORKDIR}/host.log" 2>&1 &
HOST_PID=$!
sleep 2
kill -0 "${HOST_PID}" 2>/dev/null || { err "pPowerMangerHost 未存活"; return 1; }
}
#-----------------------------------------------------------------------
# 测试项
#-----------------------------------------------------------------------
test_http() {
info "测试: 网页 HTTP (http://${SERVER_HOST}:${WEB_PORT}/) ..."
local code
code="$(curl -s -o /dev/null -w '%{http_code}' "http://${SERVER_HOST}:${WEB_PORT}/" 2>/dev/null || echo 000)"
if [ "${code}" = "200" ]; then
ok "网页 HTTP 200 通过"
return 0
else
err "网页 HTTP 失败 (code=${code})"
return 1
fi
}
# 反馈链路:向 MOOSDB 注入一条假的反馈变量 → 宿主订阅后应经 WS 推给浏览器
test_monitor_loop() {
info "测试: 反馈链路 (MOOSDB -> pPowerMangerHost -> WebSocket) ..."
if [ -z "${POKE_BIN}" ]; then
warn "未找到 uPokeDB,跳过反馈链路测试"
return 0
fi
if ! python3 -c "import websockets" >/dev/null 2>&1; then
warn "未安装 python3 websockets,跳过反馈链路测试"
return 0
fi
local fb_key="uPower_pmState_fb"
local fb_val='{"workCondition_uint8":9,"currentMod_uint8":1,"powerState_uint8":1,"faultLevel_uint8":0,"soc_uint16":88,"sustainableTime_int64":3600,"leakage_uint8":0,"info_str":"host-sim-test"}'
# 先让 WS 客户端连接等待快照,再注入反馈
FB_KEY="${fb_key}" python3 - "${WEB_PORT}" > "${WORKDIR}/ws_fb.log" 2>&1 <<'PY' &
import asyncio, json, os, sys
import websockets
PORT = int(sys.argv[1])
KEY = os.environ["FB_KEY"]
async def main():
uri = f"ws://localhost:{PORT}/ws"
async with websockets.connect(uri) as ws:
try:
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=8)
snap = json.loads(msg)
if isinstance(snap, dict) and KEY in snap:
print(f"FEEDBACK_OK {KEY}")
return
except asyncio.TimeoutError:
pass
print("FEEDBACK_TIMEOUT")
asyncio.run(main())
PY
WS_PID=$!
sleep 1
"${POKE_BIN}" --host="${SERVER_HOST}" --port="${SERVER_PORT}" "${fb_key}=${fb_val}" >/dev/null 2>&1
wait "${WS_PID}" 2>/dev/null
if grep -q "FEEDBACK_OK" "${WORKDIR}/ws_fb.log" 2>/dev/null; then
ok "反馈链路通过(${fb_key} 已推送到浏览器)"
return 0
else
err "反馈链路失败(WS 未收到 ${fb_key})"
cat "${WORKDIR}/ws_fb.log" >&2
return 1
fi
}
# 指令下发:网页 WS 下发 -> 宿主 Notify -> MOOSDB -> (宿主订阅回环) -> 推回浏览器
# 利用宿主订阅了 uPower_pmState_fb,下发该变量后经 MOOSDB 回环,能收到即证明
# 指令确实进入了 MOOSDB。
test_command_notify() {
info "测试: 指令下发 (WebSocket -> pPowerMangerHost -> MOOSDB) ..."
if ! python3 -c "import websockets" >/dev/null 2>&1; then
warn "未安装 python3 websockets,跳过指令下发校验"
return 0
fi
python3 - "${WEB_PORT}" > "${WORKDIR}/ws_cmd.log" 2>&1 <<'PY'
import asyncio, json, sys
import websockets
PORT = int(sys.argv[1])
KEY = "uPower_pmState_fb"
TAG = "cmd-loopback-test"
async def main():
uri = f"ws://localhost:{PORT}/ws"
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({"key": KEY, "value": {"info_str": TAG, "workCondition_uint8": 7}}))
try:
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=8)
snap = json.loads(msg)
if isinstance(snap, dict) and KEY in snap:
obj = json.loads(snap[KEY])
if obj.get("info_str") == TAG:
print("CMD_OK")
return
except asyncio.TimeoutError:
pass
print("CMD_TIMEOUT")
asyncio.run(main())
PY
local r=$?
if [ ${r} -eq 0 ] && grep -q "CMD_OK" "${WORKDIR}/ws_cmd.log" 2>/dev/null; then
ok "指令下发到 MOOSDB 通过(uPower_pmState_fb 已回环)"
return 0
else
err "指令下发校验失败:未收到回环快照"
cat "${WORKDIR}/ws_cmd.log" >&2
return 1
fi
}
#-----------------------------------------------------------------------
# 命令入口
#-----------------------------------------------------------------------
cmd_run() {
check_binaries || return 1
start_moosdb || return 1
[ "${WITH_POWERMANGER}" = "1" ] && start_pm
start_host || return 1
info "全部启动完成。"
info "浏览器访问: http://${SERVER_HOST}:${WEB_PORT}/"
info "按 Ctrl-C 退出并清理。"
echo ""
trap 'info "退出清理..."; cleanup; exit 0' INT TERM
while kill -0 "${HOST_PID}" 2>/dev/null; do sleep 1; done
info "pPowerMangerHost 已退出。"
cleanup
}
cmd_test() {
check_binaries || return 1
start_moosdb || return 1
start_host || return 1
local pass=0 fail=0 r
test_http; r=$?; [ $r -eq 0 ] && pass=$((pass+1)) || fail=$((fail+1))
test_monitor_loop; r=$?; [ $r -eq 0 ] && pass=$((pass+1)) || fail=$((fail+1))
test_command_notify;r=$?; [ $r -eq 0 ] && pass=$((pass+1)) || fail=$((fail+1))
echo ""
info "==== 测试结果: 通过 ${pass} / 失败 ${fail} ===="
cleanup
[ "${fail}" -eq 0 ]
}
cmd_stop() {
pkill -f "${HOST_BIN}" 2>/dev/null
pkill -f "${PM_BIN}" 2>/dev/null
pkill -x MOOSDB 2>/dev/null
ok "已清理残留进程"
}
case "${1:-run}" in
run) cmd_run ;;
test) cmd_test ;;
build) build_targets ;;
stop) cmd_stop ;;
help|-h|--help) usage ;;
*) usage ;;
esac
-176
View File
@@ -1,176 +0,0 @@
#!/bin/bash
#=======================================================================
# FILE: scripts/sync-board-time.sh
# DESC: 校正 223 板卡(RK3588,默认 192.168.0.223)的系统时间:以
# 本机时间(或 --time 指定时间)为基准,经 SSH 设置板卡系统时
# 钟,并尽力写入硬件时钟 (RTC)。板卡网络隔离无 NTP,用本脚本对时。
#
# 用法:
# ./scripts/sync-board-time.sh # 用本机时间校正板卡
# ./scripts/sync-board-time.sh status # 只查看时间与偏差
# ./scripts/sync-board-time.sh --time "2026-09-01 12:00:00"
# # 用指定时间校正(本地时区)
#
# 参数:
# --host <ip> 目标主机(默认 192.168.0.223)
# --user <user> 登录用户(默认 root)
# --time <str> 手动指定基准时间(date 可解析的格式,按本地时区解释)
# --keep-ntp 不执行 timedatectl set-ntp false
# --no-rtc 不写硬件时钟(跳过 hwclock --systohc)
# -h, --help 显示帮助
#
# 说明:
# - 需要 root 登录与免密 SSH(deploy.sh setup-ssh)。
# - 对时经 SSH 往返时延 (RTT) 折半补偿,精度约 ±0.1s(受网络抖动影响)。
# - 板卡若无可用 RTC,hwclock 失败仅告警;重启后时间可能回跳。
# - 对时会调整系统时钟,运行中的服务日志时间戳会跳变,属正常现象。
#=======================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOST="192.168.0.223"
USER="root"
SET_TIME=""
KEEP_NTP=0
NO_RTC=0
info() { printf '\033[1;36m[time]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[time]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[time]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[time]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
#-------------------------------------------------------------------
# 参数解析
#-------------------------------------------------------------------
ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
--host) [ $# -ge 2 ] || { err "--host 需要参数"; exit 1; }; HOST="$2"; shift 2 ;;
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--time) [ $# -ge 2 ] || { err "--time 需要参数"; exit 1; }; SET_TIME="$2"; shift 2 ;;
--keep-ntp) KEEP_NTP=1; shift ;;
--no-rtc) NO_RTC=1; shift ;;
status|help|-h|--help) ACTION="$1"; shift ;;
"") shift ;;
*) err "未知参数: $1(见 help)"; exit 1 ;;
esac
done
[ -n "${ACTION}" ] || ACTION="sync"
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
#-------------------------------------------------------------------
# 读取板卡时间并测 RTT
# 输出: "<epoch>|<板卡可读时间> <t0> <t1>"(t0/t1 为本机采样时刻)
#-------------------------------------------------------------------
read_board_time() {
local t0 t1 out
t0="$(date +%s.%N)"
out="$(${SSH} "${SSH_TARGET}" 'date "+%s.%N|%F %T %Z"' 2>/dev/null)" || {
err "无法免密 SSH 到 ${SSH_TARGET}(先执行: ./scripts/deploy.sh setup-ssh)" >&2
return 1
}
t1="$(date +%s.%N)"
printf '%s %.3f %.3f\n' "${out}" "${t0}" "${t1}"
}
# 从 read_board_time 输出解析: set -- <out> 后取 $1/$2/$3
parse_board_time() {
B_EPOCH="${1%%|*}"
B_DATE="${1#*|}"
T0="$2"
T1="$3"
MID="$(awk -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t0 + (t1-t0)/2}')"
DIFF="$(awk -v b="${B_EPOCH}" -v m="${MID}" 'BEGIN{printf "%+.3f", b - m}')"
}
#-------------------------------------------------------------------
# 命令: status
#-------------------------------------------------------------------
cmd_status() {
local out
out="$(read_board_time)" || return 1
parse_board_time ${out}
local rtt
rtt="$(awk -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t1-t0}')"
printf '本机时间: %s (RTT %.3fs)\n' "$(date -d "@${MID}" '+%F %T')" "${rtt}"
printf '板卡时间: %s\n' "${B_DATE}"
printf '偏差: 板卡比本机快 %s 秒\n' "${DIFF}"
}
#-------------------------------------------------------------------
# 命令: sync(默认)
#-------------------------------------------------------------------
cmd_sync() {
local out target board_reply
# 前置检查:SSH 连通 + 板卡 root 权限
out="$(read_board_time)" || return 1
parse_board_time ${out}
if ! ${SSH} "${SSH_TARGET}" '[ "$(id -u)" = 0 ]' 2>/dev/null; then
err "板卡登录用户需要 root 权限(当前 --user ${USER})"
return 1
fi
ok "SSH 连接正常: ${SSH_TARGET}"
info "当前偏差: 板卡比本机快 ${DIFF} 秒"
# 关闭 NTP 自动同步(板卡网络隔离本就无 NTP 源,防止干扰手动对时)
if [ "${KEEP_NTP}" = "0" ]; then
info "关闭板卡 NTP 自动同步 (timedatectl set-ntp false) ..."
${SSH} "${SSH_TARGET}" "command -v timedatectl >/dev/null 2>&1 && timedatectl set-ntp false || true"
fi
# 计算目标时间:手动指定值直接用;否则取本机时间 + RTT/2 补偿
if [ -n "${SET_TIME}" ]; then
target="$(date -d "${SET_TIME}" '+%s.%N' 2>/dev/null)" || {
err "无法解析时间: ${SET_TIME}"; return 1;
}
info "基准时间(手动): ${SET_TIME}"
else
local t2
t2="$(date +%s.%N)"
target="$(awk -v t2="${t2}" -v t0="${T0}" -v t1="${T1}" 'BEGIN{printf "%.3f", t2 + (t1-t0)/2}')"
info "基准时间(本机 + RTT/2 补偿)"
fi
info "设置板卡系统时间 -> $(date -d "@${target}" '+%F %T') ..."
board_reply="$(${SSH} "${SSH_TARGET}" "date -s @${target} 2>&1")" || {
err "date -s 失败: ${board_reply}"
return 1
}
ok "板卡返回: ${board_reply}"
# 写硬件时钟(无 RTC 时仅告警)
if [ "${NO_RTC}" = "0" ]; then
info "同步硬件时钟 (hwclock --systohc) ..."
if ${SSH} "${SSH_TARGET}" "hwclock --systohc" 2>/dev/null; then
ok "RTC 已同步"
else
warn "hwclock 失败(板卡可能无 RTC 或驱动未加载),重启后时间可能回跳"
fi
fi
# 校验
out="$(read_board_time)" || return 1
parse_board_time ${out}
info "校正后偏差: 板卡比本机快 ${DIFF} 秒"
ok "板卡时间: ${B_DATE}"
}
#-------------------------------------------------------------------
# 命令入口
#-------------------------------------------------------------------
case "${ACTION}" in
sync) cmd_sync ;;
status) cmd_status ;;
help|-h|--help) usage ;;
*) usage ;;
esac
-29
View File
@@ -1,29 +0,0 @@
##############################################################################
# FILE: moos-ivp-extend/src/CMakeLists.txt
# DATE: 2010/09/07
# 2020/05/09 minor mods
# DESCRIPTION: CMakeLists.txt file for the moos-ivp-extend source directory
##############################################################################
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -g -O1")
#============================================================================
# Add the libraries in the current directory to the include path
#============================================================================
FILE(GLOB LOCAL_LIBRARY_DIRS ./lib_*)
INCLUDE_DIRECTORIES(${LOCAL_LIBRARY_DIRS})
#============================================================================
# List the subdirectories to build...
#============================================================================
ADD_SUBDIRECTORY(pPowerManger)
# ADD_SUBDIRECTORY(appPowerMangerWebUI)
add_subdirectory(pPMtest)
add_subdirectory(pPowerMangerHost)
add_subdirectory(pCCU)
add_subdirectory(pCanBridge)
add_subdirectory(pMotor)
##############################################################################
# END of CMakeLists.txt
##############################################################################
-653
View File
@@ -1,653 +0,0 @@
#include "CCU.h"
#include "MBUtils.h"
#include "../pPowerManger/logc/loguru.hpp"
#include "protocol/Frame.h"
#include "protocol/CanBms.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
using namespace ccu;
using namespace std;
//---------------------------------------------------------
// Constructor / Destructor
CCU::CCU() {}
CCU::~CCU() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
if (m_coord) { delete m_coord; m_coord = nullptr; }
if (m_bridge) { delete m_bridge; m_bridge = nullptr; }
if (m_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; }
if (m_rcuLink) { m_rcuLink->stop(); delete m_rcuLink; m_rcuLink = nullptr; }
if (m_db) { m_db->close(); delete m_db; m_db = nullptr; }
if (m_snap) { delete m_snap; m_snap = nullptr; }
if (m_sysData) { delete m_sysData; m_sysData = nullptr; }
}
//---------------------------------------------------------
// OnStartUp:读取配置并初始化各组件
bool CCU::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 == "fc_local_port") m_fcLocalPort = atol(value.c_str());
else if (param == "fc_remote_ip") m_fcHost = value;
else if (param == "fc_remote_port") m_fcPort = atol(value.c_str());
else if (param == "pm_local_port") m_pmLocalPort = atol(value.c_str());
else if (param == "pm_remote_ip") m_pmHost = value;
else if (param == "pm_remote_port") m_pmPort = atol(value.c_str());
else if (param == "rcu_enable") m_rcuEnable = (tolower(value) == "true" || value == "1");
else if (param == "rcu_local_port") m_rcuLocalPort = atol(value.c_str());
else if (param == "rcu_remote_ip") m_rcuHost = value;
else if (param == "rcu_remote_port") m_rcuPort = atol(value.c_str());
else if (param == "dbpath") m_dbPath = value;
else if (param == "logpath") m_logPath = value;
else if (param == "bcu_can_channel") m_bcuCanChannel = value;
else if (param == "web_port") m_webPort = atoi(value.c_str());
else if (param == "web_enable") {
m_webEnable = (tolower(value) == "true" || value == "1");
}
else handled = false;
if (!handled)
reportUnhandledConfigWarning(orig);
}
registerVariables();
// 日志文件
if (m_logPath.empty()) m_logPath = "pCCU.log";
loguru::add_file(m_logPath.c_str(), loguru::Append, loguru::Verbosity_MAX);
LOG_F(INFO, "pCCU log path: %s", m_logPath.c_str());
// 数据库
m_db = new DbStore(m_dbPath);
if (!m_db->open()) {
LOG_F(ERROR, "open database failed: %s", m_db->lastError().c_str());
delete m_db;
m_db = nullptr;
} else {
LOG_F(INFO, "database path: %s", m_dbPath.c_str());
}
// 系统数据快照
m_sysData = new SystemData();
// 配电桥接器(须在链路 start 前完成依赖注入,避免接收线程空指针)
m_bridge = new DisBridge();
// FC 链路
m_fcLink = new FcLinkManager(m_fcLocalPort, m_fcHost, m_fcPort);
m_fcLink->setLogSink(m_db);
m_fcLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
handleFcMessage(msg, frame);
});
m_fcLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) {
// 原始帧已通过 LogSink 落库;如需 Web 实时原始报文可在此扩展
});
// PM 链路
m_pmLink = new PmLinkManager(m_pmLocalPort, m_pmHost, m_pmPort);
m_pmLink->setLogSink(m_db);
m_pmLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
// 配电指令(0x0001~0x0004,Sum8 短帧)优先桥接转发真实CCU;
// 注册表按帧总长区分配电指令与 PM 操控/参数指令(共用 id)
if (m_bridge && m_bridge->onPmFrame(msg, frame)) return;
handlePmMessage(msg, frame);
});
m_pmLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
});
// 真实 CCU 链路(配电桥接,rcu_enable 开启时创建)
if (m_rcuEnable) {
m_rcuLink = new RcuLinkManager(m_rcuLocalPort, m_rcuHost, m_rcuPort);
m_rcuLink->setLogSink(m_db);
m_rcuLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
handleRcuMessage(msg, frame);
});
m_rcuLink->setOnRawFrame([](int, const std::vector<uint8_t>&, bool) {
});
}
// 注入桥接依赖(m_rcuLink 可为空:未启用桥接时相关入口自动失效)
m_bridge->setup(m_pmLink, m_rcuLink, m_sysData);
// 启动链路
if (!m_fcLink->start())
LOG_F(ERROR, "FC link start failed");
if (!m_pmLink->start())
LOG_F(ERROR, "PM link start failed");
if (m_rcuLink && !m_rcuLink->start())
LOG_F(ERROR, "RCU link start failed on port %ld", m_rcuLocalPort);
// 锂电池:CAN 总线(BMS 协议),数据经 pCanBridge 以 CAN_0x* 消息透传,
// 在 OnNewMail 中订阅解码(见 registerVariables/handleCanMessage)。
// 电源协调器(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_rcuLink, m_db);
// 网页
if (m_webEnable) {
m_web = new WebServer();
m_web->setOnOpen([this]() { return buildSnapshot(); });
m_web->setApiHandler([this](const std::string& uri, const std::string& query) {
return handleApi(uri, query);
});
if (!m_web->start(m_webPort)) {
LOG_F(ERROR, "web server start failed on port %d", m_webPort);
delete m_web;
m_web = nullptr;
}
}
if (m_rcuLink) {
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"rcu_link local=%ld -> %s:%ld (配电桥接), "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort,
m_rcuLocalPort, m_rcuHost.c_str(), m_rcuPort);
} else {
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld, "
"rcu_link disabled, "
"battery=BMS/CAN via pCanBridge(CAN_0x*)",
m_fcLocalPort, m_fcHost.c_str(), m_fcPort,
m_pmLocalPort, m_pmHost.c_str(), m_pmPort);
}
return true;
}
//---------------------------------------------------------
// OnConnectToServer
bool CCU::OnConnectToServer() {
registerVariables();
return true;
}
//---------------------------------------------------------
// registerVariables
void CCU::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// 订阅 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);
Register("CCU_RCU_LINK_STATE", 0);
}
//---------------------------------------------------------
// OnNewMail
bool CCU::OnNewMail(MOOSMSG_LIST &NewMail) {
AppCastingMOOSApp::OnNewMail(NewMail);
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
// 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;
// 合并语义:先取该节点当前快照 -> 解码部分更新 -> 写回
// (每个功能码报文只更新自己的字段,不能整体覆盖)
uint8_t addr = bcuAddrOf(canId);
if (!addr) return;
BcuNodeStatus node = m_sysData->bcuNode(addr);
if (decodeCanBcuFrame(canId, d, static_cast<int>(n), addr, node)) {
node.valid = true;
node.lastRxTime = MOOSTime(false);
m_sysData->updateBcuNode(addr, node);
// 锂电池解析数据落库(bcu_node 表,每帧一行,含节点最新合成状态)
if (m_db) m_db->insertBcuNode(addr, bcuFuncOf(canId), node);
// 节流日志:1s 一条,便于联调观察
double now = MOOSTime();
if (now - m_lastBmsLog >= 1.0) {
m_lastBmsLog = now;
LOG_F(INFO, "[BMS/CAN] 节点%d u=%.1fV i=%.1fA soc=%.1f%% alarm=%02X self=%d vMax=%.3fV",
addr, node.totalVoltage, node.current, node.soc,
node.alarmCode, node.selfCheck, node.maxCellVoltage);
}
}
}
}
//---------------------------------------------------------
// Iterate:周期任务(AppTick 次/秒)
bool CCU::Iterate() {
AppCastingMOOSApp::Iterate();
double now = MOOSTime();
// 周期(1Hz)整合 FC 状态并发送 PM 状态报文给控制主机
if (now - m_lastStatusTx >= 1.0) {
sendPmStatus();
m_lastStatusTx = now;
}
// BCU 断路器控制指令下发(网页 -> 队列 -> MOOS CAN_TX_*)
sendPendingBcuCtrl();
// 电源协调器:协调算法占位
if (m_coord)
m_coord->tick(now);
// 周期(1Hz)推送网页快照
if (m_web) {
static double lastWebPush = 0;
if (now - lastWebPush >= 1.0) {
lastWebPush = now;
m_web->broadcast(buildSnapshot());
}
}
AppCastingMOOSApp::PostReport();
return true;
}
//---------------------------------------------------------
// handleFcMessage:处理 FC 链路收到的消息
void CCU::handleFcMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return;
switch (msg->id()) {
case 0x0002: { // FC 状态反馈
FcStatusValue v;
if (msg->decode(frame, static_cast<void*>(&v))) {
m_sysData->updateFcStatus(v);
LOG_F(INFO, "[FC] status: mode=%d status=%d fault_level=%d",
v.fc_mode, v.fc_status, v.fc_fault_level);
} else {
LOG_F(ERROR, "[FC] status decode failed");
}
break;
}
default:
LOG_F(WARNING, "[FC] unhandled msg id 0x%04X", msg->id());
break;
}
}
//---------------------------------------------------------
// handlePmMessage:处理 PM 链路收到的消息
void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return;
// 配电指令已在入口由 DisBridge 处理(见 OnStartUp 的 onMessage 接线),
// 到达此处的均为 PM 协议消息(Sum32 校验)
switch (msg->id()) {
case 0x0001: { // PM 操控指令 -> 转发给 FC
PmControlValue c;
if (msg->decode(frame, static_cast<void*>(&c))) {
m_sysData->updatePmControl(c);
LOG_F(INFO, "[PM] control: mode=%d cmd=%d power=%d",
c.mode, c.cmd, c.outputPower);
// 映射并转发到 FC(0827协议:姿态仅数据1有效,数据2字节预留填0)
FcControlValue f;
f.mode = c.mode;
f.cmd = c.cmd;
f.outputPower = c.outputPower;
f.pitch1 = c.pitch;
f.roll1 = c.roll;
f.emergencyAllow = c.emergencyAllow;
f.depth = c.depth;
f.supplyCmd = c.supplyCmd;
f.reservedCmd5 = c.reservedCmd5;
f.reservedCmd6 = c.reservedCmd6;
f.heartbeat = c.heartbeat;
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");
}
break;
}
case 0x0002: { // PM 参数设定 -> 回参数反馈
PmParamSetValue p;
if (msg->decode(frame, static_cast<void*>(&p))) {
m_sysData->updatePmParamSet(p);
LOG_F(INFO, "[PM] param set received");
// 当前阶段直接回成功
PmParamSetFbValue fb;
fb.flag = 0x10; // 设定成功
fb.failCode = 0;
m_sysData->updatePmParamFb(fb);
m_pmLink->sendMessage(0x0003, &fb);
LOG_F(INFO, "[PM] param set feedback sent");
} else {
LOG_F(ERROR, "[PM] param set decode failed");
}
break;
}
default:
LOG_F(WARNING, "[PM] unhandled msg id 0x%04X", msg->id());
break;
}
}
//---------------------------------------------------------
// handleRcuMessage:处理真实 CCU 链路收到的消息(配电桥接)
//
// 配电反馈(0x0005~0x0008)与真实CCU状态报文(0x0004)由 DisBridge
// 处理(原帧转发 pPowerManger / 仅解码显示);其余(如配电指令
// 回显)仅记录,不转发。
void CCU::handleRcuMessage(Message* msg, const std::vector<uint8_t>& frame) {
if (!msg) return;
if (m_bridge && m_bridge->onRcuFrame(msg, frame)) return;
LOG_F(WARNING, "[RCU] unhandled msg id 0x%04X, len=%zu", msg->id(), frame.size());
}
//---------------------------------------------------------
// sendPmStatus:整合最新 FC 状态,编码 PM 状态报文发送
void CCU::sendPmStatus() {
if (!m_pmLink || !m_sysData) return;
const FcStatusValue& fc = m_sysData->fcStatus();
PmStatusValue s;
s.fc_mode = fc.fc_mode;
s.fc_status = fc.fc_status;
s.fault_level_1 = fc.fault_level_1;
s.fault_level_2 = fc.fault_level_2;
s.fault_level_3 = fc.fault_level_3;
s.fault_level_4 = fc.fault_level_4;
s.total_generation_time = fc.total_generation_time;
s.fc_fault_level = fc.fc_fault_level;
s.output_power_limit = 0; // FC 状态未含此字段,置 0
s.generation_power = fc.generation_power;
s.hydrogen_capacity = fc.hydrogen_capacity;
s.liquid_oxygen_capacity = fc.liquid_oxygen_capacity;
s.fc1_min_cell_voltage = fc.fc1_min_cell_voltage;
s.fc1_min_cell_pos = fc.fc1_min_cell_pos;
s.fc1_avg_cell_voltage = fc.fc1_avg_cell_voltage;
s.fc2_min_cell_voltage = fc.fc2_min_cell_voltage;
s.fc2_min_cell_pos = fc.fc2_min_cell_pos;
s.fc2_avg_cell_voltage = fc.fc2_avg_cell_voltage;
s.palladium_temp = fc.palladium_temp;
s.buffer_tank_pressure = fc.buffer_tank_pressure;
s.flue_total_emission = fc.flue_total_emission;
s.flue_pressure = fc.flue_pressure;
s.reactor_pressure = fc.reactor_pressure;
s.electric_valve_open = fc.electric_valve_open;
s.main_pipe_pressure = 0;
s.aux_pipe_pressure = 0;
s.dcdc1_in_voltage = fc.dcdc1_in_voltage;
s.dcdc1_in_current = fc.dcdc1_in_current;
s.dcdc2_in_voltage = fc.dcdc2_in_voltage;
s.dcdc2_in_current = fc.dcdc2_in_current;
s.dcdc_out_voltage = fc.dcdc_out_voltage;
s.dcdc_out_current = fc.dcdc_out_current;
s.dcdc_ctrl_voltage = fc.dcdc_ctrl_voltage;
s.dcdc_aux_voltage = fc.dcdc_aux_voltage;
s.methanol_total_use = fc.methanol_total_use;
s.methanol_feed = fc.methanol_feed;
s.oxygen_side_water_level = fc.oxygen_side_water_level;
s.hydrogen_side_water_level = fc.hydrogen_side_water_level;
s.ballast_water_level = fc.ballast_water_level;
s.exhaust_inlet_pressure = fc.exhaust_inlet_pressure;
s.exhaust_outlet_pressure = fc.exhaust_outlet_pressure;
s.exhaust_run_freq = fc.exhaust_run_freq;
s.exhaust_inlet_temp = fc.exhaust_inlet_temp;
s.exhaust_outlet_temp = fc.exhaust_outlet_temp;
s.exhaust_water_in_pressure = fc.exhaust_water_in_pressure;
s.exhaust_water_out_pressure = fc.exhaust_water_out_pressure;
s.tank_lo2_pressure = fc.tank_lo2_pressure;
s.tank_co2_pressure = fc.tank_co2_pressure;
s.tank_lo2_level = fc.tank_lo2_level;
s.alloy_h2_flow = fc.alloy_h2_flow;
s.fc_h2_flow = fc.fc_h2_flow;
s.fc_o2_flow = fc.fc_o2_flow;
s.emergency_float_depth = fc.emergency_float_depth;
s.emergency_float_time = fc.emergency_float_time;
s.cabin_pressure1 = fc.cabin_pressure1;
s.cabin_pressure2 = fc.cabin_pressure2;
s.cabin_temp1 = fc.cabin_temp1;
s.cabin_temp2 = fc.cabin_temp2;
s.cabin_humidity1 = fc.cabin_humidity1;
s.cabin_humidity2 = fc.cabin_humidity2;
s.h2_concentration1 = fc.h2_concentration1;
s.h2_concentration2 = fc.h2_concentration2;
s.h2_concentration3 = fc.h2_concentration3;
s.o2_concentration1 = fc.o2_concentration1;
s.o2_concentration2 = fc.o2_concentration2;
s.ch3oh_concentration1 = fc.ch3oh_concentration1;
s.ch3oh_concentration2 = fc.ch3oh_concentration2;
s.flame_detector1 = fc.flame_detector1;
s.flame_detector2 = fc.flame_detector2;
// 通信心跳自增
s.heartbeat = m_pmHeartbeat++;
s.emergency_cmd = fc.emergency_cmd;
m_sysData->updatePmStatus(s);
m_pmLink->sendMessage(0x0004, &s);
LOG_F(INFO, "[PM] status sent: mode=%d status=%d heartbeat=%d",
s.fc_mode, s.fc_status, s.heartbeat);
}
//---------------------------------------------------------
// buildSnapshot
std::string CCU::buildSnapshot() {
if (m_snap) return m_snap->build();
return "{}";
}
//---------------------------------------------------------
// handleApi:/api/logs、/api/bcu_ctrl
std::string CCU::handleApi(const std::string& uri, const std::string& query) {
if (uri == "/api/logs") {
if (m_snap) return m_snap->buildLogs(50);
return "[]";
}
if (uri == "/api/bcu_ctrl") {
return handleBcuCtrlApi(query);
}
return "";
}
//---------------------------------------------------------
// handleBcuCtrlApi:BCU 断路器控制指令下发
//
// GET /api/bcu_ctrl?addr=5&action=1 action: 1 闭合 / 0 断开(总正+总负)
// GET /api/bcu_ctrl?addr=5&pos=1&neg=0 也可单独指定总正/总负
// Web 线程不可直接 Notify,指令入队后由 Iterate(MOOS 线程)下发。
std::string CCU::handleBcuCtrlApi(const std::string& query) {
if (!m_sysData) return "{\"ok\":false,\"error\":\"not ready\"}";
// 简易 query 解析(k=v&k=v)
auto getParam = [&query](const char* key, int& out) -> bool {
std::string k = std::string(key) + "=";
size_t p = query.find(k);
if (p == std::string::npos) return false;
size_t e = query.find('&', p);
std::string v = query.substr(p + k.size(),
(e == std::string::npos) ? std::string::npos
: e - p - k.size());
out = atoi(v.c_str());
return true;
};
int addr = 0, action = -1, pos = -1, neg = -1;
getParam("addr", addr);
getParam("action", action);
getParam("pos", pos);
getParam("neg", neg);
if (addr < kBcuAddrMin || addr > kBcuAddrMax)
return "{\"ok\":false,\"error\":\"bad addr\"}";
if (pos < 0) pos = action; // 未单独指定时跟随 action
if (neg < 0) neg = action;
if (action < 0 && (pos < 0 || neg < 0))
return "{\"ok\":false,\"error\":\"missing action\"}";
if (action > 1 || pos > 1 || neg > 1)
return "{\"ok\":false,\"error\":\"bad value\"}";
BcuCtrlCmd c;
c.addr = static_cast<uint8_t>(addr);
c.pos = static_cast<uint8_t>(pos);
c.neg = static_cast<uint8_t>(neg);
if (!m_sysData->pushBcuCtrlCmd(c))
return "{\"ok\":false,\"error\":\"queue full\"}";
LOG_F(INFO, "[BCU/CAN] 断路器指令入队: 节点%d 总正=%d 总负=%d", addr, pos, neg);
char buf[96];
std::snprintf(buf, sizeof(buf),
"{\"ok\":true,\"addr\":%d,\"pos\":%d,\"neg\":%d}", addr, pos, neg);
return buf;
}
//---------------------------------------------------------
// sendPendingBcuCtrl:出队控制指令,编码 0x10XX81FF 经 MOOS 下发
// 链路:MOOS CAN_TX_0x*(m_sSrcAux=目标通道) -> pCanBridge 订阅
// -> 按通道路由 CanEndpoint(TCP) -> CANET -> CAN 总线
void CCU::sendPendingBcuCtrl() {
if (!m_sysData) return;
BcuCtrlCmd c;
while (m_sysData->popBcuCtrlCmd(c)) {
uint8_t data[8];
buildBcuRelayCtrlData(c.pos, c.neg, data);
uint32_t canId = bcuCtrlCanId(c.addr);
char key[32];
std::snprintf(key, sizeof(key), "CAN_TX_0x%08X", canId);
// 二进制构造同 pCanBridge 上行发布(MOOS_BINARY_STRING)
CMOOSMsg msg(MOOS_NOTIFY, key,
static_cast<unsigned int>(sizeof(data)), data);
// 目标通道经 m_sSrcAux 指定(与上行 CAN_0x* 的通道标注对称)
msg.SetSourceAux(m_bcuCanChannel);
bool ok = m_Comms.Post(msg);
m_sysData->noteBcuCtrlSent(c, ok);
LOG_F(INFO, "[BCU/CAN] 断路器指令 0x%08X 节点%d 总正=%d 总负=%d 通道=%s %s",
canId, c.addr, c.pos, c.neg, m_bcuCanChannel.c_str(),
ok ? "已投递MOOS" : "投递失败");
}
}
//---------------------------------------------------------
// buildReport
bool CCU::buildReport() {
m_msgs << "============================================" << "\n";
m_msgs << "pCCU 复合管控器" << "\n";
m_msgs << "============================================" << "\n";
if (m_fcLink) {
m_msgs << "FC 链路 local:" << m_fcLink->localPort()
<< " rx:" << m_fcLink->rxCount()
<< " tx:" << m_fcLink->txCount()
<< " err:" << m_fcLink->errorCount() << "\n";
}
if (m_pmLink) {
m_msgs << "PM 链路 local:" << m_pmLink->localPort()
<< " rx:" << m_pmLink->rxCount()
<< " tx:" << m_pmLink->txCount()
<< " err:" << m_pmLink->errorCount() << "\n";
}
if (m_rcuLink) {
m_msgs << "RCU 链路 local:" << m_rcuLink->localPort()
<< " -> " << m_rcuLink->rcuHost() << ":" << m_rcuLink->rcuPort()
<< " rx:" << m_rcuLink->rxCount()
<< " tx:" << m_rcuLink->txCount()
<< " err:" << m_rcuLink->errorCount() << "\n";
if (m_sysData) {
const RealCcuState& st = m_sysData->realCcu();
m_msgs << "配电桥接: 反馈转发 " << st.fwdFbOk << " 成功 / " << st.fwdFbErr
<< " 失败, 指令转发 " << st.fwdCmdOk << " 成功 / " << st.fwdCmdErr
<< " 失败\n";
}
}
if (m_sysData) {
m_msgs << "锂电池: BMS/CAN (经 pCanBridge CAN_0x* 订阅)\n";
m_msgs << "CAN 帧接收计数:" << m_sysData->canFrameCount() << "\n";
m_msgs << "BMS 报文接收计数:" << m_sysData->bmsStatusCount() << "\n";
m_msgs << "FC 状态接收次数:" << m_sysData->fcStatusCount() << "\n";
m_msgs << "PM 指令接收次数:" << m_sysData->pmControlCount() << "\n";
const BcuCtrlStat& bc = m_sysData->bcuCtrlStat();
m_msgs << "断路器指令下发:" << bc.sentCount << " 成功 / " << bc.errCount
<< " 失败 (最近: 节点" << static_cast<int>(bc.last.addr)
<< " 总正=" << static_cast<int>(bc.last.pos)
<< " 总负=" << static_cast<int>(bc.last.neg) << ")\n";
}
if (m_db) {
m_msgs << "数据库记录数:" << m_db->count() << "\n";
m_msgs << "数据库BMS记录数:" << m_db->countBcu() << "\n";
}
return true;
}
-112
View File
@@ -1,112 +0,0 @@
#ifndef PCCU_CCU_H
#define PCCU_CCU_H
#define UNIX
#include "MOOS/libMOOS/Thirdparty/AppCasting/AppCastingMOOSApp.h"
#include "comm/FcLinkManager.h"
#include "comm/PmLinkManager.h"
#include "comm/RcuLinkManager.h"
#include "core/SystemData.h"
#include "core/SnapshotBuilder.h"
#include "core/PowerCoordinator.h"
#include "core/DisBridge.h"
#include "store/DbStore.h"
#include "web/WebServer.h"
namespace ccu {
//============================================================================
// CCU:pCCU 程序主类(MOOS 应用外壳)。
//
// 数据流:
// 收 FC 状态(0x0002) -> SystemData 快照 -> 周期整合为 PM 状态(0x0004)发给 pPowerManger
// 收 PM 操控(0x0001) -> 转发为 FC 控制(0x0001)发给燃料电池
// 收 PM 参数设定(0x0002) -> 回 PM 参数反馈(0x0003)
// 收 PM 配电指令(0x0001~0x0004,Sum8短帧) -> DisBridge 原帧转发真实CCU(rcu 链路)
// 收真实CCU 配电反馈(0x0005~0x0008) -> DisBridge 原帧转发 pPowerManger + 解码显示
// 收真实CCU 状态报文(0x0004) -> 仅解码显示,不转发(避免状态冲突)
// 订阅 pCanBridge 透传的 CAN_0x* 消息 -> 解析锂电池 BMS 报文
// (docs/BMS_协议字段定义.xlsx:总电压/SOC/电流/故障码等)-> SystemData 快照
// (BMS CAN 协议未定义控制报文,电池侧远程控制暂不可用)
// 收发全部帧落库 SQLite(锂电池 BMS 解析数据另存 bcu_node 表);网页周期推送 JSON 快照。
//============================================================================
class CCU : public AppCastingMOOSApp {
public:
CCU();
~CCU();
protected:
bool OnNewMail(MOOSMSG_LIST &NewMail);
bool Iterate();
bool OnConnectToServer();
bool OnStartUp();
bool buildReport();
void registerVariables();
private:
// 链路收帧处理
void handleFcMessage(Message* msg, const std::vector<uint8_t>& frame);
void handlePmMessage(Message* msg, const std::vector<uint8_t>& frame);
void handleRcuMessage(Message* msg, const std::vector<uint8_t>& frame);
// MOOS 订阅消息处理(CAN_0x* 由 pCanBridge 发布)
void handleCanMessage(CMOOSMsg& msg);
// 整合并发送 PM 状态报文
void sendPmStatus();
// 构建网页快照
std::string buildSnapshot();
std::string handleApi(const std::string& uri, const std::string& query);
// BCU 断路器控制:/api/bcu_ctrl 解析与下发(经 MOOS CAN_TX_* -> pCanBridge)
std::string handleBcuCtrlApi(const std::string& query);
// 出队并下发一条 BCU 控制指令(Iterate 调用,MOOS 线程内 Notify 安全)
void sendPendingBcuCtrl();
// 配置
long m_fcLocalPort = 6000;
std::string m_fcHost = "192.168.1.162";
long m_fcPort = 7000;
long m_pmLocalPort = 7000;
std::string m_pmHost = "192.168.0.140";
long m_pmPort = 5001;
// 真实 CCU 桥接链路(配电协议透传)
bool m_rcuEnable = false; // 默认关闭,.moos 中开启
long m_rcuLocalPort = 7100;
std::string m_rcuHost = "192.168.0.200"; // 占位地址,.moos 中改为实际值
long m_rcuPort = 7000;
std::string m_dbPath = "pccu_data.db";
std::string m_logPath = "pCCU.log";
int m_webPort = 8080; // 与 pPowerManger(8090)/pPowerMangerHost(18080) 错开
bool m_webEnable = true;
// BCU 断路器控制帧下发的目标 CAN 通道(经 pCanBridge 的 m_sSrcAux 路由)
std::string m_bcuCanChannel = "CAN0";
// CAN BMS 日志节流(避免 60帧/s 刷爆日志)
double m_lastBmsLog = 0;
// 心跳
uint8_t m_pmHeartbeat = 0;
double m_lastStatusTx = 0;
// 组件
FcLinkManager* m_fcLink = nullptr;
PmLinkManager* m_pmLink = nullptr;
RcuLinkManager* m_rcuLink = nullptr;
DisBridge* m_bridge = nullptr;
SystemData* m_sysData = nullptr;
DbStore* m_db = nullptr;
SnapshotBuilder* m_snap = nullptr;
WebServer* m_web = nullptr;
PowerCoordinator* m_coord = nullptr;
};
} // namespace ccu
#endif // PCCU_CCU_H
-116
View File
@@ -1,116 +0,0 @@
/****************************************************************/
/* NAME: CCU_Info */
/* FILE: CCU_Info.cpp */
/****************************************************************/
#include <cstdlib>
#include <iostream>
#include "CCU_Info.h"
#include "ColorParse.h"
#include "ReleaseInfo.h"
using namespace std;
void showSynopsis() {
blk("SYNOPSIS: ");
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(" maps pPowerManger commands to FC operations, receives CAN ");
blk(" bus BMS battery frames via pCanBridge (CAN_0x* MOOS msgs), ");
blk(" bridges power-distribution (配电) frames between pPowerManger");
blk(" and a real CCU device (rcu link), stores data in SQLite and ");
blk(" provides a web monitoring page. ");
blk(" ");
}
void showHelpAndExit() {
blk(" ");
blu("=============================================================== ");
blu("Usage: pCCU file.moos [OPTIONS] ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("Options: ");
mag(" --alias","=<ProcessName> ");
blk(" Launch pCCU with the given process name ");
blk(" rather than pCCU. ");
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 pCCU. ");
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("pCCU Example MOOS Configuration ");
blu("=============================================================== ");
blk(" ");
blk("ProcessConfig = pCCU ");
blk("{ ");
blk(" AppTick = 4 ");
blk(" CommsTick = 4 ");
blk(" ");
blk(" fc_local_port = 6000 // FC 状态接收端口 ");
blk(" fc_remote_ip = 192.168.1.162 // 燃料电池控制器 IP ");
blk(" fc_remote_port = 7000 // FC 控制端口 ");
blk(" pm_local_port = 7000 // PM 指令接收端口 ");
blk(" pm_remote_ip = 192.168.0.140 // 控制主机 IP ");
blk(" pm_remote_port = 5001 // 控制主机接收端口 ");
blk(" rcu_enable = true // 是否启用真实CCU配电桥接 ");
blk(" rcu_local_port = 7100 // 真实CCU 数据接收端口 ");
blk(" rcu_remote_ip = 192.168.0.200 // 真实CCU IP(占位,按实际修改) ");
blk(" rcu_remote_port= 7000 // 真实CCU 接收端口 ");
blk(" // 锂电池 BMS 走 CAN 总线(经 pCanBridge 的 CAN_0x* 透传) ");
blk(" dbpath = pccu_data.db // 数据库路径 ");
blk(" logpath = pCCU.log // 日志路径 ");
blk(" web_port = 8080 // 网页端口(避开8090/18080) ");
blk(" web_enable = true // 是否启用网页 ");
blk("} ");
blk(" ");
exit(0);
}
void showInterfaceAndExit() {
blk(" ");
blu("=============================================================== ");
blu("pCCU INTERFACE ");
blu("=============================================================== ");
blk(" ");
showSynopsis();
blk(" ");
blk("SUBSCRIPTIONS: ");
blk("------------------------------------ ");
blk(" CAN_0x* = pCanBridge 透传的 CAN 帧(锂电池 BMS) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CAN_TX_0x%08X = BCU 断路器控制帧 0x10XX81FF(二进制 ");
blk(" 数据域,经 pCanBridge 下行到 CAN 总线) ");
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
blk(" CCU_RCU_LINK_STATE = 运行状态字符串 ");
blk(" ");
exit(0);
}
void showReleaseInfoAndExit() {
showReleaseInfo("pCCU", "gpl");
exit(0);
}
-15
View File
@@ -1,15 +0,0 @@
/****************************************************************/
/* NAME: CCU_Info */
/* FILE: CCU_Info.h */
/****************************************************************/
#ifndef PCCU_INFO_HEADER
#define PCCU_INFO_HEADER
void showSynopsis();
void showHelpAndExit();
void showExampleConfigAndExit();
void showInterfaceAndExit();
void showReleaseInfoAndExit();
#endif
-10
View File
@@ -1,10 +0,0 @@
1.接收来自燃料电池的消息,消息协议格式见docs/超滑FC和复合管控器通讯协议 - 0821.docx(备注:日期会随着时间进行更新)
2.将消息进行整合处理后,发送到pPowerManger,消息协议见docs/控制主机与复合管控器通信协议- 0824.xlsx
3.接收来自pPowermanger的指令,解析和编码后转发给燃料电池
4.具备sqlit数据库存储功能,可以把接收和发送的数据都存储到sqlit数据库中
5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置
6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介
7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署
8.接收动力/仪表锂电池的消息,协议见docs/锂电池协议20230324.docx;动力/仪表锂电池的ip和端口均可通过.moos文件配置
9.锂电池需要额外操作(自检/上下电/电池切换/功率设定),pCCU根据操作按协议时序下发指令(0x0000自检/0x0001控制,1s心跳)
10.pCCU按自身算法进行锂电池与燃料电池的协调操作,算法入口为core/PowerCoordinator.cpp的coordinationTick()(当前为占位,待完善)
-81
View File
@@ -1,81 +0,0 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pCCU
# 复合管控器网关:FC <-> pPowerManger,配电桥接 <-> 真实CCU
#--------------------------------------------------------
if (${WIN32})
SET(SYSTEM_LIBS wsock32)
else (${WIN32})
SET(SYSTEM_LIBS m pthread)
endif (${WIN32})
# 复用仓库内 pPowerManger 的公共源码(相对路径引用,避免重复维护)
SET(PM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../pPowerManger)
SET(SHARED_SRC
${PM_DIR}/logc/loguru.cpp
${PM_DIR}/sqlit3/sqlite3.c
${PM_DIR}/httpserver/mongoose.c
)
SET(CCU_PROTOCOL_SRC
protocol/Frame.cpp
protocol/Message.cpp
protocol/MessageRegistry.cpp
protocol/FcProtocol.cpp
protocol/PmProtocol.cpp
protocol/DisProtocol.cpp
protocol/CanBms.cpp
)
SET(CCU_COMM_SRC
comm/UdpEndpoint.cpp
comm/LinkManager.cpp
)
SET(CCU_CORE_SRC
core/SnapshotBuilder.cpp
core/PowerCoordinator.cpp
core/CoordFsm.cpp
core/DisBridge.cpp
)
SET(CCU_STORE_SRC
store/DbStore.cpp
)
SET(CCU_WEB_SRC
web/WebServer.cpp
)
SET(SRC
${SHARED_SRC}
${CCU_PROTOCOL_SRC}
${CCU_COMM_SRC}
${CCU_CORE_SRC}
${CCU_STORE_SRC}
${CCU_WEB_SRC}
CCU.cpp
CCU_Info.cpp
main.cpp
)
ADD_EXECUTABLE(pCCU ${SRC})
TARGET_INCLUDE_DIRECTORIES(pCCU PRIVATE
${PM_DIR}
${PM_DIR}/sqlit3
${PM_DIR}/httpserver
${PM_DIR}/logc
)
TARGET_LINK_LIBRARIES(pCCU
${MOOS_LIBRARIES}
apputil
mbutil
m
pthread
jsoncpp
dl
${SYSTEM_LIBS}
)
-42
View File
@@ -1,42 +0,0 @@
#ifndef PCCU_FC_LINK_MANAGER_H
#define PCCU_FC_LINK_MANAGER_H
#include "LinkManager.h"
#include "../protocol/FcProtocol.h"
namespace ccu {
//============================================================================
// FcLinkManager:面向燃料电池控制器的链路。
//
// - 本地监听:燃料电池状态反馈端口(默认 6000)
// - 发送目标:燃料电池控制器(默认 192.168.1.162:7000)
// 端口/地址均可由 .moos 配置覆盖。
//============================================================================
class FcLinkManager : public LinkManager {
public:
FcLinkManager(long localPort,
const std::string& fcHost, long fcPort)
: LinkManager("fc", localPort), m_fcHost(fcHost), m_fcPort(fcPort) {
registerFcMessages(registry());
}
void setFcAddress(const std::string& host, long port) {
m_fcHost = host; m_fcPort = port;
}
const std::string& fcHost() const { return m_fcHost; }
long fcPort() const { return m_fcPort; }
protected:
std::string defaultRemoteHost() const override { return m_fcHost; }
long defaultRemotePort() const override { return m_fcPort; }
private:
std::string m_fcHost;
long m_fcPort;
};
} // namespace ccu
#endif // PCCU_FC_LINK_MANAGER_H
-137
View File
@@ -1,137 +0,0 @@
#include "LinkManager.h"
#include "../protocol/Frame.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "loguru.hpp"
namespace ccu {
LinkManager::LinkManager(const std::string& linkName, long localPort)
: m_linkName(linkName), m_localPort(localPort) {}
LinkManager::~LinkManager() {
stop();
}
bool LinkManager::start() {
if (m_running) return true;
if (!m_endpoint.init(m_localPort)) {
LOG_F(ERROR, "[%s] bind failed on port %ld", m_linkName.c_str(), m_localPort);
return false;
}
m_running = true;
m_rxThread = std::thread([this]() { rxThreadFunc(); });
LOG_F(INFO, "[%s] listening on local port %ld", m_linkName.c_str(), m_localPort);
return true;
}
void LinkManager::stop() {
if (!m_running) return;
m_running = false;
if (m_rxThread.joinable()) m_rxThread.join();
}
bool LinkManager::sendMessage(uint16_t id, const void* value) {
return sendMessageTo(id, value, defaultRemoteHost(), defaultRemotePort());
}
bool LinkManager::sendMessageTo(uint16_t id, const void* value,
const std::string& host, long port) {
if (host.empty() || port <= 0) return false;
Message* msg = m_registry.find(id);
if (!msg) {
LOG_F(ERROR, "[%s] unknown message id 0x%04X", m_linkName.c_str(), id);
return false;
}
std::vector<uint8_t> frame = msg->encode(value);
if (!m_endpoint.sendTo(host, port, frame)) {
LOG_F(ERROR, "[%s] send msg 0x%04X to %s:%ld failed",
m_linkName.c_str(), id, host.c_str(), port);
return false;
}
++m_txCount;
if (m_logSink) m_logSink->onRawFrame(1, id, m_linkName, frame, true);
if (m_onRawFrame) m_onRawFrame(1, frame, true);
return true;
}
bool LinkManager::sendRaw(const std::vector<uint8_t>& frame) {
if (frame.empty()) return false;
const std::string& host = defaultRemoteHost();
long port = defaultRemotePort();
if (host.empty() || port <= 0) return false;
if (!m_endpoint.sendTo(host, port, frame)) {
LOG_F(ERROR, "[%s] send raw frame (%zu B) to %s:%ld failed",
m_linkName.c_str(), frame.size(), host.c_str(), port);
return false;
}
++m_txCount;
uint16_t id = 0;
Frame::parseId(frame, id);
if (m_logSink) m_logSink->onRawFrame(1, id, m_linkName, frame, true);
if (m_onRawFrame) m_onRawFrame(1, frame, true);
return true;
}
bool LinkManager::handleIncoming(const std::vector<uint8_t>& frame) {
if (frame.size() < FRAME_HEADER_LEN) return false;
uint16_t id = 0;
if (!Frame::parseId(frame, id)) {
// 非本协议帧(如真实CCU 混发的旧协议 0x20 0x20 状态帧、无效帧):
// 原样落库便于排查,告警按 10s 节流避免刷屏
++m_errorCount;
if (m_logSink) m_logSink->onRawFrame(0, 0, m_linkName, frame, false);
double now = MOOSTime(false);
if (now - m_lastBadHdrLog >= 10.0) {
m_lastBadHdrLog = now;
LOG_F(WARNING, "[%s] 非本协议帧头(%02X %02X, %zu B),已忽略(10s节流)",
m_linkName.c_str(), frame[0], frame[1], frame.size());
}
return false;
}
// 优先按 id + 实际帧总长精确匹配(配电指令 0x0001~0x0004 与 PM 指令
// 共用 id,仅帧长不同;帧头 length 字段不可信,须用实际收到的字节数),
// 未命中回退该 id 的第一条(保持旧行为:decode 失败报错)
Message* msg = m_registry.find(id, frame.size());
if (!msg) {
LOG_F(WARNING, "[%s] unknown msg id 0x%04X, len=%zu",
m_linkName.c_str(), id, frame.size());
++m_errorCount;
return false;
}
bool checksumOk = msg->checksum().verify(frame);
++m_rxCount;
m_lastRxTime = MOOSTime(false);
// 原始帧落库 + 回调
if (m_logSink) m_logSink->onRawFrame(0, id, m_linkName, frame, checksumOk);
if (m_onRawFrame) m_onRawFrame(0, frame, checksumOk);
if (!checksumOk) {
LOG_F(WARNING, "[%s] checksum error msg 0x%04X", m_linkName.c_str(), id);
++m_errorCount;
return false;
}
// 解码交给上层(CCU 层知道具体消息类型)
if (m_onMessage) m_onMessage(msg, frame);
return true;
}
void LinkManager::rxThreadFunc() {
uint8_t buf[2048];
while (m_running) {
int n = m_endpoint.receive(buf, sizeof(buf));
if (n > 0) {
std::vector<uint8_t> frame(buf, buf + n);
handleIncoming(frame);
} else {
// 无数据,短暂休眠避免忙等
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}
} // namespace ccu
-119
View File
@@ -1,119 +0,0 @@
#ifndef PCCU_LINK_MANAGER_H
#define PCCU_LINK_MANAGER_H
#include <cstdint>
#include <string>
#include <atomic>
#include <thread>
#include <functional>
#include "UdpEndpoint.h"
#include "../protocol/MessageRegistry.h"
#include "../protocol/Message.h"
namespace ccu {
// 落库接口:由 DbStore 实现,供链路层记录原始帧(实现收发数据入库)
class ILinkLogSink {
public:
virtual ~ILinkLogSink() = default;
// direction: 0=收 1=发;data 为原始完整帧;checksumOk 为校验结果
virtual void onRawFrame(int direction, uint16_t msgId, const std::string& link,
const std::vector<uint8_t>& data, bool checksumOk) = 0;
};
//============================================================================
// LinkManager:单条 UDP 链路的管理者。
//
// 职责:
// - 绑定本地端口并维护独立接收线程
// - 持有该链路的 MessageRegistry(收发共用,id 按链路隔离)
// - 收帧:解析 id -> 查注册表 -> 校验 -> 回调 onMessage(携带消息对象与原始帧)
// - 发帧:编码 -> 发送 -> 落库
//
// 派生类(FcLinkManager / PmLinkManager)仅需提供链路名与默认收发地址。
//============================================================================
class LinkManager {
public:
LinkManager(const std::string& linkName, long localPort);
virtual ~LinkManager();
// 启动:绑定端口 + 启动接收线程。返回是否成功。
bool start();
void stop();
bool isRunning() const { return m_running; }
// 注册表(收发共用)
MessageRegistry& registry() { return m_registry; }
const MessageRegistry& registry() const { return m_registry; }
// 收帧回调:msg 为注册表中命中的消息对象(可调用 decode 解码),frame 为原始完整帧。
// 注意:同 id 不同长度的消息(如配电指令与 PM 指令)按帧总长精确区分,
// 回调携带的 msg 即精确命中的对象;长度未命中时回退该 id 的第一条。
void setOnMessage(std::function<void(Message* msg, const std::vector<uint8_t>& frame)> cb) {
m_onMessage = std::move(cb);
}
// 原始帧回调(用于 Web/调试展示原始报文)
void setOnRawFrame(std::function<void(int direction, const std::vector<uint8_t>& frame, bool checksumOk)> cb) {
m_onRawFrame = std::move(cb);
}
// 落库接口(可空)
void setLogSink(ILinkLogSink* sink) { m_logSink = sink; }
// 编码并发送一条已注册消息;返回是否成功
bool sendMessage(uint16_t id, const void* value);
// 编码并发送一条已注册消息,同时指定目标地址(覆盖默认)
bool sendMessageTo(uint16_t id, const void* value,
const std::string& host, long port);
// 原样发送一帧到默认远端(桥接透传用,不做编码);
// 消息 id 从帧头解析,收发计数与落库口径与 sendMessage 一致
bool sendRaw(const std::vector<uint8_t>& frame);
// 处理一帧收到的原始数据(供接收线程调用,也可测试时手动喂入)
bool handleIncoming(const std::vector<uint8_t>& frame);
// 链路状态:最近一次收到消息的时间(MOOSTime),0 表示从未收到
double lastRxTime() const { return m_lastRxTime; }
long localPort() const { return m_localPort; }
const std::string& linkName() const { return m_linkName; }
// 收发统计
unsigned long rxCount() const { return m_rxCount; }
unsigned long txCount() const { return m_txCount; }
unsigned long errorCount() const { return m_errorCount; }
protected:
// 派生类提供发送默认目标
virtual std::string defaultRemoteHost() const { return ""; }
virtual long defaultRemotePort() const { return 0; }
private:
void rxThreadFunc();
std::string m_linkName;
long m_localPort;
UdpEndpoint m_endpoint;
MessageRegistry m_registry;
std::atomic<bool> m_running{false};
std::thread m_rxThread;
std::function<void(Message*, const std::vector<uint8_t>&)> m_onMessage;
std::function<void(int, const std::vector<uint8_t>&, bool)> m_onRawFrame;
ILinkLogSink* m_logSink = nullptr;
double m_lastRxTime = 0;
double m_lastBadHdrLog = 0; // 非本协议帧头告警节流
unsigned long m_rxCount = 0;
unsigned long m_txCount = 0;
unsigned long m_errorCount = 0;
};
} // namespace ccu
#endif // PCCU_LINK_MANAGER_H
-50
View File
@@ -1,50 +0,0 @@
#ifndef PCCU_PM_LINK_MANAGER_H
#define PCCU_PM_LINK_MANAGER_H
#include "LinkManager.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
//============================================================================
// PmLinkManager:面向控制主机 pPowerManger 的链路。
//
// - 本地监听:控制主机指令端口(默认 7000)
// - 发送目标:控制主机(默认 192.168.0.140:5001)
//
// 注册的消息:
// - PM 协议 0x0001~0x0004(Sum32)
// - 配电指令 0x0001~0x0004(Sum8,13~21B 短帧):pPowerManger 下发的
// 配电指令与其操控/参数指令共用 id,仅帧长不同,由注册表按
// (id+帧长) 区分,DisBridge 据此原桥接转发真实CCU
// 端口/地址均可由 .moos 配置覆盖。
//============================================================================
class PmLinkManager : public LinkManager {
public:
PmLinkManager(long localPort,
const std::string& pmHost, long pmPort)
: LinkManager("pm", localPort), m_pmHost(pmHost), m_pmPort(pmPort) {
registerPmMessages(registry());
registerDisCmdMessages(registry());
}
void setPmAddress(const std::string& host, long port) {
m_pmHost = host; m_pmPort = port;
}
const std::string& pmHost() const { return m_pmHost; }
long pmPort() const { return m_pmPort; }
protected:
std::string defaultRemoteHost() const override { return m_pmHost; }
long defaultRemotePort() const override { return m_pmPort; }
private:
std::string m_pmHost;
long m_pmPort;
};
} // namespace ccu
#endif // PCCU_PM_LINK_MANAGER_H
-51
View File
@@ -1,51 +0,0 @@
#ifndef PCCU_RCU_LINK_MANAGER_H
#define PCCU_RCU_LINK_MANAGER_H
#include "LinkManager.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
//============================================================================
// RcuLinkManager:面向真实 CCU 设备的链路(配电桥接)。
//
// - 本地监听:rcu_local_port(默认 7100),接收真实CCU 上报的数据
// - 发送目标:真实CCU(默认占位 192.168.0.200:7000,.moos 可配)
//
// 注册的消息:
// - 配电反馈 0x0005~0x0008(Sum8):真实CCU 上报的配电状态,
// 由 DisBridge 原帧转发给 pPowerManger 并解码显示
// - 配电指令 0x0001~0x0004(Sum8):真实CCU 的指令回显仅识别,不转发
// - PM 消息 0x0001~0x0004(Sum32):真实CCU 状态报文 0x0004 仅解码
// 显示,不转发(避免与 pCCU 自身模拟的状态报文冲突)
//============================================================================
class RcuLinkManager : public LinkManager {
public:
RcuLinkManager(long localPort,
const std::string& rcuHost, long rcuPort)
: LinkManager("rcu", localPort), m_rcuHost(rcuHost), m_rcuPort(rcuPort) {
registerDisFbMessages(registry());
registerDisCmdMessages(registry());
registerPmMessages(registry());
}
void setRcuAddress(const std::string& host, long port) {
m_rcuHost = host; m_rcuPort = port;
}
const std::string& rcuHost() const { return m_rcuHost; }
long rcuPort() const { return m_rcuPort; }
protected:
std::string defaultRemoteHost() const override { return m_rcuHost; }
long defaultRemotePort() const override { return m_rcuPort; }
private:
std::string m_rcuHost;
long m_rcuPort;
};
} // namespace ccu
#endif // PCCU_RCU_LINK_MANAGER_H
-58
View File
@@ -1,58 +0,0 @@
#include "UdpEndpoint.h"
#include <iostream>
namespace ccu {
UdpEndpoint::UdpEndpoint() : m_socket(nullptr), m_localPort(0) {}
UdpEndpoint::~UdpEndpoint() {
if (m_socket) {
delete m_socket;
m_socket = nullptr;
}
}
bool UdpEndpoint::init(long localPort) {
m_localPort = localPort;
m_socket = new XPCUdpSocket(localPort);
try {
m_socket->vBindSocket();
} catch (const std::exception& e) {
std::cerr << "[UdpEndpoint] bind failed on port " << localPort
<< ": " << e.what() << std::endl;
delete m_socket;
m_socket = nullptr;
return false;
} catch (...) {
std::cerr << "[UdpEndpoint] bind failed on port " << localPort
<< ": unknown error" << std::endl;
delete m_socket;
m_socket = nullptr;
return false;
}
return true;
}
bool UdpEndpoint::sendTo(const std::string& host, long port, const std::vector<uint8_t>& data) {
if (!m_socket || data.empty()) return false;
try {
std::lock_guard<std::mutex> lock(m_sendMutex);
m_socket->iSendMessageTo(const_cast<uint8_t*>(data.data()),
static_cast<int>(data.size()),
port, host);
return true;
} catch (...) {
return false;
}
}
int UdpEndpoint::receive(uint8_t* buf, size_t len) {
if (!m_socket || !buf || len == 0) return 0;
try {
return m_socket->iRecieveMessage(buf, static_cast<int>(len));
} catch (...) {
return 0;
}
}
} // namespace ccu
-50
View File
@@ -1,50 +0,0 @@
#ifndef PCCU_UDP_ENDPOINT_H
#define PCCU_UDP_ENDPOINT_H
#define UNIX
#include "MOOS/libMOOS/Comms/XPCUdpSocket.h"
#include <cstdint>
#include <mutex>
#include <string>
#include <vector>
namespace ccu {
//============================================================================
// UdpEndpoint:单个 UDP 套接字的封装。
//
// 职责:
// - 绑定本地端口
// - 向指定远端 (host:port) 发送数据报
// - 阻塞接收数据报
// 不关心协议内容,仅负责字节的收发。
// 发送可能来自多个线程(MOOS 主线程 + 各链路接收线程的桥接转发),
// sendTo 内部加锁串行化。
//============================================================================
class UdpEndpoint {
public:
UdpEndpoint();
~UdpEndpoint();
// 绑定本地端口;返回是否成功(端口占用/权限失败返回 false)
bool init(long localPort);
// 向指定远端发送完整数据报
bool sendTo(const std::string& host, long port, const std::vector<uint8_t>& data);
// 阻塞接收一帧;返回接收字节数,0 或负值表示无数据/错误
int receive(uint8_t* buf, size_t len);
bool isOpen() const { return m_socket != nullptr; }
long localPort() const { return m_localPort; }
private:
XPCUdpSocket* m_socket = nullptr;
long m_localPort = 0;
std::mutex m_sendMutex; // 串行化并发 sendTo
};
} // namespace ccu
#endif // PCCU_UDP_ENDPOINT_H
-103
View File
@@ -1,103 +0,0 @@
#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()->bcuNode(addr) 锂电池 BCU 节点状态(电压/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()->bcuNode(addr) / 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()->bcuNode(addr).alarmBits 等。
(void)e;
}
void CoordFaultState::react(StepDoneEvent const &e) {
// TODO(策略填写):故障态下操作步骤结果处理(如降级恢复的确认)。
(void)e;
}
} // namespace ccu
// 初始状态声明(TinyFSM 宏要求全局作用域 + 命名空间限定名)
FSM_INITIAL_STATE(ccu::CoordFsm, ccu::CoordNormalState)
-137
View File
@@ -1,137 +0,0 @@
#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 携带完成时刻的 BCU 节点状态快照(SOC/电压/电流/告警,可判故障)
struct StepDoneEvent : tinyfsm::Event {
bool confirmed; // true=确认完成;false=超时未确认
BcuNodeStatus status; // 完成时刻 BCU 节点状态快照
StepDoneEvent() : confirmed(false) {}
StepDoneEvent(bool ok, const BcuNodeStatus& 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
-173
View File
@@ -1,173 +0,0 @@
#include "DisBridge.h"
#include "../comm/LinkManager.h"
#include "../protocol/DisProtocol.h"
#include "SystemData.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "../pPowerManger/logc/loguru.hpp"
namespace ccu {
void DisBridge::setup(LinkManager* pmLink, LinkManager* rcuLink, SystemData* sys) {
m_pmLink = pmLink;
m_rcuLink = rcuLink;
m_sys = sys;
}
//--------------------------------------------------------------------------
// PM 链路入口:pPowerManger -> 真实CCU(配电指令)
//--------------------------------------------------------------------------
bool DisBridge::onPmFrame(Message* msg, const std::vector<uint8_t>& frame) {
if (!isDisCmdMessage(msg)) return false;
forwardCmdToRcu(msg, frame);
return true;
}
void DisBridge::forwardCmdToRcu(Message* msg, const std::vector<uint8_t>& frame) {
if (!m_rcuLink || !m_sys) return;
// 原帧透传给真实CCU(帧字节不变,Sum8 校验和保持有效)
bool ok = m_rcuLink->sendRaw(frame);
// 取副本 -> 更新 -> 写回(跨线程共享快照的既有模式)
RealCcuState st = m_sys->realCcu();
st.cmdCount++;
if (ok) st.fwdCmdOk++; else st.fwdCmdErr++;
// 解码缓存最近指令(仅用于展示;解码失败不影响透传结果)
if (msg) {
bool decoded = false;
switch (msg->id()) {
case DIS_HV_CMD_ID: {
DisHighVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvCmd);
break;
}
case DIS_HVA_CMD_ID: {
DisHighAVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvaCmd);
break;
}
case DIS_HVB_CMD_ID: {
DisHighBVolBusCmdMessage m;
decoded = m.decode(frame, st.lastHvbCmd);
break;
}
case DIS_LV_CMD_ID: {
DisLowBusCmdMessage m;
decoded = m.decode(frame, st.lastLvCmd);
break;
}
default:
break;
}
if (decoded) {
st.cmdValid = true;
st.lastCmdRx = MOOSTime(false);
} else {
LOG_F(WARNING, "[桥接] 配电指令 0x%04X 解码失败(透传不受影响)", msg->id());
}
}
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 配电指令 0x%04X (%zu B) -> 真实CCU %s", msg ? msg->id() : 0,
frame.size(), ok ? "成功" : "失败");
}
//--------------------------------------------------------------------------
// RCU 链路入口:真实CCU -> pPowerManger(配电反馈)+ 状态报文显示
//--------------------------------------------------------------------------
bool DisBridge::onRcuFrame(Message* msg, const std::vector<uint8_t>& frame) {
if (isDisFbMessage(msg)) {
forwardFbToPm(msg, frame);
return true;
}
if (!isDisCmdMessage(msg) && msg && msg->id() == 0x0004) {
// 真实CCU 状态报文:仅解码显示,不转发(避免与 pCCU 模拟状态冲突)
storeRealCcuStatus(frame);
return true;
}
return false; // 配电指令回显等:不处理,交上层记录
}
void DisBridge::forwardFbToPm(Message* msg, const std::vector<uint8_t>& frame) {
if (!m_pmLink || !m_sys) return;
// 原帧透传给 pPowerManger
bool ok = m_pmLink->sendRaw(frame);
RealCcuState st = m_sys->realCcu();
st.fbCount++;
if (ok) st.fwdFbOk++; else st.fwdFbErr++;
// 解码缓存四条母线最新状态
if (msg) {
bool decoded = false;
switch (msg->id()) {
case DIS_HV_FB_ID: {
DisHighVolBusFbMessage m;
decoded = m.decode(frame, st.hvBus);
if (decoded) st.hvLastRx = MOOSTime(false);
break;
}
case DIS_HVA_FB_ID: {
DisHighAVolBusFbMessage m;
decoded = m.decode(frame, st.hvaBus);
if (decoded) st.hvaLastRx = MOOSTime(false);
break;
}
case DIS_HVB_FB_ID: {
DisHighBVolBusFbMessage m;
decoded = m.decode(frame, st.hvbBus);
if (decoded) st.hvbLastRx = MOOSTime(false);
break;
}
case DIS_LV_FB_ID: {
DisLowBusFbMessage m;
decoded = m.decode(frame, st.lvBus);
if (decoded) st.lvLastRx = MOOSTime(false);
break;
}
default:
break;
}
if (decoded) {
switch (msg->id()) {
case DIS_HV_FB_ID: st.hvValid = true; break;
case DIS_HVA_FB_ID: st.hvaValid = true; break;
case DIS_HVB_FB_ID: st.hvbValid = true; break;
case DIS_LV_FB_ID: st.lvValid = true; break;
default: break;
}
} else {
LOG_F(WARNING, "[桥接] 配电反馈 0x%04X 解码失败(透传不受影响)", msg->id());
}
}
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 配电反馈 0x%04X (%zu B) -> pPowerManger %s", msg ? msg->id() : 0,
frame.size(), ok ? "成功" : "失败");
}
void DisBridge::storeRealCcuStatus(const std::vector<uint8_t>& frame) {
if (!m_sys) return;
PmStatusMessage m;
PmStatusValue v;
if (!m.decode(frame, v)) {
LOG_F(WARNING, "[桥接] 真实CCU 状态报文 0x0004 解码失败");
return;
}
RealCcuState st = m_sys->realCcu();
st.ccuStatus = v;
st.ccuStatusValid = true;
st.ccuStatusLastRx = MOOSTime(false);
m_sys->updateRealCcu(st);
LOG_F(INFO, "[桥接] 真实CCU 状态报文: mode=%d status=%d heartbeat=%d (仅显示)",
v.fc_mode, v.fc_status, v.heartbeat);
}
} // namespace ccu
-55
View File
@@ -1,55 +0,0 @@
#ifndef PCCU_DIS_BRIDGE_H
#define PCCU_DIS_BRIDGE_H
#include <cstdint>
#include <vector>
#include "../protocol/Message.h"
namespace ccu {
class LinkManager;
class SystemData;
//============================================================================
// DisBridge:配电协议桥接器(pPowerManger <-> 真实CCU)。
//
// pCCU 居中桥接,原帧透传(不做解析重编码,校验和无需重算):
// pPowerManger --配电指令 0x0001~0x0004(Sum8,短帧)--> pCCU --原帧--> 真实CCU
// 真实CCU --配电反馈 0x0005~0x0008(Sum8)------> pCCU --原帧--> pPowerManger
//
// 同时把配电反馈(及真实CCU 状态报文 0x0004)解码写入 SystemData
// 供网页显示与统计;真实CCU 的 0x0004 仅显示不转发,避免与 pCCU
// 自身模拟的状态报文冲突。
//
// 调用约定:onPmFrame 在 PM 链路接收线程回调,onRcuFrame 在 RCU
// 链路接收线程回调;返回 true 表示该帧已被桥接处理,上层不再处理。
//============================================================================
class DisBridge {
public:
// 注入依赖(在 CCU::OnStartUp 中链路创建完成后调用)
void setup(LinkManager* pmLink, LinkManager* rcuLink, SystemData* sys);
// PM 链路收帧入口:命中配电指令则原帧转发真实CCU,返回是否已处理
bool onPmFrame(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收帧入口:命中配电反馈则原帧转发 pPowerManger 并解码
// 显示;真实CCU 状态报文 0x0004 仅解码显示。返回是否已处理
bool onRcuFrame(Message* msg, const std::vector<uint8_t>& frame);
private:
// PM 链路收到配电指令:原帧转发 + 解码缓存
void forwardCmdToRcu(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收到配电反馈:原帧转发 + 解码缓存
void forwardFbToPm(Message* msg, const std::vector<uint8_t>& frame);
// RCU 链路收到真实CCU 状态报文:仅解码缓存(显示)
void storeRealCcuStatus(const std::vector<uint8_t>& frame);
LinkManager* m_pmLink = nullptr;
LinkManager* m_rcuLink = nullptr;
SystemData* m_sys = nullptr;
};
} // namespace ccu
#endif // PCCU_DIS_BRIDGE_H
-53
View File
@@ -1,53 +0,0 @@
#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->bcuNode(addr) : 锂电池 BCU 节点最新状态(电压/SOC/电流/告警,
// CAN 经 pCanBridge 透传,protocol/CanBms.h)
// - m_sys->pmControl() : 控制主机最新操控指令
//
// 可用操作:
// - sendFcControl(FcControlValue) : 直接下发燃料电池控制指令
//
// 说明:BMS CAN 协议未定义远程控制报文(启停/接触器/功率设定),
// 电池侧控制暂不可用;协调策略围绕 FC 与 BMS 状态联动设计。
//
// TODO(开发者完善):在此实现具体的功率分配、燃料电池与
// 锂电池联合调度等协调策略。当前为空实现。
//---------------------------------------------------------
void PowerCoordinator::coordinationTick(double now) {
(void)now; // 占位:暂不执行任何协调动作
}
} // namespace ccu
-54
View File
@@ -1,54 +0,0 @@
#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
-741
View File
@@ -1,741 +0,0 @@
#include "SnapshotBuilder.h"
#include "../store/DbStore.h"
#include "../protocol/CanBms.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "json/json.h"
#include <sstream>
#include <cstdio>
#include <utility>
namespace ccu {
using JsonVal = Json::Value;
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=发),给出中文名与
// "来源→去向"。方向是相对 pCCU 而言:收=对方→CCU,发=CCU→对方。
// length 用于区分同 id 消息:配电指令 0x0001~0x0004 为 13~21B 短帧,
// 与 PM 操控/参数指令(28/45B)共用 id。
//--------------------------------------------------------------------------
std::string describeMessageName(const std::string& link, uint16_t id, int length) {
if (link == "fc") {
switch (id) {
case 0x0001: return "FC控制指令";
case 0x0002: return "FC状态反馈";
default: break;
}
} else if (link == "pm") {
switch (id) {
case 0x0001:
return (length > 0 && length <= 21) ? "PM配电指令(高压母线)" : "PM操控指令";
case 0x0002:
return (length > 0 && length <= 21) ? "PM配电指令(高压母线A)" : "PM参数设定";
case 0x0003:
return (length > 0 && length <= 21) ? "PM配电指令(低压主母线)" : "PM参数反馈";
case 0x0004:
return (length > 0 && length <= 21) ? "PM配电指令(仪表汇流排)" : "PM状态报文";
case 0x0005: return "PM配电反馈(高压母线)";
case 0x0006: return "PM配电反馈(高压母线A)";
case 0x0007: return "PM配电反馈(低压主母线)";
case 0x0008: return "PM配电反馈(仪表汇流排)";
default: break;
}
} else if (link == "rcu") {
switch (id) {
case 0x0001: return "配电指令(高压母线)";
case 0x0002: return "配电指令(高压母线A)";
case 0x0003: return "配电指令(低压主母线)";
case 0x0004: return "配电指令(仪表汇流排)";
case 0x0005: return "配电反馈(高压母线)";
case 0x0006: return "配电反馈(高压母线A)";
case 0x0007: return "配电反馈(低压主母线)";
case 0x0008: return "配电反馈(仪表汇流排)";
default: break;
}
}
char buf[32];
std::snprintf(buf, sizeof(buf), "未知(0x%04X)", id);
return buf;
}
std::string describeDirection(const std::string& link, int direction) {
std::string peer = "PM";
if (link == "fc") peer = "FC";
else if (link == "rcu") peer = "真实CCU";
return (direction == 1) ? ("CCU→" + peer) : (peer + "→CCU");
}
// 把数据库一行记录转成 JSON(含消息名与方向描述)
JsonVal commLogToJson(const CommLogRow& r) {
JsonVal e(Json::objectValue);
e["time"] = JsonVal(r.time);
e["link"] = r.link;
e["direction"] = r.direction;
e["msgId"] = r.msgId;
e["checksumOk"] = r.checksumOk;
e["hex"] = r.hex;
e["name"] = describeMessageName(r.link, r.msgId, r.length);
e["dirText"] = describeDirection(r.link, r.direction);
return e;
}
// 燃料电池状态 -> JSON
JsonVal fcStatusJson(const FcStatusValue& v) {
JsonVal j(Json::objectValue);
put(j, "fc_mode", v.fc_mode);
put(j, "fc_status", v.fc_status);
put(j, "fault_level_1", v.fault_level_1);
put(j, "fault_level_2", v.fault_level_2);
put(j, "fault_level_3", v.fault_level_3);
put(j, "fault_level_4", v.fault_level_4);
put(j, "total_generation_time", v.total_generation_time);
put(j, "fc_fault_level", v.fc_fault_level);
put(j, "generation_power", v.generation_power);
put(j, "hydrogen_capacity", v.hydrogen_capacity);
put(j, "liquid_oxygen_capacity", v.liquid_oxygen_capacity);
put(j, "fc1_min_cell_voltage", v.fc1_min_cell_voltage);
put(j, "fc1_min_cell_pos", v.fc1_min_cell_pos);
put(j, "fc1_avg_cell_voltage", v.fc1_avg_cell_voltage);
put(j, "fc2_min_cell_voltage", v.fc2_min_cell_voltage);
put(j, "fc2_min_cell_pos", v.fc2_min_cell_pos);
put(j, "fc2_avg_cell_voltage", v.fc2_avg_cell_voltage);
put(j, "palladium_temp", v.palladium_temp);
put(j, "buffer_tank_pressure", v.buffer_tank_pressure);
put(j, "flue_total_emission", v.flue_total_emission);
put(j, "flue_pressure", v.flue_pressure);
put(j, "reactor_pressure", v.reactor_pressure);
put(j, "electric_valve_open", v.electric_valve_open);
put(j, "dcdc1_in_voltage", v.dcdc1_in_voltage);
put(j, "dcdc1_in_current", v.dcdc1_in_current);
put(j, "dcdc2_in_voltage", v.dcdc2_in_voltage);
put(j, "dcdc2_in_current", v.dcdc2_in_current);
put(j, "dcdc_out_voltage", v.dcdc_out_voltage);
put(j, "dcdc_out_current", v.dcdc_out_current);
put(j, "dcdc_ctrl_voltage", v.dcdc_ctrl_voltage);
put(j, "dcdc_aux_voltage", v.dcdc_aux_voltage);
put(j, "methanol_total_use", v.methanol_total_use);
put(j, "methanol_feed", v.methanol_feed);
put(j, "oxygen_side_water_level", v.oxygen_side_water_level);
put(j, "hydrogen_side_water_level", v.hydrogen_side_water_level);
put(j, "ballast_water_level", v.ballast_water_level);
put(j, "exhaust_inlet_pressure", v.exhaust_inlet_pressure);
put(j, "exhaust_outlet_pressure", v.exhaust_outlet_pressure);
put(j, "cabin_pressure1", v.cabin_pressure1);
put(j, "cabin_pressure2", v.cabin_pressure2);
put(j, "cabin_temp1", v.cabin_temp1);
put(j, "cabin_temp2", v.cabin_temp2);
put(j, "cabin_humidity1", v.cabin_humidity1);
put(j, "cabin_humidity2", v.cabin_humidity2);
put(j, "h2_concentration1", v.h2_concentration1);
put(j, "h2_concentration2", v.h2_concentration2);
put(j, "h2_concentration3", v.h2_concentration3);
put(j, "o2_concentration1", v.o2_concentration1);
put(j, "o2_concentration2", v.o2_concentration2);
put(j, "ch3oh_concentration1", v.ch3oh_concentration1);
put(j, "ch3oh_concentration2", v.ch3oh_concentration2);
put(j, "flame_detector1", v.flame_detector1);
put(j, "flame_detector2", v.flame_detector2);
put(j, "emergency_float_depth", v.emergency_float_depth);
put(j, "emergency_float_time", v.emergency_float_time);
put(j, "exhaust_run_freq", v.exhaust_run_freq);
put(j, "exhaust_inlet_temp", v.exhaust_inlet_temp);
put(j, "exhaust_outlet_temp", v.exhaust_outlet_temp);
put(j, "exhaust_water_in_pressure", v.exhaust_water_in_pressure);
put(j, "exhaust_water_out_pressure", v.exhaust_water_out_pressure);
put(j, "tank_lo2_pressure", v.tank_lo2_pressure);
put(j, "tank_co2_pressure", v.tank_co2_pressure);
put(j, "tank_lo2_level", v.tank_lo2_level);
put(j, "alloy_h2_flow", v.alloy_h2_flow);
put(j, "fc_h2_flow", v.fc_h2_flow);
put(j, "fc_o2_flow", v.fc_o2_flow);
put(j, "remaining_generation", v.remaining_generation);
put(j, "heartbeat", v.heartbeat);
put(j, "emergency_cmd", v.emergency_cmd);
return j;
}
// PM 操控指令 -> JSON
JsonVal pmControlJson(const PmControlValue& v) {
JsonVal j(Json::objectValue);
put(j, "year", v.year);
put(j, "month", v.month);
put(j, "day", v.day);
put(j, "hour", v.hour);
put(j, "minute", v.minute);
put(j, "second", v.second);
put(j, "millisecond10", v.millisecond10);
put(j, "mode", v.mode);
put(j, "cmd", v.cmd);
put(j, "outputPower", v.outputPower);
j["pitch"] = JsonVal(static_cast<int>(v.pitch)); // 有符号 int16,×0.1°
j["roll"] = JsonVal(static_cast<int>(v.roll));
put(j, "emergencyAllow", v.emergencyAllow);
put(j, "depth", v.depth);
put(j, "supplyCmd", v.supplyCmd);
put(j, "reservedCmd5", v.reservedCmd5);
put(j, "reservedCmd6", v.reservedCmd6);
put(j, "insBatCmd", v.insBatCmd);
put(j, "dynBatCmd", v.dynBatCmd);
put(j, "dynBatPower", v.dynBatPower);
put(j, "heartbeat", v.heartbeat);
put(j, "hostState", v.hostState);
return j;
}
// FC 控制指令(转发给 FC)-> JSON
JsonVal fcControlJson(const FcControlValue& v) {
JsonVal j(Json::objectValue);
put(j, "mode", v.mode);
put(j, "cmd", v.cmd);
put(j, "outputPower", v.outputPower);
j["pitch1"] = JsonVal(static_cast<int>(v.pitch1)); // 有符号 int16,×0.1°
j["roll1"] = JsonVal(static_cast<int>(v.roll1));
put(j, "emergencyAllow", v.emergencyAllow);
put(j, "depth", v.depth);
put(j, "supplyCmd", v.supplyCmd);
put(j, "reservedCmd5", v.reservedCmd5);
put(j, "reservedCmd6", v.reservedCmd6);
put(j, "heartbeat", v.heartbeat);
return j;
}
// PM 参数设定(收到)-> JSON
JsonVal pmParamJson(const PmParamSetValue& v) {
JsonVal j(Json::objectValue);
put(j, "insSocHold1", v.insSocHold1);
put(j, "insSocHold2", v.insSocHold2);
put(j, "insSocHold3", v.insSocHold3);
put(j, "insOutputPower", v.insOutputPower);
put(j, "dynSocHold1", v.dynSocHold1);
put(j, "dynSocHold2", v.dynSocHold2);
put(j, "dynSocHold3", v.dynSocHold3);
put(j, "dynOutputPower", v.dynOutputPower);
return j;
}
// PM 参数反馈(发送)-> JSON
JsonVal pmFbJson(const PmParamSetFbValue& v) {
JsonVal j(Json::objectValue);
put(j, "flag", v.flag);
put(j, "failCode", v.failCode);
return j;
}
// 锂电池 BCU 节点(CAN,docs/04KT38电池BCU-MBMS通信(CAN)定义.docx)-> JSON
// 同一节点(0x10XX00YY 中 XX)的全部报文字段归并在一个对象内,
// 告警位由附录1 表解析出中文含义放入 alarms 数组。
// nowSec 为当前 MOOSTime,用于计算节点数据最后更新距今的秒数 age。
JsonVal bcuNodeJson(uint8_t addr, const BcuNodeStatus& v, double nowSec) {
JsonVal j(Json::objectValue);
j["addr"] = JsonVal(addr);
j["valid"] = JsonVal(v.valid);
j["lastRx"] = JsonVal(v.lastRxTime);
j["age"] = JsonVal(nowSec - v.lastRxTime); // 距最后收到该节点报文的秒数
j["rx"] = JsonVal(static_cast<Json::UInt>(v.rxMask));
// 0x10XX0000:电压 / 电流 / SOC / 告警码 / 自检
j["totalVoltage"] = JsonVal(v.totalVoltage);
j["current"] = JsonVal(v.current);
j["soc"] = JsonVal(v.soc);
j["alarmCode"] = JsonVal(v.alarmCode);
j["selfCheck"] = JsonVal(v.selfCheck);
// 0x10XX0001:单体电压
j["maxCellVoltage"] = JsonVal(v.maxCellVoltage);
j["maxCellVoltageNo"] = JsonVal(v.maxCellVoltageNo);
j["minCellVoltage"] = JsonVal(v.minCellVoltage);
j["minCellVoltageNo"] = JsonVal(v.minCellVoltageNo);
j["avgCellVoltage"] = JsonVal(v.avgCellVoltage);
// 0x10XX0002:单体温度
j["maxCellTemp"] = JsonVal(v.maxCellTemp);
j["maxCellTempNo"] = JsonVal(v.maxCellTempNo);
j["minCellTemp"] = JsonVal(v.minCellTemp);
j["minCellTempNo"] = JsonVal(v.minCellTempNo);
j["avgCellTemp"] = JsonVal(v.avgCellTemp);
// 0x10XX0003:继电器
j["posRelay"] = JsonVal(v.posRelay);
j["negRelay"] = JsonVal(v.negRelay);
// 0x10XX0006:绝缘 / 端口电压
j["posInsulationKohm"] = JsonVal(v.posInsulationKohm);
j["negInsulationKohm"] = JsonVal(v.negInsulationKohm);
j["portVoltage"] = JsonVal(v.portVoltage);
j["posRelayOuterVoltage"] = JsonVal(v.posRelayOuterVoltage);
// 0x10XX0010:告警位(原始 hex + 附录1 解释)
JsonVal bits(Json::arrayValue);
for (int i = 0; i < 6; ++i)
bits.append(JsonVal(static_cast<Json::UInt>(v.alarmBits[i])));
j["alarmBits"] = bits;
JsonVal alarms(Json::arrayValue);
const char* texts[32];
int n = bcuAlarmTexts(v, texts, 32);
for (int i = 0; i < n; ++i)
alarms.append(JsonVal(texts[i]));
j["alarms"] = alarms;
j["alarmCount"] = JsonVal(n);
return j;
}
// 全部 BCU 节点 -> JSON(只输出收到过报文的节点,按地址升序)
JsonVal bcuNodesJson(const BcuNodeStatus nodes[kBcuNodeCount], double nowSec) {
JsonVal j(Json::arrayValue);
for (int i = 0; i < kBcuNodeCount; ++i) {
if (nodes[i].valid)
j.append(bcuNodeJson(static_cast<uint8_t>(i + kBcuAddrMin), nodes[i], nowSec));
}
return j;
}
// PM 状态报文(整合后发送给控制主机)-> JSON(关键字段)
JsonVal pmStatusJson(const PmStatusValue& v) {
JsonVal j(Json::objectValue);
put(j, "fc_mode", v.fc_mode);
put(j, "fc_status", v.fc_status);
put(j, "fault_level_1", v.fault_level_1);
put(j, "fault_level_2", v.fault_level_2);
put(j, "fault_level_3", v.fault_level_3);
put(j, "fault_level_4", v.fault_level_4);
put(j, "total_generation_time", v.total_generation_time);
put(j, "fc_fault_level", v.fc_fault_level);
put(j, "output_power_limit", v.output_power_limit);
put(j, "generation_power", v.generation_power);
put(j, "hydrogen_capacity", v.hydrogen_capacity);
put(j, "liquid_oxygen_capacity", v.liquid_oxygen_capacity);
put(j, "palladium_temp", v.palladium_temp);
put(j, "buffer_tank_pressure", v.buffer_tank_pressure);
put(j, "main_pipe_pressure", v.main_pipe_pressure);
put(j, "aux_pipe_pressure", v.aux_pipe_pressure);
put(j, "dcdc1_in_voltage", v.dcdc1_in_voltage);
put(j, "dcdc1_in_current", v.dcdc1_in_current);
put(j, "dcdc2_in_voltage", v.dcdc2_in_voltage);
put(j, "dcdc2_in_current", v.dcdc2_in_current);
put(j, "dcdc_out_voltage", v.dcdc_out_voltage);
put(j, "dcdc_out_current", v.dcdc_out_current);
put(j, "methanol_total_use", v.methanol_total_use);
put(j, "methanol_feed", v.methanol_feed);
put(j, "exhaust_inlet_pressure", v.exhaust_inlet_pressure);
put(j, "exhaust_outlet_pressure", v.exhaust_outlet_pressure);
put(j, "exhaust_run_freq", v.exhaust_run_freq);
put(j, "tank_lo2_pressure", v.tank_lo2_pressure);
put(j, "tank_co2_pressure", v.tank_co2_pressure);
put(j, "tank_lo2_level", v.tank_lo2_level);
put(j, "alloy_h2_flow", v.alloy_h2_flow);
put(j, "fc_h2_flow", v.fc_h2_flow);
put(j, "fc_o2_flow", v.fc_o2_flow);
put(j, "emergency_float_depth", v.emergency_float_depth);
put(j, "emergency_float_time", v.emergency_float_time);
put(j, "cabin_pressure1", v.cabin_pressure1);
put(j, "cabin_pressure2", v.cabin_pressure2);
put(j, "cabin_temp1", v.cabin_temp1);
put(j, "cabin_temp2", v.cabin_temp2);
put(j, "cabin_humidity1", v.cabin_humidity1);
put(j, "cabin_humidity2", v.cabin_humidity2);
put(j, "h2_concentration1", v.h2_concentration1);
put(j, "h2_concentration2", v.h2_concentration2);
put(j, "h2_concentration3", v.h2_concentration3);
put(j, "o2_concentration1", v.o2_concentration1);
put(j, "o2_concentration2", v.o2_concentration2);
put(j, "ch3oh_concentration1", v.ch3oh_concentration1);
put(j, "ch3oh_concentration2", v.ch3oh_concentration2);
put(j, "flame_detector1", v.flame_detector1);
put(j, "flame_detector2", v.flame_detector2);
put(j, "emergency_battery1_voltage", v.emergency_battery1_voltage);
put(j, "emergency_battery1_current", v.emergency_battery1_current);
put(j, "emergency_battery1_max_temp", v.emergency_battery1_max_temp);
put(j, "emergency_battery1_fault_word", v.emergency_battery1_fault_word);
put(j, "emergency_battery2_voltage", v.emergency_battery2_voltage);
put(j, "emergency_battery2_current", v.emergency_battery2_current);
put(j, "emergency_battery2_max_temp", v.emergency_battery2_max_temp);
put(j, "emergency_battery2_fault_word", v.emergency_battery2_fault_word);
put(j, "ins_cabin_ox_concentration", v.ins_cabin_ox_concentration);
put(j, "ins_cabin_temperature", v.ins_cabin_temperature);
put(j, "ins_cabin_humidity", v.ins_cabin_humidity);
put(j, "ins_cabin_pressure", v.ins_cabin_pressure);
put(j, "dyn_cabin_ox_concentration", v.dyn_cabin_ox_concentration);
put(j, "dyn_cabin_temperature", v.dyn_cabin_temperature);
put(j, "dyn_cabin_humidity", v.dyn_cabin_humidity);
put(j, "dyn_cabin_pressure", v.dyn_cabin_pressure);
put(j, "ins_relay_status1", v.ins_relay_status1);
put(j, "ins_relay_status2", v.ins_relay_status2);
put(j, "dyn_relay_status1", v.dyn_relay_status1);
put(j, "dyn_relay_status2", v.dyn_relay_status2);
put(j, "ins_max_discharge_power", v.ins_max_discharge_power);
put(j, "dyn_max_discharge_power", v.dyn_max_discharge_power);
put(j, "ins_soc", v.ins_soc);
put(j, "dyn_soc", v.dyn_soc);
put(j, "ins_total_energy", v.ins_total_energy);
put(j, "dyn_total_energy", v.dyn_total_energy);
put(j, "ins_power_input", v.ins_power_input);
put(j, "dyn_power_input", v.dyn_power_input);
put(j, "ins_charge_status", v.ins_charge_status);
put(j, "dyn_charge_status", v.dyn_charge_status);
put(j, "ins_voltage_link", v.ins_voltage_link);
put(j, "ins_voltage_pack", v.ins_voltage_pack);
put(j, "ins_current", v.ins_current);
put(j, "ins_resistance_pos", v.ins_resistance_pos);
put(j, "ins_resistance_neg", v.ins_resistance_neg);
put(j, "dyn_voltage_link", v.dyn_voltage_link);
put(j, "dyn_voltage_pack", v.dyn_voltage_pack);
put(j, "dyn_current", v.dyn_current);
put(j, "dyn_resistance_pos", v.dyn_resistance_pos);
put(j, "dyn_resistance_neg", v.dyn_resistance_neg);
put(j, "ins_emergency_status", v.ins_emergency_status);
put(j, "dyn_emergency_status", v.dyn_emergency_status);
put(j, "device_online_flag1", v.device_online_flag1);
put(j, "device_online_flag2", v.device_online_flag2);
put(j, "heartbeat", v.heartbeat);
put(j, "emergency_cmd", v.emergency_cmd);
return j;
}
JsonVal linkJson(const LinkManager* lm) {
JsonVal j(Json::objectValue);
if (!lm) return j;
j["rx"] = JsonVal(static_cast<Json::UInt>(lm->rxCount()));
j["tx"] = JsonVal(static_cast<Json::UInt>(lm->txCount()));
j["err"] = JsonVal(static_cast<Json::UInt>(lm->errorCount()));
j["lastRx"] = JsonVal(lm->lastRxTime());
return j;
}
//--------------------------------------------------------------------------
// 真实CCU 配电桥接 -> JSON(rcu 节)
// 断路器/开关量按原始值下发(0x55 闭合 / 0xAA 断开 / 0x5A 故障),
// 由前端渲染;age 为距最后收到该类帧的秒数(-1 表示从未收到)。
//--------------------------------------------------------------------------
JsonVal ageJson(bool valid, double lastRx, double now) {
JsonVal j(Json::objectValue);
j["valid"] = JsonVal(valid);
j["age"] = JsonVal(valid ? (now - lastRx) : -1.0);
return j;
}
// 断路器组( 名称 -> 原始值 )
JsonVal disBreakersJson(const std::vector<std::pair<const char*, uint8_t>>& items) {
JsonVal j(Json::objectValue);
for (const auto& it : items) put(j, it.first, it.second);
return j;
}
JsonVal hvBusJson(const disHighVolBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"fuelCell", v.fuelCellCircuitBreaker},
{"powerLithiumBattery", v.powerLithiumBatteryCircuitBreaker},
{"propulsionMotor", v.propulsionMotorCircuitBreaker},
{"lithiumBatteryGroupInstrument", v.lithiumBatteryGroupInstrumentCircuitBreaker},
{"dcDc5Module", v.dcDc5ModuleCircuitBreaker},
{"bowHighVoltageDistributionBox", v.bowHighVoltageDistributionBoxCircuitBreaker},
{"sternFTDevice45", v.sternFTDevice45CircuitBreaker},
{"fbReserved", v.fbReservedCircuitBreaker},
{"sternRudderSwitch1", v.sternRudderSwitch1CircuitBreaker},
{"sternRudderSwitch2", v.sternRudderSwitch2CircuitBreaker},
{"tyzReserved", v.tyzReservedCircuitBreaker},
{"reserved", v.reservedCircuitBreaker},
});
put(j, "dcDcFaultWord1", v.dcDcFaultWord1);
put(j, "dcDcFaultWord2", v.dcDcFaultWord2);
put(j, "powerBusInsulationStatus", v.powerBusInsulationStatus);
put(j, "aBusInsulationStatus", v.aBusInsulationStatus);
put(j, "meterPowerLossSignal", v.meterPowerLossSignal);
put(j, "emergencyPowerLossSignal", v.emergencyPowerLossSignal);
put(j, "dcDcTemperature", v.dcDcTemperature);
put(j, "powerBusVoltage", v.powerBusVoltage);
put(j, "dcDc5ModuleCurrent", v.dcDc5ModuleCurrent);
put(j, "powerBusCurrent", v.powerBusCurrent);
put(j, "busbarAVoltage", v.busbarAVoltage);
put(j, "propulsionMotorControlBoxCurrent", v.propulsionMotorControlBoxCurrent);
put(j, "busbarACurrent", v.busbarACurrent);
put(j, "lithiumBatteryGroupMeterCurrent", v.lithiumBatteryGroupMeterCurrent);
put(j, "bowHighVoltageDistributionBoxCurrent", v.bowHighVoltageDistributionBoxCurrent);
put(j, "sternFTDevice45Current", v.sternFTDevice45Current);
put(j, "tyzReservedSwitchCurrent", v.tyzReservedSwitchCurrent);
put(j, "sternRudderSwitch1Current", v.sternRudderSwitch1Current);
put(j, "reservedCurrent1", v.reservedCurrent1);
put(j, "sternRudderSwitch2Current", v.sternRudderSwitch2Current);
put(j, "reservedCurrent2", v.reservedCurrent2);
put(j, "fbReservedSwitchCurrent", v.fbReservedSwitchCurrent);
put(j, "reservedCurrent3", v.reservedCurrent3);
put(j, "coolWaterPressure", v.coolWaterPressure);
return j;
}
JsonVal hvaBusJson(const disHighAVolBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"bowFTDevice123", v.bowFTDevice123CircuitBreaker},
{"actuator", v.actuatorCircuitBreaker},
{"mastSteeringGearControlBox", v.mastSteeringGearControlBoxCircuitBreaker},
{"xcz", v.xczCircuitBreaker},
{"bowRudderControlBox", v.bowRudderControlBoxCircuitBreaker},
{"openWaterCoverStartCylinder", v.openWaterCoverStartCylinderCircuitBreaker},
});
put(j, "instrumentPowerFailureSignal", v.instrumentPowerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "waterIngressionAlarm", v.waterIngressionAlarm);
put(j, "busbarBVoltage", v.busbarBVoltage);
put(j, "busbarBCurrent", v.busbarBCurrent);
put(j, "bowFTDevice123Current", v.bowFTDevice123Current);
put(j, "actuatorCurrent", v.actuatorCurrent);
put(j, "mastSteeringGearControlBoxCurrent", v.mastSteeringGearControlBoxCurrent);
put(j, "xczCurrent", v.xczCurrent);
put(j, "bowRudderControlBoxCurrent", v.bowRudderControlBoxCurrent);
put(j, "openWaterCoverStartCylinderCurrent", v.openWaterCoverStartCylinderCurrent);
return j;
}
JsonVal hvbBusJson(const disLowMainBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"lithiumBatteryGroupInstrument", v.lithiumBatteryGroupInstrumentCircuitBreaker},
{"bowLowVoltageDistributionBox", v.bowLowVoltageDistributionBoxCircuitBreaker},
{"unit4InstrumentDC48V", v.unit4InstrumentDC48VCircuitBreaker},
{"dcC1DCDistributionPanel", v.dcC1DCDistributionPanelCircuitBreaker},
{"reserved1", v.reservedCircuitBreaker1},
{"reserved2", v.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup2", v.emergencyLithiumBatteryGroup2CircuitBreaker},
});
put(j, "instrumentPowerFailureSignal", v.instrumentPowerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "instrumentBusbarInsulationLow", v.instrumentBusbarInsulationLow);
put(j, "instrumentBusbarVoltage", v.instrumentBusbarVoltage);
put(j, "bowLowVoltageDistributionBoxCurrent", v.bowLowVoltageDistributionBoxCurrent);
put(j, "dcC1InstrumentDC48VCurrent", v.dcC1InstrumentDC48VCurrent);
put(j, "reservedCurrent1", v.reservedCurrent1);
put(j, "emergencyLithiumBatteryGroup2Current", v.emergencyLithiumBatteryGroup2Current);
put(j, "lithiumBatteryGroupInstrumentCurrent", v.lithiumBatteryGroupInstrumentCurrent);
put(j, "unit4InstrumentDC48VCurrent", v.unit4InstrumentDC48VCurrent);
put(j, "reservedCurrent2", v.reservedCurrent2);
put(j, "emergency2BusbarVoltage", v.emergency2BusbarVoltage);
put(j, "compositeEnergyEmergencyDC48VCurrent", v.compositeEnergyManagementSystemEmergencyDC48VCurrent);
put(j, "fuelCellSecurityEmergencyDC48VCurrent", v.fuelCellSecuritySystemEmergencyDC48VCurrent);
put(j, "plcControlPowerCurrent", v.plcControlPowerCurrent);
return j;
}
JsonVal lvBusJson(const disLowBusState& v) {
JsonVal j(Json::objectValue);
j["breakers"] = disBreakersJson({
{"unit1", v.unit1CircuitBreaker},
{"unit2", v.unit2CircuitBreaker},
{"unit3", v.unit3CircuitBreaker},
{"unit5", v.unit5CircuitBreaker},
{"bowPZDevice", v.bowPZDeviceCircuitBreaker},
{"reserved1", v.reservedCircuitBreaker1},
{"reserved2", v.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup1", v.emergencyLithiumBatteryGroup1CircuitBreaker},
});
put(j, "powerFailureSignal", v.powerFailureSignal);
put(j, "emergencyPowerFailureSignal", v.emergencyPowerFailureSignal);
put(j, "waterIngressionAlarm", v.waterIngressionAlarm);
put(j, "instrumentBusbarVoltage", v.instrumentBusbarVoltage);
put(j, "instrumentBusbarCurrent", v.instrumentBusbarCurrent);
put(j, "unit1Current", v.unit1Current);
put(j, "unit2Current", v.unit2Current);
put(j, "unit3Current", v.unit3Current);
put(j, "unit5Current", v.unit5Current);
put(j, "bowPZDeviceCurrent", v.bowPZDeviceCurrent);
put(j, "emergencyLithiumBatteryGroup1Current", v.emergencyLithiumBatteryGroup1Current);
put(j, "emergency1BusbarVoltage", v.emergency1BusbarVoltage);
return j;
}
JsonVal realCcuJson(const RealCcuState& st, double now) {
JsonVal j(Json::objectValue);
// 桥接统计
JsonVal fwd(Json::objectValue);
fwd["fbCount"] = JsonVal(static_cast<Json::UInt>(st.fbCount));
fwd["fwdFbOk"] = JsonVal(static_cast<Json::UInt>(st.fwdFbOk));
fwd["fwdFbErr"] = JsonVal(static_cast<Json::UInt>(st.fwdFbErr));
fwd["cmdCount"] = JsonVal(static_cast<Json::UInt>(st.cmdCount));
fwd["fwdCmdOk"] = JsonVal(static_cast<Json::UInt>(st.fwdCmdOk));
fwd["fwdCmdErr"] = JsonVal(static_cast<Json::UInt>(st.fwdCmdErr));
j["forward"] = fwd;
// 四条母线最新反馈(全字段)+ 新鲜度
j["hvAge"] = ageJson(st.hvValid, st.hvLastRx, now);
j["hvaAge"] = ageJson(st.hvaValid, st.hvaLastRx, now);
j["hvbAge"] = ageJson(st.hvbValid, st.hvbLastRx, now);
j["lvAge"] = ageJson(st.lvValid, st.lvLastRx, now);
j["hv"] = st.hvValid ? hvBusJson(st.hvBus) : JsonVal(Json::objectValue);
j["hva"] = st.hvaValid ? hvaBusJson(st.hvaBus) : JsonVal(Json::objectValue);
j["hvb"] = st.hvbValid ? hvbBusJson(st.hvbBus) : JsonVal(Json::objectValue);
j["lv"] = st.lvValid ? lvBusJson(st.lvBus) : JsonVal(Json::objectValue);
// 最近配电指令(pPowerManger -> 真实CCU)
JsonVal lastCmd(Json::objectValue);
lastCmd["valid"] = JsonVal(st.cmdValid);
lastCmd["age"] = JsonVal(st.cmdValid ? (now - st.lastCmdRx) : -1.0);
JsonVal cmds(Json::objectValue);
cmds["hv"] = disBreakersJson({
{"fuelCell", st.lastHvCmd.fuelCellCircuitBreaker},
{"powerLithiumBattery", st.lastHvCmd.powerLithiumBatteryCircuitBreaker},
{"propulsionMotor", st.lastHvCmd.propulsionMotorCircuitBreaker},
{"lithiumBatteryGroupInstrument", st.lastHvCmd.lithiumBatteryGroupInstrumentCircuitBreaker},
{"dcDc5Module", st.lastHvCmd.dcDc5ModuleCircuitBreaker},
{"bowHighVoltageDistributionBox", st.lastHvCmd.bowHighVoltageDistributionBoxCircuitBreaker},
{"sternFTDevice45", st.lastHvCmd.sternFTDevice45CircuitBreaker},
{"fbReserved", st.lastHvCmd.fbReservedCircuitBreaker},
{"sternRudderSwitch1", st.lastHvCmd.sternRudderSwitch1CircuitBreaker},
{"sternRudderSwitch2", st.lastHvCmd.sternRudderSwitch2CircuitBreaker},
{"tyzReserved", st.lastHvCmd.tyzReservedCircuitBreaker},
{"reserved", st.lastHvCmd.reservedCircuitBreaker},
{"dcdc5ModuleStart", st.lastHvCmd.dcdc5Module},
{"coolingSystemStart", st.lastHvCmd.coolingSystem},
});
cmds["hva"] = disBreakersJson({
{"bowFTDevice123", st.lastHvaCmd.bowFTDevice123CircuitBreaker},
{"actuator", st.lastHvaCmd.actuatorCircuitBreaker},
{"mastSteeringGearControlBox", st.lastHvaCmd.mastSteeringGearControlBoxCircuitBreaker},
{"xcz", st.lastHvaCmd.xczCircuitBreaker},
{"bowRudderControlBox", st.lastHvaCmd.bowRudderControlBoxCircuitBreaker},
{"openWaterCoverStartCylinder", st.lastHvaCmd.openWaterCoverStartCylinderCircuitBreaker},
});
cmds["hvb"] = disBreakersJson({
{"lithiumBatteryGroupInstrument", st.lastHvbCmd.lithiumBatteryGroupInstrumentCircuitBreaker},
{"bowLowVoltageDistributionBox", st.lastHvbCmd.bowLowVoltageDistributionBoxCircuitBreaker},
{"unit4InstrumentDC48V", st.lastHvbCmd.unit4InstrumentDC48VCircuitBreaker},
{"dcC1DCDistributionPanel", st.lastHvbCmd.dcC1DCDistributionPanelCircuitBreaker},
{"reserved1", st.lastHvbCmd.reservedCircuitBreaker1},
{"reserved2", st.lastHvbCmd.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup2", st.lastHvbCmd.emergencyLithiumBatteryGroup2CircuitBreaker},
});
cmds["lv"] = disBreakersJson({
{"unit1", st.lastLvCmd.unit1CircuitBreaker},
{"unit2", st.lastLvCmd.unit2CircuitBreaker},
{"unit3", st.lastLvCmd.unit3CircuitBreaker},
{"unit5", st.lastLvCmd.unit5CircuitBreaker},
{"bowPZDevice", st.lastLvCmd.bowPZDeviceCircuitBreaker},
{"reserved1", st.lastLvCmd.reservedCircuitBreaker1},
{"reserved2", st.lastLvCmd.reservedCircuitBreaker2},
{"emergencyLithiumBatteryGroup1", st.lastLvCmd.emergencyLithiumBatteryGroup1CircuitBreaker},
});
lastCmd["cmds"] = cmds;
j["lastCmd"] = lastCmd;
// 真实CCU 状态报文 0x0004(仅显示)
JsonVal ccu(Json::objectValue);
ccu["valid"] = JsonVal(st.ccuStatusValid);
ccu["age"] = JsonVal(st.ccuStatusValid ? (now - st.ccuStatusLastRx) : -1.0);
if (st.ccuStatusValid) {
put(ccu, "fc_mode", st.ccuStatus.fc_mode);
put(ccu, "fc_status", st.ccuStatus.fc_status);
put(ccu, "fc_fault_level", st.ccuStatus.fc_fault_level);
put(ccu, "generation_power", st.ccuStatus.generation_power);
put(ccu, "dyn_soc", st.ccuStatus.dyn_soc);
put(ccu, "ins_soc", st.ccuStatus.ins_soc);
put(ccu, "heartbeat", st.ccuStatus.heartbeat);
put(ccu, "emergency_cmd", st.ccuStatus.emergency_cmd);
}
j["ccuStatus"] = ccu;
return j;
}
} // namespace
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
LinkManager* rcu, DbStore* db)
: m_sys(sys), m_fc(fc), m_pm(pm), m_rcu(rcu), m_db(db) {}
std::string SnapshotBuilder::build() const {
JsonVal root(Json::objectValue);
if (m_sys) {
root["fc"] = fcStatusJson(m_sys->fcStatus());
root["pmCmd"] = pmControlJson(m_sys->pmControl());
root["fcCmd"] = fcControlJson(m_sys->fcControl());
root["pmParam"] = pmParamJson(m_sys->pmParamSet());
root["pmFb"] = pmFbJson(m_sys->pmParamFb());
root["pmStatus"] = pmStatusJson(m_sys->pmStatus());
// 锂电池 BCU 节点(CAN,经 pCanBridge 透传,按节点分组)
BcuNodeStatus nodes[kBcuNodeCount];
m_sys->bcuNodes(nodes);
root["bms"] = bcuNodesJson(nodes, MOOSTime(false));
root["fcStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcStatusCount()));
root["pmControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmControlCount()));
root["fcControlCount"] = JsonVal(static_cast<Json::UInt>(m_sys->fcControlCount()));
root["pmStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->pmStatusCount()));
root["bmsStatusCount"] = JsonVal(static_cast<Json::UInt>(m_sys->bmsStatusCount()));
root["canFrameCount"] = JsonVal(static_cast<Json::UInt>(m_sys->canFrameCount()));
// 真实CCU 配电桥接数据(RCU 链路)
root["realCcu"] = realCcuJson(m_sys->realCcu(), MOOSTime(false));
// BCU 断路器控制下发统计(网页 -> 0x10XX81FF -> pCanBridge)
{
BcuCtrlStat bc = m_sys->bcuCtrlStat();
JsonVal jc(Json::objectValue);
jc["sentCount"] = JsonVal(static_cast<Json::UInt>(bc.sentCount));
jc["errCount"] = JsonVal(static_cast<Json::UInt>(bc.errCount));
jc["valid"] = JsonVal(bc.lastTime > 0);
jc["age"] = JsonVal(bc.lastTime > 0 ? (MOOSTime(false) - bc.lastTime) : -1.0);
put(jc, "addr", bc.last.addr);
put(jc, "pos", bc.last.pos);
put(jc, "neg", bc.last.neg);
jc["lastOk"] = JsonVal(bc.lastOk);
root["bcuCtrl"] = jc;
}
} else {
root["fc"] = JsonVal(Json::objectValue);
root["pmCmd"] = JsonVal(Json::objectValue);
root["fcCmd"] = JsonVal(Json::objectValue);
root["pmParam"] = JsonVal(Json::objectValue);
root["pmFb"] = JsonVal(Json::objectValue);
root["pmStatus"] = JsonVal(Json::objectValue);
root["bms"] = JsonVal(Json::arrayValue);
root["realCcu"] = JsonVal(Json::objectValue);
root["bcuCtrl"] = JsonVal(Json::objectValue);
}
JsonVal links(Json::objectValue);
links["fc"] = linkJson(m_fc);
links["pm"] = linkJson(m_pm);
if (m_rcu) links["rcu"] = linkJson(m_rcu);
root["links"] = links;
// 最近原始帧(默认 20 条)
if (m_db) {
JsonVal logs(Json::arrayValue);
auto rows = m_db->queryRecent("", -1, 0, 20);
for (const auto& r : rows) {
logs.append(commLogToJson(r));
}
root["logs"] = logs;
}
Json::StreamWriterBuilder wb;
wb.settings_["indentation"] = "";
return Json::writeString(wb, root);
}
std::string SnapshotBuilder::buildLogs(int limit) const {
if (!m_db) return "[]";
JsonVal logs(Json::arrayValue);
auto rows = m_db->queryRecent("", -1, 0, limit);
for (const auto& r : rows) {
logs.append(commLogToJson(r));
}
Json::StreamWriterBuilder wb;
wb.settings_["indentation"] = "";
return Json::writeString(wb, logs);
}
} // namespace ccu
-40
View File
@@ -1,40 +0,0 @@
#ifndef PCCU_SNAPSHOT_BUILDER_H
#define PCCU_SNAPSHOT_BUILDER_H
#include <string>
#include "SystemData.h"
#include "../comm/LinkManager.h"
namespace ccu {
class DbStore;
//============================================================================
// SnapshotBuilder:构建网页推送的 JSON 快照。
//
// 组合 SystemData(最新状态:FC + 锂电池 BMS/CAN + 真实CCU 配电桥接)、
// 三条 UDP 链路统计、最近原始帧日志,序列化为 JSON 字符串(jsoncpp)。
//============================================================================
class SnapshotBuilder {
public:
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm,
LinkManager* rcu, DbStore* db);
// 生成完整快照 JSON
std::string build() const;
// 生成最近原始帧日志 JSON(供 /api/logs)
std::string buildLogs(int limit) const;
private:
SystemData* m_sys;
LinkManager* m_fc;
LinkManager* m_pm;
LinkManager* m_rcu;
DbStore* m_db;
};
} // namespace ccu
#endif // PCCU_SNAPSHOT_BUILDER_H
-269
View File
@@ -1,269 +0,0 @@
#ifndef PCCU_SYSTEM_DATA_H
#define PCCU_SYSTEM_DATA_H
#include <mutex>
#include <atomic>
#include <vector>
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include "../protocol/FcProtocol.h"
#include "../protocol/PmProtocol.h"
#include "../protocol/CanBms.h"
#include "../protocol/DisProtocol.h"
namespace ccu {
//============================================================================
// RealCcuState:真实 CCU 设备的桥接数据缓存(配电协议)。
//
// 数据来源:RCU 链路(真实CCU -> pCCU),由 DisBridge 在转发的同时
// 解码写入;网页快照读取展示。各 lastRx 时间由写入方更新(MOOSTime),
// 供在线判定与新鲜度展示。
//============================================================================
struct RealCcuState {
// 四条配电母线最新反馈(valid 表示收到过对应反馈帧)
disHighVolBusState hvBus{}; bool hvValid = false;
disHighAVolBusState hvaBus{}; bool hvaValid = false;
disLowMainBusState hvbBus{}; bool hvbValid = false;
disLowBusState lvBus{}; bool lvValid = false;
double hvLastRx = 0, hvaLastRx = 0, hvbLastRx = 0, lvLastRx = 0;
// 最近收到的配电指令(pPowerManger 下发、已转发真实CCU,用于展示)
disHighVolBusCmd lastHvCmd{};
disHighAVolBusCmd lastHvaCmd{};
disLowMainBusCmd lastHvbCmd{};
disLowBusCmd lastLvCmd{};
bool cmdValid = false;
double lastCmdRx = 0;
// 真实CCU 状态报文 0x0004(仅解码显示,不转发)
PmStatusValue ccuStatus{};
bool ccuStatusValid = false;
double ccuStatusLastRx = 0;
// 桥接统计
unsigned long fbCount = 0; // 收到配电反馈帧数
unsigned long fwdFbOk = 0; // 反馈转发 pPowerManger 成功帧数
unsigned long fwdFbErr = 0; // 反馈转发失败帧数
unsigned long cmdCount = 0; // 收到配电指令帧数
unsigned long fwdCmdOk = 0; // 指令转发真实CCU 成功帧数
unsigned long fwdCmdErr = 0; // 指令转发失败帧数
};
//============================================================================
// SystemData:跨线程共享的最新状态快照。
//
// - 接收线程(FC 链路)写 FcStatus 快照
// - MOOS 主线程(OnNewMail)写 BCU 节点快照(CAN 经 pCanBridge 透传)
// - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送
// - 网页线程读快照展示
// 通过互斥锁保护读写。
//============================================================================
//============================================================================
// BcuCtrlCmd:BCU 断路器(总正/总负继电器)控制指令。
//
// 链路:网页 /api/bcu_ctrl(Web 线程)-> 入队 -> CCU::Iterate(MOOS 线程)
// 出队编码 0x10XX81FF -> MOOS CAN_TX_0x* -> pCanBridge 下行 CAN 总线。
// 队列化是为了让 MOOS Notify 只发生在 MOOS 线程(Web 线程不可直接 Notify)。
//============================================================================
struct BcuCtrlCmd {
uint8_t addr = 0; // BCU 节点地址 1~54
uint8_t pos = 0; // 总正继电器 1 闭合 / 0 断开
uint8_t neg = 0; // 总负继电器 1 闭合 / 0 断开
};
// BCU 断路器控制的下发统计(网页展示)
struct BcuCtrlStat {
unsigned long sentCount = 0; // 已下发帧数(成功投递 MOOS)
unsigned long errCount = 0; // 投递失败帧数
BcuCtrlCmd last{};
bool lastOk = false;
double lastTime = 0; // 最近下发时刻(MOOSTime)
};
class SystemData {
public:
// 更新/获取燃料电池最新状态
void updateFcStatus(const FcStatusValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_fcStatus = v;
m_fcStatusCount++;
}
FcStatusValue fcStatus() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_fcStatus;
}
unsigned long fcStatusCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_fcStatusCount;
}
// 更新/获取最新 PM 操控指令(用于转发到 FC)
void updatePmControl(const PmControlValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_pmControl = v;
m_pmControlCount++;
}
PmControlValue pmControl() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_pmControl;
}
unsigned long pmControlCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_pmControlCount;
}
// 更新/获取最新 PM 参数设定(预留,用于未来转发/应用)
void updatePmParamSet(const PmParamSetValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_pmParamSet = v;
}
PmParamSetValue pmParamSet() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_pmParamSet;
}
// 更新/获取最新转发给 FC 的控制指令
void updateFcControl(const FcControlValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_fcControl = v;
m_fcControlCount++;
}
FcControlValue fcControl() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_fcControl;
}
unsigned long fcControlCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_fcControlCount;
}
// 更新/获取最新发送给 PM 的状态报文(整合后的)
void updatePmStatus(const PmStatusValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_pmStatus = v;
m_pmStatusCount++;
}
PmStatusValue pmStatus() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_pmStatus;
}
unsigned long pmStatusCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_pmStatusCount;
}
// 更新/获取最新发送给 PM 的参数反馈
void updatePmParamFb(const PmParamSetFbValue& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_pmParamFb = v;
}
PmParamSetFbValue pmParamFb() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_pmParamFb;
}
//-------- 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传) --------
// 更新单个 BCU 节点状态(每帧部分更新,由调用方先取出快照再合并)
void updateBcuNode(uint8_t addr, const BcuNodeStatus& v) {
std::lock_guard<std::mutex> lock(m_mutex);
if (addr >= kBcuAddrMin && addr <= kBcuAddrMax) {
m_bcuNodes[addr - kBcuAddrMin] = v;
m_bmsStatusCount++;
}
}
// 读取单个 BCU 节点快照(地址非法返回 valid=false 的空快照)
BcuNodeStatus bcuNode(uint8_t addr) const {
std::lock_guard<std::mutex> lock(m_mutex);
if (addr >= kBcuAddrMin && addr <= kBcuAddrMax)
return m_bcuNodes[addr - kBcuAddrMin];
return BcuNodeStatus();
}
// 读取全部节点快照(数组下标 = 地址-1)
void bcuNodes(BcuNodeStatus out[kBcuNodeCount]) const {
std::lock_guard<std::mutex> lock(m_mutex);
for (int i = 0; i < kBcuNodeCount; ++i) out[i] = m_bcuNodes[i];
}
unsigned long bmsStatusCount() const {
std::lock_guard<std::mutex> lock(m_mutex);
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;
}
//-------- 真实 CCU 桥接数据(配电协议,RCU 链路) --------
// 整体更新真实CCU 快照(DisBridge 在接收线程先取副本->修改->写回)
void updateRealCcu(const RealCcuState& v) {
std::lock_guard<std::mutex> lock(m_mutex);
m_realCcu = v;
}
RealCcuState realCcu() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_realCcu;
}
//-------- BCU 断路器控制(网页 -> MOOS 下行队列) --------
// 入队一条控制指令(Web API 线程调用);队列上限 16,满则丢弃并返回 false
bool pushBcuCtrlCmd(const BcuCtrlCmd& c) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_bcuCtrlQueue.size() >= 16) return false;
m_bcuCtrlQueue.push_back(c);
return true;
}
// 出队一条控制指令(MOOS 主循环 Iterate 调用);空队列返回 false
bool popBcuCtrlCmd(BcuCtrlCmd& out) {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_bcuCtrlQueue.empty()) return false;
out = m_bcuCtrlQueue.front();
m_bcuCtrlQueue.erase(m_bcuCtrlQueue.begin());
return true;
}
// 记录一次下发结果(Iterate 调用,供网页统计展示)
void noteBcuCtrlSent(const BcuCtrlCmd& c, bool ok) {
std::lock_guard<std::mutex> lock(m_mutex);
if (ok) m_bcuCtrlStat.sentCount++; else m_bcuCtrlStat.errCount++;
m_bcuCtrlStat.last = c;
m_bcuCtrlStat.lastOk = ok;
m_bcuCtrlStat.lastTime = MOOSTime(false);
}
BcuCtrlStat bcuCtrlStat() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_bcuCtrlStat;
}
private:
mutable std::mutex m_mutex;
FcStatusValue m_fcStatus;
PmControlValue m_pmControl;
PmParamSetValue m_pmParamSet;
FcControlValue m_fcControl;
PmStatusValue m_pmStatus;
PmParamSetFbValue m_pmParamFb;
BcuNodeStatus m_bcuNodes[kBcuNodeCount]; // 下标 = 节点地址-1
RealCcuState m_realCcu;
std::vector<BcuCtrlCmd> m_bcuCtrlQueue;
BcuCtrlStat m_bcuCtrlStat;
unsigned long m_fcStatusCount = 0;
unsigned long m_pmControlCount = 0;
unsigned long m_fcControlCount = 0;
unsigned long m_pmStatusCount = 0;
unsigned long m_bmsStatusCount = 0;
unsigned long m_canFrameCount = 0;
};
} // namespace ccu
#endif // PCCU_SYSTEM_DATA_H
-251
View File
@@ -1,251 +0,0 @@
/*
* 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 */
-54
View File
@@ -1,54 +0,0 @@
/************************************************************/
/* NAME: pCCU */
/* FILE: main.cpp */
/************************************************************/
#include <string>
#include "MBUtils.h"
#include "ColorParse.h"
#include "CCU.h"
#include "CCU_Info.h"
#include "../pPowerManger/logc/loguru.hpp"
using namespace std;
int main(int argc, char *argv[])
{
loguru::init(argc, argv);
string mission_file;
string run_command = argv[0];
// 默认以程序文件名(去掉路径)作为进程名,保证与 .moos 中 ProcessConfig 匹配
{
size_t slash = run_command.find_last_of('/');
if (slash != string::npos) run_command = run_command.substr(slash + 1);
}
for (int i = 1; i < argc; i++) {
string argi = argv[i];
if ((argi == "-v") || (argi == "--version") || (argi == "-version"))
showReleaseInfoAndExit();
else if ((argi == "-e") || (argi == "--example") || (argi == "-example"))
showExampleConfigAndExit();
else if ((argi == "-h") || (argi == "--help") || (argi == "-help"))
showHelpAndExit();
else if ((argi == "-i") || (argi == "--interface"))
showInterfaceAndExit();
else if (strEnds(argi, ".moos") || strEnds(argi, ".moos++"))
mission_file = argv[i];
else if (strBegins(argi, "--alias="))
run_command = argi.substr(8);
else if (i == 2)
run_command = argi;
}
if (mission_file == "")
showHelpAndExit();
LOG_F(INFO, "pCCU launching as %s", run_command.c_str());
ccu::CCU CCU;
CCU.Run(run_command.c_str(), mission_file.c_str());
return 0;
}
-62
View File
@@ -1,62 +0,0 @@
//============================================================================
// pCCU 配置示例
//
// 链路拓扑:
// 燃料电池控制器(FC) 192.168.1.162:7000(UDP)
// 控制主机(pPowerManger) 192.168.0.140:5001(UDP)
// 真实CCU设备(rcu) 192.168.0.200:7000(UDP,配电桥接,占位地址需按实际修改)
// 锂电池 BMS CAN 总线(经 pCanBridge 订阅 CAN_0x* 透传)
//
// 配电桥接(rcu_enable = true 时启用):
// pPowerManger 下发的配电指令(0x0001~0x0004) 经 pCCU 原帧转发真实CCU;
// 真实CCU 上报的配电反馈(0x0005~0x0008) 经 pCCU 原帧转发 pPowerManger,
// 同时解码显示在网页"真实CCU"页签;真实CCU 状态报文(0x0004)仅显示不转发。
//============================================================================
ProcessConfig = pCCU
{
AppTick = 4
CommsTick = 4
//======== FC 链路(与燃料电池通信) ========
// 本机接收 FC 状态反馈的端口
fc_local_port = 6000
// 燃料电池控制器地址(发送 FC 控制指令目标)
fc_remote_ip = 192.168.1.162
fc_remote_port = 7000
//======== PM 链路(与控制主机通信) ========
// 本机接收 pPowerManger 指令的端口
pm_local_port = 7000
// 控制主机地址(发送 PM 状态报文目标)
pm_remote_ip = 192.168.0.140
pm_remote_port = 5001
//======== 真实CCU 桥接链路(配电协议) ========
// 是否启用桥接(关闭时 pCCU 行为与纯模拟版一致)
rcu_enable = true
// 本机接收真实CCU 数据的端口
rcu_local_port = 7100
// 真实CCU 地址(转发配电指令目标;占位地址,按实际设备修改)
rcu_remote_ip = 192.168.0.200
rcu_remote_port = 7000
//======== 锂电池 ========
// CAN 总线 BMS 协议(docs/BMS_协议字段定义.xlsx),
// 经 pCanBridge 发布的 CAN_0x* 消息透传,无需额外配置。
//======== BCU 断路器控制下发 ========
// 下行 CAN_TX_0x* 消息的目标通道(经 m_sSrcAux 由 pCanBridge 路由)
bcu_can_channel = CAN0
//======== 存储与日志 ========
// 数据库路径(SQLite)
dbpath = pccu_data.db
// 日志文件路径
logpath = pCCU.log
//======== 网页监控 ========
// 端口避开 pPowerManger(8090) / pPowerMangerHost(18080)
web_port = 8080
web_enable = true
}
-159
View File
@@ -1,159 +0,0 @@
#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));
}
// 有符号 8 位(温度)
inline int8_t rdS8(uint8_t b) { return static_cast<int8_t>(b); }
// 设置功能码已收位
inline void markRx(BcuNodeStatus& n, uint8_t func) {
n.rxMask |= (1u << (func & 31));
}
} // namespace
//============================================================================
// 附录1:0x10XX0010 告警位定义(04KT38电池BCU-MBMS通信(CAN)定义.docx)
//============================================================================
const BcuAlarmDesc kBcuAlarmTable[] = {
// byte0:2 级告警
{0, 0, "BCU绝缘2级"},
{0, 1, "BCU最高温度过高2级"},
{0, 2, "BCU最低温度过低2级"},
{0, 3, "BCU单体电压不均衡2级"},
{0, 4, "BCU单体温度不均衡2级"},
{0, 5, "BCU放电过流2级"},
{0, 6, "BCU单体电压过高2级"},
{0, 7, "BCU单体电压过低2级"},
// byte1
{1, 0, "BCU总电压过高2级"},
{1, 1, "BCU总电压过低2级"},
{1, 2, "BCU烟雾报警1级"},
{1, 3, "烟雾报警器不在线"},
{1, 4, "烟雾报警器上报自身故障"},
{1, 5, "BCU单包绝缘3级"},
{1, 6, "BCU最高温度过高3级"},
{1, 7, "BCU最低温度过低3级"},
// byte2
{2, 0, "单体电压不均衡3级"},
{2, 1, "单体温度不均衡3级"},
{2, 2, "单包放电过流3级"},
{2, 3, "BCU单体电压过高3级"},
{2, 4, "BCU单体电压过低3级"},
{2, 5, "BCU总电压过高3级"},
{2, 6, "BCU总电压过低3级"},
{2, 7, "BCU单体温度传感器检测故障"},
// byte3
{3, 0, "BCU正极继电器失效"},
{3, 1, "BCU负极继电器失效"},
{3, 3, "单包BMS内部通讯故障"},
{3, 4, "BCU电流传感器故障"},
{3, 5, "BCU电压传感器故障"},
{3, 7, "BCU高压回路连接异常"},
// byte4
{4, 1, "烟雾报警2级"},
{4, 4, "烟雾报警3级"},
{4, 6, "烟雾报警4级"},
{4, 7, "回馈电流过大2级"},
// byte5
{5, 0, "回馈电流过大3级"},
{5, 1, "充电电流过大3级"},
{0xFF, 0, nullptr}, // 表尾
};
int bcuAlarmTexts(const BcuNodeStatus& node, const char* texts[], int cap) {
if (!texts || cap <= 0) return 0;
int count = 0;
for (int i = 0; kBcuAlarmTable[i].text; ++i) {
const BcuAlarmDesc& d = kBcuAlarmTable[i];
if (d.byteIdx < sizeof(node.alarmBits) &&
(node.alarmBits[d.byteIdx] >> d.bitIdx) & 0x01) {
if (count >= cap) break;
texts[count++] = d.text;
}
}
return count;
}
bool decodeCanBcuFrame(uint32_t canId, const uint8_t* data, int dlc,
uint8_t& nodeAddr, BcuNodeStatus& node) {
if (!data || dlc <= 0) return false;
uint8_t addr = bcuAddrOf(canId);
if (!addr || !isCanBcuId(canId)) return false;
nodeAddr = addr;
// 缩放用除法(而非乘 0.1/0.001),保证结果与十进制字面量严格一致。
switch (bcuFuncOf(canId)) {
case BCU_FUNC_BASE: // 0x10XX0000:电压 / 电流 / SOC / 告警码 / 自检
if (dlc >= 8) {
node.totalVoltage = rdU16BE(data + 0) / 10.0; // 0.1V
node.current = rdS16BE(data + 2) / 10.0 - 600.0; // 0.1A,偏移-600A
node.soc = rdU16BE(data + 4) / 10.0; // 0.1%
node.alarmCode = data[6];
node.selfCheck = data[7];
markRx(node, BCU_FUNC_BASE);
}
break;
case BCU_FUNC_CELL_V: // 0x10XX0001:最高/最低/平均单体电压
if (dlc >= 2) node.maxCellVoltage = rdU16BE(data + 0) / 1000.0; // 1mV
if (dlc >= 3) node.maxCellVoltageNo = data[2];
if (dlc >= 5) node.minCellVoltage = rdU16BE(data + 3) / 1000.0; // 1mV
if (dlc >= 6) node.minCellVoltageNo = data[5];
if (dlc >= 8) node.avgCellVoltage = rdU16BE(data + 6) / 1000.0; // 1mV
markRx(node, BCU_FUNC_CELL_V);
break;
case BCU_FUNC_CELL_T: // 0x10XX0002:最高/最低/平均单体温度
if (dlc >= 1) node.maxCellTemp = rdS8(data[0]) - 40.0; // 偏移-40℃
if (dlc >= 2) node.maxCellTempNo = data[1];
if (dlc >= 3) node.minCellTemp = rdS8(data[2]) - 40.0; // 偏移-40℃
if (dlc >= 4) node.minCellTempNo = data[3];
if (dlc >= 5) node.avgCellTemp = rdS8(data[4]) - 40.0; // 偏移-40℃(文档写偏移0,
// 但实测固件与最高/最低一致均带-40偏移,见0x10020002:
// 原始68/69减40后为28/29℃,落在最低28~最高30区间内)
markRx(node, BCU_FUNC_CELL_T);
break;
case BCU_FUNC_RELAY: // 0x10XX0003:正/负极继电器状态
if (dlc >= 1) node.posRelay = data[0];
if (dlc >= 2) node.negRelay = data[1];
markRx(node, BCU_FUNC_RELAY);
break;
case BCU_FUNC_ISU: // 0x10XX0006:绝缘阻值 / 端口电压
if (dlc >= 2) node.posInsulationKohm = rdU16BE(data + 0); // 1kΩ
if (dlc >= 4) node.negInsulationKohm = rdU16BE(data + 2); // 1kΩ
if (dlc >= 6) node.portVoltage = rdU16BE(data + 4) / 10.0; // 0.1V
if (dlc >= 8) node.posRelayOuterVoltage = rdU16BE(data + 6) / 10.0; // 0.1V
markRx(node, BCU_FUNC_ISU);
break;
case BCU_FUNC_ALARM: // 0x10XX0010:告警位(附录1)
for (int i = 0; i < 6 && i < dlc; ++i) node.alarmBits[i] = data[i];
if (dlc >= 7) node.alarmExtraByte6 = data[6];
if (dlc >= 8) node.alarmExtraByte7 = data[7];
markRx(node, BCU_FUNC_ALARM);
break;
default: // Reserve(0x10XX0004/0005 等)不解析
return false;
}
return true;
}
} // namespace ccu
-175
View File
@@ -1,175 +0,0 @@
#ifndef PCCU_CAN_BMS_H
#define PCCU_CAN_BMS_H
#include <cstdint>
namespace ccu {
//============================================================================
// 锂电池 BCU-MBMS CAN 协议(docs/04KT38电池BCU-MBMS通信(CAN)定义.docx)
//
// 数据来源:pCanBridge 透传的 MOOS 消息
// 变量名 CAN_0x%08X = CAN ID(扩展帧),m_sVal = 二进制数据域,
// m_dfVal2 = 原始帧信息字节。本模块只负责按报文 ID 解析数据域字段。
//
// 【节点地址】一个 MBMS 下最多挂 54 个 BCU,CAN ID 中间的 XX(01h~36h)
// 为 BCU 节点地址编号,其余位固定:
// 0x10XX00YY BCU 主动上报消息(YY=功能码 00/01/02/03/06/10)
// 0x10XX81FF MBMS 下发指令(继电器控制,经 MOOS CAN_TX_0x* 下行)
//
// 【下发指令 0x10XX81FF】数据域 8 字节(BYTE1~8 = byte0~7):
// byte0 = 0x02(索引号) byte1 = 0x00(索引号)
// byte2 = 总正继电器控制(1 闭合 / 0 断开)
// byte3 = 总负继电器控制(1 闭合 / 0 断开)
// byte4~7 = Reserve(填充 0xFF)
//
// 【上报报文】均为扩展帧,字段大端,BYTE1~8 对应数据域 byte0~7:
// 0x10XX0000 (200ms) 累加电压 u16@0 ÷10 V
// 回路电流 s16@2 ÷10-600 A(偏移 -600A,放电为正)
// SOC u16@4 ÷10 %
// 告警码 u8 @6(循环上报,协议未附码表,原样显示)
// 自检状态 u8 @7
// 0x10XX0001 (200ms) 最高单体电压 u16@0 mV,编号 u8@2
// 最低单体电压 u16@3 mV,编号 u8@5
// 单体平均电压 u16@6 mV
// 0x10XX0002 (1000ms) 最高单体温度 s8@0(偏移 -40℃),编号 u8@1
// 最低单体温度 s8@2(偏移 -40℃),编号 u8@3
// 单体平均温度 s8@4(偏移 -40℃,协议文档写"偏移0"
// 有误:实测原始值减 40 后才落在最低~最高区间内)
// 0x10XX0003 (200ms) 正极继电器状态 u8@0(1 闭合 / 0 断开)
// 负极继电器状态 u8@1(1 闭合 / 0 断开)
// 0x10XX0006 (200ms) 正极绝缘阻抗 u16@0 kΩ
// 负极绝缘阻抗 u16@2 kΩ
// 正负极端口电压 u16@4 ÷10 V
// 正极继电器外侧电压 u16@6 ÷10 V
// 0x10XX0010 (200ms) byte0~5 告警位(bit 定义见附录1,
// kBcuAlarmTable,可经 bcuAlarmTexts() 取告警描述)
// 0x10XX0004/0005 Reserve(调试预留,不解析)
//============================================================================
// BCU 节点地址范围:01h~36h(1~54)
static const int kBcuAddrMin = 0x01;
static const int kBcuAddrMax = 0x36;
static const int kBcuNodeCount = kBcuAddrMax - kBcuAddrMin + 1;
// BCU 上报报文功能码(CAN ID 低字节 YY)
enum BcuFunc : uint8_t {
BCU_FUNC_BASE = 0x00, // 0x10XX0000 电压 / 电流 / SOC / 告警码 / 自检
BCU_FUNC_CELL_V = 0x01, // 0x10XX0001 单体电压
BCU_FUNC_CELL_T = 0x02, // 0x10XX0002 单体温度
BCU_FUNC_RELAY = 0x03, // 0x10XX0003 继电器状态
BCU_FUNC_ISU = 0x06, // 0x10XX0006 绝缘阻值 / 端口电压
BCU_FUNC_ALARM = 0x10, // 0x10XX0010 告警位(附录1)
};
// 从 CAN ID 提取 BCU 节点地址(0x10XX00YY -> XX);非 BCU 报文格式返回 0
inline uint8_t bcuAddrOf(uint32_t canId) {
if ((canId & 0xFF00FF00u) != 0x10000000u) return 0;
uint8_t addr = static_cast<uint8_t>((canId >> 16) & 0xFF);
return (addr >= kBcuAddrMin && addr <= kBcuAddrMax) ? addr : 0;
}
// 从 CAN ID 提取功能码(0x10XX00YY -> YY)
inline uint8_t bcuFuncOf(uint32_t canId) {
return static_cast<uint8_t>(canId & 0xFF);
}
//--------------------------------------------------------------------------
// BCU 断路器(总正/总负继电器)下行控制
//--------------------------------------------------------------------------
// BCU 节点继电器控制指令 CAN ID(0x10XX81FF,XX=节点地址 01h~36h)
inline uint32_t bcuCtrlCanId(uint8_t addr) {
return 0x10000000u | (static_cast<uint32_t>(addr) << 16) | 0x81FFu;
}
// 编码继电器控制数据域(8 字节,经 MOOS CAN_TX_0x%08X 下发,pCanBridge 发送):
// [0]=0x02 [1]=0x00 [2]=总正(1闭合/0断开) [3]=总负 [4..7]=0xFF
// pos/neg 非 0 值均视为闭合(1)。
inline void buildBcuRelayCtrlData(int pos, int neg, uint8_t out[8]) {
out[0] = 0x02;
out[1] = 0x00;
out[2] = pos ? 0x01 : 0x00;
out[3] = neg ? 0x01 : 0x00;
out[4] = 0xFF;
out[5] = 0xFF;
out[6] = 0xFF;
out[7] = 0xFF;
}
// 是否为 BCU 主动上报报文 ID(0x10XX00YY,YY 为已定义功能码)
inline bool isCanBcuId(uint32_t canId) {
uint8_t addr = bcuAddrOf(canId);
if (!addr) return false;
switch (bcuFuncOf(canId)) {
case BCU_FUNC_BASE:
case BCU_FUNC_CELL_V:
case BCU_FUNC_CELL_T:
case BCU_FUNC_RELAY:
case BCU_FUNC_ISU:
case BCU_FUNC_ALARM:
return true;
default:
return false;
}
}
// 单个 BCU 节点最新状态(各报文字段按功能码部分更新,最终合成完整快照)
struct BcuNodeStatus {
// 0x10XX0000 基本信息
double totalVoltage = 0; // 累加电压 V
double current = 0; // 回路电流 A(放电为正,偏移 -600A 已折算)
double soc = 0; // SOC %
int alarmCode = 0; // 告警码(原始值,协议未附码表)
int selfCheck = 0; // 自检状态(原始值)
// 0x10XX0001 单体电压
double maxCellVoltage = 0; // 最高单体电压 V
int maxCellVoltageNo = 0; // 最高单体电压编号
double minCellVoltage = 0; // 最低单体电压 V
int minCellVoltageNo = 0; // 最低单体电压编号
double avgCellVoltage = 0; // 单体平均电压 V
// 0x10XX0002 单体温度
double maxCellTemp = 0; // 最高单体温度 ℃
int maxCellTempNo = 0; // 最高单体温度编号
double minCellTemp = 0; // 最低单体温度 ℃
int minCellTempNo = 0; // 最低单体温度编号
double avgCellTemp = 0; // 单体平均温度 ℃
// 0x10XX0003 继电器(1 闭合 / 0 断开)
int posRelay = 0;
int negRelay = 0;
// 0x10XX0006 绝缘 / 端口电压
double posInsulationKohm = 0; // 正极绝缘阻抗 kΩ
double negInsulationKohm = 0; // 负极绝缘阻抗 kΩ
double portVoltage = 0; // 正负极端口电压 V
double posRelayOuterVoltage = 0; // 正极继电器外侧电压 V
// 0x10XX0010 告警位(附录1,byte0~5)
uint8_t alarmBits[6] = {0, 0, 0, 0, 0, 0};
// 0x10XX0010 附加字节(协议未详列,原样保留)
int alarmExtraByte6 = 0;
int alarmExtraByte7 = 0;
// 元数据
uint32_t rxMask = 0; // 收到过的功能码位图(bit = BcuFunc)
double lastRxTime = 0; // 最近收到该节点任一报文的时刻(MOOSTime)
bool valid = false; // 是否收到过该节点报文
};
// 解析一帧 CAN 数据域;命中 BCU 上报报文返回 true,nodeAddr 为节点地址(1~54),
// 字段按功能码部分更新合并进 node。data/dlc 非法或非 BCU 报文返回 false。
bool decodeCanBcuFrame(uint32_t canId, const uint8_t* data, int dlc,
uint8_t& nodeAddr, BcuNodeStatus& node);
// 附录1:0x10XX0010 告警位定义表(byteIdx/bitIdx -> 告警描述),表尾以 byteIdx=0xFF 结束
struct BcuAlarmDesc {
uint8_t byteIdx;
uint8_t bitIdx;
const char* text;
};
extern const BcuAlarmDesc kBcuAlarmTable[];
// 收集节点当前置位的告警描述;返回条数(不超过 cap,texts 填描述指针)
int bcuAlarmTexts(const BcuNodeStatus& node, const char* texts[], int cap);
} // namespace ccu
#endif // PCCU_CAN_BMS_H
-89
View File
@@ -1,89 +0,0 @@
#ifndef PCCU_CHECKSUM_POLICY_H
#define PCCU_CHECKSUM_POLICY_H
#include <cstdint>
#include <cstddef>
#include <vector>
namespace ccu {
//============================================================================
// ChecksumPolicy:校验和策略(可插拔)。
//
// 协议联调阶段校验和的实现可能调整,因此对每条消息独立配置策略:
// - None : 无校验和字段(如 FC 状态帧文档未列校验行)
// - Sum8 : uint8 字节和(配电协议,取字节和低 8 位)
// - Sum32 : uint32 字节和,从域起始符到校验和之前所有数据的字节和
// - Sum16 : uint16 字节和(预留,备用)
// 新增策略只需在此类扩展,不影响其它代码。
//============================================================================
enum class ChecksumType {
None, // 无校验
Sum8, // uint8 字节和(配电协议)
Sum32, // uint32 字节和
Sum16, // uint16 字节和
};
class ChecksumPolicy {
public:
explicit ChecksumPolicy(ChecksumType type) : m_type(type) {}
ChecksumType type() const { return m_type; }
// 校验和字段占用的字节数;None 返回 0
size_t size() const {
switch (m_type) {
case ChecksumType::Sum8: return 1;
case ChecksumType::Sum32: return 4;
case ChecksumType::Sum16: return 2;
case ChecksumType::None: return 0;
}
return 0;
}
// 计算校验和:对 data[0 .. data.size()] 全部字节求和(调用方保证不含校验和自身)
uint32_t compute(const std::vector<uint8_t>& data) const {
uint32_t sum = 0;
for (size_t i = 0; i < data.size(); ++i) {
sum += data[i];
}
return sum;
}
// 校验给定帧(含校验和字段):返回帧尾校验和字段是否与计算值一致
// frame 必须包含从域起始符开始的完整帧。校验和位于末尾 size() 字节。
bool verify(const std::vector<uint8_t>& frame) const {
size_t cs = size();
if (cs == 0) return true; // 无校验,视为通过
if (frame.size() < cs) return false;
std::vector<uint8_t> body(frame.begin(), frame.end() - cs);
uint32_t expect = compute(body);
uint32_t actual = 0;
size_t offset = frame.size() - cs;
if (m_type == ChecksumType::Sum8) {
actual = static_cast<uint32_t>(frame[offset]);
return (expect & 0xFF) == actual;
}
if (m_type == ChecksumType::Sum32) {
actual = static_cast<uint32_t>(frame[offset]) |
(static_cast<uint32_t>(frame[offset + 1]) << 8) |
(static_cast<uint32_t>(frame[offset + 2]) << 16) |
(static_cast<uint32_t>(frame[offset + 3]) << 24);
return expect == actual;
}
// Sum16
actual = static_cast<uint32_t>(frame[offset]) |
(static_cast<uint32_t>(frame[offset + 1]) << 8);
return (expect & 0xFFFF) == actual;
}
private:
ChecksumType m_type;
};
} // namespace ccu
#endif // PCCU_CHECKSUM_POLICY_H
-155
View File
@@ -1,155 +0,0 @@
#include "DisProtocol.h"
#include "Frame.h"
#include "MessageRegistry.h"
#include <cstring>
namespace ccu {
//============================================================================
// 结构体尺寸核对:pPowerManger 按同结构裸解析收帧,布局必须与文档一致;
// 若 pmSysvariable.h 结构调整导致尺寸变化,此处编译期报错提醒同步。
//============================================================================
static_assert(sizeof(disHighVolBusCmd) == 14, "disHighVolBusCmd 尺寸与协议不符");
static_assert(sizeof(disHighAVolBusCmd) == 6, "disHighAVolBusCmd 尺寸与协议不符");
static_assert(sizeof(disLowMainBusCmd) == 7, "disLowMainBusCmd 尺寸与协议不符");
static_assert(sizeof(disLowBusCmd) == 8, "disLowBusCmd 尺寸与协议不符");
static_assert(sizeof(disHighVolBusState) == 53, "disHighVolBusState 尺寸与协议不符");
static_assert(sizeof(disHighAVolBusState) == 25, "disHighAVolBusState 尺寸与协议不符");
static_assert(sizeof(disLowMainBusState) == 34, "disLowMainBusState 尺寸与协议不符");
static_assert(sizeof(disLowBusState) == 29, "disLowBusState 尺寸与协议不符");
//============================================================================
// 通用编解码:配电结构体为 #pragma pack(1) 的 POD,与线上字节布局一致
//(pPowerManger 亦按同结构裸解析),因此直接按字节拷贝。
//============================================================================
namespace {
std::vector<uint8_t> encodePacked(uint16_t id, const void* obj, size_t size,
const ChecksumPolicy& policy) {
std::vector<uint8_t> p(size, 0);
std::memcpy(p.data(), obj, size);
return Frame::build(id, p, policy);
}
bool decodePacked(const std::vector<uint8_t>& frame, uint16_t id,
const ChecksumPolicy& policy, size_t size, void* obj) {
// 注意:不依赖帧头 length 字段(pPowerManger 发送的配电指令该字段
// 与总长不符),validateFrame 只要实际帧长足够即放行,校验和兜底。
if (!Message::validateFrame(frame, id, policy,
FRAME_HEADER_LEN + size + policy.size()))
return false;
std::memcpy(obj, frame.data() + FRAME_HEADER_LEN, size);
return true;
}
} // namespace
//--------------------------------------------------------------------------
// 指令消息编解码
//--------------------------------------------------------------------------
std::vector<uint8_t> DisHighVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighVolBusCmd), m_checksum);
}
bool DisHighVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusCmd), obj);
}
std::vector<uint8_t> DisHighAVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighAVolBusCmd), m_checksum);
}
bool DisHighAVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusCmd), obj);
}
std::vector<uint8_t> DisHighBVolBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowMainBusCmd), m_checksum);
}
bool DisHighBVolBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusCmd), obj);
}
std::vector<uint8_t> DisLowBusCmdMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowBusCmd), m_checksum);
}
bool DisLowBusCmdMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowBusCmd), obj);
}
//--------------------------------------------------------------------------
// 反馈消息编解码
//--------------------------------------------------------------------------
std::vector<uint8_t> DisHighVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighVolBusState), m_checksum);
}
bool DisHighVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighVolBusState), obj);
}
std::vector<uint8_t> DisHighAVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disHighAVolBusState), m_checksum);
}
bool DisHighAVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disHighAVolBusState), obj);
}
std::vector<uint8_t> DisHighBVolBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowMainBusState), m_checksum);
}
bool DisHighBVolBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowMainBusState), obj);
}
std::vector<uint8_t> DisLowBusFbMessage::encode(const void* obj) const {
return encodePacked(id(), obj, sizeof(disLowBusState), m_checksum);
}
bool DisLowBusFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
return decodePacked(frame, id(), m_checksum, sizeof(disLowBusState), obj);
}
//--------------------------------------------------------------------------
// 消息类型判断(供桥接器/上层分发使用)
//--------------------------------------------------------------------------
bool isDisMessage(const Message* msg) {
return isDisCmdMessage(msg) || isDisFbMessage(msg);
}
bool isDisCmdMessage(const Message* msg) {
if (!msg) return false;
return dynamic_cast<const DisHighVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighAVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighBVolBusCmdMessage*>(msg) != nullptr ||
dynamic_cast<const DisLowBusCmdMessage*>(msg) != nullptr;
}
bool isDisFbMessage(const Message* msg) {
if (!msg) return false;
return dynamic_cast<const DisHighVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighAVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisHighBVolBusFbMessage*>(msg) != nullptr ||
dynamic_cast<const DisLowBusFbMessage*>(msg) != nullptr;
}
//--------------------------------------------------------------------------
// 注册
//--------------------------------------------------------------------------
void registerDisCmdMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new DisHighVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighAVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighBVolBusCmdMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisLowBusCmdMessage()));
}
void registerDisFbMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new DisHighVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighAVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisHighBVolBusFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new DisLowBusFbMessage()));
}
} // namespace ccu
-115
View File
@@ -1,115 +0,0 @@
#ifndef PCCU_DIS_PROTOCOL_H
#define PCCU_DIS_PROTOCOL_H
#include <cstdint>
#include <vector>
#include "Message.h"
#include "pmSysvariable.h"
namespace ccu {
//============================================================================
// 配电协议(配电控制器 <-> 复合管控器,经 CCU 在 PM 链路与真实 CCU 链路间桥接)
// 依据:docs/配电控制器与复合管控器的通信协议20260824.docx
// 数据结构复用 src/pPowerManger/pmSysvariable.h(pPowerManger 按同结构
// 裸解析收帧,复用可保证两侧 sizeof/字段布局完全一致)。
//
// 帧格式:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + uint8 校验和
// 校验和 = 从域起始符到校验和之前所有数据的字节和低 8 位(Sum8)。
//
// 注意(重要):
// 配电指令 0x0001~0x0004 与 PM 协议操控/参数指令共用 id,仅能靠
// 帧总长区分(配电指令 13~21B,PM 指令 28/45B)。且 pPowerManger 发送
// 配电指令时帧头 length 字段并非总长(见 udpComm.cpp TODO),
// 分发必须用实际帧长(见 MessageRegistry::find(id, totalLength))。
//
// 八条消息(均 Sum8 校验):
// 指令(pPowerManger -> CCU -> 真实CCU) payload 总长
// 0x0001 高压母线配电指令 disHighVolBusCmd 14B 21B
// 0x0002 高压母线A配电指令 disHighAVolBusCmd 6B 13B
// 0x0003 低压主母线配电指令 disLowMainBusCmd 7B 14B
// 0x0004 仪表汇流排配电指令 disLowBusCmd 8B 15B
// 反馈(真实CCU -> CCU -> pPowerManger)
// 0x0005 高压母线配电反馈 disHighVolBusState 53B 60B
// 0x0006 高压母线A配电反馈 disHighAVolBusState 25B 32B
// 0x0007 低压主母线配电反馈 disLowMainBusState 34B 41B
// 0x0008 仪表汇流排配电反馈 disLowBusState 29B 36B
// (payload/总长由 static_assert 在编译期核对,见 DisProtocol.cpp)
//============================================================================
enum : uint16_t {
DIS_HV_CMD_ID = 0x0001, // 高压母线配电指令
DIS_HVA_CMD_ID = 0x0002, // 高压母线A配电指令
DIS_HVB_CMD_ID = 0x0003, // 低压主母线配电指令
DIS_LV_CMD_ID = 0x0004, // 仪表汇流排配电指令
DIS_HV_FB_ID = 0x0005, // 高压母线配电反馈
DIS_HVA_FB_ID = 0x0006, // 高压母线A配电反馈
DIS_HVB_FB_ID = 0x0007, // 低压主母线配电反馈
DIS_LV_FB_ID = 0x0008, // 仪表汇流排配电反馈
};
// 断路器/开关类指令与状态值
enum : uint8_t {
DIS_BREAKER_ON = 0x55, // 闭合
DIS_BREAKER_OFF = 0xAA, // 断开
DIS_BREAKER_FLT = 0x5A, // 故障脱扣
};
// 判断是否配电协议消息(指令或反馈),桥接器据此识别
bool isDisMessage(const Message* msg);
bool isDisCmdMessage(const Message* msg);
bool isDisFbMessage(const Message* msg);
//--------------------------------------------------------------------------
// 指令消息(pPowerManger 下发,pCCU 原帧转发给真实CCU)
//--------------------------------------------------------------------------
#define PCCU_DIS_MSG_CLASS(clsName, msgId, msgName, dataT) \
class clsName : public Message { \
public: \
uint16_t id() const override { return msgId; } \
const char* name() const override { return msgName; } \
const ChecksumPolicy& checksum() const override { return m_checksum; } \
size_t payloadLength() const override { return sizeof(dataT); } \
std::vector<uint8_t> encode(const void* obj) const override; \
bool decode(const std::vector<uint8_t>& frame, void* obj) const override; \
std::vector<uint8_t> encode(const dataT& v) const { return encode(&v); } \
bool decode(const std::vector<uint8_t>& frame, dataT& v) const { \
return decode(frame, static_cast<void*>(&v)); \
} \
private: \
ChecksumPolicy m_checksum{ChecksumType::Sum8}; \
}
// 高压母线配电指令(0x0001)
PCCU_DIS_MSG_CLASS(DisHighVolBusCmdMessage, DIS_HV_CMD_ID, "dis_hv_cmd", disHighVolBusCmd);
// 高压母线A配电指令(0x0002)
PCCU_DIS_MSG_CLASS(DisHighAVolBusCmdMessage, DIS_HVA_CMD_ID, "dis_hva_cmd", disHighAVolBusCmd);
// 低压主母线配电指令(0x0003)
PCCU_DIS_MSG_CLASS(DisHighBVolBusCmdMessage, DIS_HVB_CMD_ID, "dis_hvb_cmd", disLowMainBusCmd);
// 仪表汇流排配电指令(0x0004)
PCCU_DIS_MSG_CLASS(DisLowBusCmdMessage, DIS_LV_CMD_ID, "dis_lv_cmd", disLowBusCmd);
//--------------------------------------------------------------------------
// 反馈消息(真实CCU 上报,pCCU 原帧转发给 pPowerManger 并解码显示)
//--------------------------------------------------------------------------
// 高压母线配电反馈(0x0005)
PCCU_DIS_MSG_CLASS(DisHighVolBusFbMessage, DIS_HV_FB_ID, "dis_hv_fb", disHighVolBusState);
// 高压母线A配电反馈(0x0006)
PCCU_DIS_MSG_CLASS(DisHighAVolBusFbMessage, DIS_HVA_FB_ID, "dis_hva_fb", disHighAVolBusState);
// 低压主母线配电反馈(0x0007)
PCCU_DIS_MSG_CLASS(DisHighBVolBusFbMessage, DIS_HVB_FB_ID, "dis_hvb_fb", disLowMainBusState);
// 仪表汇流排配电反馈(0x0008)
PCCU_DIS_MSG_CLASS(DisLowBusFbMessage, DIS_LV_FB_ID, "dis_lv_fb", disLowBusState);
#undef PCCU_DIS_MSG_CLASS
// 注册配电指令消息(PM 链路用:识别 pPowerManger 下发的配电指令)
void registerDisCmdMessages(class MessageRegistry& reg);
// 注册配电反馈消息(RCU 链路用:识别真实CCU 上报的配电反馈)
void registerDisFbMessages(class MessageRegistry& reg);
} // namespace ccu
#endif // PCCU_DIS_PROTOCOL_H
-344
View File
@@ -1,344 +0,0 @@
#include "FcProtocol.h"
#include "FieldCodec.h"
#include "Frame.h"
#include "MessageRegistry.h"
namespace ccu {
void registerFcMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new FcControlMessage()));
reg.registerMessage(std::unique_ptr<Message>(new FcStatusMessage()));
}
//============================================================================
// 0x0001 控制指令域编解码
// 数据域 18 字节(字节7~24),偏移 0~17;整包 24 字节
//============================================================================
namespace {
// 数据域内偏移(相对数据域起点,offset = 文档字节号 - 7)
enum : size_t {
CO_MODE = 0, // 模式设定 (字节7)
CO_CMD = 1, // 操控指令 (字节8)
CO_OUT_POWER = 2, // 输出功率指令 (字节9)
CO_PITCH1 = 3, // 纵倾姿态数据1 int16 (字节10,11)
CO_RESERVED1 = 5, // 预留 (字节12,13),0827协议由纵倾姿态数据2改为预留
CO_ROLL1 = 7, // 横倾姿态数据1 int16 (字节14,15)
CO_RESERVED2 = 9, // 预留 (字节16,17),0827协议由横倾姿态数据2改为预留
CO_EMER_ALLOW = 11, // 应急允许 (字节18)
CO_DEPTH = 12, // 潜深深度 u16 (字节19,20)
CO_SUPPLY_CMD = 14, // 补给/排放指令 (字节21)
CO_RESERVED5 = 15, // 预留指令5 (字节22)
CO_RESERVED6 = 16, // 预留指令6 (字节23)
CO_HEARTBEAT = 17, // 通信心跳 (字节24)
};
} // namespace
std::vector<uint8_t> FcControlMessage::encode(const void* obj) const {
const FcControlValue* v = static_cast<const FcControlValue*>(obj);
std::vector<uint8_t> payload(payloadLength(), 0);
FieldCodec::putU8(payload, CO_MODE, v->mode);
FieldCodec::putU8(payload, CO_CMD, v->cmd);
FieldCodec::putU8(payload, CO_OUT_POWER, v->outputPower);
FieldCodec::putU16(payload, CO_PITCH1, static_cast<uint16_t>(v->pitch1));
FieldCodec::putU16(payload, CO_RESERVED1, 0); // 预留恒填0
FieldCodec::putU16(payload, CO_ROLL1, static_cast<uint16_t>(v->roll1));
FieldCodec::putU16(payload, CO_RESERVED2, 0); // 预留恒填0
FieldCodec::putU8(payload, CO_EMER_ALLOW, v->emergencyAllow);
FieldCodec::putU16(payload, CO_DEPTH, v->depth);
FieldCodec::putU8(payload, CO_SUPPLY_CMD, v->supplyCmd);
FieldCodec::putU8(payload, CO_RESERVED5, v->reservedCmd5);
FieldCodec::putU8(payload, CO_RESERVED6, v->reservedCmd6);
FieldCodec::putU8(payload, CO_HEARTBEAT, v->heartbeat);
return Frame::build(id(), payload, checksum());
}
bool FcControlMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
FcControlValue* v = static_cast<FcControlValue*>(obj);
size_t o = FRAME_HEADER_LEN; // 数据域起点
v->mode = FieldCodec::getU8(frame, o + CO_MODE);
v->cmd = FieldCodec::getU8(frame, o + CO_CMD);
v->outputPower = FieldCodec::getU8(frame, o + CO_OUT_POWER);
v->pitch1 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_PITCH1));
// 字节12,13 预留,忽略
v->roll1 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_ROLL1));
// 字节16,17 预留,忽略
v->emergencyAllow = FieldCodec::getU8(frame, o + CO_EMER_ALLOW);
v->depth = FieldCodec::getU16(frame, o + CO_DEPTH);
v->supplyCmd = FieldCodec::getU8(frame, o + CO_SUPPLY_CMD);
v->reservedCmd5 = FieldCodec::getU8(frame, o + CO_RESERVED5);
v->reservedCmd6 = FieldCodec::getU8(frame, o + CO_RESERVED6);
v->heartbeat = FieldCodec::getU8(frame, o + CO_HEARTBEAT);
return true;
}
//============================================================================
// 0x0002 状态反馈域编解码
// 数据域 144 字节
//============================================================================
namespace {
// 数据域内各字段偏移(相对数据域起点,offset = 文档字节号 - 7)
// 注意:文档中"系统故障等级"为字节19(1字节),字节20为隐式保留位,
// "系统发电功率"从字节21(offset 14)开始,其后的字段均按此定位。
enum : size_t {
OFF_MODE = 0, // 运行模式 (字节7)
OFF_STATUS = 1, // 运行状态 (字节8)
OFF_FL1 = 2, // 一级故障码 u16 (字节9-10)
OFF_FL2 = 4, // 二级故障码 (字节11-12)
OFF_FL3 = 6, // 三级故障码 (字节13-14)
OFF_FL4 = 8, // 四级故障码 (字节15-16)
OFF_GEN_TIME = 10, // 累积发电时间 u16 (字节17-18)
OFF_FAULT_LEVEL = 12, // 系统故障等级 (字节19)
// 字节20 隐式保留(offset 13)
OFF_GEN_POWER = 14, // 系统发电功率 u16 (字节21-22)
OFF_H2_CAP = 16, // 储氢剩余容量 (字节23)
OFF_LO2_CAP = 17, // 液氧剩余容量 (字节24)
OFF_FC1_MIN_V = 18, // I#FC最低单片电压 (字节25)
OFF_FC1_MIN_POS = 19, // I#FC最低单片电压位置 (字节26)
OFF_FC1_AVG_V = 20, // I#FC平均单片电压 (字节27)
OFF_FC2_MIN_V = 21, // 2#FC最低单片电压 (字节28)
OFF_FC2_MIN_POS = 22, // 2#FC最低单片电压位置 (字节29)
OFF_FC2_AVG_V = 23, // 2#FC平均单片电压 (字节30)
OFF_PALLADIUM_TEMP = 24, // 钯膜最高温度 u16 (字节31-32)
OFF_BUFFER_PRES = 26, // 缓冲罐压力 (字节33-34)
OFF_FLUE_TOTAL = 28, // 烟气累计排放量 (字节35-36)
OFF_FLUE_PRES = 30, // 烟气压力 (字节37-38)
OFF_REACTOR_PRES = 32, // 反应器压力 (字节39-40)
OFF_EVALVE_OPEN = 34, // 电动阀开度 (字节41-42)
OFF_DCDC1_IN_V = 36, // DC/DC通道1输入电压 (字节43-44)
OFF_DCDC1_IN_I = 38, // DC/DC通道1输入电流 (字节45-46)
OFF_DCDC2_IN_V = 40, // DC/DC通道2输入电压 (字节47-48)
OFF_DCDC2_IN_I = 42, // DC/DC通道2输入电流 (字节49-50)
OFF_DCDC_OUT_V = 44, // DC/DC输出电压 (字节51-52)
OFF_DCDC_OUT_I = 46, // DC/DC输出电流 (字节53-54)
OFF_DCDC_CTRL_V = 48, // DC/DC控制电源电压 (字节55)
OFF_DCDC_AUX_V = 49, // DC/DC辅电输出电压 (字节56)
OFF_METHANOL_TOTAL = 50, // 甲醇累计使用量 u16 (字节57-58)
OFF_METHANOL_FEED = 52, // 甲醇溶液进料量 (字节59-60)
OFF_O2_WATER = 54, // 氧侧生成水箱液位 (字节61-62)
OFF_H2_WATER = 56, // 氢侧生成水箱液位 (字节63-64)
OFF_BALLAST_WATER = 58, // 配重水箱液位 (字节65-66)
OFF_EXH_IN_PRES = 60, // 尾气装置进气压力 (字节67-68)
OFF_EXH_OUT_PRES = 62, // 尾气装置排气压力 (字节69-70)
OFF_CABIN_P1 = 64, // 舱室压力1 (字节71-72)
OFF_CABIN_P2 = 66, // 舱室压力2 (字节73-74)
OFF_CABIN_T1 = 68, // 舱室温度1 (字节75-76)
OFF_CABIN_T2 = 70, // 舱室温度2 (字节77-78)
OFF_CABIN_H1 = 72, // 舱室湿度1 (字节79-80)
OFF_CABIN_H2 = 74, // 舱室湿度2 (字节81-82)
OFF_H2_C1 = 76, // 舱室H2浓度1 (字节83-84)
OFF_H2_C2 = 78, // 舱室H2浓度2 (字节85-86)
OFF_H2_C3 = 80, // 舱室H2浓度3 (字节87-88)
OFF_O2_C1 = 82, // 舱室O2浓度1 (字节89-90)
OFF_O2_C2 = 84, // 舱室O2浓度2 (字节91-92)
OFF_CH3OH_C1 = 86, // 舱室甲醇浓度1 (字节93-94)
OFF_CH3OH_C2 = 88, // 舱室甲醇浓度2 (字节95-96)
OFF_FLAME1 = 90, // 火焰探测器1 (字节97)
OFF_FLAME2 = 91, // 火焰探测器2 (字节98)
OFF_EMER_DEPTH = 92, // 应急上浮深度 u16 (字节99-100)
OFF_EMER_TIME = 94, // 应急上浮时间 (字节101-102)
OFF_EXH_FREQ = 96, // 尾气运行频率 (字节103-104)
OFF_EXH_IN_TEMP = 98, // 尾气进气温度 (字节105-106)
OFF_EXH_OUT_TEMP = 100, // 尾气排气温度 (字节107-108)
OFF_EXH_WIN_PRES = 102, // 尾气进水压力 (字节109-110)
OFF_EXH_WOUT_PRES = 104, // 尾气排水压力 (字节111-112)
OFF_TANK_LO2_PRES = 106, // 液氧罐压力 (字节113-114)
OFF_TANK_CO2_PRES = 108, // 二氧化碳压力 (字节115-116)
OFF_TANK_LO2_LEVEL = 110, // 液氧罐液位 (字节117-118)
OFF_ALLOY_H2_FLOW = 112, // 合金供氢流量 (字节119-120)
OFF_FC_H2_FLOW = 114, // FC供氢流量 (字节121-122)
OFF_FC_O2_FLOW = 116, // FC供氧流量 (字节123-124)
OFF_RESERVED = 118, // 预留1~11 u16*11 -> 118..138 (字节125-146)
OFF_REMAIN_GEN = 140, // 剩余发电量 u16 (字节147,148),0827协议由预留12改为
OFF_HEARTBEAT = 142, // 通信心跳 (字节149)
OFF_EMERGENCY_CMD = 143, // 应急指令 (字节150)
};
inline void getU16Arr(const std::vector<uint8_t>& frame, size_t off, uint16_t* dst, size_t n) {
for (size_t i = 0; i < n; ++i) dst[i] = FieldCodec::getU16(frame, off + i * 2);
}
inline void putU16Arr(std::vector<uint8_t>& p, size_t off, const uint16_t* src, size_t n) {
for (size_t i = 0; i < n; ++i) FieldCodec::putU16(p, off + i * 2, src[i]);
}
} // namespace
std::vector<uint8_t> FcStatusMessage::encode(const void* obj) const {
const FcStatusValue* v = static_cast<const FcStatusValue*>(obj);
std::vector<uint8_t> p(payloadLength(), 0);
FieldCodec::putU8(p, OFF_MODE, v->fc_mode);
FieldCodec::putU8(p, OFF_STATUS, v->fc_status);
FieldCodec::putU16(p, OFF_FL1, v->fault_level_1);
FieldCodec::putU16(p, OFF_FL2, v->fault_level_2);
FieldCodec::putU16(p, OFF_FL3, v->fault_level_3);
FieldCodec::putU16(p, OFF_FL4, v->fault_level_4);
FieldCodec::putU16(p, OFF_GEN_TIME, v->total_generation_time);
FieldCodec::putU8(p, OFF_FAULT_LEVEL, v->fc_fault_level);
FieldCodec::putU16(p, OFF_GEN_POWER, v->generation_power);
FieldCodec::putU8(p, OFF_H2_CAP, v->hydrogen_capacity);
FieldCodec::putU8(p, OFF_LO2_CAP, v->liquid_oxygen_capacity);
FieldCodec::putU8(p, OFF_FC1_MIN_V, v->fc1_min_cell_voltage);
FieldCodec::putU8(p, OFF_FC1_MIN_POS, v->fc1_min_cell_pos);
FieldCodec::putU8(p, OFF_FC1_AVG_V, v->fc1_avg_cell_voltage);
FieldCodec::putU8(p, OFF_FC2_MIN_V, v->fc2_min_cell_voltage);
FieldCodec::putU8(p, OFF_FC2_MIN_POS, v->fc2_min_cell_pos);
FieldCodec::putU8(p, OFF_FC2_AVG_V, v->fc2_avg_cell_voltage);
FieldCodec::putU16(p, OFF_PALLADIUM_TEMP, v->palladium_temp);
FieldCodec::putU16(p, OFF_BUFFER_PRES, v->buffer_tank_pressure);
FieldCodec::putU16(p, OFF_FLUE_TOTAL, v->flue_total_emission);
FieldCodec::putU16(p, OFF_FLUE_PRES, v->flue_pressure);
FieldCodec::putU16(p, OFF_REACTOR_PRES, v->reactor_pressure);
FieldCodec::putU16(p, OFF_EVALVE_OPEN, v->electric_valve_open);
FieldCodec::putU16(p, OFF_DCDC1_IN_V, v->dcdc1_in_voltage);
FieldCodec::putU16(p, OFF_DCDC1_IN_I, v->dcdc1_in_current);
FieldCodec::putU16(p, OFF_DCDC2_IN_V, v->dcdc2_in_voltage);
FieldCodec::putU16(p, OFF_DCDC2_IN_I, v->dcdc2_in_current);
FieldCodec::putU16(p, OFF_DCDC_OUT_V, v->dcdc_out_voltage);
FieldCodec::putU16(p, OFF_DCDC_OUT_I, v->dcdc_out_current);
FieldCodec::putU8(p, OFF_DCDC_CTRL_V, v->dcdc_ctrl_voltage);
FieldCodec::putU8(p, OFF_DCDC_AUX_V, v->dcdc_aux_voltage);
FieldCodec::putU16(p, OFF_METHANOL_TOTAL, v->methanol_total_use);
FieldCodec::putU16(p, OFF_METHANOL_FEED, v->methanol_feed);
FieldCodec::putU16(p, OFF_O2_WATER, v->oxygen_side_water_level);
FieldCodec::putU16(p, OFF_H2_WATER, v->hydrogen_side_water_level);
FieldCodec::putU16(p, OFF_BALLAST_WATER, v->ballast_water_level);
FieldCodec::putU16(p, OFF_EXH_IN_PRES, v->exhaust_inlet_pressure);
FieldCodec::putU16(p, OFF_EXH_OUT_PRES, v->exhaust_outlet_pressure);
FieldCodec::putU16(p, OFF_CABIN_P1, v->cabin_pressure1);
FieldCodec::putU16(p, OFF_CABIN_P2, v->cabin_pressure2);
FieldCodec::putU16(p, OFF_CABIN_T1, v->cabin_temp1);
FieldCodec::putU16(p, OFF_CABIN_T2, v->cabin_temp2);
FieldCodec::putU16(p, OFF_CABIN_H1, v->cabin_humidity1);
FieldCodec::putU16(p, OFF_CABIN_H2, v->cabin_humidity2);
FieldCodec::putU16(p, OFF_H2_C1, v->h2_concentration1);
FieldCodec::putU16(p, OFF_H2_C2, v->h2_concentration2);
FieldCodec::putU16(p, OFF_H2_C3, v->h2_concentration3);
FieldCodec::putU16(p, OFF_O2_C1, v->o2_concentration1);
FieldCodec::putU16(p, OFF_O2_C2, v->o2_concentration2);
FieldCodec::putU16(p, OFF_CH3OH_C1, v->ch3oh_concentration1);
FieldCodec::putU16(p, OFF_CH3OH_C2, v->ch3oh_concentration2);
FieldCodec::putU8(p, OFF_FLAME1, v->flame_detector1);
FieldCodec::putU8(p, OFF_FLAME2, v->flame_detector2);
FieldCodec::putU16(p, OFF_EMER_DEPTH, v->emergency_float_depth);
FieldCodec::putU16(p, OFF_EMER_TIME, v->emergency_float_time);
FieldCodec::putU16(p, OFF_EXH_FREQ, v->exhaust_run_freq);
FieldCodec::putU16(p, OFF_EXH_IN_TEMP, v->exhaust_inlet_temp);
FieldCodec::putU16(p, OFF_EXH_OUT_TEMP, v->exhaust_outlet_temp);
FieldCodec::putU16(p, OFF_EXH_WIN_PRES, v->exhaust_water_in_pressure);
FieldCodec::putU16(p, OFF_EXH_WOUT_PRES, v->exhaust_water_out_pressure);
FieldCodec::putU16(p, OFF_TANK_LO2_PRES, v->tank_lo2_pressure);
FieldCodec::putU16(p, OFF_TANK_CO2_PRES, v->tank_co2_pressure);
FieldCodec::putU16(p, OFF_TANK_LO2_LEVEL, v->tank_lo2_level);
FieldCodec::putU16(p, OFF_ALLOY_H2_FLOW, v->alloy_h2_flow);
FieldCodec::putU16(p, OFF_FC_H2_FLOW, v->fc_h2_flow);
FieldCodec::putU16(p, OFF_FC_O2_FLOW, v->fc_o2_flow);
putU16Arr(p, OFF_RESERVED, v->reserved, 11);
FieldCodec::putU16(p, OFF_REMAIN_GEN, v->remaining_generation);
FieldCodec::putU8(p, OFF_HEARTBEAT, v->heartbeat);
FieldCodec::putU8(p, OFF_EMERGENCY_CMD, v->emergency_cmd);
return Frame::build(id(), p, checksum());
}
bool FcStatusMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
FcStatusValue* v = static_cast<FcStatusValue*>(obj);
size_t o = FRAME_HEADER_LEN;
v->fc_mode = FieldCodec::getU8(frame, o + OFF_MODE);
v->fc_status = FieldCodec::getU8(frame, o + OFF_STATUS);
v->fault_level_1 = FieldCodec::getU16(frame, o + OFF_FL1);
v->fault_level_2 = FieldCodec::getU16(frame, o + OFF_FL2);
v->fault_level_3 = FieldCodec::getU16(frame, o + OFF_FL3);
v->fault_level_4 = FieldCodec::getU16(frame, o + OFF_FL4);
v->total_generation_time = FieldCodec::getU16(frame, o + OFF_GEN_TIME);
v->fc_fault_level = FieldCodec::getU8(frame, o + OFF_FAULT_LEVEL);
v->generation_power = FieldCodec::getU16(frame, o + OFF_GEN_POWER);
v->hydrogen_capacity = FieldCodec::getU8(frame, o + OFF_H2_CAP);
v->liquid_oxygen_capacity = FieldCodec::getU8(frame, o + OFF_LO2_CAP);
v->fc1_min_cell_voltage = FieldCodec::getU8(frame, o + OFF_FC1_MIN_V);
v->fc1_min_cell_pos = FieldCodec::getU8(frame, o + OFF_FC1_MIN_POS);
v->fc1_avg_cell_voltage = FieldCodec::getU8(frame, o + OFF_FC1_AVG_V);
v->fc2_min_cell_voltage = FieldCodec::getU8(frame, o + OFF_FC2_MIN_V);
v->fc2_min_cell_pos = FieldCodec::getU8(frame, o + OFF_FC2_MIN_POS);
v->fc2_avg_cell_voltage = FieldCodec::getU8(frame, o + OFF_FC2_AVG_V);
v->palladium_temp = FieldCodec::getU16(frame, o + OFF_PALLADIUM_TEMP);
v->buffer_tank_pressure = FieldCodec::getU16(frame, o + OFF_BUFFER_PRES);
v->flue_total_emission = FieldCodec::getU16(frame, o + OFF_FLUE_TOTAL);
v->flue_pressure = FieldCodec::getU16(frame, o + OFF_FLUE_PRES);
v->reactor_pressure = FieldCodec::getU16(frame, o + OFF_REACTOR_PRES);
v->electric_valve_open = FieldCodec::getU16(frame, o + OFF_EVALVE_OPEN);
v->dcdc1_in_voltage = FieldCodec::getU16(frame, o + OFF_DCDC1_IN_V);
v->dcdc1_in_current = FieldCodec::getU16(frame, o + OFF_DCDC1_IN_I);
v->dcdc2_in_voltage = FieldCodec::getU16(frame, o + OFF_DCDC2_IN_V);
v->dcdc2_in_current = FieldCodec::getU16(frame, o + OFF_DCDC2_IN_I);
v->dcdc_out_voltage = FieldCodec::getU16(frame, o + OFF_DCDC_OUT_V);
v->dcdc_out_current = FieldCodec::getU16(frame, o + OFF_DCDC_OUT_I);
v->dcdc_ctrl_voltage = FieldCodec::getU8(frame, o + OFF_DCDC_CTRL_V);
v->dcdc_aux_voltage = FieldCodec::getU8(frame, o + OFF_DCDC_AUX_V);
v->methanol_total_use = FieldCodec::getU16(frame, o + OFF_METHANOL_TOTAL);
v->methanol_feed = FieldCodec::getU16(frame, o + OFF_METHANOL_FEED);
v->oxygen_side_water_level= FieldCodec::getU16(frame, o + OFF_O2_WATER);
v->hydrogen_side_water_level = FieldCodec::getU16(frame, o + OFF_H2_WATER);
v->ballast_water_level = FieldCodec::getU16(frame, o + OFF_BALLAST_WATER);
v->exhaust_inlet_pressure = FieldCodec::getU16(frame, o + OFF_EXH_IN_PRES);
v->exhaust_outlet_pressure= FieldCodec::getU16(frame, o + OFF_EXH_OUT_PRES);
v->cabin_pressure1 = FieldCodec::getU16(frame, o + OFF_CABIN_P1);
v->cabin_pressure2 = FieldCodec::getU16(frame, o + OFF_CABIN_P2);
v->cabin_temp1 = FieldCodec::getU16(frame, o + OFF_CABIN_T1);
v->cabin_temp2 = FieldCodec::getU16(frame, o + OFF_CABIN_T2);
v->cabin_humidity1 = FieldCodec::getU16(frame, o + OFF_CABIN_H1);
v->cabin_humidity2 = FieldCodec::getU16(frame, o + OFF_CABIN_H2);
v->h2_concentration1 = FieldCodec::getU16(frame, o + OFF_H2_C1);
v->h2_concentration2 = FieldCodec::getU16(frame, o + OFF_H2_C2);
v->h2_concentration3 = FieldCodec::getU16(frame, o + OFF_H2_C3);
v->o2_concentration1 = FieldCodec::getU16(frame, o + OFF_O2_C1);
v->o2_concentration2 = FieldCodec::getU16(frame, o + OFF_O2_C2);
v->ch3oh_concentration1 = FieldCodec::getU16(frame, o + OFF_CH3OH_C1);
v->ch3oh_concentration2 = FieldCodec::getU16(frame, o + OFF_CH3OH_C2);
v->flame_detector1 = FieldCodec::getU8(frame, o + OFF_FLAME1);
v->flame_detector2 = FieldCodec::getU8(frame, o + OFF_FLAME2);
v->emergency_float_depth = FieldCodec::getU16(frame, o + OFF_EMER_DEPTH);
v->emergency_float_time = FieldCodec::getU16(frame, o + OFF_EMER_TIME);
v->exhaust_run_freq = FieldCodec::getU16(frame, o + OFF_EXH_FREQ);
v->exhaust_inlet_temp = FieldCodec::getU16(frame, o + OFF_EXH_IN_TEMP);
v->exhaust_outlet_temp = FieldCodec::getU16(frame, o + OFF_EXH_OUT_TEMP);
v->exhaust_water_in_pressure = FieldCodec::getU16(frame, o + OFF_EXH_WIN_PRES);
v->exhaust_water_out_pressure= FieldCodec::getU16(frame, o + OFF_EXH_WOUT_PRES);
v->tank_lo2_pressure = FieldCodec::getU16(frame, o + OFF_TANK_LO2_PRES);
v->tank_co2_pressure = FieldCodec::getU16(frame, o + OFF_TANK_CO2_PRES);
v->tank_lo2_level = FieldCodec::getU16(frame, o + OFF_TANK_LO2_LEVEL);
v->alloy_h2_flow = FieldCodec::getU16(frame, o + OFF_ALLOY_H2_FLOW);
v->fc_h2_flow = FieldCodec::getU16(frame, o + OFF_FC_H2_FLOW);
v->fc_o2_flow = FieldCodec::getU16(frame, o + OFF_FC_O2_FLOW);
getU16Arr(frame, o + OFF_RESERVED, v->reserved, 11);
v->remaining_generation = FieldCodec::getU16(frame, o + OFF_REMAIN_GEN);
v->heartbeat = FieldCodec::getU8(frame, o + OFF_HEARTBEAT);
v->emergency_cmd = FieldCodec::getU8(frame, o + OFF_EMERGENCY_CMD);
return true;
}
} // namespace ccu
-178
View File
@@ -1,178 +0,0 @@
#ifndef PCCU_FC_PROTOCOL_H
#define PCCU_FC_PROTOCOL_H
#include <cstdint>
#include <vector>
#include <memory>
#include "Message.h"
namespace ccu {
//============================================================================
// FC 协议(燃料电池系统 <-> 复合管控器)
// 依据:docs/超滑FC和复合管控器通讯协议 - 0827.docx
//
// 帧格式统一:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + [校验和]
// 注意:FC 协议文档未列出校验和行,故默认采用 None(可插拔,联调可切换)。
//
// 两条消息:
// 0x0001 复合管控器->燃料电池控制器(控制指令域) payload 18B,总长 24B
// 0x0002 燃料电池控制器->复合管控器(状态反馈域) payload 144B,总长 150B
//============================================================================
//--------------------------------------------------------------------------
// 0x0001 控制指令域(复合管控器 -> 燃料电池)
// payload 18 字节,对应文档字节 7~24
//--------------------------------------------------------------------------
struct FcControlValue {
uint8_t mode = 0; // 模式设定 00~03
uint8_t cmd = 0; // 操控指令 00~0B
uint8_t outputPower = 0; // 输出功率指令 0~250,分辨率0.1kW
int16_t pitch1 = 0; // 纵倾姿态数据1 int16,分辨率0.1°(字节10,11)
uint16_t reserved1 = 0; // 预留(字节12,13),0827协议由纵倾姿态数据2改为预留
int16_t roll1 = 0; // 横倾姿态数据1 int16,分辨率0.1°(字节14,15)
uint16_t reserved2 = 0; // 预留(字节16,17),0827协议由横倾姿态数据2改为预留
uint8_t emergencyAllow = 0; // 应急允许:Bit0 允许降载 / Bit1 允许排气(字节18)
uint16_t depth = 0; // 潜深深度,单位 m(字节19,20)
uint8_t supplyCmd = 0; // 补给/排放指令 00~0B(字节21)
uint8_t reservedCmd5 = 0; // 预留指令5(字节22)
uint8_t reservedCmd6 = 0; // 预留指令6(字节23)
uint8_t heartbeat = 0; // 通信心跳,每次+1(字节24)
};
//--------------------------------------------------------------------------
// 0x0002 状态反馈域(燃料电池 -> 复合管控器)
// payload 144 字节,对应文档字节 7~150
//--------------------------------------------------------------------------
struct FcStatusValue {
uint8_t fc_mode = 0; // 燃料电池系统运行模式
uint8_t fc_status = 0; // 燃料电池系统运行状态
uint16_t fault_level_1 = 0; // 一级故障码
uint16_t fault_level_2 = 0; // 二级故障码
uint16_t fault_level_3 = 0; // 三级故障码
uint16_t fault_level_4 = 0; // 四级故障码
uint16_t total_generation_time = 0;// 累积发电时间,0.1h
uint8_t fc_fault_level = 0; // 系统故障等级 00~04
uint16_t generation_power = 0; // 系统发电功率,0.01kW
uint8_t hydrogen_capacity = 0; // 储氢剩余容量 %
uint8_t liquid_oxygen_capacity = 0; // 液氧剩余容量 %
uint8_t fc1_min_cell_voltage = 0; // I#FC最低单片电压,10mV
uint8_t fc1_min_cell_pos = 0; // I#FC最低单片电压位置
uint8_t fc1_avg_cell_voltage = 0; // I#FC平均单片电压,10mV
uint8_t fc2_min_cell_voltage = 0; // 2#FC最低单片电压
uint8_t fc2_min_cell_pos = 0; // 2#FC最低单片电压位置
uint8_t fc2_avg_cell_voltage = 0; // 2#FC平均单片电压
uint16_t palladium_temp = 0; // 甲醇制氢装置钯膜最高温度,0.1℃
uint16_t buffer_tank_pressure = 0; // 缓冲罐压力,0.1kPa
uint16_t flue_total_emission = 0; // 烟气累计排放量,0.1kg
uint16_t flue_pressure = 0; // 烟气压力,0.001MPa
uint16_t reactor_pressure = 0; // 反应器压力,0.001MPa
uint16_t electric_valve_open = 0; // 电动阀开度,0.1%
uint16_t dcdc1_in_voltage = 0; // DC/DC通道1输入电压,0.1V
uint16_t dcdc1_in_current = 0; // DC/DC通道1输入电流,0.1A
uint16_t dcdc2_in_voltage = 0; // DC/DC通道2输入电压,0.1V
uint16_t dcdc2_in_current = 0; // DC/DC通道2输入电流,0.1A
uint16_t dcdc_out_voltage = 0; // DC/DC输出电压,0.1V
uint16_t dcdc_out_current = 0; // DC/DC输出电流,0.1A
uint8_t dcdc_ctrl_voltage = 0; // DC/DC控制电源电压,0.25V
uint8_t dcdc_aux_voltage = 0; // DC/DC辅电输出电压,0.125V
uint16_t methanol_total_use = 0; // 甲醇累计使用量,0.1kg
uint16_t methanol_feed = 0; // 甲醇溶液进料量,1mL/min
uint16_t oxygen_side_water_level = 0; // 氧侧生成水箱液位,0.01mm
uint16_t hydrogen_side_water_level = 0;// 氢侧生成水箱液位,0.01mm
uint16_t ballast_water_level = 0; // 配重水箱液位,0.01mm
uint16_t exhaust_inlet_pressure = 0; // 尾气装置进气压力,0.01MPa
uint16_t exhaust_outlet_pressure = 0; // 尾气装置排气压力,0.01MPa
uint16_t cabin_pressure1 = 0; // 舱室压力1,0.01kPa
uint16_t cabin_pressure2 = 0; // 舱室压力2,0.01kPa
uint16_t cabin_temp1 = 0; // 舱室温度1,0.01℃
uint16_t cabin_temp2 = 0; // 舱室温度2,0.01℃
uint16_t cabin_humidity1 = 0; // 舱室湿度1,0.01%RH
uint16_t cabin_humidity2 = 0; // 舱室湿度2,0.01%RH
uint16_t h2_concentration1 = 0; // 舱室H2浓度1,0.01%LEL
uint16_t h2_concentration2 = 0; // 舱室H2浓度2,0.01%LEL
uint16_t h2_concentration3 = 0; // 舱室H2浓度3,0.01%LEL
uint16_t o2_concentration1 = 0; // 舱室O2浓度1,0.01%Vol
uint16_t o2_concentration2 = 0; // 舱室O2浓度2,0.01%Vol
uint16_t ch3oh_concentration1 = 0; // 舱室甲醇浓度1,0.01%LEL
uint16_t ch3oh_concentration2 = 0; // 舱室甲醇浓度2,0.01%LEL
uint8_t flame_detector1 = 0; // 火焰探测器1状态
uint8_t flame_detector2 = 0; // 火焰探测器2状态
uint16_t emergency_float_depth = 0; // 应急上浮深度,1m
uint16_t emergency_float_time = 0; // 应急上浮时间,1min
uint16_t exhaust_run_freq = 0; // 尾气装置运行频率,0.01Hz
uint16_t exhaust_inlet_temp = 0; // 尾气装置进气温度,0.01℃
uint16_t exhaust_outlet_temp = 0; // 尾气装置排气温度,0.01℃
uint16_t exhaust_water_in_pressure = 0;// 尾气装置进水压力,0.01kPa
uint16_t exhaust_water_out_pressure = 0;// 尾气装置排水压力,0.01kPa
uint16_t tank_lo2_pressure = 0; // 一体化罐装置液氧罐压力,0.01MPa
uint16_t tank_co2_pressure = 0; // 一体化罐装置二氧化碳压力,0.01MPa
uint16_t tank_lo2_level = 0; // 一体化罐装置液氧罐液位,0.01mm
uint16_t alloy_h2_flow = 0; // 合金供氢流量,0.01L/min
uint16_t fc_h2_flow = 0; // FC供氢流量,0.01L/min
uint16_t fc_o2_flow = 0; // FC供氧流量,0.01L/min
uint16_t reserved[11] = {0}; // 预留1~11(字节125~146)
uint16_t remaining_generation = 0; // 剩余发电量,MWh(字节147,148),0827协议由预留12改为
uint8_t heartbeat = 0; // 通信心跳,每次+1
uint8_t emergency_cmd = 0; // 应急指令:Bit0 降载/Bit1 上浮/...
};
//--------------------------------------------------------------------------
// 消息类
//--------------------------------------------------------------------------
class FcControlMessage : public Message {
public:
uint16_t id() const override { return 0x0001; }
const char* name() const override { return "fc_control"; }
const ChecksumPolicy& checksum() const override { return m_checksum; }
// 1.1.1 控制指令域:数据域 18 字节(字节7~24),整包 24 字节(域字节数 0x0018)
size_t payloadLength() const override { return 18; }
std::vector<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
// 便捷重载
std::vector<uint8_t> encode(const FcControlValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, FcControlValue& v) const {
return decode(frame, static_cast<void*>(&v));
}
private:
ChecksumPolicy m_checksum{ChecksumType::None};
};
class FcStatusMessage : public Message {
public:
uint16_t id() const override { return 0x0002; }
const char* name() const override { return "fc_status"; }
const ChecksumPolicy& checksum() const override { return m_checksum; }
size_t payloadLength() const override { return 144; }
std::vector<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const FcStatusValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, FcStatusValue& v) const {
return decode(frame, static_cast<void*>(&v));
}
private:
ChecksumPolicy m_checksum{ChecksumType::None};
};
// 注册 FC 消息到给定注册表(见 MessageRegistry.h)
void registerFcMessages(class MessageRegistry& reg);
} // namespace ccu
#endif // PCCU_FC_PROTOCOL_H
-58
View File
@@ -1,58 +0,0 @@
#ifndef PCCU_FIELD_CODEC_H
#define PCCU_FIELD_CODEC_H
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <stdexcept>
namespace ccu {
//============================================================================
// FieldCodec:小端序(Little-Endian)编解码原语。
//
// 两条协议(FC 协议 / PM 协议)均采用:
// - 多字节数据小端序
// - 域起始符 0x40 0x40
// - 域字节数 = 整个协议包的数据长度
// 本类提供对裸缓冲区的字段读写,不关心具体业务含义。
//============================================================================
class FieldCodec {
public:
// 在 dst 偏移 offset 处写入单字节
static void putU8(std::vector<uint8_t>& dst, size_t offset, uint8_t v) {
dst[offset] = v;
}
// 小端写入 2 字节
static void putU16(std::vector<uint8_t>& dst, size_t offset, uint16_t v) {
dst[offset] = static_cast<uint8_t>(v & 0xFF);
dst[offset + 1] = static_cast<uint8_t>((v >> 8) & 0xFF);
}
// 小端写入 4 字节
static void putU32(std::vector<uint8_t>& dst, size_t offset, uint32_t v) {
dst[offset] = static_cast<uint8_t>(v & 0xFF);
dst[offset + 1] = static_cast<uint8_t>((v >> 8) & 0xFF);
dst[offset + 2] = static_cast<uint8_t>((v >> 16) & 0xFF);
dst[offset + 3] = static_cast<uint8_t>((v >> 24) & 0xFF);
}
static uint8_t getU8 (const std::vector<uint8_t>& src, size_t offset) {
return src[offset];
}
static uint16_t getU16(const std::vector<uint8_t>& src, size_t offset) {
return static_cast<uint16_t>(src[offset]) |
(static_cast<uint16_t>(src[offset + 1]) << 8);
}
static uint32_t getU32(const std::vector<uint8_t>& src, size_t offset) {
return static_cast<uint32_t>(src[offset]) |
(static_cast<uint32_t>(src[offset + 1]) << 8) |
(static_cast<uint32_t>(src[offset + 2]) << 16) |
(static_cast<uint32_t>(src[offset + 3]) << 24);
}
};
} // namespace ccu
#endif // PCCU_FIELD_CODEC_H
-51
View File
@@ -1,51 +0,0 @@
#include "Frame.h"
#include "FieldCodec.h"
namespace ccu {
bool Frame::parseId(const std::vector<uint8_t>& buf, uint16_t& outId) {
if (buf.size() < FRAME_HEADER_LEN) return false;
if (buf[0] != FRAME_START1 || buf[1] != FRAME_START2) return false;
outId = FieldCodec::getU16(buf, 2);
return true;
}
bool Frame::parseHeader(const std::vector<uint8_t>& buf, FrameHeader& h) {
if (buf.size() < FRAME_HEADER_LEN) return false;
if (buf[0] != FRAME_START1 || buf[1] != FRAME_START2) return false;
h.start1 = buf[0];
h.start2 = buf[1];
h.id = FieldCodec::getU16(buf, 2);
h.length = FieldCodec::getU16(buf, 4);
return true;
}
std::vector<uint8_t> Frame::build(
uint16_t id,
const std::vector<uint8_t>& payload,
const ChecksumPolicy& checksum) {
size_t csSize = checksum.size();
size_t total = FRAME_HEADER_LEN + payload.size() + csSize;
std::vector<uint8_t> frame(total, 0);
frame[0] = FRAME_START1;
frame[1] = FRAME_START2;
FieldCodec::putU16(frame, 2, id);
FieldCodec::putU16(frame, 4, static_cast<uint16_t>(total)); // 域字节数=整个包长度
std::copy(payload.begin(), payload.end(), frame.begin() + FRAME_HEADER_LEN);
if (csSize > 0) {
// 校验和:对起始符到校验和之前所有字节求和
std::vector<uint8_t> body(frame.begin(), frame.end() - csSize);
uint32_t sum = checksum.compute(body);
size_t off = frame.size() - csSize;
if (csSize == 4) FieldCodec::putU32(frame, off, sum);
else if (csSize == 1) frame[off] = static_cast<uint8_t>(sum & 0xFF);
else FieldCodec::putU16(frame, off, static_cast<uint16_t>(sum));
}
return frame;
}
} // namespace ccu
-54
View File
@@ -1,54 +0,0 @@
#ifndef PCCU_FRAME_H
#define PCCU_FRAME_H
#include <cstdint>
#include <cstddef>
#include <vector>
#include "ChecksumPolicy.h"
namespace ccu {
//============================================================================
// Frame:两套协议共用的通用帧模型。
//
// 两条协议帧结构完全一致:
// 域起始符1 (1B) + 域起始符2 (1B) + 域标识符 (2B) + 域字节数 (2B) + 数据域 + 校验和
//
// - header 域字节数 = 整个协议包的数据长度(含起始符与校验和)
// - 小端序
// - 校验策略可插拔(见 ChecksumPolicy)
//============================================================================
static constexpr uint8_t FRAME_START1 = 0x40;
static constexpr uint8_t FRAME_START2 = 0x40;
static constexpr size_t FRAME_HEADER_LEN = 6; // 2B start + 2B id + 2B length
struct FrameHeader {
uint8_t start1;
uint8_t start2;
uint16_t id;
uint16_t length; // 整个协议包的数据长度
};
class Frame {
public:
Frame() = default;
// 从原始缓冲解析出消息标识符(不校验完整性)
// 返回 true 且 id 有效(起始符正确)时填充 outId
static bool parseId(const std::vector<uint8_t>& buf, uint16_t& outId);
// 解析帧头;返回是否成功且起始符正确
static bool parseHeader(const std::vector<uint8_t>& buf, FrameHeader& h);
// 构造一个完整帧:data 为数据域(不含起始符与校验和),校验和自动追加
// 调用方需保证 data.size() >= 帧头之前已经包含起始符+id+length 部分
static std::vector<uint8_t> build(
uint16_t id,
const std::vector<uint8_t>& payload,
const ChecksumPolicy& checksum);
};
} // namespace ccu
#endif // PCCU_FRAME_H
-24
View File
@@ -1,24 +0,0 @@
#include "Message.h"
#include "Frame.h"
namespace ccu {
bool Message::validateFrame(const std::vector<uint8_t>& frame,
uint16_t expectId,
const ChecksumPolicy& policy,
size_t expectTotalLen) {
if (frame.size() < FRAME_HEADER_LEN) return false;
FrameHeader h;
if (!Frame::parseHeader(frame, h)) return false;
if (h.id != expectId) return false;
// 协议规定域字节数=整个包长度;联调发现 FC 设备该字段填 120 而非 150,
// 与文档不符但不影响数据域布局,若实际包体足够容纳数据域则放行
if (h.length != expectTotalLen && frame.size() < expectTotalLen) return false;
// 校验和(长度不足由 verify 内部判断)
if (!policy.verify(frame)) return false;
return true;
}
} // namespace ccu
-59
View File
@@ -1,59 +0,0 @@
#ifndef PCCU_MESSAGE_H
#define PCCU_MESSAGE_H
#include <cstdint>
#include <cstddef>
#include <string>
#include <vector>
#include "ChecksumPolicy.h"
#include "Frame.h"
namespace ccu {
//============================================================================
// Message:协议消息抽象接口。
//
// 每条协议消息实现该接口。协议变更时只需:
// 1. 修改实现类的字段偏移/长度定义
// 2. 修改 encode()/decode() 中字段取值逻辑
// 无需改动上层(Comm / Web / DB / 主流程)。
//
// 提供统一的:
// - id()/name() :消息标识与显示名
// - checksum() :该校验策略
// - payloadLength() :数据域长度(不含 6 字节帧头与校验和)
// - encode()/decode() :业务值 <-> 完整帧
//============================================================================
class Message {
public:
virtual ~Message() = default;
virtual uint16_t id() const = 0;
virtual const char* name() const = 0;
virtual const ChecksumPolicy& checksum() const = 0;
// 数据域长度(不含 6 字节帧头与校验和)
virtual size_t payloadLength() const = 0;
// 整个协议包长度 = 6 + payloadLength + checksum.size()
size_t totalLength() const {
return FRAME_HEADER_LEN + payloadLength() + checksum().size();
}
// 业务值编码为完整帧(含帧头+数据域+校验和)
// obj 为具体业务对象指针(如 FcStatusValue*)
virtual std::vector<uint8_t> encode(const void* obj) const = 0;
// 从完整帧解析业务值;成功返回 true 并填充 obj
virtual bool decode(const std::vector<uint8_t>& frame, void* obj) const = 0;
// 供实现类解码前做通用校验:起始符 / id / 总长度 / 校验和
static bool validateFrame(const std::vector<uint8_t>& frame,
uint16_t expectId,
const ChecksumPolicy& policy,
size_t expectTotalLen);
};
} // namespace ccu
#endif // PCCU_MESSAGE_H
-42
View File
@@ -1,42 +0,0 @@
#include "MessageRegistry.h"
namespace ccu {
bool MessageRegistry::registerMessage(std::unique_ptr<Message> msg) {
if (!msg) return false;
uint16_t id = msg->id();
// 同 id 且同总长的重复注册视为冲突;同 id 不同总长为合法
//(配电指令与 PM 指令共用 id,仅帧长不同)
for (auto it = m_msgs.lower_bound(id); it != m_msgs.upper_bound(id); ++it) {
if (it->second->totalLength() == msg->totalLength()) return false;
}
m_msgs.insert(std::make_pair(id, std::move(msg)));
return true;
}
Message* MessageRegistry::find(uint16_t id) const {
auto it = m_msgs.find(id);
return (it != m_msgs.end()) ? it->second.get() : nullptr;
}
Message* MessageRegistry::find(uint16_t id, size_t totalLength) const {
Message* first = nullptr;
for (auto it = m_msgs.lower_bound(id); it != m_msgs.upper_bound(id); ++it) {
if (it->second->totalLength() == totalLength) return it->second.get();
if (!first) first = it->second.get();
}
return first; // 总长未命中时回退该 id 的第一条
}
size_t MessageRegistry::size() const {
return m_msgs.size();
}
std::vector<Message*> MessageRegistry::all() const {
std::vector<Message*> out;
out.reserve(m_msgs.size());
for (const auto& kv : m_msgs) out.push_back(kv.second.get());
return out;
}
} // namespace ccu
-51
View File
@@ -1,51 +0,0 @@
#ifndef PCCU_MESSAGE_REGISTRY_H
#define PCCU_MESSAGE_REGISTRY_H
#include <cstdint>
#include <cstddef>
#include <map>
#include <memory>
#include <vector>
#include "Message.h"
namespace ccu {
//============================================================================
// MessageRegistry:消息注册表与收发分发中心。
//
// 每条链路(FC 链路 / PM 链路 / RCU 链路)持有各自的注册表实例。
// 由于 FC 协议与 PM 协议的域标识符存在重叠(0x0001/0x0002),
// 分属不同注册表可完全隔离,互不干扰。
//
// 同 id 多消息:配电指令 0x0001~0x0004 与 PM 协议操控/参数指令共用 id,
// 仅帧总长不同,因此注册表允许同 id 多条消息,并支持按总长精确查找:
// find(id, totalLength) 精确匹配(收帧分发用,未命中回退 find(id))
// find(id) 返回该 id 的第一条(兼容旧接口)
//
// 协议新增/调整消息时只需 register() 对应 Message 实现,上层无需改动。
//============================================================================
class MessageRegistry {
public:
// 注册一条消息;返回是否成功(同 id 且同总长的重复注册返回 false)
bool registerMessage(std::unique_ptr<Message> msg);
// 按 id 查找消息(同 id 多条时返回第一条);未注册返回 nullptr
Message* find(uint16_t id) const;
// 按 id + 帧总长精确查找;未命中时回退为 find(id)
Message* find(uint16_t id, size_t totalLength) const;
// 已注册消息数量
size_t size() const;
// 所有已注册消息(按 id 升序,同 id 按注册顺序)
std::vector<Message*> all() const;
private:
std::multimap<uint16_t, std::unique_ptr<Message>> m_msgs;
};
} // namespace ccu
#endif // PCCU_MESSAGE_REGISTRY_H
-662
View File
@@ -1,662 +0,0 @@
#include "PmProtocol.h"
#include "FieldCodec.h"
#include "Frame.h"
#include "MessageRegistry.h"
namespace ccu {
void registerPmMessages(MessageRegistry& reg) {
reg.registerMessage(std::unique_ptr<Message>(new PmControlMessage()));
reg.registerMessage(std::unique_ptr<Message>(new PmParamSetMessage()));
reg.registerMessage(std::unique_ptr<Message>(new PmParamSetFbMessage()));
reg.registerMessage(std::unique_ptr<Message>(new PmStatusMessage()));
}
//============================================================================
// 0x0001 操控指令域编解码
// 数据域 35 字节,偏移 0~34(对应文档字节 7~41)
//============================================================================
namespace {
enum : size_t {
PO_YEAR = 0, // u16
PO_MONTH = 2,
PO_DAY = 3,
PO_HOUR = 4,
PO_MINUTE = 5,
PO_SECOND = 6,
PO_MS10 = 7,
PO_MODE = 8,
PO_CMD = 9,
PO_OUTPOWER = 10,
PO_PITCH = 11, // 纵倾姿态数据1 int16 (字节18-19)
PO_ROLL = 13, // 横倾姿态数据1 int16 (字节20-21)
PO_EMER_ALLOW = 15,
PO_DEPTH = 16, // u16
PO_SUPPLY_CMD = 18,
PO_RESV_CMD5 = 19,
PO_RESV_CMD6 = 20,
PO_INS_BAT_CMD = 21,
PO_DYN_BAT_CMD = 22,
PO_DYN_BAT_PWR = 23, // u16
PO_HEARTBEAT = 25,
PO_HOST_STATE = 26,
PO_RESV1 = 27, // u32
PO_RESV2 = 31, // u32
};
}
std::vector<uint8_t> PmControlMessage::encode(const void* obj) const {
const PmControlValue* v = static_cast<const PmControlValue*>(obj);
std::vector<uint8_t> p(payloadLength(), 0);
FieldCodec::putU16(p, PO_YEAR, v->year);
FieldCodec::putU8(p, PO_MONTH, v->month);
FieldCodec::putU8(p, PO_DAY, v->day);
FieldCodec::putU8(p, PO_HOUR, v->hour);
FieldCodec::putU8(p, PO_MINUTE, v->minute);
FieldCodec::putU8(p, PO_SECOND, v->second);
FieldCodec::putU8(p, PO_MS10, v->millisecond10);
FieldCodec::putU8(p, PO_MODE, v->mode);
FieldCodec::putU8(p, PO_CMD, v->cmd);
FieldCodec::putU8(p, PO_OUTPOWER, v->outputPower);
FieldCodec::putU16(p, PO_PITCH, static_cast<uint16_t>(v->pitch)); // 姿态数据1,数据2字节预留
FieldCodec::putU16(p, PO_ROLL, static_cast<uint16_t>(v->roll));
FieldCodec::putU8(p, PO_EMER_ALLOW, v->emergencyAllow);
FieldCodec::putU16(p, PO_DEPTH, v->depth);
FieldCodec::putU8(p, PO_SUPPLY_CMD, v->supplyCmd);
FieldCodec::putU8(p, PO_RESV_CMD5, v->reservedCmd5);
FieldCodec::putU8(p, PO_RESV_CMD6, v->reservedCmd6);
FieldCodec::putU8(p, PO_INS_BAT_CMD, v->insBatCmd);
FieldCodec::putU8(p, PO_DYN_BAT_CMD, v->dynBatCmd);
FieldCodec::putU16(p, PO_DYN_BAT_PWR, v->dynBatPower);
FieldCodec::putU8(p, PO_HEARTBEAT, v->heartbeat);
FieldCodec::putU8(p, PO_HOST_STATE, v->hostState);
FieldCodec::putU32(p, PO_RESV1, v->reserved1);
FieldCodec::putU32(p, PO_RESV2, v->reserved2);
return Frame::build(id(), p, checksum());
}
bool PmControlMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
PmControlValue* v = static_cast<PmControlValue*>(obj);
size_t o = FRAME_HEADER_LEN;
v->year = FieldCodec::getU16(frame, o + PO_YEAR);
v->month = FieldCodec::getU8(frame, o + PO_MONTH);
v->day = FieldCodec::getU8(frame, o + PO_DAY);
v->hour = FieldCodec::getU8(frame, o + PO_HOUR);
v->minute = FieldCodec::getU8(frame, o + PO_MINUTE);
v->second = FieldCodec::getU8(frame, o + PO_SECOND);
v->millisecond10 = FieldCodec::getU8(frame, o + PO_MS10);
v->mode = FieldCodec::getU8(frame, o + PO_MODE);
v->cmd = FieldCodec::getU8(frame, o + PO_CMD);
v->outputPower = FieldCodec::getU8(frame, o + PO_OUTPOWER);
v->pitch = static_cast<int16_t>(FieldCodec::getU16(frame, o + PO_PITCH));
v->roll = static_cast<int16_t>(FieldCodec::getU16(frame, o + PO_ROLL));
v->emergencyAllow= FieldCodec::getU8(frame, o + PO_EMER_ALLOW);
v->depth = FieldCodec::getU16(frame, o + PO_DEPTH);
v->supplyCmd = FieldCodec::getU8(frame, o + PO_SUPPLY_CMD);
v->reservedCmd5 = FieldCodec::getU8(frame, o + PO_RESV_CMD5);
v->reservedCmd6 = FieldCodec::getU8(frame, o + PO_RESV_CMD6);
v->insBatCmd = FieldCodec::getU8(frame, o + PO_INS_BAT_CMD);
v->dynBatCmd = FieldCodec::getU8(frame, o + PO_DYN_BAT_CMD);
v->dynBatPower = FieldCodec::getU16(frame, o + PO_DYN_BAT_PWR);
v->heartbeat = FieldCodec::getU8(frame, o + PO_HEARTBEAT);
v->hostState = FieldCodec::getU8(frame, o + PO_HOST_STATE);
v->reserved1 = FieldCodec::getU32(frame, o + PO_RESV1);
v->reserved2 = FieldCodec::getU32(frame, o + PO_RESV2);
return true;
}
//============================================================================
// 0x0002 参数设定指令域
// 数据域 18 字节,偏移 0~17
//============================================================================
namespace {
enum : size_t {
PP_INS_H1 = 0,
PP_INS_H2 = 1,
PP_INS_H3 = 2,
PP_INS_PWR = 3,
PP_INS_RESV = 4,
PP_DYN_H1 = 5,
PP_DYN_H2 = 6,
PP_DYN_H3 = 7,
PP_DYN_PWR = 8,
PP_DYN_RESV = 9,
PP_RESV1 = 10, // u32
PP_RESV2 = 14, // u32
};
}
std::vector<uint8_t> PmParamSetMessage::encode(const void* obj) const {
const PmParamSetValue* v = static_cast<const PmParamSetValue*>(obj);
std::vector<uint8_t> p(payloadLength(), 0);
FieldCodec::putU8(p, PP_INS_H1, v->insSocHold1);
FieldCodec::putU8(p, PP_INS_H2, v->insSocHold2);
FieldCodec::putU8(p, PP_INS_H3, v->insSocHold3);
FieldCodec::putU8(p, PP_INS_PWR, v->insOutputPower);
FieldCodec::putU8(p, PP_INS_RESV, v->insReserved);
FieldCodec::putU8(p, PP_DYN_H1, v->dynSocHold1);
FieldCodec::putU8(p, PP_DYN_H2, v->dynSocHold2);
FieldCodec::putU8(p, PP_DYN_H3, v->dynSocHold3);
FieldCodec::putU8(p, PP_DYN_PWR, v->dynOutputPower);
FieldCodec::putU8(p, PP_DYN_RESV, v->dynReserved);
FieldCodec::putU32(p, PP_RESV1, v->reserved1);
FieldCodec::putU32(p, PP_RESV2, v->reserved2);
return Frame::build(id(), p, checksum());
}
bool PmParamSetMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
PmParamSetValue* v = static_cast<PmParamSetValue*>(obj);
size_t o = FRAME_HEADER_LEN;
v->insSocHold1 = FieldCodec::getU8(frame, o + PP_INS_H1);
v->insSocHold2 = FieldCodec::getU8(frame, o + PP_INS_H2);
v->insSocHold3 = FieldCodec::getU8(frame, o + PP_INS_H3);
v->insOutputPower = FieldCodec::getU8(frame, o + PP_INS_PWR);
v->insReserved = FieldCodec::getU8(frame, o + PP_INS_RESV);
v->dynSocHold1 = FieldCodec::getU8(frame, o + PP_DYN_H1);
v->dynSocHold2 = FieldCodec::getU8(frame, o + PP_DYN_H2);
v->dynSocHold3 = FieldCodec::getU8(frame, o + PP_DYN_H3);
v->dynOutputPower = FieldCodec::getU8(frame, o + PP_DYN_PWR);
v->dynReserved = FieldCodec::getU8(frame, o + PP_DYN_RESV);
v->reserved1 = FieldCodec::getU32(frame, o + PP_RESV1);
v->reserved2 = FieldCodec::getU32(frame, o + PP_RESV2);
return true;
}
//============================================================================
// 0x0003 参数设定反馈域
// 数据域 2 字节
//============================================================================
std::vector<uint8_t> PmParamSetFbMessage::encode(const void* obj) const {
const PmParamSetFbValue* v = static_cast<const PmParamSetFbValue*>(obj);
std::vector<uint8_t> p(payloadLength(), 0);
FieldCodec::putU8(p, 0, v->flag);
FieldCodec::putU8(p, 1, v->failCode);
return Frame::build(id(), p, checksum());
}
bool PmParamSetFbMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
PmParamSetFbValue* v = static_cast<PmParamSetFbValue*>(obj);
size_t o = FRAME_HEADER_LEN;
v->flag = FieldCodec::getU8(frame, o + 0);
v->failCode = FieldCodec::getU8(frame, o + 1);
return true;
}
//============================================================================
// 0x0004 状态报文域
// 数据域 238 字节,偏移 0~237(对应文档字节 7~244)
//============================================================================
namespace {
enum : size_t {
PS_MODE = 0,
PS_STATUS = 1,
PS_FL1 = 2, // u16
PS_FL2 = 4,
PS_FL3 = 6,
PS_FL4 = 8,
PS_GEN_TIME = 10, // u16
PS_FAULT_LEVEL = 12,
PS_OUT_POWER_LIMIT = 13, // u16
PS_GEN_POWER = 15, // u16
PS_H2_CAP = 17,
PS_LO2_CAP = 18,
PS_FC1_MIN_V = 19,
PS_FC1_MIN_POS = 20,
PS_FC1_AVG_V = 21,
PS_FC2_MIN_V = 22,
PS_FC2_MIN_POS = 23,
PS_FC2_AVG_V = 24,
PS_PALLADIUM = 25, // u16
PS_BUFFER_PRES = 27,
PS_FLUE_TOTAL = 29,
PS_FLUE_PRES = 31,
PS_REACTOR_PRES = 33,
PS_EVALVE_OPEN = 35,
PS_MAIN_PIPE_PRES = 37,
PS_AUX_PIPE_PRES = 39,
PS_DCDC1_IN_V = 41,
PS_DCDC1_IN_I = 43,
PS_DCDC2_IN_V = 45,
PS_DCDC2_IN_I = 47,
PS_DCDC_OUT_V = 49,
PS_DCDC_OUT_I = 51,
PS_DCDC_CTRL_V = 53,
PS_DCDC_AUX_V = 54,
PS_METHANOL_TOTAL = 55, // u16
PS_METHANOL_FEED = 57,
PS_O2_WATER = 59,
PS_H2_WATER = 61,
PS_BALLAST_WATER = 63,
PS_EXH_IN_PRES = 65,
PS_EXH_OUT_PRES = 67,
PS_EXH_FREQ = 69,
PS_EXH_IN_TEMP = 71,
PS_EXH_OUT_TEMP = 73,
PS_EXH_WIN_PRES = 75,
PS_EXH_WOUT_PRES = 77,
PS_TANK_LO2_PRES = 79,
PS_TANK_CO2_PRES = 81,
PS_TANK_LO2_LEVEL = 83,
PS_ALLOY_H2_FLOW = 85,
PS_FC_H2_FLOW = 87,
PS_FC_O2_FLOW = 89,
PS_EMER_DEPTH = 91, // u16
PS_EMER_TIME = 93,
PS_CABIN_P1 = 95,
PS_CABIN_P2 = 97,
PS_CABIN_T1 = 99,
PS_CABIN_T2 = 101,
PS_CABIN_H1 = 103,
PS_CABIN_H2 = 105,
PS_H2_C1 = 107,
PS_H2_C2 = 109,
PS_H2_C3 = 111,
PS_O2_C1 = 113,
PS_O2_C2 = 115,
PS_CH3OH_C1 = 117,
PS_CH3OH_C2 = 119,
PS_FLAME1 = 121,
PS_FLAME2 = 122,
PS_RESV1 = 123, // u16
PS_RESV2 = 125, // u16
PS_EB1_VOLTAGE = 127, // u16
PS_EB1_CURRENT = 129,
PS_EB1_MAX_TEMP = 131,
PS_EB1_FAULT = 133,
PS_EB2_VOLTAGE = 135,
PS_EB2_CURRENT = 137,
PS_EB2_MAX_TEMP = 139,
PS_EB2_FAULT = 141,
PS_INS_CABIN_OX = 143, // u16
PS_INS_CABIN_TEMP = 145,
PS_INS_CABIN_HUM = 147,
PS_INS_CABIN_PRES = 149,
PS_DYN_CABIN_OX = 151,
PS_DYN_CABIN_TEMP = 153,
PS_DYN_CABIN_HUM = 155,
PS_DYN_CABIN_PRES = 157,
PS_RESV3 = 159, // u8
PS_DYN_ALARM = 160, // u8 * 6
PS_INS_ALARM = 166, // u8 * 6
PS_INS_RELAY1 = 172,
PS_INS_RELAY2 = 173,
PS_DYN_RELAY1 = 174,
PS_DYN_RELAY2 = 175,
PS_INS_MAX_PWR = 176, // u16
PS_DYN_MAX_PWR = 178, // u16
PS_INS_SOC = 180,
PS_DYN_SOC = 181,
PS_INS_TOTAL_ENERGY= 182, // u16
PS_DYN_TOTAL_ENERGY= 184, // u16
PS_INS_PWR_IN = 186, // u16
PS_DYN_PWR_IN = 188, // u16
PS_INS_CHARGE_ST = 190,
PS_DYN_CHARGE_ST = 191,
PS_INS_V_LINK = 192, // u16
PS_INS_V_PACK = 194, // u16
PS_INS_CURRENT = 196, // u16
PS_INS_RES_POS = 198, // u16
PS_INS_RES_NEG = 200, // u16
PS_DYN_V_LINK = 202, // u16
PS_DYN_V_PACK = 204, // u16
PS_DYN_CURRENT = 206, // u16
PS_DYN_RES_POS = 208, // u16
PS_DYN_RES_NEG = 210, // u16
PS_INS_EMERGENCY = 212,
PS_DYN_EMERGENCY = 213,
PS_INS_SOC_H1 = 214,
PS_INS_SOC_H2 = 215,
PS_INS_SOC_H3 = 216,
PS_INS_PWR_L1 = 217,
PS_INS_PWR_L2 = 218,
PS_DYN_SOC_H1 = 219,
PS_DYN_SOC_H2 = 220,
PS_DYN_SOC_H3 = 221,
PS_DYN_PWR_L1 = 222,
PS_DYN_PWR_L2 = 223,
PS_ONLINE_FLAG1 = 224, // u32
PS_ONLINE_FLAG2 = 228, // u32
PS_RESV4 = 232, // u32
PS_HEARTBEAT = 236,
PS_EMERGENCY_CMD = 237,
};
inline void getU8Arr(const std::vector<uint8_t>& f, size_t off, uint8_t* dst, size_t n) {
for (size_t i = 0; i < n; ++i) dst[i] = f[off + i];
}
inline void putU8Arr(std::vector<uint8_t>& p, size_t off, const uint8_t* src, size_t n) {
for (size_t i = 0; i < n; ++i) p[off + i] = src[i];
}
}
std::vector<uint8_t> PmStatusMessage::encode(const void* obj) const {
const PmStatusValue* v = static_cast<const PmStatusValue*>(obj);
std::vector<uint8_t> p(payloadLength(), 0);
FieldCodec::putU8(p, PS_MODE, v->fc_mode);
FieldCodec::putU8(p, PS_STATUS, v->fc_status);
FieldCodec::putU16(p, PS_FL1, v->fault_level_1);
FieldCodec::putU16(p, PS_FL2, v->fault_level_2);
FieldCodec::putU16(p, PS_FL3, v->fault_level_3);
FieldCodec::putU16(p, PS_FL4, v->fault_level_4);
FieldCodec::putU16(p, PS_GEN_TIME, v->total_generation_time);
FieldCodec::putU8(p, PS_FAULT_LEVEL, v->fc_fault_level);
FieldCodec::putU16(p, PS_OUT_POWER_LIMIT, v->output_power_limit);
FieldCodec::putU16(p, PS_GEN_POWER, v->generation_power);
FieldCodec::putU8(p, PS_H2_CAP, v->hydrogen_capacity);
FieldCodec::putU8(p, PS_LO2_CAP, v->liquid_oxygen_capacity);
FieldCodec::putU8(p, PS_FC1_MIN_V, v->fc1_min_cell_voltage);
FieldCodec::putU8(p, PS_FC1_MIN_POS, v->fc1_min_cell_pos);
FieldCodec::putU8(p, PS_FC1_AVG_V, v->fc1_avg_cell_voltage);
FieldCodec::putU8(p, PS_FC2_MIN_V, v->fc2_min_cell_voltage);
FieldCodec::putU8(p, PS_FC2_MIN_POS, v->fc2_min_cell_pos);
FieldCodec::putU8(p, PS_FC2_AVG_V, v->fc2_avg_cell_voltage);
FieldCodec::putU16(p, PS_PALLADIUM, v->palladium_temp);
FieldCodec::putU16(p, PS_BUFFER_PRES, v->buffer_tank_pressure);
FieldCodec::putU16(p, PS_FLUE_TOTAL, v->flue_total_emission);
FieldCodec::putU16(p, PS_FLUE_PRES, v->flue_pressure);
FieldCodec::putU16(p, PS_REACTOR_PRES, v->reactor_pressure);
FieldCodec::putU16(p, PS_EVALVE_OPEN, v->electric_valve_open);
FieldCodec::putU16(p, PS_MAIN_PIPE_PRES, v->main_pipe_pressure);
FieldCodec::putU16(p, PS_AUX_PIPE_PRES, v->aux_pipe_pressure);
FieldCodec::putU16(p, PS_DCDC1_IN_V, v->dcdc1_in_voltage);
FieldCodec::putU16(p, PS_DCDC1_IN_I, v->dcdc1_in_current);
FieldCodec::putU16(p, PS_DCDC2_IN_V, v->dcdc2_in_voltage);
FieldCodec::putU16(p, PS_DCDC2_IN_I, v->dcdc2_in_current);
FieldCodec::putU16(p, PS_DCDC_OUT_V, v->dcdc_out_voltage);
FieldCodec::putU16(p, PS_DCDC_OUT_I, v->dcdc_out_current);
FieldCodec::putU8(p, PS_DCDC_CTRL_V, v->dcdc_ctrl_voltage);
FieldCodec::putU8(p, PS_DCDC_AUX_V, v->dcdc_aux_voltage);
FieldCodec::putU16(p, PS_METHANOL_TOTAL, v->methanol_total_use);
FieldCodec::putU16(p, PS_METHANOL_FEED, v->methanol_feed);
FieldCodec::putU16(p, PS_O2_WATER, v->oxygen_side_water_level);
FieldCodec::putU16(p, PS_H2_WATER, v->hydrogen_side_water_level);
FieldCodec::putU16(p, PS_BALLAST_WATER, v->ballast_water_level);
FieldCodec::putU16(p, PS_EXH_IN_PRES, v->exhaust_inlet_pressure);
FieldCodec::putU16(p, PS_EXH_OUT_PRES, v->exhaust_outlet_pressure);
FieldCodec::putU16(p, PS_EXH_FREQ, v->exhaust_run_freq);
FieldCodec::putU16(p, PS_EXH_IN_TEMP, v->exhaust_inlet_temp);
FieldCodec::putU16(p, PS_EXH_OUT_TEMP, v->exhaust_outlet_temp);
FieldCodec::putU16(p, PS_EXH_WIN_PRES, v->exhaust_water_in_pressure);
FieldCodec::putU16(p, PS_EXH_WOUT_PRES, v->exhaust_water_out_pressure);
FieldCodec::putU16(p, PS_TANK_LO2_PRES, v->tank_lo2_pressure);
FieldCodec::putU16(p, PS_TANK_CO2_PRES, v->tank_co2_pressure);
FieldCodec::putU16(p, PS_TANK_LO2_LEVEL, v->tank_lo2_level);
FieldCodec::putU16(p, PS_ALLOY_H2_FLOW, v->alloy_h2_flow);
FieldCodec::putU16(p, PS_FC_H2_FLOW, v->fc_h2_flow);
FieldCodec::putU16(p, PS_FC_O2_FLOW, v->fc_o2_flow);
FieldCodec::putU16(p, PS_EMER_DEPTH, v->emergency_float_depth);
FieldCodec::putU16(p, PS_EMER_TIME, v->emergency_float_time);
FieldCodec::putU16(p, PS_CABIN_P1, v->cabin_pressure1);
FieldCodec::putU16(p, PS_CABIN_P2, v->cabin_pressure2);
FieldCodec::putU16(p, PS_CABIN_T1, v->cabin_temp1);
FieldCodec::putU16(p, PS_CABIN_T2, v->cabin_temp2);
FieldCodec::putU16(p, PS_CABIN_H1, v->cabin_humidity1);
FieldCodec::putU16(p, PS_CABIN_H2, v->cabin_humidity2);
FieldCodec::putU16(p, PS_H2_C1, v->h2_concentration1);
FieldCodec::putU16(p, PS_H2_C2, v->h2_concentration2);
FieldCodec::putU16(p, PS_H2_C3, v->h2_concentration3);
FieldCodec::putU16(p, PS_O2_C1, v->o2_concentration1);
FieldCodec::putU16(p, PS_O2_C2, v->o2_concentration2);
FieldCodec::putU16(p, PS_CH3OH_C1, v->ch3oh_concentration1);
FieldCodec::putU16(p, PS_CH3OH_C2, v->ch3oh_concentration2);
FieldCodec::putU8(p, PS_FLAME1, v->flame_detector1);
FieldCodec::putU8(p, PS_FLAME2, v->flame_detector2);
FieldCodec::putU16(p, PS_RESV1, v->pmReserved1);
FieldCodec::putU16(p, PS_RESV2, v->pmReserved2);
FieldCodec::putU16(p, PS_EB1_VOLTAGE, v->emergency_battery1_voltage);
FieldCodec::putU16(p, PS_EB1_CURRENT, v->emergency_battery1_current);
FieldCodec::putU16(p, PS_EB1_MAX_TEMP, v->emergency_battery1_max_temp);
FieldCodec::putU16(p, PS_EB1_FAULT, v->emergency_battery1_fault_word);
FieldCodec::putU16(p, PS_EB2_VOLTAGE, v->emergency_battery2_voltage);
FieldCodec::putU16(p, PS_EB2_CURRENT, v->emergency_battery2_current);
FieldCodec::putU16(p, PS_EB2_MAX_TEMP, v->emergency_battery2_max_temp);
FieldCodec::putU16(p, PS_EB2_FAULT, v->emergency_battery2_fault_word);
FieldCodec::putU16(p, PS_INS_CABIN_OX, v->ins_cabin_ox_concentration);
FieldCodec::putU16(p, PS_INS_CABIN_TEMP, v->ins_cabin_temperature);
FieldCodec::putU16(p, PS_INS_CABIN_HUM, v->ins_cabin_humidity);
FieldCodec::putU16(p, PS_INS_CABIN_PRES, v->ins_cabin_pressure);
FieldCodec::putU16(p, PS_DYN_CABIN_OX, v->dyn_cabin_ox_concentration);
FieldCodec::putU16(p, PS_DYN_CABIN_TEMP, v->dyn_cabin_temperature);
FieldCodec::putU16(p, PS_DYN_CABIN_HUM, v->dyn_cabin_humidity);
FieldCodec::putU16(p, PS_DYN_CABIN_PRES, v->dyn_cabin_pressure);
FieldCodec::putU8(p, PS_RESV3, v->pmReserved3);
putU8Arr(p, PS_DYN_ALARM, v->dyn_alarm_flag, 6);
putU8Arr(p, PS_INS_ALARM, v->ins_alarm_flag, 6);
FieldCodec::putU8(p, PS_INS_RELAY1, v->ins_relay_status1);
FieldCodec::putU8(p, PS_INS_RELAY2, v->ins_relay_status2);
FieldCodec::putU8(p, PS_DYN_RELAY1, v->dyn_relay_status1);
FieldCodec::putU8(p, PS_DYN_RELAY2, v->dyn_relay_status2);
FieldCodec::putU16(p, PS_INS_MAX_PWR, v->ins_max_discharge_power);
FieldCodec::putU16(p, PS_DYN_MAX_PWR, v->dyn_max_discharge_power);
FieldCodec::putU8(p, PS_INS_SOC, v->ins_soc);
FieldCodec::putU8(p, PS_DYN_SOC, v->dyn_soc);
FieldCodec::putU16(p, PS_INS_TOTAL_ENERGY, v->ins_total_energy);
FieldCodec::putU16(p, PS_DYN_TOTAL_ENERGY, v->dyn_total_energy);
FieldCodec::putU16(p, PS_INS_PWR_IN, v->ins_power_input);
FieldCodec::putU16(p, PS_DYN_PWR_IN, v->dyn_power_input);
FieldCodec::putU8(p, PS_INS_CHARGE_ST, v->ins_charge_status);
FieldCodec::putU8(p, PS_DYN_CHARGE_ST, v->dyn_charge_status);
FieldCodec::putU16(p, PS_INS_V_LINK, v->ins_voltage_link);
FieldCodec::putU16(p, PS_INS_V_PACK, v->ins_voltage_pack);
FieldCodec::putU16(p, PS_INS_CURRENT, v->ins_current);
FieldCodec::putU16(p, PS_INS_RES_POS, v->ins_resistance_pos);
FieldCodec::putU16(p, PS_INS_RES_NEG, v->ins_resistance_neg);
FieldCodec::putU16(p, PS_DYN_V_LINK, v->dyn_voltage_link);
FieldCodec::putU16(p, PS_DYN_V_PACK, v->dyn_voltage_pack);
FieldCodec::putU16(p, PS_DYN_CURRENT, v->dyn_current);
FieldCodec::putU16(p, PS_DYN_RES_POS, v->dyn_resistance_pos);
FieldCodec::putU16(p, PS_DYN_RES_NEG, v->dyn_resistance_neg);
FieldCodec::putU8(p, PS_INS_EMERGENCY, v->ins_emergency_status);
FieldCodec::putU8(p, PS_DYN_EMERGENCY, v->dyn_emergency_status);
FieldCodec::putU8(p, PS_INS_SOC_H1, v->ins_soc_threshold1);
FieldCodec::putU8(p, PS_INS_SOC_H2, v->ins_soc_threshold2);
FieldCodec::putU8(p, PS_INS_SOC_H3, v->ins_soc_threshold3);
FieldCodec::putU8(p, PS_INS_PWR_L1, v->ins_power_limit1);
FieldCodec::putU8(p, PS_INS_PWR_L2, v->ins_power_limit2);
FieldCodec::putU8(p, PS_DYN_SOC_H1, v->dyn_soc_threshold1);
FieldCodec::putU8(p, PS_DYN_SOC_H2, v->dyn_soc_threshold2);
FieldCodec::putU8(p, PS_DYN_SOC_H3, v->dyn_soc_threshold3);
FieldCodec::putU8(p, PS_DYN_PWR_L1, v->dyn_power_limit1);
FieldCodec::putU8(p, PS_DYN_PWR_L2, v->dyn_power_limit2);
FieldCodec::putU32(p, PS_ONLINE_FLAG1, v->device_online_flag1);
FieldCodec::putU32(p, PS_ONLINE_FLAG2, v->device_online_flag2);
FieldCodec::putU32(p, PS_RESV4, v->pmReserved4);
FieldCodec::putU8(p, PS_HEARTBEAT, v->heartbeat);
FieldCodec::putU8(p, PS_EMERGENCY_CMD, v->emergency_cmd);
return Frame::build(id(), p, checksum());
}
bool PmStatusMessage::decode(const std::vector<uint8_t>& frame, void* obj) const {
if (!validateFrame(frame, id(), checksum(), totalLength())) return false;
PmStatusValue* v = static_cast<PmStatusValue*>(obj);
size_t o = FRAME_HEADER_LEN;
v->fc_mode = FieldCodec::getU8(frame, o + PS_MODE);
v->fc_status = FieldCodec::getU8(frame, o + PS_STATUS);
v->fault_level_1 = FieldCodec::getU16(frame, o + PS_FL1);
v->fault_level_2 = FieldCodec::getU16(frame, o + PS_FL2);
v->fault_level_3 = FieldCodec::getU16(frame, o + PS_FL3);
v->fault_level_4 = FieldCodec::getU16(frame, o + PS_FL4);
v->total_generation_time = FieldCodec::getU16(frame, o + PS_GEN_TIME);
v->fc_fault_level = FieldCodec::getU8(frame, o + PS_FAULT_LEVEL);
v->output_power_limit = FieldCodec::getU16(frame, o + PS_OUT_POWER_LIMIT);
v->generation_power = FieldCodec::getU16(frame, o + PS_GEN_POWER);
v->hydrogen_capacity = FieldCodec::getU8(frame, o + PS_H2_CAP);
v->liquid_oxygen_capacity = FieldCodec::getU8(frame, o + PS_LO2_CAP);
v->fc1_min_cell_voltage = FieldCodec::getU8(frame, o + PS_FC1_MIN_V);
v->fc1_min_cell_pos = FieldCodec::getU8(frame, o + PS_FC1_MIN_POS);
v->fc1_avg_cell_voltage = FieldCodec::getU8(frame, o + PS_FC1_AVG_V);
v->fc2_min_cell_voltage = FieldCodec::getU8(frame, o + PS_FC2_MIN_V);
v->fc2_min_cell_pos = FieldCodec::getU8(frame, o + PS_FC2_MIN_POS);
v->fc2_avg_cell_voltage = FieldCodec::getU8(frame, o + PS_FC2_AVG_V);
v->palladium_temp = FieldCodec::getU16(frame, o + PS_PALLADIUM);
v->buffer_tank_pressure = FieldCodec::getU16(frame, o + PS_BUFFER_PRES);
v->flue_total_emission = FieldCodec::getU16(frame, o + PS_FLUE_TOTAL);
v->flue_pressure = FieldCodec::getU16(frame, o + PS_FLUE_PRES);
v->reactor_pressure = FieldCodec::getU16(frame, o + PS_REACTOR_PRES);
v->electric_valve_open = FieldCodec::getU16(frame, o + PS_EVALVE_OPEN);
v->main_pipe_pressure = FieldCodec::getU16(frame, o + PS_MAIN_PIPE_PRES);
v->aux_pipe_pressure = FieldCodec::getU16(frame, o + PS_AUX_PIPE_PRES);
v->dcdc1_in_voltage = FieldCodec::getU16(frame, o + PS_DCDC1_IN_V);
v->dcdc1_in_current = FieldCodec::getU16(frame, o + PS_DCDC1_IN_I);
v->dcdc2_in_voltage = FieldCodec::getU16(frame, o + PS_DCDC2_IN_V);
v->dcdc2_in_current = FieldCodec::getU16(frame, o + PS_DCDC2_IN_I);
v->dcdc_out_voltage = FieldCodec::getU16(frame, o + PS_DCDC_OUT_V);
v->dcdc_out_current = FieldCodec::getU16(frame, o + PS_DCDC_OUT_I);
v->dcdc_ctrl_voltage = FieldCodec::getU8(frame, o + PS_DCDC_CTRL_V);
v->dcdc_aux_voltage = FieldCodec::getU8(frame, o + PS_DCDC_AUX_V);
v->methanol_total_use = FieldCodec::getU16(frame, o + PS_METHANOL_TOTAL);
v->methanol_feed = FieldCodec::getU16(frame, o + PS_METHANOL_FEED);
v->oxygen_side_water_level = FieldCodec::getU16(frame, o + PS_O2_WATER);
v->hydrogen_side_water_level = FieldCodec::getU16(frame, o + PS_H2_WATER);
v->ballast_water_level = FieldCodec::getU16(frame, o + PS_BALLAST_WATER);
v->exhaust_inlet_pressure = FieldCodec::getU16(frame, o + PS_EXH_IN_PRES);
v->exhaust_outlet_pressure = FieldCodec::getU16(frame, o + PS_EXH_OUT_PRES);
v->exhaust_run_freq = FieldCodec::getU16(frame, o + PS_EXH_FREQ);
v->exhaust_inlet_temp = FieldCodec::getU16(frame, o + PS_EXH_IN_TEMP);
v->exhaust_outlet_temp = FieldCodec::getU16(frame, o + PS_EXH_OUT_TEMP);
v->exhaust_water_in_pressure = FieldCodec::getU16(frame, o + PS_EXH_WIN_PRES);
v->exhaust_water_out_pressure = FieldCodec::getU16(frame, o + PS_EXH_WOUT_PRES);
v->tank_lo2_pressure = FieldCodec::getU16(frame, o + PS_TANK_LO2_PRES);
v->tank_co2_pressure = FieldCodec::getU16(frame, o + PS_TANK_CO2_PRES);
v->tank_lo2_level = FieldCodec::getU16(frame, o + PS_TANK_LO2_LEVEL);
v->alloy_h2_flow = FieldCodec::getU16(frame, o + PS_ALLOY_H2_FLOW);
v->fc_h2_flow = FieldCodec::getU16(frame, o + PS_FC_H2_FLOW);
v->fc_o2_flow = FieldCodec::getU16(frame, o + PS_FC_O2_FLOW);
v->emergency_float_depth = FieldCodec::getU16(frame, o + PS_EMER_DEPTH);
v->emergency_float_time = FieldCodec::getU16(frame, o + PS_EMER_TIME);
v->cabin_pressure1 = FieldCodec::getU16(frame, o + PS_CABIN_P1);
v->cabin_pressure2 = FieldCodec::getU16(frame, o + PS_CABIN_P2);
v->cabin_temp1 = FieldCodec::getU16(frame, o + PS_CABIN_T1);
v->cabin_temp2 = FieldCodec::getU16(frame, o + PS_CABIN_T2);
v->cabin_humidity1 = FieldCodec::getU16(frame, o + PS_CABIN_H1);
v->cabin_humidity2 = FieldCodec::getU16(frame, o + PS_CABIN_H2);
v->h2_concentration1 = FieldCodec::getU16(frame, o + PS_H2_C1);
v->h2_concentration2 = FieldCodec::getU16(frame, o + PS_H2_C2);
v->h2_concentration3 = FieldCodec::getU16(frame, o + PS_H2_C3);
v->o2_concentration1 = FieldCodec::getU16(frame, o + PS_O2_C1);
v->o2_concentration2 = FieldCodec::getU16(frame, o + PS_O2_C2);
v->ch3oh_concentration1 = FieldCodec::getU16(frame, o + PS_CH3OH_C1);
v->ch3oh_concentration2 = FieldCodec::getU16(frame, o + PS_CH3OH_C2);
v->flame_detector1 = FieldCodec::getU8(frame, o + PS_FLAME1);
v->flame_detector2 = FieldCodec::getU8(frame, o + PS_FLAME2);
v->pmReserved1 = FieldCodec::getU16(frame, o + PS_RESV1);
v->pmReserved2 = FieldCodec::getU16(frame, o + PS_RESV2);
v->emergency_battery1_voltage = FieldCodec::getU16(frame, o + PS_EB1_VOLTAGE);
v->emergency_battery1_current = FieldCodec::getU16(frame, o + PS_EB1_CURRENT);
v->emergency_battery1_max_temp = FieldCodec::getU16(frame, o + PS_EB1_MAX_TEMP);
v->emergency_battery1_fault_word = FieldCodec::getU16(frame, o + PS_EB1_FAULT);
v->emergency_battery2_voltage = FieldCodec::getU16(frame, o + PS_EB2_VOLTAGE);
v->emergency_battery2_current = FieldCodec::getU16(frame, o + PS_EB2_CURRENT);
v->emergency_battery2_max_temp = FieldCodec::getU16(frame, o + PS_EB2_MAX_TEMP);
v->emergency_battery2_fault_word = FieldCodec::getU16(frame, o + PS_EB2_FAULT);
v->ins_cabin_ox_concentration = FieldCodec::getU16(frame, o + PS_INS_CABIN_OX);
v->ins_cabin_temperature = FieldCodec::getU16(frame, o + PS_INS_CABIN_TEMP);
v->ins_cabin_humidity = FieldCodec::getU16(frame, o + PS_INS_CABIN_HUM);
v->ins_cabin_pressure = FieldCodec::getU16(frame, o + PS_INS_CABIN_PRES);
v->dyn_cabin_ox_concentration = FieldCodec::getU16(frame, o + PS_DYN_CABIN_OX);
v->dyn_cabin_temperature = FieldCodec::getU16(frame, o + PS_DYN_CABIN_TEMP);
v->dyn_cabin_humidity = FieldCodec::getU16(frame, o + PS_DYN_CABIN_HUM);
v->dyn_cabin_pressure = FieldCodec::getU16(frame, o + PS_DYN_CABIN_PRES);
v->pmReserved3 = FieldCodec::getU8(frame, o + PS_RESV3);
getU8Arr(frame, o + PS_DYN_ALARM, v->dyn_alarm_flag, 6);
getU8Arr(frame, o + PS_INS_ALARM, v->ins_alarm_flag, 6);
v->ins_relay_status1 = FieldCodec::getU8(frame, o + PS_INS_RELAY1);
v->ins_relay_status2 = FieldCodec::getU8(frame, o + PS_INS_RELAY2);
v->dyn_relay_status1 = FieldCodec::getU8(frame, o + PS_DYN_RELAY1);
v->dyn_relay_status2 = FieldCodec::getU8(frame, o + PS_DYN_RELAY2);
v->ins_max_discharge_power = FieldCodec::getU16(frame, o + PS_INS_MAX_PWR);
v->dyn_max_discharge_power = FieldCodec::getU16(frame, o + PS_DYN_MAX_PWR);
v->ins_soc = FieldCodec::getU8(frame, o + PS_INS_SOC);
v->dyn_soc = FieldCodec::getU8(frame, o + PS_DYN_SOC);
v->ins_total_energy = FieldCodec::getU16(frame, o + PS_INS_TOTAL_ENERGY);
v->dyn_total_energy = FieldCodec::getU16(frame, o + PS_DYN_TOTAL_ENERGY);
v->ins_power_input = FieldCodec::getU16(frame, o + PS_INS_PWR_IN);
v->dyn_power_input = FieldCodec::getU16(frame, o + PS_DYN_PWR_IN);
v->ins_charge_status = FieldCodec::getU8(frame, o + PS_INS_CHARGE_ST);
v->dyn_charge_status = FieldCodec::getU8(frame, o + PS_DYN_CHARGE_ST);
v->ins_voltage_link = FieldCodec::getU16(frame, o + PS_INS_V_LINK);
v->ins_voltage_pack = FieldCodec::getU16(frame, o + PS_INS_V_PACK);
v->ins_current = FieldCodec::getU16(frame, o + PS_INS_CURRENT);
v->ins_resistance_pos = FieldCodec::getU16(frame, o + PS_INS_RES_POS);
v->ins_resistance_neg = FieldCodec::getU16(frame, o + PS_INS_RES_NEG);
v->dyn_voltage_link = FieldCodec::getU16(frame, o + PS_DYN_V_LINK);
v->dyn_voltage_pack = FieldCodec::getU16(frame, o + PS_DYN_V_PACK);
v->dyn_current = FieldCodec::getU16(frame, o + PS_DYN_CURRENT);
v->dyn_resistance_pos = FieldCodec::getU16(frame, o + PS_DYN_RES_POS);
v->dyn_resistance_neg = FieldCodec::getU16(frame, o + PS_DYN_RES_NEG);
v->ins_emergency_status = FieldCodec::getU8(frame, o + PS_INS_EMERGENCY);
v->dyn_emergency_status = FieldCodec::getU8(frame, o + PS_DYN_EMERGENCY);
v->ins_soc_threshold1 = FieldCodec::getU8(frame, o + PS_INS_SOC_H1);
v->ins_soc_threshold2 = FieldCodec::getU8(frame, o + PS_INS_SOC_H2);
v->ins_soc_threshold3 = FieldCodec::getU8(frame, o + PS_INS_SOC_H3);
v->ins_power_limit1 = FieldCodec::getU8(frame, o + PS_INS_PWR_L1);
v->ins_power_limit2 = FieldCodec::getU8(frame, o + PS_INS_PWR_L2);
v->dyn_soc_threshold1 = FieldCodec::getU8(frame, o + PS_DYN_SOC_H1);
v->dyn_soc_threshold2 = FieldCodec::getU8(frame, o + PS_DYN_SOC_H2);
v->dyn_soc_threshold3 = FieldCodec::getU8(frame, o + PS_DYN_SOC_H3);
v->dyn_power_limit1 = FieldCodec::getU8(frame, o + PS_DYN_PWR_L1);
v->dyn_power_limit2 = FieldCodec::getU8(frame, o + PS_DYN_PWR_L2);
v->device_online_flag1 = FieldCodec::getU32(frame, o + PS_ONLINE_FLAG1);
v->device_online_flag2 = FieldCodec::getU32(frame, o + PS_ONLINE_FLAG2);
v->pmReserved4 = FieldCodec::getU32(frame, o + PS_RESV4);
v->heartbeat = FieldCodec::getU8(frame, o + PS_HEARTBEAT);
v->emergency_cmd = FieldCodec::getU8(frame, o + PS_EMERGENCY_CMD);
return true;
}
} // namespace ccu
-317
View File
@@ -1,317 +0,0 @@
#ifndef PCCU_PM_PROTOCOL_H
#define PCCU_PM_PROTOCOL_H
#include <cstdint>
#include <vector>
#include "Message.h"
namespace ccu {
//============================================================================
// PM 协议(复合管控器 <-> 控制主机 pPowerManger)
// 依据:docs/控制主机与复合管控器通信协议 - 0824.xlsx
//
// 帧格式统一:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + uint32 校验和
// 校验和为从域起始符到校验和之前所有数据的字节和。
//
// 四条消息(均带 uint32 校验和):
// 0x0001 操控指令域 (控制主机->复合管控器)payload 35B,总长 45B
// 0x0002 参数设定指令域 (控制主机->复合管控器)payload 18B,总长 28B
// 0x0003 参数设定反馈域 (复合管控器->控制主机)payload 2B,总长 12B
// 0x0004 状态报文域 (复合管控器->控制主机)payload 238B,总长 248B
//============================================================================
//--------------------------------------------------------------------------
// 0x0001 操控指令域
// payload 35 字节,对应文档字节 7~41
//--------------------------------------------------------------------------
struct PmControlValue {
// 系统时间
uint16_t year = 0;
uint8_t month = 0;
uint8_t day = 0;
uint8_t hour = 0;
uint8_t minute = 0;
uint8_t second = 0;
uint8_t millisecond10 = 0; // 10毫秒
// 指令
uint8_t mode = 0; // 模式设定 00~03
uint8_t cmd = 0; // 操控指令 00~0B
uint8_t outputPower = 0; // 输出功率指令 0~250,0.1kW
int16_t pitch = 0; // 纵倾姿态数据1 int16,分辨率0.1°(字节18,19)
int16_t roll = 0; // 横倾姿态数据1 int16,分辨率0.1°(字节20,21)
uint8_t emergencyAllow = 0; // 应急允许
uint16_t depth = 0; // 潜深深度 m
uint8_t supplyCmd = 0; // 补给/排放指令
uint8_t reservedCmd5 = 0; // 预留指令5
uint8_t reservedCmd6 = 0; // 预留指令6
uint8_t insBatCmd = 0; // 仪表锂电池启停指令 00/10/20
uint8_t dynBatCmd = 0; // 动力锂电池启停指令 00/10/20
uint16_t dynBatPower = 0; // 动力锂电池功率配置值 kW
uint8_t heartbeat = 0; // 通信心跳,每次+1
uint8_t hostState = 0; // 主机状态 运行AAH/关机FFH/故障其他
uint32_t reserved1 = 0; // 预留
uint32_t reserved2 = 0; // 预留
};
//--------------------------------------------------------------------------
// 0x0002 参数设定指令域
// payload 18 字节,对应文档字节 7~24
//--------------------------------------------------------------------------
struct PmParamSetValue {
uint8_t insSocHold1 = 0; // 仪表电池充电一级功率SOC门限
uint8_t insSocHold2 = 0; // 仪表电池充电二级功率SOC门限
uint8_t insSocHold3 = 0; // 仪表电池充电三级功率SOC门限
uint8_t insOutputPower = 0; // 仪表锂电池输出功率 %
uint8_t insReserved = 0; // 预留
uint8_t dynSocHold1 = 0; // 动力电池充电一级功率SOC门限
uint8_t dynSocHold2 = 0; // 动力电池充电二级功率SOC门限
uint8_t dynSocHold3 = 0; // 动力电池充电三级功率SOC门限
uint8_t dynOutputPower = 0; // 动力电池输出功率 %
uint8_t dynReserved = 0; // 预留
uint32_t reserved1 = 0; // 预留
uint32_t reserved2 = 0; // 预留
};
//--------------------------------------------------------------------------
// 0x0003 参数设定反馈域
// payload 2 字节,对应文档字节 7~8
//--------------------------------------------------------------------------
struct PmParamSetFbValue {
uint8_t flag = 0; // 设定标志:00H无效 / 10H成功 / 20H失败
uint8_t failCode = 0; // 设定失败原因
};
//--------------------------------------------------------------------------
// 0x0004 状态报文域
// payload 238 字节,对应文档字节 7~244
//--------------------------------------------------------------------------
struct PmStatusValue {
uint8_t fc_mode = 0; // 燃料电池系统运行模式
uint8_t fc_status = 0; // 燃料电池系统运行状态
uint16_t fault_level_1 = 0; // 一级故障码
uint16_t fault_level_2 = 0; // 二级故障码
uint16_t fault_level_3 = 0; // 三级故障码
uint16_t fault_level_4 = 0; // 四级故障码
uint16_t total_generation_time = 0; // 累积发电时间 0.1h
uint8_t fc_fault_level = 0; // 系统故障等级 00~04
uint16_t output_power_limit = 0; // 系统输出功率限定功率 W
uint16_t generation_power = 0; // 系统发电功率 0.01kW
uint8_t hydrogen_capacity = 0; // 储氢剩余容量 %
uint8_t liquid_oxygen_capacity = 0; // 液氧剩余容量 %
uint8_t fc1_min_cell_voltage = 0; // I#FC最低单片电压 10mV
uint8_t fc1_min_cell_pos = 0; // I#FC最低单片电压位置
uint8_t fc1_avg_cell_voltage = 0; // I#FC平均单片电压
uint8_t fc2_min_cell_voltage = 0; // 2#FC最低单片电压
uint8_t fc2_min_cell_pos = 0; // 2#FC最低单片电压位置
uint8_t fc2_avg_cell_voltage = 0; // 2#FC平均单片电压
uint16_t palladium_temp = 0; // 甲醇制氢装置钯膜最高温度 0.1℃
uint16_t buffer_tank_pressure = 0; // 缓冲罐压力 0.1kPa
uint16_t flue_total_emission = 0; // 烟气累计排放量 0.1kg
uint16_t flue_pressure = 0; // 烟气压力 0.001MPa
uint16_t reactor_pressure = 0; // 反应器压力 0.001MPa
uint16_t electric_valve_open = 0; // 电动阀开度 0.1%
uint16_t main_pipe_pressure = 0; // 主水路盘管侧压力 0.01kPa
uint16_t aux_pipe_pressure = 0; // 辅水路盘管侧压力 0.01kPa
uint16_t dcdc1_in_voltage = 0; // DC/DC通道1输入电压 0.1V
uint16_t dcdc1_in_current = 0; // DC/DC通道1输入电流 0.1A
uint16_t dcdc2_in_voltage = 0; // DC/DC通道2输入电压 0.1V
uint16_t dcdc2_in_current = 0; // DC/DC通道2输入电流 0.1A
uint16_t dcdc_out_voltage = 0; // DC/DC输出电压 0.1V
uint16_t dcdc_out_current = 0; // DC/DC输出电流 0.1A
uint8_t dcdc_ctrl_voltage = 0; // DC/DC控制电源电压 0.25V
uint8_t dcdc_aux_voltage = 0; // DC/DC辅电输出电压 0.125V
uint16_t methanol_total_use = 0; // 甲醇累计使用量 0.1kg
uint16_t methanol_feed = 0; // 甲醇溶液进料量 1mL/min
uint16_t oxygen_side_water_level = 0; // 氧侧生成水箱液位 0.01mm
uint16_t hydrogen_side_water_level = 0; // 氢侧生成水箱液位
uint16_t ballast_water_level = 0; // 配重水箱液位
uint16_t exhaust_inlet_pressure = 0; // 尾气装置进气压力 0.01MPa
uint16_t exhaust_outlet_pressure = 0; // 尾气装置排气压力 0.01MPa
uint16_t exhaust_run_freq = 0; // 尾气装置运行频率 0.01Hz
uint16_t exhaust_inlet_temp = 0; // 尾气装置进气温度 0.01℃
uint16_t exhaust_outlet_temp = 0; // 尾气装置排气温度 0.01℃
uint16_t exhaust_water_in_pressure = 0; // 尾气装置进水压力 0.01kPa
uint16_t exhaust_water_out_pressure = 0; // 尾气装置排水压力 0.01kPa
uint16_t tank_lo2_pressure = 0; // 一体化罐装置液氧罐压力 0.01MPa
uint16_t tank_co2_pressure = 0; // 一体化罐装置二氧化碳压力 0.01MPa
uint16_t tank_lo2_level = 0; // 一体化罐装置液氧罐液位 0.01mm
uint16_t alloy_h2_flow = 0; // 合金供氢流量 0.01L/min
uint16_t fc_h2_flow = 0; // FC供氢流量 0.01L/min
uint16_t fc_o2_flow = 0; // FC供氧流量 0.01L/min
uint16_t emergency_float_depth = 0; // 应急上浮深度 1m
uint16_t emergency_float_time = 0; // 应急上浮时间 1min
uint16_t cabin_pressure1 = 0; // 舱室压力1 0.01kPa
uint16_t cabin_pressure2 = 0; // 舱室压力2
uint16_t cabin_temp1 = 0; // 舱室温度1 0.01℃
uint16_t cabin_temp2 = 0; // 舱室温度2
uint16_t cabin_humidity1 = 0; // 舱室湿度1 0.01%RH
uint16_t cabin_humidity2 = 0; // 舱室湿度2
uint16_t h2_concentration1 = 0; // 舱室H2浓度1 0.01%LEL
uint16_t h2_concentration2 = 0; // 舱室H2浓度2
uint16_t h2_concentration3 = 0; // 舱室H2浓度3
uint16_t o2_concentration1 = 0; // 舱室O2浓度1 0.01%Vol
uint16_t o2_concentration2 = 0; // 舱室O2浓度2
uint16_t ch3oh_concentration1 = 0; // 舱室甲醇浓度1 0.01%LEL
uint16_t ch3oh_concentration2 = 0; // 舱室甲醇浓度2
uint8_t flame_detector1 = 0; // 火焰探测器1状态
uint8_t flame_detector2 = 0; // 火焰探测器2状态
uint16_t pmReserved1 = 0; // 预留
uint16_t pmReserved2 = 0; // 预留
uint16_t emergency_battery1_voltage = 0; // 应急电池1总电压
uint16_t emergency_battery1_current = 0; // 应急电池1总电流
uint16_t emergency_battery1_max_temp = 0; // 应急电池1最高温度
uint16_t emergency_battery1_fault_word = 0;// 应急电池1故障字
uint16_t emergency_battery2_voltage = 0; // 应急电池2总电压
uint16_t emergency_battery2_current = 0; // 应急电池2总电流
uint16_t emergency_battery2_max_temp = 0; // 应急电池2最高温度
uint16_t emergency_battery2_fault_word = 0;// 应急电池2故障字
uint16_t ins_cabin_ox_concentration = 0; // 仪表舱内氧气浓度 %
uint16_t ins_cabin_temperature = 0; // 仪表舱内温度 ℃
uint16_t ins_cabin_humidity = 0; // 仪表舱内湿度 %RH
uint16_t ins_cabin_pressure = 0; // 仪表舱内大气压 kPa
uint16_t dyn_cabin_ox_concentration = 0; // 动力舱内氧气浓度 %
uint16_t dyn_cabin_temperature = 0; // 动力舱内温度 ℃
uint16_t dyn_cabin_humidity = 0; // 动力舱内湿度 %RH
uint16_t dyn_cabin_pressure = 0; // 动力舱内大气压 kPa
uint8_t pmReserved3 = 0; // 预留
uint8_t dyn_alarm_flag[6] = {0}; // 动力锂电池报警标识字1~6
uint8_t ins_alarm_flag[6] = {0}; // 仪表锂电池报警标识字1~6
uint8_t ins_relay_status1 = 0; // 仪表电池正极+充电继电器状态
uint8_t ins_relay_status2 = 0; // 仪表电池预充+负极继电器状态
uint8_t dyn_relay_status1 = 0; // 动力电池正极+充电继电器状态
uint8_t dyn_relay_status2 = 0; // 动力电池预充+负极继电器状态
uint16_t ins_max_discharge_power = 0; // 仪表电池允许最高放电功率 0.05kW
uint16_t dyn_max_discharge_power = 0; // 动力电池允许最高放电功率 0.05kW
uint8_t ins_soc = 0; // 仪表锂电池SOC %
uint8_t dyn_soc = 0; // 动力锂电池SOC %
uint16_t ins_total_energy = 0; // 仪表锂电池总能量 0.1kWh
uint16_t dyn_total_energy = 0; // 动力锂电池总电量 0.1kWh
uint16_t ins_power_input = 0; // 仪表电池当前接入功率 kW
uint16_t dyn_power_input = 0; // 动力电池当前接入功率 kW
uint8_t ins_charge_status = 0; // 仪表电池充电状态
uint8_t dyn_charge_status = 0; // 动力电池充电状态
uint16_t ins_voltage_link = 0; // 仪表电池电压LINK端 0.01V
uint16_t ins_voltage_pack = 0; // 仪表电池电压PACK端 0.01V
uint16_t ins_current = 0; // 仪表电池放电电流 0.05A
uint16_t ins_resistance_pos = 0; // 仪表电池正端绝缘电阻 kΩ
uint16_t ins_resistance_neg = 0; // 仪表电池负端绝缘电阻 kΩ
uint16_t dyn_voltage_link = 0; // 动力电池电压LINK端 0.01V
uint16_t dyn_voltage_pack = 0; // 动力电池电压PACK端 0.01V
uint16_t dyn_current = 0; // 动力电池放电电流 0.05A
uint16_t dyn_resistance_pos = 0; // 动力电池正端绝缘电阻 kΩ
uint16_t dyn_resistance_neg = 0; // 动力电池负端绝缘电阻 kΩ
uint8_t ins_emergency_status = 0; // 仪表电池紧急状态
uint8_t dyn_emergency_status = 0; // 动力电池紧急状态
uint8_t ins_soc_threshold1 = 0; // 仪表电池充电一级功率SOC门限当前值
uint8_t ins_soc_threshold2 = 0; // 仪表电池充电二级
uint8_t ins_soc_threshold3 = 0; // 仪表电池充电三级
uint8_t ins_power_limit1 = 0; // 仪表通路FC输出功率一级限制当前值
uint8_t ins_power_limit2 = 0; // 仪表通路FC输出功率二级限制当前值
uint8_t dyn_soc_threshold1 = 0; // 动力电池充电一级功率SOC门限当前值
uint8_t dyn_soc_threshold2 = 0; // 动力电池充电二级
uint8_t dyn_soc_threshold3 = 0; // 动力电池充电三级
uint8_t dyn_power_limit1 = 0; // 动力通路FC输出功率一级限制当前值
uint8_t dyn_power_limit2 = 0; // 动力通路FC输出功率二级限制当前值
uint32_t device_online_flag1 = 0; // 设备在线状态标志字1
uint32_t device_online_flag2 = 0; // 设备在线状态标志字2
uint32_t pmReserved4 = 0; // 预留
uint8_t heartbeat = 0; // 通信心跳,每次+1
uint8_t emergency_cmd = 0; // 应急指令
};
//--------------------------------------------------------------------------
// 消息类
//--------------------------------------------------------------------------
class PmControlMessage : public Message {
public:
uint16_t id() const override { return 0x0001; }
const char* name() const override { return "pm_control"; }
const ChecksumPolicy& checksum() const override { return m_checksum; }
size_t payloadLength() const override { return 35; }
std::vector<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const PmControlValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, PmControlValue& v) const {
return decode(frame, static_cast<void*>(&v));
}
private:
ChecksumPolicy m_checksum{ChecksumType::Sum32};
};
class PmParamSetMessage : public Message {
public:
uint16_t id() const override { return 0x0002; }
const char* name() const override { return "pm_param_set"; }
const ChecksumPolicy& checksum() const override { return m_checksum; }
size_t payloadLength() const override { return 18; }
std::vector<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const PmParamSetValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, PmParamSetValue& v) const {
return decode(frame, static_cast<void*>(&v));
}
private:
ChecksumPolicy m_checksum{ChecksumType::Sum32};
};
class PmParamSetFbMessage : public Message {
public:
uint16_t id() const override { return 0x0003; }
const char* name() const override { return "pm_param_set_fb"; }
const ChecksumPolicy& checksum() const override { return m_checksum; }
size_t payloadLength() const override { return 2; }
std::vector<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const PmParamSetFbValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, PmParamSetFbValue& v) const {
return decode(frame, static_cast<void*>(&v));
}
private:
ChecksumPolicy m_checksum{ChecksumType::Sum32};
};
class PmStatusMessage : public Message {
public:
uint16_t id() const override { return 0x0004; }
const char* name() const override { return "pm_status"; }
const ChecksumPolicy& checksum() const override { return m_checksum; }
size_t payloadLength() const override { return 238; }
std::vector<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
std::vector<uint8_t> encode(const PmStatusValue& v) const { return encode(&v); }
bool decode(const std::vector<uint8_t>& frame, PmStatusValue& v) const {
return decode(frame, static_cast<void*>(&v));
}
private:
ChecksumPolicy m_checksum{ChecksumType::Sum32};
};
// 注册 PM 消息到给定注册表
void registerPmMessages(class MessageRegistry& reg);
} // namespace ccu
#endif // PCCU_PM_PROTOCOL_H
-298
View File
@@ -1,298 +0,0 @@
#include "DbStore.h"
#include "../protocol/Message.h"
#include "sqlite3.h"
#include <sstream>
#include <iomanip>
#include <cstdio>
#include <ctime>
namespace ccu {
namespace {
std::string toHex(const std::vector<uint8_t>& data) {
static const char* hex = "0123456789ABCDEF";
std::string s;
s.reserve(data.size() * 2);
for (uint8_t b : data) {
s += hex[(b >> 4) & 0xF];
s += hex[b & 0xF];
}
return s;
}
} // namespace
DbStore::DbStore(const std::string& dbPath) : m_dbPath(dbPath) {}
DbStore::~DbStore() {
close();
}
bool DbStore::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 DbStore::open() {
if (m_db) return true;
if (sqlite3_open(m_dbPath.c_str(), &m_db) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
sqlite3_close(m_db);
m_db = nullptr;
return false;
}
sqlite3_busy_timeout(m_db, 2000);
exec("PRAGMA journal_mode=WAL;");
exec("PRAGMA synchronous=NORMAL;");
// comm_log:收发全部原始帧
if (!exec(
"CREATE TABLE IF NOT EXISTS comm_log ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" time REAL NOT NULL,"
" link TEXT NOT NULL,"
" direction INTEGER NOT NULL," // 0=收 1=发
" msg_id INTEGER NOT NULL,"
" msg_name TEXT NOT NULL,"
" length INTEGER NOT NULL,"
" checksum_ok INTEGER NOT NULL,"
" hex TEXT NOT NULL"
");")) return false;
exec("CREATE INDEX IF NOT EXISTS idx_comm_log_time ON comm_log(time);");
// bcu_node:锂电池 BCU 节点解析数据(每收到一帧 BMS 报文落一行,
// 记录该节点合并后的最新状态;BMS/CAN 协议字段固定故用结构化表)
if (!exec(
"CREATE TABLE IF NOT EXISTS bcu_node ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" time REAL NOT NULL,"
" addr INTEGER NOT NULL," // BCU 节点地址 1~54
" func INTEGER NOT NULL," // 触发本行的报文功能码
" total_voltage REAL," // 累加电压 V
" current REAL," // 回路电流 A(放电为正)
" soc REAL," // SOC %
" alarm_code INTEGER," // 告警码(原始值)
" self_check INTEGER," // 自检状态(原始值)
" max_cell_voltage REAL," // 最高单体电压 V
" max_cell_voltage_no INTEGER,"
" min_cell_voltage REAL," // 最低单体电压 V
" min_cell_voltage_no INTEGER,"
" avg_cell_voltage REAL," // 单体平均电压 V
" max_cell_temp REAL," // 最高单体温度 ℃
" max_cell_temp_no INTEGER,"
" min_cell_temp REAL," // 最低单体温度 ℃
" min_cell_temp_no INTEGER,"
" avg_cell_temp REAL," // 单体平均温度 ℃
" pos_relay INTEGER," // 正极继电器 1闭合/0断开
" neg_relay INTEGER," // 负极继电器 1闭合/0断开
" pos_insulation REAL," // 正极绝缘阻抗 kΩ
" neg_insulation REAL," // 负极绝缘阻抗 kΩ
" port_voltage REAL," // 正负极端口电压 V
" pos_relay_outer_voltage REAL," // 正极继电器外侧电压 V
" alarm_bits TEXT," // 告警位 byte0~5 十六进制
" alarm_extra_byte6 INTEGER,"
" alarm_extra_byte7 INTEGER,"
" rx_mask INTEGER," // 收到过的功能码位图
" last_rx_time REAL" // 节点最近收帧时刻(MOOSTime)
");")) return false;
exec("CREATE INDEX IF NOT EXISTS idx_bcu_node_time ON bcu_node(time);");
exec("CREATE INDEX IF NOT EXISTS idx_bcu_node_addr ON bcu_node(addr);");
return prepareInsert() && prepareBcuInsert();
}
bool DbStore::prepareInsert() {
const char* sql =
"INSERT INTO comm_log (time, link, direction, msg_id, msg_name, length, checksum_ok, hex) "
"VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);";
if (sqlite3_prepare_v2(m_db, sql, -1, &m_stmtInsert, nullptr) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
return false;
}
return true;
}
bool DbStore::prepareBcuInsert() {
const char* sql =
"INSERT INTO bcu_node (time, addr, func, total_voltage, current, soc, "
"alarm_code, self_check, "
"max_cell_voltage, max_cell_voltage_no, min_cell_voltage, min_cell_voltage_no, avg_cell_voltage, "
"max_cell_temp, max_cell_temp_no, min_cell_temp, min_cell_temp_no, avg_cell_temp, "
"pos_relay, neg_relay, pos_insulation, neg_insulation, port_voltage, pos_relay_outer_voltage, "
"alarm_bits, alarm_extra_byte6, alarm_extra_byte7, rx_mask, last_rx_time) "
"VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,"
"?19,?20,?21,?22,?23,?24,?25,?26,?27,?28,?29);";
if (sqlite3_prepare_v2(m_db, sql, -1, &m_stmtBcuInsert, nullptr) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
return false;
}
return true;
}
void DbStore::close() {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_stmtInsert) {
sqlite3_finalize(m_stmtInsert);
m_stmtInsert = nullptr;
}
if (m_stmtBcuInsert) {
sqlite3_finalize(m_stmtBcuInsert);
m_stmtBcuInsert = nullptr;
}
if (m_db) {
sqlite3_close(m_db);
m_db = nullptr;
}
}
void DbStore::onRawFrame(int direction, uint16_t msgId, const std::string& link,
const std::vector<uint8_t>& data, bool checksumOk) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db || !m_stmtInsert) return;
// 消息名:尝试从链路名无法直接获取,这里用 msgId 的十六进制作为名称的一部分
char name[32];
std::snprintf(name, sizeof(name), "msg_0x%04X", msgId);
std::string hex = toHex(data);
sqlite3_reset(m_stmtInsert);
sqlite3_clear_bindings(m_stmtInsert);
// 时间:使用当前 unix 时间(秒),供网页展示
sqlite3_bind_double(m_stmtInsert, 1, static_cast<double>(::time(nullptr)));
sqlite3_bind_text(m_stmtInsert, 2, link.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(m_stmtInsert, 3, direction);
sqlite3_bind_int(m_stmtInsert, 4, msgId);
sqlite3_bind_text(m_stmtInsert, 5, name, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(m_stmtInsert, 6, static_cast<int>(data.size()));
sqlite3_bind_int(m_stmtInsert, 7, checksumOk ? 1 : 0);
sqlite3_bind_text(m_stmtInsert, 8, hex.c_str(), -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(m_stmtInsert);
if (rc != SQLITE_DONE) {
m_lastError = sqlite3_errmsg(m_db);
}
}
void DbStore::insertBcuNode(uint8_t addr, uint8_t func, const BcuNodeStatus& v) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db || !m_stmtBcuInsert) return;
char bits[16];
std::snprintf(bits, sizeof(bits), "%02X%02X%02X%02X%02X%02X",
v.alarmBits[0], v.alarmBits[1], v.alarmBits[2],
v.alarmBits[3], v.alarmBits[4], v.alarmBits[5]);
sqlite3_reset(m_stmtBcuInsert);
sqlite3_clear_bindings(m_stmtBcuInsert);
// 时间:与 comm_log 一致用 unix 时间(秒)
int i = 1;
sqlite3_bind_double(m_stmtBcuInsert, i++, static_cast<double>(::time(nullptr)));
sqlite3_bind_int(m_stmtBcuInsert, i++, addr);
sqlite3_bind_int(m_stmtBcuInsert, i++, func);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.totalVoltage);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.current);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.soc);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.alarmCode);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.selfCheck);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.maxCellVoltage);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.maxCellVoltageNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.minCellVoltage);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.minCellVoltageNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.avgCellVoltage);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.maxCellTemp);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.maxCellTempNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.minCellTemp);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.minCellTempNo);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.avgCellTemp);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.posRelay);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.negRelay);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.posInsulationKohm);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.negInsulationKohm);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.portVoltage);
sqlite3_bind_double(m_stmtBcuInsert, i++, v.posRelayOuterVoltage);
sqlite3_bind_text(m_stmtBcuInsert, i++, bits, -1, SQLITE_TRANSIENT);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.alarmExtraByte6);
sqlite3_bind_int(m_stmtBcuInsert, i++, v.alarmExtraByte7);
sqlite3_bind_int64(m_stmtBcuInsert, i++, static_cast<sqlite3_int64>(v.rxMask));
sqlite3_bind_double(m_stmtBcuInsert, i++, v.lastRxTime);
int rc = sqlite3_step(m_stmtBcuInsert);
if (rc != SQLITE_DONE) {
m_lastError = sqlite3_errmsg(m_db);
}
}
std::vector<CommLogRow> DbStore::queryRecent(const std::string& link, int direction,
uint16_t msgId, int limit) {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<CommLogRow> rows;
if (!m_db) return rows;
if (limit <= 0) limit = 100;
std::string sql = "SELECT id, time, link, direction, msg_id, msg_name, length, checksum_ok, hex "
"FROM comm_log WHERE 1=1";
if (!link.empty()) {
sql += " AND link='" + link + "'";
}
if (direction >= 0) {
sql += " AND direction=" + std::to_string(direction);
}
if (msgId != 0) {
char buf[16];
std::snprintf(buf, sizeof(buf), "%u", msgId);
sql += " AND msg_id=" + std::string(buf);
}
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) {
CommLogRow r;
r.id = sqlite3_column_int64(stmt, 0);
r.time = sqlite3_column_double(stmt, 1);
r.link = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
r.direction = sqlite3_column_int(stmt, 3);
r.msgId = static_cast<uint16_t>(sqlite3_column_int(stmt, 4));
r.msgName = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5));
r.length = sqlite3_column_int(stmt, 6);
r.checksumOk = sqlite3_column_int(stmt, 7);
r.hex = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 8));
rows.push_back(std::move(r));
}
sqlite3_finalize(stmt);
return rows;
}
long long DbStore::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 comm_log;", -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;
}
long long DbStore::countBcu() const {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return 0;
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(m_db, "SELECT COUNT(*) FROM bcu_node;", -1, &stmt, nullptr) != SQLITE_OK)
return 0;
long long n = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) n = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
return n;
}
} // namespace ccu
-81
View File
@@ -1,81 +0,0 @@
#ifndef PCCU_DB_STORE_H
#define PCCU_DB_STORE_H
#include <cstdint>
#include <string>
#include <mutex>
#include <vector>
#include "../comm/LinkManager.h"
#include "../protocol/CanBms.h"
struct sqlite3;
struct sqlite3_stmt;
namespace ccu {
//============================================================================
// DbStore:SQLite 存储。
//
// 设计要点(应对协议变更):
// - 以“原始帧日志”为主表(comm_log),收发全部数据按帧落库,
// 协议字段变化不会导致建表失败。
// - 提供按 link / direction / msg_id 的查询接口供网页展示。
// - 锂电池 BMS/CAN 协议字段固定,另设解析数据表 bcu_node:
// 每收到一帧 BMS 报文,把该 BCU 节点的合成状态落一行
// (电压/电流/SOC/单体电压温度/继电器/绝缘/告警位)。
// 复用仓库内 src/pPowerManger/sqlit3/sqlite3.c 与 sqlite3.h。
//============================================================================
struct CommLogRow {
long long id;
double time;
std::string link; // fc / pm
int direction; // 0=收 1=发
uint16_t msgId;
std::string msgName;
int length;
int checksumOk;
std::string hex;
};
class DbStore : public ILinkLogSink {
public:
explicit DbStore(const std::string& dbPath);
~DbStore();
bool open();
void close();
bool isOpen() const { return m_db != nullptr; }
std::string lastError() const { return m_lastError; }
// 实现 ILinkLogSink:原始帧落库(收发双向)
void onRawFrame(int direction, uint16_t msgId, const std::string& link,
const std::vector<uint8_t>& data, bool checksumOk) override;
// 锂电池解析数据落库(bcu_node 表):addr=节点地址(1~54),
// func=本次触发落库的 BCU 报文功能码,v=合并后的节点最新状态
void insertBcuNode(uint8_t addr, uint8_t func, const BcuNodeStatus& v);
// 查询最近 N 条记录(可限定链路/方向/消息id;limit<=0 表示默认 100)
std::vector<CommLogRow> queryRecent(const std::string& link, int direction,
uint16_t msgId, int limit);
long long count() const;
long long countBcu() const;
private:
bool exec(const char* sql);
bool prepareInsert();
bool prepareBcuInsert();
std::string m_dbPath;
sqlite3* m_db = nullptr;
sqlite3_stmt* m_stmtInsert = nullptr;
sqlite3_stmt* m_stmtBcuInsert = nullptr;
mutable std::mutex m_mutex;
std::string m_lastError;
};
} // namespace ccu
#endif // PCCU_DB_STORE_H
-144
View File
@@ -1,144 +0,0 @@
#include "WebServer.h"
#include "pages/index.h"
#include <iostream>
namespace ccu {
//---------------------------------------------------------
// 事件处理:HTTP 路由 + WebSocket 收发
void WebServer::handleEvent(struct mg_connection* c, int ev, void* ev_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message* hm = (struct mg_http_message*)ev_data;
// WebSocket 升级
if (mg_match(hm->uri, mg_str("/ws"), NULL)) {
mg_ws_upgrade(c, hm, NULL);
return;
}
// /api/* 接口:交给宿主程序处理,返回 JSON
std::string path(hm->uri.buf, hm->uri.len);
if (path.rfind("/api/", 0) == 0) {
std::string body;
if (m_apiHandler) {
std::string query(hm->query.buf, hm->query.len);
body = m_apiHandler(path, query);
}
if (!body.empty()) {
mg_http_reply(c, 200, "Content-Type: application/json\r\n",
"%.*s", (int)body.size(), body.c_str());
} else {
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
}
return;
}
// 监控页面
if (path == "/" || path == "/index.html") {
mg_http_reply(c, 200, "Content-Type: text/html; charset=utf-8\r\n",
"%.*s", (int)INDEX_HTML.size(), INDEX_HTML.c_str());
return;
}
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
} else if (ev == MG_EV_WS_OPEN) {
// Web 线程内:先构建欢迎快照(可能涉及 DB 查询,勿持锁),再登记连接
std::string welcome;
if (m_onOpen) welcome = m_onOpen();
m_wsConnections.push_back(c);
if (!welcome.empty()) {
mg_ws_send(c, welcome.c_str(), welcome.size(), WEBSOCKET_OP_TEXT);
}
} else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) {
for (auto it = m_wsConnections.begin(); it != m_wsConnections.end(); ++it) {
if (*it == c) {
m_wsConnections.erase(it);
break;
}
}
} else if (ev == MG_EV_WS_MSG) {
// 纯监控,忽略客户端消息
}
}
WebServer::WebServer() {
mg_mgr_init(&m_mgr);
}
WebServer::~WebServer() {
stop();
mg_mgr_free(&m_mgr);
}
void WebServer::setOnOpen(std::function<std::string()> handler) {
m_onOpen = std::move(handler);
}
void WebServer::setApiHandler(std::function<std::string(const std::string&, const std::string&)> handler) {
m_apiHandler = std::move(handler);
}
bool WebServer::start(int port) {
if (m_running) return true;
m_port = port;
std::string addr = "http://0.0.0.0:" + std::to_string(port);
mg_http_listen(&m_mgr, addr.c_str(), [](mg_connection* c, int ev, void* ev_data) {
WebServer* server = static_cast<WebServer*>(c->fn_data);
if (server) server->handleEvent(c, ev, ev_data);
}, this);
m_running = true;
m_thread = std::thread([this]() { serverThreadFunc(); });
std::cout << "pCCU web UI listening on " << addr << std::endl;
return true;
}
void WebServer::stop() {
if (!m_running) return;
m_running = false;
if (m_thread.joinable()) m_thread.join();
// Web 线程已退出,此处触碰连接列表安全
for (auto c : m_wsConnections) {
mg_ws_send(c, "", 0, WEBSOCKET_OP_CLOSE);
}
m_wsConnections.clear();
}
bool WebServer::serverThreadCB(void* pParam) {
WebServer* pThis = static_cast<WebServer*>(pParam);
return pThis->serverThreadFunc();
}
bool WebServer::serverThreadFunc() {
while (m_running) {
mg_mgr_poll(&m_mgr, 200);
// 仅在本线程(poll 间隙)发送,避免跨线程操作 mongoose 连接
flushPending();
}
return true;
}
void WebServer::flushPending() {
std::string text;
{
std::lock_guard<std::mutex> lock(m_pendingMutex);
text = std::move(m_pending);
m_pending.clear();
}
if (text.empty()) return;
for (auto c : m_wsConnections) {
mg_ws_send(c, text.c_str(), text.size(), WEBSOCKET_OP_TEXT);
}
}
void WebServer::broadcast(const std::string& text) {
// 只投递到队列,实际发送在 Web 线程 flushPending() 中完成
std::lock_guard<std::mutex> lock(m_pendingMutex);
m_pending = text;
}
} // namespace ccu
-66
View File
@@ -1,66 +0,0 @@
#ifndef PCCU_WEB_SERVER_H
#define PCCU_WEB_SERVER_H
#define UNIX
#include "mongoose.h"
#include <string>
#include <functional>
#include <vector>
#include <mutex>
#include <atomic>
#include <thread>
namespace ccu {
//============================================================================
// WebServer:HTTP + WebSocket 监控服务器(基于 mongoose)。
//
// 路由:
// / -> 内嵌监控页面(纯展示)
// /ws -> WebSocket,推送 JSON 快照(由 CCU 周期调用 broadcast)
// /api/logs -> 最近原始帧日志(JSON)
// 采用与仓库内 pPowerMangerHost/WebServer 一致的回调模式。
//============================================================================
class WebServer {
public:
WebServer();
~WebServer();
bool start(int port);
void stop();
bool isRunning() const { return m_running; }
// 向所有 WebSocket 客户端广播文本
// 线程安全:mongoose 非线程安全,本接口只把文本放入待发队列,
// 由 Web 线程在 mg_mgr_poll 返回后统一发送(禁止跨线程直接 mg_ws_send)
void broadcast(const std::string& text);
// 新客户端连接时调用,返回初始快照
void setOnOpen(std::function<std::string()> handler);
// 处理 /api/* 请求,返回 JSON 响应体(空串表示 404)
void setApiHandler(std::function<std::string(const std::string& uri, const std::string& query)> handler);
private:
static bool serverThreadCB(void* pParam);
bool serverThreadFunc();
void handleEvent(struct mg_connection* c, int ev, void* ev_data);
// Web 线程内:把待发队列中的快照推给所有 WS 客户端(仅本线程触碰连接列表)
void flushPending();
struct mg_mgr m_mgr;
int m_port = 0;
std::atomic<bool> m_running{false};
std::thread m_thread;
std::mutex m_pendingMutex; // 保护 m_pending
std::string m_pending; // 最新待广播快照(新快照覆盖旧快照)
std::vector<mg_connection*> m_wsConnections; // 仅 Web 线程访问
std::function<std::string()> m_onOpen;
std::function<std::string(const std::string&, const std::string&)> m_apiHandler;
};
} // namespace ccu
#endif // PCCU_WEB_SERVER_H
-857
View File
@@ -1,857 +0,0 @@
#ifndef PCCU_PAGE_INDEX_H
#define PCCU_PAGE_INDEX_H
#include <string>
//============================================================================
// pCCU 监控页面(纯前端,内嵌 HTML)。
//
// - 通过 WebSocket /ws 接收服务器推送的 JSON 快照(每秒)
// - 每条消息一个标签页(接收/发送/系统分组),数据按消息归类展示
// - 故障码/故障等级突出高亮;每条协议数据单独一行
// - 纯监控展示,不提供反向控制
//============================================================================
namespace ccu {
const std::string INDEX_HTML = R"HTML(<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pCCU 复合管控器监控</title>
<style>
:root{--bg:#0f1420;--card:#1a2130;--line:#2a3447;--fg:#e6edf3;--dim:#8b98ab;--ok:#2ea043;--warn:#d29922;--bad:#f85149;}
*{margin:0;padding:0;box-sizing:border-box;}
body{background:var(--bg);color:var(--fg);font-family:'Segoe UI',system-ui,sans-serif;font-size:13px;padding:16px;}
h1{font-size:18px;margin-bottom:12px;display:flex;align-items:center;gap:10px;}
h2{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px;border-bottom:1px solid var(--line);padding-bottom:6px;}
.badge{font-size:11px;padding:2px 8px;border-radius:10px;background:var(--card);border:1px solid var(--line);}
.badge.ok{color:var(--ok);border-color:var(--ok);}
.badge.warn{color:var(--warn);border-color:var(--warn);}
.badge.err{color:var(--bad);border-color:var(--bad);}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:12px;}
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px;}
.row{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px dashed #222c3d;}
.row:last-child{border-bottom:none;}
.row .k{color:var(--dim);}
.row .v{font-family:Consolas,monospace;color:var(--fg);}
.raw{font-family:Consolas,monospace;font-size:11px;color:var(--dim);word-break:break-all;max-height:200px;overflow-y:auto;}
.raw-group{margin-bottom:12px;border:1px solid var(--line);border-radius:6px;padding:8px;}
.raw-group:last-child{margin-bottom:0;}
.raw-group-hdr{display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;}
.raw-id{font-family:Consolas,monospace;color:#79c0ff;font-weight:700;}
.raw-name{color:var(--fg);font-weight:600;}
.raw-count{color:var(--dim);font-size:11px;margin-left:auto;}
table{width:100%;border-collapse:collapse;font-size:12px;}
th,td{text-align:left;padding:4px 6px;border-bottom:1px solid #222c3d;}
th{color:var(--dim);font-weight:500;}
.status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;}
.dot-ok{background:var(--ok);} .dot-warn{background:var(--warn);} .dot-bad{background:var(--bad);}
#status{color:var(--dim);font-size:12px;}
.tabs{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px;align-items:center;}
.tab-group{display:flex;align-items:center;gap:6px;padding-right:16px;margin-right:16px;border-right:1px solid var(--line);}
.tab-group:last-child{border-right:none;margin-right:0;padding-right:0;}
.tg-label{font-size:11px;color:var(--dim);font-weight:700;letter-spacing:.5px;}
.tab{padding:6px 14px;border:1px solid var(--line);border-radius:16px;background:var(--card);color:var(--dim);cursor:pointer;font-size:12px;transition:all .15s;}
.tab:hover{color:var(--fg);border-color:#3b4a63;}
.tab.active{background:#1f6feb;border-color:#1f6feb;color:#fff;font-weight:600;}
.page-hdr{display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:10px 14px;background:#141b29;border:1px solid var(--line);border-radius:8px;font-size:14px;}
/* 故障等级高亮 */
.fault-lvl{display:inline-block;padding:3px 12px;border-radius:6px;font-weight:700;font-size:13px;letter-spacing:.5px;}
.lvl0{background:rgba(46,160,67,.15);color:#3fb950;border:1px solid rgba(46,160,67,.45);}
.lvl1{background:rgba(210,153,34,.15);color:#d29922;border:1px solid rgba(210,153,34,.45);}
.lvl2{background:rgba(219,97,20,.18);color:#e3841d;border:1px solid rgba(219,97,20,.55);}
.lvl3{background:rgba(248,81,73,.22);color:#f85149;border:1px solid rgba(248,81,73,.55);}
.lvl4{background:rgba(248,81,73,.38);color:#ff7b72;border:1px solid #f85149;}
.fc-bad{color:#f85149;font-weight:700;background:rgba(248,81,73,.12);padding:1px 6px;border-radius:4px;}
.fc-ok{color:#3fb950;}
/* 卡片内分组小标题 */
.sub{font-size:11px;color:#79c0ff;margin:10px 0 2px;border-bottom:1px solid #22304a;padding-bottom:2px;}
.sub:first-child{margin-top:0;}
.btn{padding:3px 10px;margin:0 4px 4px 0;border:1px solid var(--line);border-radius:6px;background:#222b3d;color:var(--fg);font-size:11px;cursor:pointer;}
.btn:hover{border-color:#3b4a63;}
.btn.on{background:#1f6feb;border-color:#1f6feb;color:#fff;font-weight:600;}
.btn.off{background:#3d2226;border-color:#f85149;color:#f85149;}
.btn:disabled{opacity:.4;cursor:not-allowed;}
</style>
</head>
<body>
<h1>⚡ pCCU 复合管控器监控 <span id="status" class="badge">连接中...</span></h1>
<div class="tabs">
<div class="tab-group"><span class="tg-label">接收</span>
<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>
<button class="tab" data-tab="pmStatus">PM状态报文</button>
<button class="tab" data-tab="pmFb">PM参数反馈</button>
</div>
<div class="tab-group"><span class="tg-label">系统</span>
<button class="tab" data-tab="links">链路状态</button>
<button class="tab" data-tab="realCcu">真实CCU</button>
<button class="tab" data-tab="raw">原始报文</button>
</div>
</div>
<div id="pages">加载中...</div>
<script>
const $ = id => document.getElementById(id);
const pagesEl = $('pages');
const statusEl = $('status');
const TABS = ['fcStatus','pmCmd','pmParam','bms','fcCmd','pmStatus','pmFb','links','realCcu','raw'];
let activeTab = 'fcStatus';
function dot(ok){ return ok ? 'dot-ok' : (ok === undefined ? 'dot-warn' : 'dot-bad'); }
function row(k, v){ return '<div class="row"><span class="k">'+k+'</span><span class="v">'+v+'</span></div>'; }
function sub(t){ return '<div class="sub">'+t+'</div>'; }
function hex(v){ return '0x' + (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
function hex16(v){ return '0x' + (v & 0xFFFF).toString(16).toUpperCase().padStart(4,'0'); }
function hex32(v){ return '0x' + (v >>> 0).toString(16).toUpperCase().padStart(8,'0'); }
//------------------ 指令码解析 ------------------
function modeText(m){ return {0:'无效',1:'调试',2:'自动',3:'补给/排放'}[m] || ('未知('+hex(m)+')'); }
function cmdText(c){
const map={0x00:'无效',0x01:'自检',0x02:'启动',0x03:'停机',0x04:'复位',0x05:'紧急停机',
0x06:'补给',0x07:'制氢预热',0x08:'停机维护',0x0A:'开机',0x0B:'关机'};
return map[c] || ('未知('+hex(c)+')');
}
function batCmdText(b){ return {0x00:'无效',0x10:'启动',0x20:'关闭'}[b] || ('未知('+hex(b)+')'); }
function hostStateText(s){ return s===0xAA?'运行':s===0xFF?'关机':'故障'; }
function supplyCmdText(s){
const map={0x00:'无效',0x01:'液氧加注',0x02:'甲醇加注',0x03:'液氧排空',0x04:'甲醇排空',
0x05:'CO2排空',0x07:'合金加注',0x08:'氮气加注',0x09:'纯水加注'};
return map[s] || ('未知('+hex(s)+')');
}
function emerAllowText(v){
const bits=[];
if(v & 0x01) bits.push('允许降载');
if(v & 0x02) bits.push('允许排气');
return bits.length ? bits.join('+') : '无';
}
function statusText(s){
const m={1:'初始化',2:'待机',3:'自检异常',4:'就绪',5:'启动',6:'运行',7:'停机',8:'故障',
9:'补给',10:'制氢预热',11:'制氢预热完成',12:'停机维护',13:'维护完成'};
return m[s] || ('未知('+hex(s)+')');
}
function flameText(f){ return {0:'无效',1:'火焰报警',2:'探测器故障'}[f] || ('未知('+hex(f)+')'); }
//------------------ 锂电池 BCU 节点(CAN) 解析 ------------------
// 报文定义见 docs/04KT38电池BCU-MBMS通信(CAN)定义.docx:
// 0x10XX0000 累加电压/回路电流/SOC/告警码/自检状态
// 0x10XX0001 单体电压 0x10XX0002 单体温度 0x10XX0003 继电器
// 0x10XX0006 绝缘阻值/端口电压 0x10XX0010 告警位(附录1)
// XX(01h~36h) 为 BCU 节点地址:同一节点的全部报文归入同一张卡片;
// 告警含义由服务器端按附录1 解析为中文列表(alarms 字段)
function pad2(v){ return (v & 0xFF).toString(16).toUpperCase().padStart(2,'0'); }
function relayText(v){ return v===1 ? '<span class="fc-ok">闭合</span>' : '断开'; }
// 告警码(0x10XX0000 BYTE7,协议未附码表,显示原始值)
function bmsFault(c){ return c===0 ? '<span class="fc-ok">'+hex(c)+'</span>'
: '<span class="fc-bad">'+hex(c)+'</span>'; }
// 数据新鲜度:10s 内视为在线。优先用后端算好的 age(距最后收到该节点
// 报文的秒数,不依赖浏览器与板卡时钟同步),无 age 时退回 lastRx 比对本地时钟
function bmsOnline(n){
if(!n) return undefined;
if(typeof n.age === 'number') return n.age < 10;
if(typeof n.lastRx === 'number') return (Date.now()/1000 - n.lastRx) < 10;
return undefined;
}
// 最后更新时间文本(a:距最后收到报文的秒数)
function ageText(a){
if(typeof a !== 'number' || isNaN(a)) return '—';
if(a < 90) return a.toFixed(1)+' 秒前';
if(a < 3600) return Math.floor(a/60)+' 分 '+Math.floor(a%60)+' 秒前';
return Math.floor(a/3600)+' 时 '+Math.floor((a%3600)/60)+' 分前';
}
// 带解析的值:hex 码 + 括号解析
function parsed(v, text){ return hex(v)+' ('+text+')'; }
// 故障码高亮:0 显示绿色正常,非 0 显示红色
function faultHex(c){ return c===0 ? '<span class="fc-ok">'+hex16(c)+' 正常</span>'
: '<span class="fc-bad">'+hex16(c)+'</span>'; }
// 故障等级高亮徽标
function faultLvlBadge(level){
const names=['无故障','一级故障','二级故障','三级故障','四级故障'];
const n = names[level] || ('未知('+level+')');
const cls = ['lvl0','lvl1','lvl2','lvl3','lvl4'][level] || 'lvl4';
return '<span class="fault-lvl '+cls+'">'+n+'</span>';
}
function card(title, html){ return '<div class="card"><h2>'+title+'</h2>'+html+'</div>'; }
//------------------ FC 状态反馈(0x0002) 卡片 ------------------
function fcCard(d){
if(!d) return '';
let h='';
h+=row('运行模式', modeText(d.fc_mode));
h+=row('运行状态', statusText(d.fc_status));
h+=row('系统故障等级', faultLvlBadge(d.fc_fault_level));
h+=row('一级故障码', faultHex(d.fault_level_1));
h+=row('二级故障码', faultHex(d.fault_level_2));
h+=row('三级故障码', faultHex(d.fault_level_3));
h+=row('四级故障码', faultHex(d.fault_level_4));
h+=row('累积发电时间', (d.total_generation_time*0.1).toFixed(1)+' h');
h+=row('系统发电功率', (d.generation_power*0.01).toFixed(2)+' kW');
h+=row('剩余发电量', d.remaining_generation+' MWh');
h+=row('储氢剩余容量', d.hydrogen_capacity+'%');
h+=row('液氧剩余容量', d.liquid_oxygen_capacity+'%');
h+=row('通信心跳', d.heartbeat);
h+=row('应急指令', hex(d.emergency_cmd));
return card('燃料电池运行', h);
}
function hydroCard(d){
if(!d) return '';
let h='';
h+=row('钯膜最高温度', (d.palladium_temp*0.1).toFixed(1)+' ℃');
h+=row('缓冲罐压力', (d.buffer_tank_pressure*0.1).toFixed(1)+' kPa');
h+=row('烟气累计排放量', (d.flue_total_emission*0.1).toFixed(1)+' kg');
h+=row('烟气压力', (d.flue_pressure*0.001).toFixed(3)+' MPa');
h+=row('反应器压力', (d.reactor_pressure*0.001).toFixed(3)+' MPa');
h+=row('电动阀开度', (d.electric_valve_open*0.1).toFixed(1)+'%');
h+=row('甲醇累计使用量', (d.methanol_total_use*0.1).toFixed(1)+' kg');
h+=row('甲醇溶液进料量', d.methanol_feed+' mL/min');
h+=row('氧侧生成水箱液位', (d.oxygen_side_water_level*0.1).toFixed(1)+' mm');
h+=row('氢侧生成水箱液位', (d.hydrogen_side_water_level*0.1).toFixed(1)+' mm');
h+=row('配重水箱液位', (d.ballast_water_level*0.1).toFixed(1)+' mm');
h+=row('I#FC最低单片电压', (d.fc1_min_cell_voltage*10)+' mV (位置'+d.fc1_min_cell_pos+')');
h+=row('I#FC平均单片电压', (d.fc1_avg_cell_voltage*10)+' mV');
h+=row('2#FC最低单片电压', (d.fc2_min_cell_voltage*10)+' mV (位置'+d.fc2_min_cell_pos+')');
h+=row('2#FC平均单片电压', (d.fc2_avg_cell_voltage*10)+' mV');
return card('甲醇制氢装置', h);
}
function dcdcCard(d){
if(!d) return '';
let h='';
h+=row('通道1输入电压', (d.dcdc1_in_voltage*0.1).toFixed(1)+' V');
h+=row('通道1输入电流', (d.dcdc1_in_current*0.1).toFixed(1)+' A');
h+=row('通道2输入电压', (d.dcdc2_in_voltage*0.1).toFixed(1)+' V');
h+=row('通道2输入电流', (d.dcdc2_in_current*0.1).toFixed(1)+' A');
h+=row('输出电压', (d.dcdc_out_voltage*0.1).toFixed(1)+' V');
h+=row('输出电流', (d.dcdc_out_current*0.1).toFixed(1)+' A');
h+=row('控制电源电压', (d.dcdc_ctrl_voltage*0.25).toFixed(2)+' V');
h+=row('辅电输出电压', (d.dcdc_aux_voltage*0.125).toFixed(3)+' V');
return card('DC/DC', h);
}
function exhaustCard(d){
if(!d) return '';
let h='';
h+=row('进气压力', (d.exhaust_inlet_pressure*0.01).toFixed(2)+' MPa');
h+=row('排气压力', (d.exhaust_outlet_pressure*0.01).toFixed(2)+' MPa');
h+=row('运行频率', (d.exhaust_run_freq*0.01).toFixed(2)+' Hz');
h+=row('进气温度', (d.exhaust_inlet_temp*0.01).toFixed(2)+' ℃');
h+=row('排气温度', (d.exhaust_outlet_temp*0.01).toFixed(2)+' ℃');
h+=row('进水压力', (d.exhaust_water_in_pressure*0.01).toFixed(2)+' kPa');
h+=row('排水压力', (d.exhaust_water_out_pressure*0.01).toFixed(2)+' kPa');
return card('尾气装置', h);
}
function tankCard(d){
if(!d) return '';
let h='';
h+=row('液氧罐压力', (d.tank_lo2_pressure*0.01).toFixed(2)+' MPa');
h+=row('二氧化碳压力', (d.tank_co2_pressure*0.01).toFixed(2)+' MPa');
h+=row('液氧罐液位', (d.tank_lo2_level*0.01).toFixed(2)+' m');
h+=row('合金供氢流量', (d.alloy_h2_flow*0.01).toFixed(2)+' L/min');
h+=row('FC供氢流量', (d.fc_h2_flow*0.01).toFixed(2)+' L/min');
h+=row('FC供氧流量', (d.fc_o2_flow*0.01).toFixed(2)+' L/min');
h+=row('应急上浮深度', d.emergency_float_depth+' m');
h+=row('应急上浮时间', d.emergency_float_time+' min');
return card('一体化罐装置', h);
}
function cabinCard(d){
if(!d) return '';
let h='';
h+=row('舱室压力1', (d.cabin_pressure1*0.01).toFixed(2)+' kPa');
h+=row('舱室压力2', (d.cabin_pressure2*0.01).toFixed(2)+' kPa');
h+=row('舱室温度1', (d.cabin_temp1*0.01).toFixed(2)+' ℃');
h+=row('舱室温度2', (d.cabin_temp2*0.01).toFixed(2)+' ℃');
h+=row('舱室湿度1', (d.cabin_humidity1*0.01).toFixed(2)+' %RH');
h+=row('舱室湿度2', (d.cabin_humidity2*0.01).toFixed(2)+' %RH');
h+=row('舱室H2浓度1', (d.h2_concentration1*0.01).toFixed(2)+' %LEL');
h+=row('舱室H2浓度2', (d.h2_concentration2*0.01).toFixed(2)+' %LEL');
h+=row('舱室H2浓度3', (d.h2_concentration3*0.01).toFixed(2)+' %LEL');
h+=row('舱室O2浓度1', (d.o2_concentration1*0.01).toFixed(2)+' %Vol');
h+=row('舱室O2浓度2', (d.o2_concentration2*0.01).toFixed(2)+' %Vol');
h+=row('舱室甲醇浓度1', (d.ch3oh_concentration1*0.01).toFixed(2)+' %LEL');
h+=row('舱室甲醇浓度2', (d.ch3oh_concentration2*0.01).toFixed(2)+' %LEL');
h+=row('火焰探测器1', flameText(d.flame_detector1));
h+=row('火焰探测器2', flameText(d.flame_detector2));
return card('舱室环境', h);
}
//------------------ PM 状态报文(0x0004) 的锂电池 / 应急电池卡片 ------------------
function batteryCard(d){
if(!d) return '';
let h='';
h+=row('仪表电池SOC', d.ins_soc+'%');
h+=row('仪表电池总能量', (d.ins_total_energy*0.1).toFixed(1)+' kWh');
h+=row('动力电池SOC', d.dyn_soc+'%');
h+=row('动力电池总能量', (d.dyn_total_energy*0.1).toFixed(1)+' kWh');
h+=row('仪表LINK电压', (d.ins_voltage_link*0.01).toFixed(2)+' V');
h+=row('仪表PACK电压', (d.ins_voltage_pack*0.01).toFixed(2)+' V');
h+=row('动力LINK电压', (d.dyn_voltage_link*0.01).toFixed(2)+' V');
h+=row('动力PACK电压', (d.dyn_voltage_pack*0.01).toFixed(2)+' V');
h+=row('仪表电流', (d.ins_current*0.05).toFixed(2)+' A');
h+=row('动力电流', (d.dyn_current*0.05).toFixed(2)+' A');
h+=row('仪表充电状态', hex(d.ins_charge_status));
h+=row('动力充电状态', hex(d.dyn_charge_status));
h+=row('仪表紧急状态', hex(d.ins_emergency_status));
h+=row('动力紧急状态', hex(d.dyn_emergency_status));
h+=row('仪表最高放电功率', (d.ins_max_discharge_power*0.05).toFixed(2)+' kW');
h+=row('动力最高放电功率', (d.dyn_max_discharge_power*0.05).toFixed(2)+' kW');
return card('锂电池', h);
}
function emergCard(d){
if(!d) return '';
let h='';
h+=row('应急电池1总电压', (d.emergency_battery1_voltage*0.1).toFixed(1)+' V');
h+=row('应急电池1总电流', (d.emergency_battery1_current*0.05).toFixed(2)+' A');
h+=row('应急电池1最高温度', (d.emergency_battery1_max_temp*0.1).toFixed(1)+' ℃');
h+=row('应急电池1故障字', hex(d.emergency_battery1_fault_word));
h+=row('应急电池2总电压', (d.emergency_battery2_voltage*0.1).toFixed(1)+' V');
h+=row('应急电池2总电流', (d.emergency_battery2_current*0.05).toFixed(2)+' A');
h+=row('应急电池2最高温度', (d.emergency_battery2_max_temp*0.1).toFixed(1)+' ℃');
h+=row('应急电池2故障字', hex(d.emergency_battery2_fault_word));
return card('应急电池', h);
}
function linkCard(s){
if(!s || !s.links) return '';
const l = s.links;
const online = (t)=> (Date.now()/1000 - t) < 10;
let h='';
h+=row('<span><span class="status-dot '+dot(online(l.fc.lastRx))+'"></span>FC 链路</span>',
'收:'+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);
if(l.rcu){
h+=row('<span><span class="status-dot '+dot(online(l.rcu.lastRx))+'"></span>RCU 链路 (真实CCU)</span>',
'收:'+l.rcu.rx+' 发:'+l.rcu.tx+' 误:'+l.rcu.err);
}
// CAN/BMS 行:任一 BCU 节点在线即视为链路在线
const bmsNodes = Array.isArray(s.bms) ? s.bms : [];
const bmsAnyOnline = bmsNodes.some(function(n){ return bmsOnline(n); });
h+=row('<span><span class="status-dot '+dot(bmsNodes.length ? bmsAnyOnline : undefined)+
'"></span>CAN 链路 (pCanBridge)</span>', 'CAN帧:'+s.canFrameCount+' BMS帧:'+s.bmsStatusCount);
h+=row('FC 状态接收计数', s.fcStatusCount);
h+=row('PM 指令接收计数', s.pmControlCount);
h+=row('FC 控制转发计数', s.fcControlCount);
h+=row('PM 状态发送计数', s.pmStatusCount);
return card('链路状态', h);
}
//------------------ 控制指令卡片(代码 + 解析) ------------------
function cmdCard(d){
if(!d) return '';
let h='';
h+=row('系统时间', d.year+'/'+d.month+'/'+d.day+' '+d.hour+':'+d.minute+':'+d.second);
h+=row('模式设定', parsed(d.mode, modeText(d.mode)));
h+=row('操控指令', parsed(d.cmd, cmdText(d.cmd)));
h+=row('输出功率指令', d.outputPower+' (×0.1kW)');
h+=row('纵倾姿态', (d.pitch*0.1).toFixed(1)+'°');
h+=row('横倾姿态', (d.roll*0.1).toFixed(1)+'°');
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))+' <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);
}
function fcCmdCard(d){
if(!d) return '';
let h='';
h+=row('模式设定', parsed(d.mode, modeText(d.mode)));
h+=row('操控指令', parsed(d.cmd, cmdText(d.cmd)));
h+=row('输出功率指令', d.outputPower+' (×0.1kW)');
h+=row('纵倾姿态数据1', (d.pitch1*0.1).toFixed(1)+'°');
h+=row('横倾姿态数据1', (d.roll1*0.1).toFixed(1)+'°');
h+=row('应急允许', parsed(d.emergencyAllow, emerAllowText(d.emergencyAllow)));
h+=row('潜深深度', d.depth+' m');
h+=row('补给/排放指令', parsed(d.supplyCmd, supplyCmdText(d.supplyCmd)));
h+=row('预留指令5', hex(d.reservedCmd5));
h+=row('预留指令6', hex(d.reservedCmd6));
h+=row('通信心跳', d.heartbeat);
return card('指令内容', h);
}
function pmParamCard(d){
if(!d) return '';
let h='';
h+=row('仪表SOC门限1', d.insSocHold1);
h+=row('仪表SOC门限2', d.insSocHold2);
h+=row('仪表SOC门限3', d.insSocHold3);
h+=row('仪表输出功率', d.insOutputPower+'%');
h+=row('动力SOC门限1', d.dynSocHold1);
h+=row('动力SOC门限2', d.dynSocHold2);
h+=row('动力SOC门限3', d.dynSocHold3);
h+=row('动力输出功率', d.dynOutputPower+'%');
return card('门限参数', h);
}
function pmFbCard(d){
if(!d) return '';
const flagMap = {0x00:'无效', 0x10:'设定成功', 0x20:'设定失败'};
let h='';
h+=row('设定标志', (flagMap[d.flag]||hex(d.flag)));
h+=row('失败原因', hex(d.failCode));
return card('设定结果', h);
}
//------------------ BCU 断路器控制(下行 0x10XX81FF) ------------------
// pos/neg: 1 闭合 / 0 断开(总正+总负同帧下发,经 pCanBridge 写 USBCAN)
function bcuCtrl(addr, pos, neg, label){
if(!confirm('确认向 BCU 节点 '+addr+' 下发「'+label+'」指令?\n该指令将直接控制电池簇总正/总负继电器。')) return;
fetch('/api/bcu_ctrl?addr='+addr+'&pos='+pos+'&neg='+neg)
.then(function(r){ return r.json(); })
.then(function(j){
if(j.ok){
alert('已下发: 节点'+j.addr+' 总正='+(j.pos?'闭合':'断开')+' 总负='+(j.neg?'闭合':'断开')+
'\n继电器状态以 0x10'+pad2(j.addr)+'0003 上报刷新为准');
}else{
alert('下发失败: '+(j.error||'未知错误'));
}
})
.catch(function(){ alert('网络错误,下发未完成'); });
}
// 断路器控制下发统计卡片(最近一次指令与成败计数)
function bcuCtrlStatCard(s){
let c='';
const v = s && s.valid;
c+=row('累计下发', (s && s.sentCount || 0) + ' 成功 / ' + (s && s.errCount || 0) + ' 失败');
if(v){
c+=row('最近指令', '节点'+s.addr+' 总正='+(s.pos?'闭合':'断开')+' 总负='+(s.neg?'闭合':'断开')
+ ' ' + (s.lastOk ? '<span class="fc-ok">已投递</span>' : '<span class="fc-bad">投递失败</span>'));
c+=row('下发时间', ageCell(s.age, true));
}else{
c+=row('最近指令', '无');
}
c+=sub('链路: 网页 → pCCU(0x10XX81FF) → MOOS CAN_TX_* → pCanBridge → USBCAN → BCU');
return card('断路器控制下发', c);
}
//------------------ 锂电池 BCU 节点(CAN) 卡片 ------------------
// 每个节点(CAN ID 0x10XX00YY 中的 XX)一张卡片,
// 同一节点的 0000/0001/0002/0003/0006/0010 各报文归并在卡片内分组展示
function bmsCard(d){
const nodes = (d && d.length) ? d : [];
if(!nodes.length) return card('锂电池BMS (CAN)',
'<div class="row"><span class="k">数据状态</span><span class="v">等待报文...</span></div>');
let html='';
for(const n of nodes){ const addrTxt = 'BCU节点 '+pad2(n.addr)+'h (#'+n.addr+')';
let h='';
// 概要(0x10XX0000)
h+=sub('概要 0x10'+pad2(n.addr)+'0000');
h+=row('累加电压', n.totalVoltage.toFixed(1)+' V');
h+=row('回路电流', n.current.toFixed(1)+' A');
h+=row('SOC', n.soc.toFixed(1)+' %');
h+=row('告警码', bmsFault(n.alarmCode));
h+=row('自检状态', hex(n.selfCheck));
// 单体电压(0x10XX0001)
h+=sub('单体电压 0x10'+pad2(n.addr)+'0001');
h+=row('最高单体电压', n.maxCellVoltage.toFixed(3)+' V (#'+n.maxCellVoltageNo+')');
h+=row('最低单体电压', n.minCellVoltage.toFixed(3)+' V (#'+n.minCellVoltageNo+')');
h+=row('单体平均电压', n.avgCellVoltage.toFixed(3)+' V');
// 单体温度(0x10XX0002)
h+=sub('单体温度 0x10'+pad2(n.addr)+'0002');
h+=row('最高单体温度', n.maxCellTemp.toFixed(0)+' ℃ (#'+n.maxCellTempNo+')');
h+=row('最低单体温度', n.minCellTemp.toFixed(0)+' ℃ (#'+n.minCellTempNo+')');
h+=row('单体平均温度', n.avgCellTemp.toFixed(0)+' ℃');
// 继电器(0x10XX0003)
h+=sub('继电器 0x10'+pad2(n.addr)+'0003');
h+=row('正极继电器', relayText(n.posRelay));
h+=row('负极继电器', relayText(n.negRelay));
// 断路器控制(0x10XX81FF 下发,总正+总负继电器同帧控制)
h+=sub('断路器控制 (0x10'+pad2(n.addr)+'81FF)');
h+=row('操作',
'<button class="btn on" onclick="bcuCtrl('+n.addr+',1,1,\'断路器闭合\')">断路器闭合</button>'+
'<button class="btn off" onclick="bcuCtrl('+n.addr+',0,0,\'断路器断开\')">断路器断开</button>');
// 绝缘/端口电压(0x10XX0006)
h+=sub('绝缘/端口 0x10'+pad2(n.addr)+'0006');
h+=row('正极绝缘阻抗', n.posInsulationKohm.toFixed(0)+' kΩ');
h+=row('负极绝缘阻抗', n.negInsulationKohm.toFixed(0)+' kΩ');
h+=row('正负极端口电压', n.portVoltage.toFixed(1)+' V');
h+=row('正极继电器外侧电压', n.posRelayOuterVoltage.toFixed(1)+' V');
// 告警(0x10XX0010,附录1 解析)
h+=sub('告警明细 0x10'+pad2(n.addr)+'0010');
if(n.alarms && n.alarms.length){
h+=row('告警(' + n.alarms.length + '条)',
n.alarms.map(function(a){ return '<span class="fc-bad">'+a+'</span>'; }).join('<br>'));
}else{
h+=row('告警', '<span class="fc-ok">无</span>');
}
// 最后更新时间(判断该节点消息是否持续正常发送)
const online = bmsOnline(n);
h+=row('最后更新', (online ? '<span class="fc-ok">' : '<span class="fc-bad">')
+ ageText(n.age || 0) + (online ? ' (在线)' : ' (超时)') + '</span>');
html+=card(addrTxt, h);
}
return html;
}
function pmStatusCard(d){
if(!d) return '';
let h='';
h+=row('运行模式', modeText(d.fc_mode));
h+=row('运行状态', statusText(d.fc_status));
h+=row('系统故障等级', faultLvlBadge(d.fc_fault_level));
h+=row('一级故障码', faultHex(d.fault_level_1));
h+=row('二级故障码', faultHex(d.fault_level_2));
h+=row('三级故障码', faultHex(d.fault_level_3));
h+=row('四级故障码', faultHex(d.fault_level_4));
h+=row('系统输出功率限定', d.output_power_limit+' W');
h+=row('系统发电功率', (d.generation_power*0.01).toFixed(2)+' kW');
h+=row('储氢剩余容量', d.hydrogen_capacity+'%');
h+=row('液氧剩余容量', d.liquid_oxygen_capacity+'%');
h+=row('钯膜最高温度', (d.palladium_temp*0.1).toFixed(1)+' ℃');
h+=row('缓冲罐压力', (d.buffer_tank_pressure*0.1).toFixed(1)+' kPa');
h+=row('主水路盘管压力', (d.main_pipe_pressure*0.01).toFixed(2)+' kPa');
h+=row('辅水路盘管压力', (d.aux_pipe_pressure*0.01).toFixed(2)+' kPa');
h+=row('DC/DC输出电压', (d.dcdc_out_voltage*0.1).toFixed(1)+' V');
h+=row('DC/DC输出电流', (d.dcdc_out_current*0.1).toFixed(1)+' A');
h+=row('甲醇累计使用量', (d.methanol_total_use*0.1).toFixed(1)+' kg');
h+=row('甲醇溶液进料量', d.methanol_feed+' mL/min');
h+=row('舱室温度1', (d.cabin_temp1*0.01).toFixed(2)+' ℃');
h+=row('舱室温度2', (d.cabin_temp2*0.01).toFixed(2)+' ℃');
h+=row('舱室H2浓度1', (d.h2_concentration1*0.01).toFixed(2)+' %LEL');
h+=row('舱室O2浓度1', (d.o2_concentration1*0.01).toFixed(2)+' %Vol');
h+=row('仪表电池SOC', d.ins_soc+'%');
h+=row('动力电池SOC', d.dyn_soc+'%');
h+=row('仪表LINK电压', (d.ins_voltage_link*0.01).toFixed(2)+' V');
h+=row('动力LINK电压', (d.dyn_voltage_link*0.01).toFixed(2)+' V');
h+=row('仪表电流', (d.ins_current*0.05).toFixed(2)+' A');
h+=row('动力电流', (d.dyn_current*0.05).toFixed(2)+' A');
h+=row('设备在线标志字1', hex32(d.device_online_flag1));
h+=row('设备在线标志字2', hex32(d.device_online_flag2));
h+=row('通信心跳', d.heartbeat);
h+=row('应急指令', hex(d.emergency_cmd));
return card('整合后状态', h);
}
//------------------ 真实 CCU(配电桥接) 卡片 ------------------
// 数据来源:RCU 链路。配电反馈(0x0005~0x0008)由 pCCU 原帧转发
// pPowerManger 并在此全字段展示;配电指令(0x0001~0x0004)由 pPowerManger
// 经 pCCU 原帧转发真实CCU。断路器/开关量:0x55 闭合 0xAA 断开 0x5A 故障
function brkText(v){
if(v===0x55) return '<span class="fc-ok">闭合</span>';
if(v===0xAA) return '断开';
if(v===0x5A) return '<span class="fc-bad">故障脱扣</span>';
return hex(v);
}
// 双值信号(0x55 正常 / 0xAA 异常)
function sigText(v){
if(v===0x55) return '<span class="fc-ok">正常</span>';
if(v===0xAA) return '<span class="fc-bad">异常</span>';
return hex(v);
}
function insStatText(v){ return v===0x55 ? '<span class="fc-ok">正常</span>'
: (v===0xAA ? '<span class="fc-bad">绝缘低</span>' : hex(v)); }
// age:距最后收到该类帧的秒数;valid:是否收到过(未收到显示"从未收到")
function ageCell(age, valid){
if(!valid || typeof age !== 'number' || isNaN(age))
return '<span style="color:var(--dim)">从未收到</span>';
const online = age >= 0 && age < 5;
return (online ? '<span class="fc-ok">' : '<span class="fc-bad">') + ageText(age)
+ (online ? ' (在线)' : ' (超时)') + '</span>';
}
function fwdLine(okN, errN){
return okN + ' 成功' + (errN ? ' <span class="fc-bad">'+errN+' 失败</span>' : '');
}
// 母线卡片通用:breakers 子表 + 信号/电气量行(无数据时只显示概要行)
function busCard(title, bus, age, rows){
let h='';
const b = bus && bus.breakers ? bus.breakers : {};
const keys = Object.keys(bNamesDis).filter(function(k){ return b[k] !== undefined; });
if(!keys.length){
h+=sub('断路器状态');
h+=row('状态','暂无数据');
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
h+=sub('断路器状态');
for(const k of keys) h+=row(bNamesDis[k], brkText(b[k]));
for(const r of rows) h+=row(r[0], r[1]);
h+=row('数据更新', ageCell(age ? age.age : -1, !!(age && age.valid)));
return card(title, h);
}
// 母线断路器中文名表(busCard 共用)
const bNamesDis = {
fuelCell:'燃料电池断路器', powerLithiumBattery:'动力锂电池断路器',
propulsionMotor:'推进电机断路器', lithiumBatteryGroupInstrument:'锂电池组(仪表)断路器',
dcDc5Module:'DC/DC5模块断路器', bowHighVoltageDistributionBox:'艏部高压配电箱断路器',
sternFTDevice45:'艉部FT装置4/5断路器', fbReserved:'FB预留断路器',
sternRudderSwitch1:'艉舵开关1断路器', sternRudderSwitch2:'艉舵开关2断路器',
tyzReserved:'TYZ预留断路器', reserved:'预留断路器',
bowFTDevice123:'艏部FT装置1/2/3断路器', actuator:'启闭机构断路器',
mastSteeringGearControlBox:'桅杆舵机控制箱断路器', xcz:'XCZ断路器',
bowRudderControlBox:'艏舵控制箱断路器', openWaterCoverStartCylinder:'敞水盖启动电缸断路器',
bowLowVoltageDistributionBox:'艏部低压配电箱断路器', unit4InstrumentDC48V:'UNIT4仪表DC48V断路器',
dcC1DCDistributionPanel:'DC-C1直流配电板断路器', reserved1:'预留断路器1', reserved2:'预留断路器2',
emergencyLithiumBatteryGroup2:'应急锂电池组2断路器',
unit1:'UNIT1断路器', unit2:'UNIT2断路器', unit3:'UNIT3断路器', unit5:'UNIT5断路器',
bowPZDevice:'艏部PZ装置断路器', emergencyLithiumBatteryGroup1:'应急锂电池组1断路器',
dcdc5ModuleStart:'DC/DC5模块启动', coolingSystemStart:'冷却系统启动'
};
function realCcuPage(d){
if(!d) return '<div class="card"><h2>真实CCU</h2><div class="row"><span class="k">数据状态</span><span class="v">等待数据...</span></div></div>';
let h='<div class="page-hdr"><span class="raw-id">RCU</span> <span class="raw-name">真实CCU 配电桥接</span> '+
'<span class="badge ok">原帧透传</span></div><div class="grid">';
// 桥接概览卡片
{
let c='';
const fwd = d.forward || {};
c+=row('配电反馈收帧', fwd.fbCount || 0);
c+=row('反馈转发PM', fwdLine(fwd.fwdFbOk || 0, fwd.fwdFbErr || 0));
c+=row('配电指令收帧', fwd.cmdCount || 0);
c+=row('指令转发真实CCU', fwdLine(fwd.fwdCmdOk || 0, fwd.fwdCmdErr || 0));
const ccu = d.ccuStatus || {};
c+=sub('真实CCU 状态报文(仅显示,不转发)');
c+=row('在线', ageCell(ccu.age, ccu.valid));
if(ccu.valid){
c+=row('运行模式', modeText(ccu.fc_mode));
c+=row('运行状态', statusText(ccu.fc_status));
c+=row('系统故障等级', faultLvlBadge(ccu.fc_fault_level));
c+=row('系统发电功率', (ccu.generation_power*0.01).toFixed(2)+' kW');
c+=row('仪表/动力SOC', ccu.ins_soc+'% / '+ccu.dyn_soc+'%');
c+=row('通信心跳', ccu.heartbeat);
}
h+=card('桥接概览', c);
}
// 高压母线(0x0005 反馈 / 0x0001 指令)
{
const hv = d.hv || {};
const rows = [
['DC/DC5故障字低', hex(hv.dcDcFaultWord1)],
['DC/DC5故障字高', hex(hv.dcDcFaultWord2)],
['动力母排绝缘状态', insStatText(hv.powerBusInsulationStatus)],
['A汇流排绝缘状态', insStatText(hv.aBusInsulationStatus)],
['仪表电源状态', sigText(hv.meterPowerLossSignal)],
['应急电源状态', sigText(hv.emergencyPowerLossSignal)],
['DC/DC温度', hv.dcDcTemperature],
['动力汇流排电压', hv.powerBusVoltage],
['DC/DC5模块电流', hv.dcDc5ModuleCurrent],
['动力汇流排电流', hv.powerBusCurrent],
['A汇流排电压', hv.busbarAVoltage],
['推进电机控制箱电流', hv.propulsionMotorControlBoxCurrent],
['A汇流排电流', hv.busbarACurrent],
['锂电池组(仪表)电流', hv.lithiumBatteryGroupMeterCurrent],
['艏部高压配电箱电流', hv.bowHighVoltageDistributionBoxCurrent],
['艉部FT装置4/5电流', hv.sternFTDevice45Current],
['TYZ预留开关电流', hv.tyzReservedSwitchCurrent],
['艉舵开关1电流', hv.sternRudderSwitch1Current],
['预留电流1', hv.reservedCurrent1],
['艉舵开关2电流', hv.sternRudderSwitch2Current],
['预留电流2', hv.reservedCurrent2],
['FB预留电流', hv.fbReservedSwitchCurrent],
['预留电流3', hv.reservedCurrent3],
['冷却水压力', hv.coolWaterPressure],
];
h+=busCard('高压母线 (0x0005 反馈 / 0x0001 指令)', hv, d.hvAge, rows);
}
// 高压母线A(0x0006 反馈 / 0x0002 指令)
{
const b = d.hva || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['汇流排B电压', b.busbarBVoltage],
['汇流排B电流', b.busbarBCurrent],
['艏部FT装置1/2/3电流', b.bowFTDevice123Current],
['启闭机构电流', b.actuatorCurrent],
['桅杆舵机控制箱电流', b.mastSteeringGearControlBoxCurrent],
['XCZ电流', b.xczCurrent],
['艏舵控制箱电流', b.bowRudderControlBoxCurrent],
['敞水盖启动电缸电流', b.openWaterCoverStartCylinderCurrent],
];
h+=busCard('高压母线A (0x0006 反馈 / 0x0002 指令)', b, d.hvaAge, rows);
}
// 低压主母线(0x0007 反馈 / 0x0003 指令)
{
const b = d.hvb || {};
const rows = [
['仪表电源失电信号', sigText(b.instrumentPowerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['仪表汇流排绝缘', insStatText(b.instrumentBusbarInsulationLow)],
['仪表母排电压', b.instrumentBusbarVoltage],
['艏部低压配电箱电流', b.bowLowVoltageDistributionBoxCurrent],
['DC-C1仪表DC48V电流', b.dcC1InstrumentDC48VCurrent],
['预留电流1', b.reservedCurrent1],
['应急锂电池组2电流', b.emergencyLithiumBatteryGroup2Current],
['锂电池组(仪表)电流', b.lithiumBatteryGroupInstrumentCurrent],
['UNIT4仪表DC48V电流', b.unit4InstrumentDC48VCurrent],
['预留电流2', b.reservedCurrent2],
['应急2汇流排电压', b.emergency2BusbarVoltage],
['复合能源应急DC48V电流', b.compositeEnergyEmergencyDC48VCurrent],
['燃料电池安全应急DC48V电流', b.fuelCellSecurityEmergencyDC48VCurrent],
['PLC控制电源电流', b.plcControlPowerCurrent],
];
h+=busCard('低压主母线 (0x0007 反馈 / 0x0003 指令)', b, d.hvbAge, rows);
}
// 仪表汇流排(0x0008 反馈 / 0x0004 指令)
{
const b = d.lv || {};
const rows = [
['仪表电源失电信号', sigText(b.powerFailureSignal)],
['应急电源失电信号', sigText(b.emergencyPowerFailureSignal)],
['浸水报警', sigText(b.waterIngressionAlarm)],
['仪表母排电压', b.instrumentBusbarVoltage],
['仪表母排电流', b.instrumentBusbarCurrent],
['UNIT1电流', b.unit1Current],
['UNIT2电流', b.unit2Current],
['UNIT3电流', b.unit3Current],
['UNIT5电流', b.unit5Current],
['艏部PZ装置电流', b.bowPZDeviceCurrent],
['应急锂电池组1电流', b.emergencyLithiumBatteryGroup1Current],
['应急1汇流排电压', b.emergency1BusbarVoltage],
];
h+=busCard('仪表汇流排 (0x0008 反馈 / 0x0004 指令)', b, d.lvAge, rows);
}
// 最近下发指令卡片
{
const lc = d.lastCmd || {};
let c='';
c+=row('最后指令时间', ageCell(lc.age, lc.valid));
const cmds = lc.cmds || {};
const groups = [['hv','高压母线'],['hva','高压母线A'],['hvb','低压主母线'],['lv','仪表汇流排']];
for(const g of groups){
const gb = cmds[g[0]] || {};
const keys = Object.keys(gb);
if(!keys.length) continue;
c+=sub(g[1]+' (0x'+({hv:'0001',hva:'0002',hvb:'0003',lv:'0004'})[g[0]]+')');
for(const k of keys) c+=row(bNamesDis[k]||k, brkText(gb[k]));
}
h+=card('最近下发的配电指令 (PM→真实CCU)', c);
}
h+='</div>';
return h;
}
//------------------ 标签页头部 ------------------
function pageHeader(id, name, dir){
const idHex = '0x'+id.toString(16).toUpperCase().padStart(4,'0');
const isTx = dir.indexOf('CCU→')===0;
return '<div class="page-hdr"><span class="raw-id">'+idHex+'</span> '+
'<span class="raw-name">'+name+'</span> '+
'<span class="badge '+(isTx?'warn':'ok')+'">'+dir+'</span></div>';
}
function rawCard(list){
if(!list || !list.length) return '<div class="card"><h2>原始报文 · 按消息ID分组</h2><div class="raw">暂无</div></div>';
const groups = {};
for(const r of list){
if(!groups[r.msgId]) groups[r.msgId] = { rows: [] };
groups[r.msgId].rows.push(r);
}
let html = '<div class="card"><h2>原始报文 · 按消息ID分组(最近 '+list.length+' 条)</h2>';
for(const id in groups){
const g = groups[id];
const first = g.rows[0];
const idHex = '0x'+Number(id).toString(16).toUpperCase().padStart(4,'0');
const name = first.name || '';
const dirText = first.dirText || (first.direction===0?'接收':'发送');
html += '<div class="raw-group">';
html += '<div class="raw-group-hdr"><span class="raw-id">'+idHex+'</span> '+
'<span class="raw-name">'+name+'</span> '+
'<span class="badge '+(first.direction===0?'ok':'warn')+'">'+dirText+'</span> '+
'<span class="raw-count">'+g.rows.length+' 帧</span></div>';
html += '<table><tr><th>时间</th><th>方向</th><th>校验</th><th>帧数据</th></tr>';
for(const r of g.rows){
const t=new Date(r.time*1000).toLocaleTimeString();
const dText = r.dirText || (r.direction===0?'接收':'发送');
const dCls = r.direction===0?'ok':'warn';
html += '<tr><td>'+t+'</td>'+
'<td><span class="badge '+dCls+'">'+dText+'</span></td>'+
'<td>'+(r.checksumOk?'✓':'✗')+'</td>'+
'<td class="raw">'+r.hex+'</td></tr>';
}
html += '</table></div>';
}
html += '</div>';
return html;
}
function render(snap){
const pages = {
// 0x0002 FC 状态反馈(FC→CCU):仅含 FC 消息字段
fcStatus: pageHeader(0x0002,'FC状态反馈','FC→CCU')+'<div class="grid">'+
fcCard(snap.fc)+hydroCard(snap.fc)+dcdcCard(snap.fc)+exhaustCard(snap.fc)
+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>',
// 锂电池 BCU 节点(CAN 总线,经 pCanBridge 透传,按节点分组展示)
bms: '<div class="grid">'+bmsCard(snap.bms)+
bcuCtrlStatCard(snap.bcuCtrl)+
'<div class="card"><h2>报文来源 (CAN ID)</h2>'+
row('0x10XX0000','累加电压 / 回路电流 / SOC / 告警码 / 自检状态')+
row('0x10XX0001','最高/最低/平均单体电压及编号')+
row('0x10XX0002','最高/最低/平均单体温度及编号')+
row('0x10XX0003','正/负极继电器状态')+
row('0x10XX0006','正/负极绝缘阻抗 / 端口电压 / 继电器外侧电压')+
row('0x10XX0010','告警位(附录1,按位解析显示告警含义)')+
row('0x10XX81FF','断路器控制下发(MBMS→BCU,总正/总负继电器,总正+总负同帧)')+
row('XX','BCU 节点地址 01h~36h,同一节点的报文归入同一卡片')+
row('订阅消息','pCanBridge 发布的 CAN_0x* 二进制报文')+
row('下行通道','pCCU 发布 CAN_TX_0x* → pCanBridge → USBCAN')+'</div></div>',
fcCmd: pageHeader(0x0001,'FC控制指令','CCU→FC')+'<div class="grid">'+fcCmdCard(snap.fcCmd)+'</div>',
// 0x0004 PM 状态报文(CCU→PM):含锂电池/应急电池数据
pmStatus: pageHeader(0x0004,'PM状态报文','CCU→PM')+'<div class="grid">'+
pmStatusCard(snap.pmStatus)+batteryCard(snap.pmStatus)+emergCard(snap.pmStatus)+'</div>',
pmFb: pageHeader(0x0003,'PM参数反馈','CCU→PM')+'<div class="grid">'+pmFbCard(snap.pmFb)+'</div>',
links: '<div class="grid">'+linkCard(snap)+'</div>',
realCcu: realCcuPage(snap.realCcu),
raw: rawCard(snap.logs)
};
let html = '';
for (const t of TABS) {
html += '<div class="page" id="page-'+t+'"'+(t===activeTab?'':' style="display:none"')+'>'+pages[t]+'</div>';
}
pagesEl.innerHTML = html;
}
// 标签页切换
document.addEventListener('click', function(e){
const t = e.target;
const btn = (t && t.closest) ? t.closest('.tab') : null;
if (!btn) return;
activeTab = btn.getAttribute('data-tab');
document.querySelectorAll('.tab').forEach(function(b){ b.classList.toggle('active', b===btn); });
document.querySelectorAll('.page').forEach(function(p){ p.style.display = (p.id==='page-'+activeTab)?'':'none'; });
});
function connect(){
const proto = location.protocol==='https:'?'wss':'ws';
const ws = new WebSocket(proto+'://'+location.host+'/ws');
ws.onopen = ()=>{ statusEl.textContent='已连接'; statusEl.className='badge ok'; };
ws.onclose = ()=>{ statusEl.textContent='已断开,重连中...'; statusEl.className='badge err'; setTimeout(connect, 2000); };
ws.onerror = ()=>{ ws.close(); };
ws.onmessage = (ev)=>{ try{ render(JSON.parse(ev.data)); }catch(e){} };
}
connect();
</script>
</body>
</html>
)HTML";
} // namespace ccu
#endif // PCCU_PAGE_INDEX_H
-43
View File
@@ -1,43 +0,0 @@
#--------------------------------------------------------
# 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}
)
-331
View File
@@ -1,331 +0,0 @@
#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() {
for (size_t i = 0; i < m_channels.size(); ++i) {
if (m_channels[i].endpoint) {
m_channels[i].endpoint->stop();
delete m_channels[i].endpoint;
m_channels[i].endpoint = nullptr;
}
}
m_channels.clear();
if (m_db) {
m_db->close();
delete m_db;
m_db = nullptr;
}
}
//---------------------------------------------------------
// OnStartUp:读取配置并启动各通道 CAN 链路
//
// 通道配置(可重复多行):
// channel = <通道名>,<转换器IP>,<工作端口>
// 兼容旧单通道配置:can_host / can_port / can_channel_name
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());
// 旧单通道配置缓存(存在 can_host 时回退使用)
bool hasLegacy = false;
string legacyHost, legacyName = "CAN0";
long legacyPort = 0;
STRING_LIST::iterator p;
for (p = sParams.begin(); p != sParams.end(); p++) {
string orig = *p;
string line = *p;
string param = stripBlankEnds(tolower(biteStringX(line, '=')));
string value = stripBlankEnds(line);
bool handled = true;
if (param == "channel") {
// channel = <通道名>,<IP>,<端口>
string name = stripBlankEnds(biteStringX(value, ','));
string host = stripBlankEnds(biteStringX(value, ','));
string port = stripBlankEnds(value);
ChannelCfg cfg;
cfg.name = name;
cfg.host = host;
cfg.port = atol(port.c_str());
if (cfg.name.empty() || cfg.host.empty() || cfg.port <= 0) {
reportConfigWarning("bad channel line (want name,host,port): " + orig);
} else {
Channel ch;
ch.cfg = cfg;
m_channels.push_back(ch);
}
}
else if (param == "default_channel") m_defaultChannel = value;
else if (param == "dbpath") m_dbPath = value;
// 旧单通道配置(向后兼容)
else if (param == "can_host") { legacyHost = value; hasLegacy = true; }
else if (param == "can_port") { legacyPort = atol(value.c_str()); hasLegacy = true; }
else if (param == "can_channel_name") { legacyName = value; }
else handled = false;
if (!handled)
reportUnhandledConfigWarning(orig);
}
// 无 channel 行但有旧配置 -> 构造单通道
if (m_channels.empty() && hasLegacy &&
!legacyHost.empty() && legacyPort > 0) {
ChannelCfg cfg;
cfg.name = legacyName;
cfg.host = legacyHost;
cfg.port = legacyPort;
Channel ch;
ch.cfg = cfg;
m_channels.push_back(ch);
reportConfigWarning("legacy can_host/can_port config used; "
"prefer 'channel = name,host,port' lines");
}
registerVariables();
if (m_channels.empty())
reportRunWarning("no CAN channel configured");
// 数据库(CAN 帧落库,channel 列区分通道)
m_db = new CanDbStore(m_dbPath);
if (!m_db->open()) {
reportRunWarning("CAN db open failed: " + m_dbPath + " (" + m_db->lastError() + ")");
delete m_db;
m_db = nullptr;
}
// 各通道 CAN 链路(TCP Client,收帧回调在各自接收线程中触发)
for (size_t i = 0; i < m_channels.size(); ++i) {
Channel& ch = m_channels[i];
ch.endpoint = new CanEndpoint();
ch.endpoint->configure(ch.cfg.host, ch.cfg.port);
const string name = ch.cfg.name; // 按通道捕获
ch.endpoint->setFrameCallback(
[this, name](uint8_t fi, uint32_t id, const uint8_t* data, int dlc) {
handleFrame(name, fi, id, data, dlc);
});
if (!ch.endpoint->start())
reportRunWarning("CAN endpoint start failed: " + ch.cfg.name +
" (" + ch.cfg.host + ")");
}
cout << "pCanBridge started with " << m_channels.size() << " channel(s):";
for (size_t i = 0; i < m_channels.size(); ++i)
cout << " " << m_channels[i].cfg.name << "="
<< m_channels[i].cfg.host << ":" << m_channels[i].cfg.port;
cout << ", db: " << m_dbPath << endl;
return true;
}
//---------------------------------------------------------
// OnConnectToServer
bool CanBridge::OnConnectToServer() {
registerVariables();
return true;
}
//---------------------------------------------------------
// registerVariables:订阅下行指令(CAN_TX_0x*,pCCU 发布的
// BCU 断路器控制帧等,通道经 m_sSrcAux 指定)。
void CanBridge::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// 通配订阅必须用三参数重载(变量模式 + 来源模式)
Register("CAN_TX_0x*", "*", 0);
}
//---------------------------------------------------------
// OnNewMail:下行指令 CAN_TX_0x%08X(二进制数据域)
bool CanBridge::OnNewMail(MOOSMSG_LIST &NewMail) {
AppCastingMOOSApp::OnNewMail(NewMail);
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
if (msg.m_sKey.rfind("CAN_TX_0x", 0) == 0)
handleTxMessage(msg);
}
return true;
}
//---------------------------------------------------------
// findChannel:按通道名查找运行通道
CanBridge::Channel* CanBridge::findChannel(const std::string& name) {
for (size_t i = 0; i < m_channels.size(); ++i) {
if (m_channels[i].cfg.name == name && m_channels[i].endpoint)
return &m_channels[i];
}
return nullptr;
}
//---------------------------------------------------------
// resolveTxChannel:下行目标通道解析(回退规则)
// 1) srcAux 非空:命中通道则用之,否则告警丢弃
// 2) srcAux 为空:default_channel 命中则用之
// 3) 仍无:仅单通道时用该通道,多通道告警丢弃
CanBridge::Channel* CanBridge::resolveTxChannel(const std::string& srcAux) {
if (!srcAux.empty()) {
Channel* ch = findChannel(srcAux);
if (!ch)
reportRunWarning("CAN_TX unknown channel: '" + srcAux + "'");
return ch;
}
if (!m_defaultChannel.empty()) {
Channel* ch = findChannel(m_defaultChannel);
if (ch) return ch;
}
if (m_channels.size() == 1)
return m_channels[0].endpoint ? &m_channels[0] : nullptr;
reportRunWarning("CAN_TX no channel (empty srcAux) with " +
to_string(m_channels.size()) + " channels, dropped");
return nullptr;
}
//---------------------------------------------------------
// handleTxMessage:MOOS 下行消息 -> CANET 13 字节帧 -> 对应通道
//
// m_sKey = "CAN_TX_0x%08X"(CAN ID)
// m_sVal = 二进制数据域(dlc<=8)
// m_sSrcAux = 目标通道名(空时按回退规则)
// 帧信息:id>0x7FF 视为扩展帧(bit7=1),DLC=数据字节数。
void CanBridge::handleTxMessage(CMOOSMsg& msg) {
++m_txCount;
uint32_t id = 0;
// "CAN_TX_0x" 前缀长 9 字符(注意不是 8)
int sr = std::sscanf(msg.m_sKey.c_str() + 9, "%x", &id);
if (sr != 1) {
++m_txDropCount;
reportRunWarning("CAN_TX bad key: " + msg.m_sKey);
return;
}
if (!msg.IsBinary() || msg.GetBinaryDataSize() <= 0 ||
msg.GetBinaryDataSize() > 8) {
++m_txDropCount;
reportRunWarning("CAN_TX bad data size: " + msg.m_sKey);
return;
}
Channel* ch = resolveTxChannel(msg.GetSourceAux());
if (!ch) {
++m_txDropCount;
return;
}
const unsigned char* d = msg.GetBinaryData();
if (!d) {
++m_txDropCount;
return;
}
int dlc = static_cast<int>(msg.GetBinaryDataSize());
uint8_t frameInfo = static_cast<uint8_t>(((id > 0x7FF) ? 0x80 : 0x00) | dlc);
if (ch->endpoint->sendFrame(frameInfo, id, d, dlc)) {
char hex[32] = {0};
for (int i = 0; i < dlc && i < 8; ++i)
std::snprintf(hex + i * 2, 3, "%02X", d[i]);
std::cout << "[CAN-TX " << ch->cfg.name << "] 0x" << std::hex << id
<< std::dec << " dlc=" << dlc << " data=" << hex << std::endl;
} else {
reportRunWarning("CAN_TX send failed on " + ch->cfg.name + ": " +
msg.m_sKey +
(ch->endpoint->isConnected() ? "" : " (未连接)"));
}
}
//---------------------------------------------------------
// Iterate:周期任务
bool CanBridge::Iterate() {
AppCastingMOOSApp::Iterate();
AppCastingMOOSApp::PostReport();
return true;
}
//---------------------------------------------------------
// handleFrame:CAN 帧 -> CMOOSMsg(二进制) -> MOOSDB
//
// 在各通道 CanEndpoint 接收线程中调用;m_Comms::Post 线程安全。
void CanBridge::handleFrame(const std::string& channel, uint8_t frameInfo,
uint32_t id, const uint8_t* data, int dlc) {
// 落库(WAL + 预处理语句,~60帧/秒无压力)
if (m_db) m_db->onFrame(channel, 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(channel); // 通道 -> m_sSrcAux
msg.SetDoubleAux(static_cast<double>(frameInfo)); // FF/RTR/DLC -> m_dfVal2
m_Comms.Post(msg);
}
//---------------------------------------------------------
// buildReport:AppCasting 报告(逐通道)
bool CanBridge::buildReport() {
m_msgs << "============================================" << "\n";
m_msgs << "pCanBridge CAN<->MOOSDB 多通道双向透传" << "\n";
m_msgs << "============================================" << "\n";
m_msgs << "通道数: " << m_channels.size() << "\n";
if (!m_defaultChannel.empty())
m_msgs << "默认通道: " << m_defaultChannel << "\n";
m_msgs << "MOOS下发数: " << m_txCount << " (丢弃 " << m_txDropCount << ")\n";
for (size_t i = 0; i < m_channels.size(); ++i) {
const Channel& ch = m_channels[i];
m_msgs << "--------------------------------------------" << "\n";
m_msgs << "[" << ch.cfg.name << "] " << ch.cfg.host << ":"
<< ch.cfg.port << "\n";
if (ch.endpoint) {
m_msgs << " 连接状态: "
<< (ch.endpoint->isConnected() ? "已连接" : "断开") << "\n";
m_msgs << " 收帧数: " << ch.endpoint->frameCount() << "\n";
m_msgs << " 下行帧数: " << ch.endpoint->txCount()
<< " (错误 " << ch.endpoint->txErrorCount() << ")\n";
m_msgs << " 错误数: " << ch.endpoint->errorCount() << "\n";
m_msgs << " 重连次数: " << ch.endpoint->reconnectCount() << "\n";
m_msgs << " 最近收帧: " << ch.endpoint->lastFrameTime() << "\n";
}
}
m_msgs << "============================================" << "\n";
if (m_db) {
m_msgs << "数据库: " << m_dbPath
<< (m_db->isOpen() ? " (已打开)" : " (未打开)") << "\n";
m_msgs << "落库记录数: " << m_db->count() << "\n";
}
return true;
}
} // namespace canbridge
-106
View File
@@ -1,106 +0,0 @@
#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>
#include <string>
#include <vector>
namespace canbridge {
//============================================================================
// CanBridge:CAN <-> MOOSDB 多通道双向透传桥(MOOS 应用外壳)。
//
// 通道配置(.moos 中可重复多行):
// channel = <通道名>,<转换器IP>,<工作端口>
// 如 channel = CAN0,192.168.0.222,4001(CANET: CAN0=4001..CAN7=4008)
// 每通道一条独立 TCP 连接 + 独立接收线程。
//
// 数据流(上行,每通道一条 TCP 连接):
// CANET 工作端口 (TCP Server)
// -> CanEndpoint(TCP Client,13 字节切帧)
// -> handleFrame() 解析
// -> Notify 到 MOOSDB
//
// 数据流(下行,BCU 断路器/继电器控制等):
// MOOS "CAN_TX_0x%08X"(二进制数据域,dlc<=8)
// -> OnNewMail 解析 CAN ID 与数据
// -> 按消息 m_sSrcAux(目标通道名)路由到对应通道的 CanEndpoint
// -> CanEndpoint::sendFrame(该通道 TCP 连接写出 13 字节帧)
// -> CANET -> CAN 总线
//
// MOOS 消息映射(上行/下行对称,通道一律走 m_sSrcAux):
// 上行 m_sKey = "CAN_0x%08X" CAN ID(完整 4 字节,标准/扩展帧同格式)
// 上行 m_sVal = 二进制 data(dlc 字节,MOOS_BINARY_STRING)
// 上行 m_sSrcAux = 通道名(如 "CAN0")
// 上行 m_dfVal2 = 原始帧信息字节 byte0(bit7 FF / bit6 RTR / bit3~0 DLC)
// 下行 m_sKey = "CAN_TX_0x%08X",m_sVal = 二进制 data,m_sSrcAux = 目标通道名
// m_nID 不使用(MOOS 内部消息序号)
//
// 下行通道回退规则(m_sSrcAux 为空时):
// 1) 配置了 default_channel 且命中 -> 用该通道
// 2) 仅配置了单通道 -> 用该通道
// 3) 多通道且无 default_channel -> 告警丢弃
// m_sSrcAux 非空但未命中任何通道 -> 告警丢弃
//
// 每帧(上行)同时落库 SQLite(can_frame 表,配置项 dbpath);
// 下行帧仅日志记录(can_frame 表无方向列,保持旧库兼容)。
//============================================================================
class CanBridge : public AppCastingMOOSApp {
public:
CanBridge();
~CanBridge();
protected:
bool OnNewMail(MOOSMSG_LIST &NewMail);
bool Iterate();
bool OnConnectToServer();
bool OnStartUp();
bool buildReport();
void registerVariables();
private:
// 通道配置(.moos 重复行 channel = name,host,port)
struct ChannelCfg {
std::string name;
std::string host;
long port = 0;
};
// 运行中的通道(配置 + 独立 TCP 连接)
struct Channel {
ChannelCfg cfg;
CanEndpoint* endpoint = nullptr;
};
// 收帧回调(CanEndpoint 接收线程调用)
void handleFrame(const std::string& channel, uint8_t frameInfo,
uint32_t id, const uint8_t* data, int dlc);
// 下行指令处理(OnNewMail 调用):CAN_TX_0x%08X -> 按通道路由发送
void handleTxMessage(CMOOSMsg& msg);
// 按通道名查找运行通道;未命中返回 nullptr
Channel* findChannel(const std::string& name);
// 下行目标通道解析(含回退规则);返回通道指针,nullptr 表示丢弃
Channel* resolveTxChannel(const std::string& srcAux);
// 配置
std::vector<Channel> m_channels;
std::string m_defaultChannel; // default_channel(可空)
std::string m_dbPath = "pCanBridge_data.db"; // SQLite 路径(moos 配置项 dbpath)
// 组件
CanDbStore* m_db = nullptr;
std::atomic<unsigned long> m_txCount{0}; // MOOS 下行接收总数
std::atomic<unsigned long> m_txDropCount{0}; // 下行因通道解析失败丢弃数
};
} // namespace canbridge
#endif // PCANBRIDGE_CAN_BRIDGE_H
-111
View File
@@ -1,111 +0,0 @@
/****************************************************************/
/* 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 between ");
blk(" multi-channel CANET Ethernet-CAN converters (TCP Server mode) ");
blk(" and the MOOSDB. Each channel uses its own TCP connection ");
blk(" (CANET working port). Every CAN frame is parsed from the ");
blk(" 13-byte CANET wire format and transparently published as a ");
blk(" binary MOOS message (channel tagged via m_sSrcAux). ");
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(" // CANET 通道(可重复多行):通道名,IP,工作端口 ");
blk(" // 工作端口: CAN0=4001 CAN1=4002 ... CAN7=4008 ");
blk(" channel = CAN0,192.168.0.222,4001 ");
blk(" channel = CAN1,192.168.0.222,4002 ");
blk(" ");
blk(" // CAN_TX 下行默认通道(m_sSrcAux 为空时使用,可省略) ");
blk(" default_channel = 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(" CAN_TX_0x%08X = 下行 CAN 帧(每帧一条二进制消息) ");
blk(" m_sVal: 二进制 data(dlc<=8 字节, MOOS_BINARY_STRING) ");
blk(" m_sSrcAux: 目标通道名(如 CAN0;空时按回退规则: ");
blk(" default_channel > 单通道 > 多通道告警丢弃) ");
blk(" ");
blk("PUBLICATIONS: ");
blk("------------------------------------ ");
blk(" CAN_0x%08X = CAN 帧(每帧一条二进制消息) ");
blk(" 变量名: CAN ID 完整 4 字节十六进制, 如 CAN_0x10010001 ");
blk(" m_sVal: 二进制 data(dlc 字节, MOOS_BINARY_STRING) ");
blk(" m_sSrcAux: 通道名(如 CAN0) ");
blk(" m_dfVal2: 原始帧信息字节 byte0 ");
blk(" bit7=FF(1扩展/0标准) bit6=RTR(1远程/0数据) ");
blk(" bit3~0=DLC ");
blk(" ");
exit(0);
}
void showReleaseInfoAndExit() {
showReleaseInfo("pCanBridge", "gpl");
exit(0);
}
-15
View File
@@ -1,15 +0,0 @@
/****************************************************************/
/* NAME: CanBridge_Info */
/* FILE: CanBridge_Info.h */
/****************************************************************/
#ifndef PCANBRIDGE_INFO_HEADER
#define PCANBRIDGE_INFO_HEADER
void showSynopsis();
void showHelpAndExit();
void showExampleConfigAndExit();
void showInterfaceAndExit();
void showReleaseInfoAndExit();
#endif
-158
View File
@@ -1,158 +0,0 @@
#include "CanDbStore.h"
#include "sqlite3.h"
#include "MOOS/libMOOS/Utils/MOOSUtilityFunctions.h"
#include <cstdio>
namespace canbridge {
namespace {
void toHex(const uint8_t* data, int len, std::string& out) {
static const char* hex = "0123456789ABCDEF";
out.clear();
out.reserve(static_cast<size_t>(len) * 2);
for (int i = 0; i < len; ++i) {
out += hex[(data[i] >> 4) & 0xF];
out += hex[data[i] & 0xF];
}
}
} // namespace
CanDbStore::CanDbStore(const std::string& dbPath) : m_dbPath(dbPath) {}
CanDbStore::~CanDbStore() {
close();
}
bool CanDbStore::exec(const char* sql) {
char* err = nullptr;
int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &err);
if (rc != SQLITE_OK) {
m_lastError = err ? err : "sqlite error";
sqlite3_free(err);
return false;
}
return true;
}
bool CanDbStore::open() {
if (m_db) return true;
if (sqlite3_open(m_dbPath.c_str(), &m_db) != SQLITE_OK) {
m_lastError = m_db ? sqlite3_errmsg(m_db) : "cannot open database";
sqlite3_close(m_db);
m_db = nullptr;
return false;
}
sqlite3_busy_timeout(m_db, 2000);
exec("PRAGMA journal_mode=WAL;");
exec("PRAGMA synchronous=NORMAL;");
// can_frame:收到的每个 CAN 帧一条记录
if (!exec(
"CREATE TABLE IF NOT EXISTS can_frame ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" time REAL NOT NULL," // unix 时间(含小数秒)
" channel TEXT NOT NULL," // 通道名(CAN0~CAN7)
" frame_info INTEGER NOT NULL," // 原始帧信息字节 byte0
" can_id INTEGER NOT NULL," // CAN ID
" dlc INTEGER NOT NULL," // 数据长度 0~8
" hex TEXT NOT NULL" // 数据十六进制
");")) return false;
exec("CREATE INDEX IF NOT EXISTS idx_can_frame_time ON can_frame(time);");
exec("CREATE INDEX IF NOT EXISTS idx_can_frame_id ON can_frame(can_id);");
return prepareInsert();
}
bool CanDbStore::prepareInsert() {
const char* sql =
"INSERT INTO can_frame (time, channel, frame_info, can_id, dlc, hex) "
"VALUES (?1, ?2, ?3, ?4, ?5, ?6);";
if (sqlite3_prepare_v2(m_db, sql, -1, &m_stmtInsert, nullptr) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
return false;
}
return true;
}
void CanDbStore::close() {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_stmtInsert) {
sqlite3_finalize(m_stmtInsert);
m_stmtInsert = nullptr;
}
if (m_db) {
sqlite3_close(m_db);
m_db = nullptr;
}
}
void CanDbStore::onFrame(const std::string& channel, uint8_t frameInfo,
uint32_t canId, const uint8_t* data, int dlc) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db || !m_stmtInsert) return;
std::string hex;
if (data && dlc > 0) toHex(data, dlc, hex);
sqlite3_reset(m_stmtInsert);
sqlite3_clear_bindings(m_stmtInsert);
sqlite3_bind_double(m_stmtInsert, 1, MOOSTime(false));
sqlite3_bind_text(m_stmtInsert, 2, channel.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(m_stmtInsert, 3, frameInfo);
sqlite3_bind_int64(m_stmtInsert, 4, static_cast<sqlite3_int64>(canId));
sqlite3_bind_int(m_stmtInsert, 5, dlc);
sqlite3_bind_text(m_stmtInsert, 6, hex.c_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(m_stmtInsert) != SQLITE_DONE) {
m_lastError = sqlite3_errmsg(m_db);
}
}
std::vector<CanFrameRow> CanDbStore::queryRecent(uint32_t canId, int limit) {
std::lock_guard<std::mutex> lock(m_mutex);
std::vector<CanFrameRow> rows;
if (!m_db) return rows;
if (limit <= 0) limit = 100;
std::string sql = "SELECT id, time, channel, frame_info, can_id, dlc, hex "
"FROM can_frame";
if (canId != 0) {
sql += " WHERE can_id=" + std::to_string(canId);
}
sql += " ORDER BY id DESC LIMIT " + std::to_string(limit) + ";";
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
m_lastError = sqlite3_errmsg(m_db);
return rows;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
CanFrameRow r;
r.id = sqlite3_column_int64(stmt, 0);
r.time = sqlite3_column_double(stmt, 1);
r.channel = sqlite3_column_text(stmt, 2)
? reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2)) : "";
r.frameInfo = sqlite3_column_int(stmt, 3);
r.canId = static_cast<uint32_t>(sqlite3_column_int64(stmt, 4));
r.dlc = sqlite3_column_int(stmt, 5);
r.hex = sqlite3_column_text(stmt, 6)
? reinterpret_cast<const char*>(sqlite3_column_text(stmt, 6)) : "";
rows.push_back(std::move(r));
}
sqlite3_finalize(stmt);
return rows;
}
long long CanDbStore::count() const {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_db) return 0;
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(m_db, "SELECT COUNT(*) FROM can_frame;", -1, &stmt, nullptr) != SQLITE_OK)
return 0;
long long n = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) n = sqlite3_column_int64(stmt, 0);
sqlite3_finalize(stmt);
return n;
}
} // namespace canbridge
-67
View File
@@ -1,67 +0,0 @@
#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
-242
View File
@@ -1,242 +0,0 @@
#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 字节 CANET 帧并写出
// 接收线程可能并发断线重连(m_fd 被关闭),用 m_txMutex 串行化:
// - 持锁期间检查 m_connected,避免对已关闭 fd 发送
// - 接收线程的 closeSocket 不持此锁,若恰逢发送中出现 EPIPE 等
// 错误按发送失败计,断线由接收线程统一检出
//----------------------------------------------------------------------
bool CanEndpoint::sendFrame(uint8_t frameInfo, uint32_t id,
const uint8_t* data, int dlc) {
if (dlc < 0 || dlc > 8) return false;
uint8_t frame[kFrameLen] = {0};
frame[0] = frameInfo;
frame[1] = static_cast<uint8_t>((id >> 24) & 0xFF);
frame[2] = static_cast<uint8_t>((id >> 16) & 0xFF);
frame[3] = static_cast<uint8_t>((id >> 8) & 0xFF);
frame[4] = static_cast<uint8_t>(id & 0xFF);
if (data && dlc > 0)
std::memcpy(frame + 5, data, static_cast<size_t>(dlc));
std::lock_guard<std::mutex> lock(m_txMutex);
if (!m_connected || m_fd < 0) {
++m_txErrorCount;
std::cerr << "[CanEndpoint] send failed: not connected" << std::endl;
return false;
}
size_t sent = 0;
while (sent < kFrameLen) {
ssize_t n = ::send(m_fd, frame + sent, kFrameLen - sent, MSG_NOSIGNAL);
if (n > 0) {
sent += static_cast<size_t>(n);
continue;
}
if (n < 0 && (errno == EINTR)) continue;
++m_txErrorCount;
std::cerr << "[CanEndpoint] send error: "
<< std::strerror(errno) << std::endl;
return false;
}
++m_txCount;
return true;
}
//----------------------------------------------------------------------
// 切帧:缓冲 >= 13 字节即解析并回调
//----------------------------------------------------------------------
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
-98
View File
@@ -1,98 +0,0 @@
#ifndef PCANBRIDGE_CAN_ENDPOINT_H
#define PCANBRIDGE_CAN_ENDPOINT_H
#include <cstdint>
#include <string>
#include <vector>
#include <mutex>
#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; }
// 下行发送一帧 CAN(MOOS->CAN 方向,须已连接)。
// frameInfo: bit7 FF(1扩展/0标准) bit6 RTR bit3~0 DLC;
// data 为 dlc(0~8) 字节数据域。返回是否成功写出。
bool sendFrame(uint8_t frameInfo, uint32_t id, const uint8_t* data, int dlc);
// 统计
unsigned long frameCount() const { return m_frameCount; }
unsigned long txCount() const { return m_txCount; }
unsigned long txErrorCount() const { return m_txErrorCount; }
unsigned long errorCount() const { return m_errorCount; }
unsigned long reconnectCount() const { return m_reconnectCount; }
double lastFrameTime() const { return m_lastFrameTime; }
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;
std::mutex m_txMutex; // 下行 send 与断线 close 的并发保护
CanFrameCallback m_onFrame;
// TCP 流式接收缓冲(跨 recv 保持半包/粘包状态)
std::vector<uint8_t> m_buf;
std::atomic<unsigned long> m_frameCount{0};
std::atomic<unsigned long> m_txCount{0};
std::atomic<unsigned long> m_txErrorCount{0};
std::atomic<unsigned long> m_errorCount{0};
std::atomic<unsigned long> m_reconnectCount{0};
std::atomic<double> m_lastFrameTime{0.0};
};
} // namespace canbridge
#endif // PCANBRIDGE_CAN_ENDPOINT_H
-49
View File
@@ -1,49 +0,0 @@
/************************************************************/
/* 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;
}
-35
View File
@@ -1,35 +0,0 @@
//============================================================================
// pCanBridge 配置示例(多通道)
//
// 拓扑(双向透传,每通道一条独立 TCP 连接):
// 上行:CANET 工作端口(TCP Server)-> pCanBridge(TCP Client)
// -> MOOS "CAN_0x%08X"(二进制 data,m_sSrcAux=通道名)
// 下行:MOOS "CAN_TX_0x%08X"(二进制 data,m_sSrcAux=目标通道,
// 如 BCU 断路器控制帧 0x10XX81FF,由 pCCU 发布)
// -> pCanBridge 按通道路由 -> CANET -> CAN 总线
// 变量名 CAN_0x%08X = CAN ID;m_sVal = 二进制 data;
// m_sSrcAux = 通道名;上行 m_dfVal2 = 原始帧信息字节(FF/RTR/DLC)
//============================================================================
ProcessConfig = pCanBridge
{
AppTick = 4
CommsTick = 4
//======== 通道配置(可重复多行) ========
// 格式:channel = <通道名>,<转换器IP>,<工作端口>
// CANET 工作端口:CAN0=4001,CAN1=4002,...,CAN7=4008
channel = CAN0,192.168.0.222,4001
// channel = CAN1,192.168.0.222,4002
// channel = CAN2,192.168.0.222,4003
// ...
//======== 下行默认通道(可空) ========
// CAN_TX 消息 m_sSrcAux 为空时:配置了本项则用它;
// 未配置且仅单通道则用该通道;多通道则告警丢弃。
default_channel = CAN0
//======== 存储 ========
// CAN 帧 SQLite 落库路径(can_frame 表,channel 列区分通道)
dbpath = pCanBridge_data.db
}
-46
View File
@@ -1,46 +0,0 @@
#--------------------------------------------------------
# The CMakeLists.txt for: pMotor
# 推进电机操作程序:网页控制 <-> pCanBridge(CAN1) <-> 电机控制器
#--------------------------------------------------------
if (${WIN32})
SET(SYSTEM_LIBS wsock32)
else (${WIN32})
SET(SYSTEM_LIBS m pthread)
endif (${WIN32})
# 复用仓库内 pPowerManger 的公共源码(相对路径引用,避免重复维护)
SET(PM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../pPowerManger)
SET(SHARED_SRC
${PM_DIR}/logc/loguru.cpp
${PM_DIR}/httpserver/mongoose.c
)
SET(SRC
${SHARED_SRC}
protocol/MotorCan.cpp
web/WebServer.cpp
Motor.cpp
Motor_Info.cpp
main.cpp
)
ADD_EXECUTABLE(pMotor ${SRC})
TARGET_INCLUDE_DIRECTORIES(pMotor PRIVATE
${PM_DIR}
${PM_DIR}/httpserver
${PM_DIR}/logc
)
TARGET_LINK_LIBRARIES(pMotor
${MOOS_LIBRARIES}
apputil
mbutil
m
pthread
jsoncpp
dl
${SYSTEM_LIBS}
)
-467
View File
@@ -1,467 +0,0 @@
#include "Motor.h"
#include "MBUtils.h"
#include "../pPowerManger/logc/loguru.hpp"
#include "json/json.h"
#include <cstdio>
#include <sstream>
using namespace motor;
using namespace std;
//---------------------------------------------------------
// Constructor / Destructor
Motor::Motor() {}
Motor::~Motor() {
if (m_web) { m_web->stop(); delete m_web; m_web = nullptr; }
}
//---------------------------------------------------------
// OnStartUp:读取配置并初始化各组件
bool Motor::OnStartUp() {
AppCastingMOOSApp::OnStartUp();
STRING_LIST sParams;
m_MissionReader.EnableVerbatimQuoting(false);
if (!m_MissionReader.GetConfiguration(GetAppName(), sParams))
reportConfigWarning("No config block found for " + GetAppName());
STRING_LIST::iterator p;
for (p = sParams.begin(); p != sParams.end(); p++) {
string orig = *p;
string line = *p;
string param = stripBlankEnds(tolower(biteStringX(line, '=')));
string value = stripBlankEnds(line);
bool handled = true;
if (param == "can_channel") m_canChannel = value;
else if (param == "red_channel") m_redChannel = value;
else if (param == "cmd_period_ms") m_cmdPeriodMs = atoi(value.c_str());
else if (param == "reset_hold_sec") m_resetHoldSec = atof(value.c_str());
else if (param == "web_port") m_webPort = atoi(value.c_str());
else if (param == "web_enable") m_webEnable = (tolower(value) == "true" || value == "1");
else if (param == "logpath") m_logPath = value;
else handled = false;
if (!handled)
reportUnhandledConfigWarning(orig);
}
if (m_cmdPeriodMs < 50) m_cmdPeriodMs = 50;
registerVariables();
// 日志文件
if (m_logPath.empty()) m_logPath = "pMotor.log";
loguru::add_file(m_logPath.c_str(), loguru::Append, loguru::Verbosity_MAX);
LOG_F(INFO, "pMotor log path: %s", m_logPath.c_str());
// 网页
if (m_webEnable) {
m_web = new WebServer();
m_web->setOnOpen([this]() { return buildSnapshot(); });
m_web->setApiHandler([this](const std::string& uri, const std::string& query) {
return handleApi(uri, query);
});
if (!m_web->start(m_webPort)) {
LOG_F(ERROR, "web server start failed on port %d", m_webPort);
delete m_web;
m_web = nullptr;
}
}
LOG_F(INFO, "pMotor started: motor CAN via pCanBridge channel=%s (red_channel=%s), "
"cmd period=%dms, web=%s",
m_canChannel.c_str(), m_redChannel.c_str(), m_cmdPeriodMs,
m_web ? ("port " + to_string(m_webPort)).c_str() : "disabled");
return true;
}
//---------------------------------------------------------
// OnConnectToServer
bool Motor::OnConnectToServer() {
registerVariables();
return true;
}
//---------------------------------------------------------
// registerVariables:订阅 pCanBridge 透传的全部 CAN 帧
void Motor::registerVariables() {
AppCastingMOOSApp::RegisterVariables();
// 通配订阅必须用三参数重载(变量模式 + 来源模式)
Register("CAN_0x*", "*", 0);
}
//---------------------------------------------------------
// OnNewMail
bool Motor::OnNewMail(MOOSMSG_LIST &NewMail) {
AppCastingMOOSApp::OnNewMail(NewMail);
MOOSMSG_LIST::iterator p;
for (p = NewMail.begin(); p != NewMail.end(); p++) {
CMOOSMsg &msg = *p;
if (msg.m_sKey.rfind("CAN_0x", 0) == 0)
handleCanMessage(msg);
}
return true;
}
//---------------------------------------------------------
// handleCanMessage:解码 pCanBridge 透传的 CAN 帧
//
// 消息格式(pCanBridge 约定):
// m_sKey = "CAN_0x%08X"(CAN ID,扩展帧)
// m_sVal = 二进制数据域(8 字节)
// m_dfVal2 = 原始帧信息字节(bit7 FF / bit6 RTR / bit3~0 DLC)
void Motor::handleCanMessage(CMOOSMsg& msg) {
// 从变量名解析 CAN ID(跳过前缀 "CAN_0x" 共 6 字符)
uint32_t canId = 0;
if (std::sscanf(msg.m_sKey.c_str() + 6, "%x", &canId) != 1) return;
if (!isCanMotorId(canId)) return;
if (!(msg.IsBinary() && msg.GetBinaryDataSize() >= 8)) return;
const unsigned char* d = msg.GetBinaryData();
if (!d) return;
// 日志值在锁内取副本,避免与 Web 线程快照读取竞争
uint32_t rx = 0;
int spd = 0;
int bus = 0;
size_t f1 = 0, f2 = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!decodeMotorFrame(canId, d, static_cast<int>(msg.GetBinaryDataSize()), m_state))
return;
m_state.rxCount++;
double now = MOOSTime(false);
switch (canId) {
case MOTOR_CAN_FAULT1: case MOTOR_CAN_FAULT1_RED:
m_state.branch1.lastRxTime = now; break;
case MOTOR_CAN_FAULT2: case MOTOR_CAN_FAULT2_RED:
m_state.branch2.lastRxTime = now; break;
case MOTOR_CAN_STATE3: case MOTOR_CAN_STATE3_RED:
m_state.state3LastRx = now; break;
case MOTOR_CAN_STATE4: case MOTOR_CAN_STATE4_RED:
m_state.state4LastRx = now; break;
default: break;
}
rx = m_state.rxCount;
spd = m_state.feedbackSpeed;
bus = m_state.busVoltage;
f1 = m_state.branch1.faults.size();
f2 = m_state.branch2.faults.size();
}
// 节流日志:1s 一条
double now = MOOSTime();
if (now - m_lastRxLog >= 1.0) {
m_lastRxLog = now;
LOG_F(INFO, "[Motor/CAN] rx=%u speed=%d rpm bus=%dV faults(b1=%zu,b2=%zu)",
rx, spd, bus, f1, f2);
}
}
//---------------------------------------------------------
// Iterate:周期任务(AppTick 次/秒)
bool Motor::Iterate() {
AppCastingMOOSApp::Iterate();
double now = MOOSTime();
// 网页指令出队并应用
processCmdQueue();
// 周期发送指令帧(500ms,协议要求)
sendCmdFrame(now);
// 网页快照 1Hz 推送
if (m_web) {
static double lastWebPush = 0;
if (now - lastWebPush >= 1.0) {
lastWebPush = now;
m_web->broadcast(buildSnapshot());
}
}
AppCastingMOOSApp::PostReport();
return true;
}
//---------------------------------------------------------
// handleApi:/api/motor_cmd
std::string Motor::handleApi(const std::string& uri, const std::string& query) {
if (uri == "/api/motor_cmd")
return handleMotorCmdApi(query);
return "";
}
//---------------------------------------------------------
// handleMotorCmdApi:电机控制指令入队(按协议指令帧字段独立下发,无联动)
//
// GET /api/motor_cmd?action=enable 使能命令=1(Byte0 bit0)
// GET /api/motor_cmd?action=disable 使能命令=0,期望转速清0(Byte0 bit0,协议"停机"示例)
// GET /api/motor_cmd?action=reset 复位命令=1(Byte0 bit1,仅故障态有效)
// GET /api/motor_cmd?action=speed&speed=1000 设定期望转速(Byte2/3,s16)
// Web 线程不可直接 Notify,指令入队后由 Iterate(MOOS 线程)应用。
std::string Motor::handleMotorCmdApi(const std::string& query) {
// 简易 query 解析(k=v&k=v)
auto getParam = [&query](const char* key, std::string& out) -> bool {
std::string k = std::string(key) + "=";
size_t p = query.find(k);
if (p == std::string::npos) return false;
size_t e = query.find('&', p);
out = query.substr(p + k.size(),
(e == std::string::npos) ? std::string::npos
: e - p - k.size());
return true;
};
std::string action, speedStr;
getParam("action", action);
getParam("speed", speedStr);
MotorCmd c;
if (action == "enable") {
c.action = MOTOR_ACT_ENABLE;
} else if (action == "disable") {
c.action = MOTOR_ACT_DISABLE;
} else if (action == "reset") {
c.action = MOTOR_ACT_RESET;
} else if (action == "speed") {
c.action = MOTOR_ACT_SPEED;
c.speed = static_cast<int16_t>(atoi(speedStr.c_str()));
} else {
return "{\"ok\":false,\"error\":\"bad action (enable/disable/reset/speed)\"}";
}
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_cmdQueue.size() >= 16)
return "{\"ok\":false,\"error\":\"queue full\"}";
m_cmdQueue.push_back(c);
m_lastCmd = c;
m_lastCmdValid = true;
m_lastCmdTime = MOOSTime(false);
}
LOG_F(INFO, "[Motor/CAN] 指令入队: action=%s speed=%d", action.c_str(), c.speed);
char buf[96];
std::snprintf(buf, sizeof(buf),
"{\"ok\":true,\"action\":\"%s\",\"speed\":%d}", action.c_str(), c.speed);
return buf;
}
//---------------------------------------------------------
// processCmdQueue:出队网页指令并更新指令状态
void Motor::processCmdQueue() {
MotorCmd c;
double now = MOOSTime();
{
std::lock_guard<std::mutex> lock(m_mutex);
while (!m_cmdQueue.empty()) {
c = m_cmdQueue.front();
m_cmdQueue.erase(m_cmdQueue.begin());
switch (c.action) {
case MOTOR_ACT_ENABLE:
m_enable = true;
break;
case MOTOR_ACT_DISABLE:
m_enable = false;
m_speed = 0;
m_resetUntil = 0;
break;
case MOTOR_ACT_SPEED:
m_speed = c.speed;
break;
case MOTOR_ACT_RESET:
m_resetUntil = now + m_resetHoldSec;
break;
default:
break;
}
LOG_F(INFO, "[Motor/CAN] 指令应用: action=%d speed=%d enable=%d",
c.action, m_speed, m_enable ? 1 : 0);
}
}
}
//---------------------------------------------------------
// sendCmdFrame:按周期编码并发送指令帧
// 主通道 0x18EF2010;配置 red_channel 时同帧发 0x18EF2011 到冗余通道
void Motor::sendCmdFrame(double now) {
double lastTx = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (now - m_lastTx < m_cmdPeriodMs / 1000.0)
return;
lastTx = m_lastTx;
}
if (now - lastTx < m_cmdPeriodMs / 1000.0)
return;
uint8_t data[8];
{
std::lock_guard<std::mutex> lock(m_mutex);
bool reset = (now < m_resetUntil);
buildMotorCmdData(m_enable, reset, m_enable ? m_speed : 0, data);
m_lastTx = now;
}
bool ok = postCanFrame(MOTOR_CAN_CMD, data, m_canChannel);
int okCount = ok ? 1 : 0, errCount = ok ? 0 : 1;
if (!m_redChannel.empty()) {
bool okRed = postCanFrame(MOTOR_CAN_CMD_RED, data, m_redChannel);
okCount += okRed ? 1 : 0;
errCount += okRed ? 0 : 1;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_txCount += static_cast<unsigned long>(okCount);
m_txErr += static_cast<unsigned long>(errCount);
}
}
//---------------------------------------------------------
// postCanFrame:MOOS CAN_TX_0x%08X(二进制数据域,m_sSrcAux=目标通道)
// 链路:MOOS -> pCanBridge 订阅 -> 按通道路由 CanEndpoint(TCP)
// -> CANET -> CAN 总线
bool Motor::postCanFrame(uint32_t canId, const uint8_t data[8], const std::string& channel) {
char key[32];
std::snprintf(key, sizeof(key), "CAN_TX_0x%08X", canId);
CMOOSMsg msg(MOOS_NOTIFY, key, 8u, data);
msg.SetSourceAux(channel);
bool ok = m_Comms.Post(msg);
if (!ok)
LOG_F(ERROR, "[Motor/CAN] 指令帧 0x%08X 投递 MOOS 失败 (通道=%s)", canId, channel.c_str());
return ok;
}
//---------------------------------------------------------
// buildSnapshot:网页 JSON 快照(jsoncpp)
namespace {
void putI(Json::Value& j, const char* k, int v) { j[k] = Json::Value(v); }
void putU(Json::Value& j, const char* k, unsigned long v) { j[k] = Json::Value(static_cast<Json::UInt64>(v)); }
void putD(Json::Value& j, const char* k, double v) { j[k] = Json::Value(v); }
// age:距最后收到该帧的秒数;valid=false 时前端显示"从未收到"
void putAge(Json::Value& j, double lastRx, double now, bool valid) {
j["valid"] = valid ? 1 : 0;
j["age"] = valid ? (now - lastRx) : -1;
}
Json::Value branchToJson(const MotorBranchState& br, double now) {
Json::Value j(Json::objectValue);
putAge(j, br.lastRxTime, now, br.valid);
Json::Value words(Json::arrayValue);
for (int i = 0; i < 4; ++i) words.append(static_cast<int>(br.word[i]));
j["words"] = words;
Json::Value faults(Json::arrayValue);
for (size_t i = 0; i < br.faults.size(); ++i) faults.append(br.faults[i]);
j["faults"] = faults;
Json::Value alarms(Json::arrayValue);
for (size_t i = 0; i < br.alarms.size(); ++i) alarms.append(br.alarms[i]);
j["alarms"] = alarms;
return j;
}
} // namespace
std::string Motor::buildSnapshot() {
Json::Value root(Json::objectValue);
double now = MOOSTime(false);
{
std::lock_guard<std::mutex> lock(m_mutex);
// 通信链路
Json::Value link(Json::objectValue);
link["channel"] = m_canChannel;
link["redChannel"] = m_redChannel;
link["periodMs"] = m_cmdPeriodMs;
putU(link, "txCount", m_txCount);
putU(link, "txErr", m_txErr);
putU(link, "rxCount", m_state.rxCount);
link["lastTxValid"] = (m_lastTx > 0) ? 1 : 0;
putD(link, "lastTxAge", m_lastTx > 0 ? (now - m_lastTx) : -1);
root["link"] = link;
// 指令状态
Json::Value cmd(Json::objectValue);
putI(cmd, "enable", m_enable ? 1 : 0);
putI(cmd, "speed", m_speed);
putI(cmd, "resetActive", (now < m_resetUntil) ? 1 : 0);
cmd["lastCmdValid"] = m_lastCmdValid ? 1 : 0;
if (m_lastCmdValid) {
static const char* kActionName[] = {"", "enable", "disable", "speed", "reset"};
int idx = (m_lastCmd.action >= 1 && m_lastCmd.action <= 4) ? m_lastCmd.action : 0;
cmd["lastCmdAction"] = kActionName[idx];
putI(cmd, "lastCmdSpeed", m_lastCmd.speed);
putD(cmd, "lastCmdAge", m_lastCmdTime > 0 ? (now - m_lastCmdTime) : -1);
}
root["cmd"] = cmd;
// 状态帧3:转速 / 母线电压
Json::Value s3(Json::objectValue);
putAge(s3, m_state.state3LastRx, now, m_state.state3Valid);
putI(s3, "speed", m_state.feedbackSpeed);
putI(s3, "voltage", m_state.busVoltage);
root["state3"] = s3;
// 状态帧4:温度
Json::Value s4(Json::objectValue);
putAge(s4, m_state.state4LastRx, now, m_state.state4Valid);
putI(s4, "tA", m_state.tempA);
putI(s4, "tB", m_state.tempB);
putI(s4, "tC", m_state.tempC);
putI(s4, "tInv", m_state.tempInv);
putI(s4, "tFilter", m_state.tempFilter);
putI(s4, "tAir", m_state.tempAir);
root["state4"] = s4;
// 故障/报警
root["fault1"] = branchToJson(m_state.branch1, now);
root["fault2"] = branchToJson(m_state.branch2, now);
}
Json::FastWriter writer;
return writer.write(root);
}
//---------------------------------------------------------
// buildReport
bool Motor::buildReport() {
m_msgs << "============================================" << "\n";
m_msgs << "pMotor 推进电机操作程序" << "\n";
m_msgs << "============================================" << "\n";
m_msgs << "CAN 通道: " << m_canChannel
<< (m_redChannel.empty() ? "" : (" 冗余: " + m_redChannel)).c_str()
<< " 指令周期: " << m_cmdPeriodMs << "ms" << "\n";
m_msgs << "指令帧下发: " << m_txCount << " 成功 / " << m_txErr << " 失败" << "\n";
{
std::lock_guard<std::mutex> lock(m_mutex);
m_msgs << "电机报文接收: " << m_state.rxCount << "\n";
m_msgs << "指令状态: enable=" << (m_enable ? 1 : 0)
<< " speed=" << m_speed
<< " reset=" << (MOOSTime(false) < m_resetUntil ? 1 : 0) << "\n";
m_msgs << "反馈转速: " << m_state.feedbackSpeed
<< " rpm 母线电压: " << m_state.busVoltage << "\n";
m_msgs << "支路一故障: " << m_state.branch1.faults.size()
<< " 条, 支路二故障: " << m_state.branch2.faults.size() << " 条" << "\n";
}
return true;
}

Some files were not shown because too many files have changed in this diff Show More