新增复合管控器(pCCU) + 惯导数据解析 + 设备控制页增强 + 0824协议文档归档
- 新增 pCCU 复合管控器程序(src/pCCU):FC/PM 双链路(UDP)、协议编解码器 (FcProtocol/PmProtocol/Frame/Message/MessageRegistry)、快照与落库(DbStore)、 内置 WebServer;挂入 src/CMakeLists.txt 与板卡 mission(h100.moos) - 惯导/深度数据:新增 uDevice_insData 消息解析,接入 UpperCommManager, pPowerMangerHost 注册监视,UpmsgTest 覆盖 - pPowerMangerHost 设备控制页增强(web/index.html) - 部署/运维脚本改为 4 个 systemd 服务(moosdb/pPowerManger/pPowerMangerHost/pCCU): deploy/clean-data/fetch-data 同步适配 - 协议文档:归档 0824 控制主机与复合管控器通信协议 xlsx 及关联 docx,移除旧 md - 测试:新增 pccu 单元/集成测试与 disSysTest Python 配电仿真 - 清理:移除未使用的 CCU 状态调试打印 coutMsg;.gitignore 补充编译产物/临时文件忽略
This commit is contained in:
+14
@@ -276,3 +276,17 @@ build-nosa-test/
|
||||
|
||||
# CMake-generated web asset table (pPowerMangerHost)
|
||||
src/pPowerMangerHost/webassets_gen.h
|
||||
|
||||
# ============================================================
|
||||
# 本机编译产物(避免误提交二进制)
|
||||
# ============================================================
|
||||
bin/pCCU
|
||||
bin/pccuTest
|
||||
|
||||
# ============================================================
|
||||
# MS Office 临时锁文件 / 编辑器、备份残留
|
||||
# ============================================================
|
||||
~$*
|
||||
*.bck
|
||||
*.bak
|
||||
* copy*
|
||||
|
||||
-1240
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
-1
@@ -1,6 +1,6 @@
|
||||
// MOOS file
|
||||
// 板卡 (RK3588 / Ubuntu 20.04) 专用 mission 配置
|
||||
// 三个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost)共用本文件。
|
||||
// 四个 systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU)共用本文件。
|
||||
// 由 scripts/deploy.sh 推送到板卡 /root/work/h100/missions/h100.moos
|
||||
|
||||
ServerHost = localhost
|
||||
@@ -30,3 +30,27 @@ ProcessConfig = pPowerMangerHost
|
||||
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 绑定 0.0.0.0:5001,
|
||||
// 故 PM 状态发送目标用回环地址即可送达。
|
||||
pm_local_port = 7000
|
||||
pm_remote_ip = 127.0.0.1
|
||||
pm_remote_port = 5001
|
||||
|
||||
dbpath = /root/work/h100/data/pccu_data.db
|
||||
logpath = /root/work/h100/data/pCCU.log
|
||||
|
||||
web_port = 8080
|
||||
web_enable = true
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
# power_data.db (+ -wal/-shm) 主机功率数据库
|
||||
# feedback_data.db 反馈落盘数据库
|
||||
# pPowerManger.log 主机运行日志
|
||||
# 默认先停止 3 个服务再清理(避免 SQLite WAL 仍在写入)。
|
||||
# 默认先停止 4 个服务再清理(避免 SQLite WAL 仍在写入)。
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/clean-data.sh # 停服务 -> 清理 data 目录(不重启)
|
||||
# ./scripts/clean-data.sh --start # 清理后重新启动 3 个服务
|
||||
# ./scripts/clean-data.sh --start # 清理后重新启动 4 个服务
|
||||
# ./scripts/clean-data.sh --no-stop # 不停服务直接清理(慎用)
|
||||
# ./scripts/clean-data.sh --stale # 额外清理 bin/ 与 src/bin/ 下的旧 db/log
|
||||
#
|
||||
@@ -38,10 +38,10 @@ DO_START=0
|
||||
DO_STOP=1
|
||||
DO_STALE=0
|
||||
|
||||
SERVICES=(moosdb pPowerManger pPowerMangerHost)
|
||||
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
|
||||
|
||||
# 板卡 data 目录下需要清理的文件(含 SQLite WAL/SHM)
|
||||
DATA_FILES=(power_data.db power_data.db-wal power_data.db-shm feedback_data.db pPowerManger.log)
|
||||
DATA_FILES=(power_data.db power_data.db-wal power_data.db-shm feedback_data.db pPowerManger.log pccu_data.db pCCU.log)
|
||||
|
||||
# 历史遗留目录(旧部署位置),仅当 --stale 时清理
|
||||
STALE_DIRS=("${BOARD_DIR}/bin" "${BOARD_DIR}/src/bin")
|
||||
@@ -123,15 +123,15 @@ fi
|
||||
# 5. 可选: 重启服务
|
||||
#-------------------------------------------------------------------
|
||||
if [ "${DO_START}" = "1" ]; then
|
||||
info "启动服务 (moosdb -> pPowerManger -> pPowerMangerHost) ..."
|
||||
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU) ..."
|
||||
${SSH} "${SSH_TARGET}" "systemctl start moosdb && sleep 1 && \
|
||||
systemctl start pPowerManger pPowerMangerHost" \
|
||||
systemctl start pPowerManger pPowerMangerHost pCCU" \
|
||||
|| { err "启动失败"; exit 1; }
|
||||
ok "服务已启动"
|
||||
else
|
||||
echo ""
|
||||
info "清理完成,服务未启动。需要重启请加 --start 或手动:"
|
||||
echo " systemctl start moosdb pPowerManger pPowerMangerHost"
|
||||
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
+22
-19
@@ -1,18 +1,18 @@
|
||||
#!/bin/bash
|
||||
#=======================================================================
|
||||
# FILE: scripts/deploy.sh
|
||||
# DESC: 部署 pPowerManger / pPowerMangerHost(aarch64 二进制 + mission)
|
||||
# 到目标板卡 (RK3588 / Ubuntu 22.04),并用三个独立 systemd 服务
|
||||
# (moosdb / pPowerManger / pPowerMangerHost)管理。
|
||||
# 只安装服务 + daemon-reload,默认不启动、不自启。
|
||||
# DESC: 部署 pPowerManger / pPowerMangerHost / pCCU(aarch64 二进制 +
|
||||
# mission)到目标板卡 (RK3588 / Ubuntu 22.04),并用四个独立
|
||||
# systemd 服务(moosdb / pPowerManger / pPowerMangerHost / pCCU)
|
||||
# 管理。只安装服务 + daemon-reload,默认不启动、不自启。
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/deploy.sh setup-ssh 一次性:生成 ed25519 key + ssh-copy-id
|
||||
# ./scripts/deploy.sh 默认:推送二进制 + mission + 装 3 个
|
||||
# ./scripts/deploy.sh 默认:推送二进制 + mission + 装 4 个
|
||||
# systemd 服务 + daemon-reload(不启动)
|
||||
# ./scripts/deploy.sh --start 部署后按依赖顺序启动服务
|
||||
# ./scripts/deploy.sh status 板卡 3 个服务状态
|
||||
# ./scripts/deploy.sh stop 停止 3 个服务
|
||||
# ./scripts/deploy.sh status 板卡 4 个服务状态
|
||||
# ./scripts/deploy.sh stop 停止 4 个服务
|
||||
# ./scripts/deploy.sh help 显示帮助
|
||||
#
|
||||
# 参数:
|
||||
@@ -24,9 +24,9 @@
|
||||
# 板卡布局:
|
||||
# /root/work/h100/bin/ aarch64 二进制
|
||||
# /root/work/h100/missions/ 板卡 mission h100.moos
|
||||
# /root/work/h100/data/ 数据库存储目录 (power_data.db)
|
||||
# /root/work/h100/data/ 数据库存储目录 (power_data.db 等)
|
||||
# /etc/systemd/system/ moosdb.service / pPowerManger.service /
|
||||
# pPowerMangerHost.service
|
||||
# pPowerMangerHost.service / pCCU.service
|
||||
#=======================================================================
|
||||
set -uo pipefail
|
||||
|
||||
@@ -38,7 +38,7 @@ USER="root"
|
||||
BOARD_DIR="/root/work/h100"
|
||||
DO_START=0
|
||||
|
||||
SERVICES=(moosdb pPowerManger pPowerMangerHost)
|
||||
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
|
||||
|
||||
info() { printf '\033[1;36m[deploy]\033[0m %s\n' "$*"; }
|
||||
ok() { printf '\033[1;32m[deploy]\033[0m %s\n' "$*"; }
|
||||
@@ -91,7 +91,7 @@ cmd_setup_ssh() {
|
||||
#-------------------------------------------------------------------
|
||||
check_local() {
|
||||
local missing=0
|
||||
for b in pPowerManger pPowerMangerHost; do
|
||||
for b in pPowerManger pPowerMangerHost pCCU; do
|
||||
if [ ! -f "${PROJECT_ROOT}/bin/arm64/${b}" ]; then
|
||||
err "缺少本地 aarch64 二进制: bin/arm64/${b}(先运行 ./scripts/build-arm64.sh)"; missing=1
|
||||
elif ! file "${PROJECT_ROOT}/bin/arm64/${b}" | grep -q "aarch64"; then
|
||||
@@ -178,16 +178,17 @@ cmd_deploy() {
|
||||
info "推送 aarch64 二进制 ..."
|
||||
${SCP} -q "${PROJECT_ROOT}/bin/arm64/pPowerManger" \
|
||||
"${PROJECT_ROOT}/bin/arm64/pPowerMangerHost" \
|
||||
"${PROJECT_ROOT}/bin/arm64/pCCU" \
|
||||
"${SSH_TARGET}:${BOARD_DIR}/bin/" || { err "推送二进制失败"; return 1; }
|
||||
|
||||
info "推送 mission h100.moos ..."
|
||||
${SCP} -q "${PROJECT_ROOT}/missions/h100.moos" \
|
||||
"${SSH_TARGET}:${BOARD_DIR}/missions/" || { err "推送 mission 失败"; return 1; }
|
||||
|
||||
${SSH} "${SSH_TARGET}" "chmod +x ${BOARD_DIR}/bin/pPowerManger ${BOARD_DIR}/bin/pPowerMangerHost"
|
||||
${SSH} "${SSH_TARGET}" "chmod +x ${BOARD_DIR}/bin/pPowerManger ${BOARD_DIR}/bin/pPowerMangerHost ${BOARD_DIR}/bin/pCCU"
|
||||
ok "二进制与 mission 已推送"
|
||||
|
||||
# 2. 生成并安装 3 个 systemd unit
|
||||
# 2. 生成并安装 4 个 systemd unit
|
||||
local unit
|
||||
for s in "${SERVICES[@]}"; do
|
||||
info "生成 ${s}.service ..."
|
||||
@@ -211,8 +212,10 @@ cmd_deploy() {
|
||||
else
|
||||
echo ""
|
||||
info "部署完成。手动启动(或加 --start):"
|
||||
echo " systemctl start moosdb pPowerManger pPowerMangerHost"
|
||||
echo " 浏览器: http://${HOST}:18080"
|
||||
echo " systemctl start moosdb pPowerManger pPowerMangerHost pCCU"
|
||||
echo " 浏览器: http://${HOST}:18080 (上位机模拟)"
|
||||
echo " http://${HOST}:8090 (pPowerManger)"
|
||||
echo " http://${HOST}:8080 (pCCU)"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -220,19 +223,19 @@ cmd_deploy() {
|
||||
# 命令: start / status / stop
|
||||
#-------------------------------------------------------------------
|
||||
cmd_start() {
|
||||
info "启动服务 (moosdb -> pPowerManger -> pPowerMangerHost) ..."
|
||||
info "启动服务 (moosdb -> pPowerManger / pPowerMangerHost / pCCU) ..."
|
||||
${SSH} "${SSH_TARGET}" "systemctl start moosdb && sleep 1 && \
|
||||
systemctl start pPowerManger pPowerMangerHost" || { err "启动失败"; return 1; }
|
||||
systemctl start pPowerManger pPowerMangerHost pCCU" || { err "启动失败"; return 1; }
|
||||
ok "服务已启动"
|
||||
cmd_status
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
${SSH} "${SSH_TARGET}" "systemctl --no-pager status moosdb pPowerManger pPowerMangerHost" || true
|
||||
${SSH} "${SSH_TARGET}" "systemctl --no-pager status ${SERVICES[*]}" || true
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
${SSH} "${SSH_TARGET}" "systemctl stop pPowerMangerHost pPowerManger moosdb" && \
|
||||
${SSH} "${SSH_TARGET}" "systemctl stop pPowerMangerHost pPowerManger pCCU moosdb" && \
|
||||
ok "服务已停止" || err "停止服务失败"
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# power_data.db (+ -wal/-shm) 主机功率数据库
|
||||
# feedback_data.db 反馈落盘数据库
|
||||
# pPowerManger.log 主机运行日志
|
||||
# 可选 --journal 一并导出三个 systemd 服务的 journal 日志。
|
||||
# 可选 --journal 一并导出四个 systemd 服务的 journal 日志。
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/fetch-data.sh # 下载 db + 日志到本地 data/board-<时间>/
|
||||
@@ -18,7 +18,7 @@
|
||||
# --user <user> 登录用户(默认 root)
|
||||
# --board-dir <dir> 板卡部署根目录(默认 /root/work/h100)
|
||||
# --out-dir <dir> 本地保存目录(默认 data/board-<时间>)
|
||||
# --journal 额外导出 3 个服务的 systemd journal 日志
|
||||
# --journal 额外导出 4 个服务的 systemd journal 日志
|
||||
# -h, --help 显示帮助
|
||||
#
|
||||
# 前置: 板卡已配免密 SSH(见 scripts/deploy.sh setup-ssh)。
|
||||
@@ -35,10 +35,10 @@ BOARD_DIR="/root/work/h100"
|
||||
OUT_DIR=""
|
||||
DO_JOURNAL=0
|
||||
|
||||
SERVICES=(moosdb pPowerManger pPowerMangerHost)
|
||||
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
|
||||
|
||||
# 板卡 data 目录下需要下载的文件(含 SQLite WAL/SHM 以保证数据完整)
|
||||
DATA_FILES=(power_data.db power_data.db-wal power_data.db-shm feedback_data.db pPowerManger.log)
|
||||
DATA_FILES=(power_data.db power_data.db-wal power_data.db-shm feedback_data.db pPowerManger.log pccu_data.db pCCU.log)
|
||||
|
||||
info() { printf '\033[1;36m[fetch-data]\033[0m %s\n' "$*"; }
|
||||
ok() { printf '\033[1;32m[fetch-data]\033[0m %s\n' "$*"; }
|
||||
|
||||
@@ -20,6 +20,7 @@ ADD_SUBDIRECTORY(pPowerManger)
|
||||
# ADD_SUBDIRECTORY(appPowerMangerWebUI)
|
||||
add_subdirectory(pPMtest)
|
||||
add_subdirectory(pPowerMangerHost)
|
||||
add_subdirectory(pCCU)
|
||||
##############################################################################
|
||||
# END of CMakeLists.txt
|
||||
##############################################################################
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
#include "CCU.h"
|
||||
#include "MBUtils.h"
|
||||
#include "../pPowerManger/logc/loguru.hpp"
|
||||
#include "protocol/Frame.h"
|
||||
#include <chrono>
|
||||
|
||||
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_fcLink) { m_fcLink->stop(); delete m_fcLink; m_fcLink = nullptr; }
|
||||
if (m_pmLink) { m_pmLink->stop(); delete m_pmLink; m_pmLink = nullptr; }
|
||||
if (m_db) { delete m_db; m_db = nullptr; }
|
||||
if (m_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 == "dbpath") m_dbPath = value;
|
||||
else if (param == "logpath") m_logPath = 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();
|
||||
|
||||
// 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 实时原始报文可在此扩展
|
||||
});
|
||||
if (!m_fcLink->start())
|
||||
LOG_F(ERROR, "FC link start failed");
|
||||
|
||||
// PM 链路
|
||||
m_pmLink = new PmLinkManager(m_pmLocalPort, m_pmHost, m_pmPort);
|
||||
m_pmLink->setLogSink(m_db);
|
||||
m_pmLink->setOnMessage([this](Message* msg, const std::vector<uint8_t>& frame) {
|
||||
handlePmMessage(msg, frame);
|
||||
});
|
||||
m_pmLink->setOnRawFrame([](int dir, const std::vector<uint8_t>&, bool) {
|
||||
});
|
||||
if (!m_pmLink->start())
|
||||
LOG_F(ERROR, "PM link start failed");
|
||||
|
||||
// 快照构建器(链路就绪后创建)
|
||||
m_snap = new SnapshotBuilder(m_sysData, m_fcLink, m_pmLink, 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;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_F(INFO, "pCCU started: fc_link local=%ld -> %s:%ld, pm_link local=%ld -> %s:%ld",
|
||||
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();
|
||||
// pCCU 数据交换主要走 UDP;MOOSDB 仅登记运行状态变量
|
||||
Register("CCU_FC_LINK_STATE", 0);
|
||||
Register("CCU_PM_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;
|
||||
// 预留:未来如需通过 MOOSDB 下发指令可在此处理
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Iterate:周期任务(AppTick 次/秒)
|
||||
|
||||
bool CCU::Iterate() {
|
||||
AppCastingMOOSApp::Iterate();
|
||||
|
||||
double now = MOOSTime();
|
||||
|
||||
// 周期(1Hz)整合 FC 状态并发送 PM 状态报文给控制主机
|
||||
if (now - m_lastStatusTx >= 1.0) {
|
||||
sendPmStatus();
|
||||
m_lastStatusTx = 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;
|
||||
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
|
||||
FcControlValue f;
|
||||
f.mode = c.mode;
|
||||
f.cmd = c.cmd;
|
||||
f.outputPower = c.outputPower;
|
||||
f.pitch = c.pitch;
|
||||
f.roll = 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);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// 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
|
||||
|
||||
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 "[]";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// 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_sysData) {
|
||||
m_msgs << "FC 状态接收次数:" << m_sysData->fcStatusCount() << "\n";
|
||||
m_msgs << "PM 指令接收次数:" << m_sysData->pmControlCount() << "\n";
|
||||
}
|
||||
if (m_db) {
|
||||
m_msgs << "数据库记录数:" << m_db->count() << "\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#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 "core/SystemData.h"
|
||||
#include "core/SnapshotBuilder.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)
|
||||
// 收发全部帧落库 SQLite;网页周期推送 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);
|
||||
|
||||
// 整合并发送 PM 状态报文
|
||||
void sendPmStatus();
|
||||
|
||||
// 构建网页快照
|
||||
std::string buildSnapshot();
|
||||
std::string handleApi(const std::string& uri, const std::string& query);
|
||||
|
||||
// 配置
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
// 心跳
|
||||
uint8_t m_pmHeartbeat = 0;
|
||||
double m_lastStatusTx = 0;
|
||||
|
||||
// 组件
|
||||
FcLinkManager* m_fcLink = nullptr;
|
||||
PmLinkManager* m_pmLink = nullptr;
|
||||
SystemData* m_sysData = nullptr;
|
||||
DbStore* m_db = nullptr;
|
||||
SnapshotBuilder* m_snap = nullptr;
|
||||
WebServer* m_web = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ccu
|
||||
|
||||
#endif // PCCU_CCU_H
|
||||
@@ -0,0 +1,102 @@
|
||||
/****************************************************************/
|
||||
/* 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(" forwards pPowerManger commands to FC, stores data in SQLite ");
|
||||
blk(" and 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(" 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(" (数据交换主要通过 UDP 链路完成,MOOSDB 交互有限) ");
|
||||
blk(" ");
|
||||
blk("PUBLICATIONS: ");
|
||||
blk("------------------------------------ ");
|
||||
blk(" CCU_FC_LINK_STATE = 运行状态字符串 ");
|
||||
blk(" CCU_PM_LINK_STATE = 运行状态字符串 ");
|
||||
blk(" ");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void showReleaseInfoAndExit() {
|
||||
showReleaseInfo("pCCU", "gpl");
|
||||
exit(0);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/****************************************************************/
|
||||
/* 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
|
||||
@@ -0,0 +1,7 @@
|
||||
1.接收来自燃料电池的消息,消息协议格式见docs/超滑FC和复合管控器通讯协议 - 0821.docx(备注:日期会随着时间进行更新)
|
||||
2.将消息进行整合处理后,发送到pPowerManger,消息协议见docs/控制主机与复合管控器通信协议- 0824.xlsx
|
||||
3.接收来自pPowermanger的指令,解析和编码后转发给燃料电池
|
||||
4.具备sqlit数据库存储功能,可以把接收和发送的数据都存储到sqlit数据库中
|
||||
5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置
|
||||
6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介
|
||||
7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署
|
||||
@@ -0,0 +1,76 @@
|
||||
#--------------------------------------------------------
|
||||
# The CMakeLists.txt for: pCCU
|
||||
# 复合管控器网关:FC <-> pPowerManger
|
||||
#--------------------------------------------------------
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
SET(CCU_COMM_SRC
|
||||
comm/UdpEndpoint.cpp
|
||||
comm/LinkManager.cpp
|
||||
)
|
||||
|
||||
SET(CCU_CORE_SRC
|
||||
core/SnapshotBuilder.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}
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
#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
|
||||
@@ -0,0 +1,108 @@
|
||||
#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::handleIncoming(const std::vector<uint8_t>& frame) {
|
||||
if (frame.size() < FRAME_HEADER_LEN) return false;
|
||||
|
||||
uint16_t id = 0;
|
||||
if (!Frame::parseId(frame, id)) {
|
||||
LOG_F(WARNING, "[%s] bad frame header", m_linkName.c_str());
|
||||
++m_errorCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
Message* msg = m_registry.find(id);
|
||||
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
|
||||
@@ -0,0 +1,112 @@
|
||||
#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 为原始完整帧
|
||||
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);
|
||||
|
||||
// 处理一帧收到的原始数据(供接收线程调用,也可测试时手动喂入)
|
||||
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;
|
||||
unsigned long m_rxCount = 0;
|
||||
unsigned long m_txCount = 0;
|
||||
unsigned long m_errorCount = 0;
|
||||
};
|
||||
|
||||
} // namespace ccu
|
||||
|
||||
#endif // PCCU_LINK_MANAGER_H
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef PCCU_PM_LINK_MANAGER_H
|
||||
#define PCCU_PM_LINK_MANAGER_H
|
||||
|
||||
#include "LinkManager.h"
|
||||
#include "../protocol/PmProtocol.h"
|
||||
|
||||
namespace ccu {
|
||||
|
||||
//============================================================================
|
||||
// PmLinkManager:面向控制主机 pPowerManger 的链路。
|
||||
//
|
||||
// - 本地监听:控制主机指令端口(默认 7000)
|
||||
// - 发送目标:控制主机(默认 192.168.0.140:5001)
|
||||
// 端口/地址均可由 .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());
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,57 @@
|
||||
#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 {
|
||||
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
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef PCCU_UDP_ENDPOINT_H
|
||||
#define PCCU_UDP_ENDPOINT_H
|
||||
|
||||
#define UNIX
|
||||
#include "MOOS/libMOOS/Comms/XPCUdpSocket.h"
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ccu {
|
||||
|
||||
//============================================================================
|
||||
// UdpEndpoint:单个 UDP 套接字的封装。
|
||||
//
|
||||
// 职责:
|
||||
// - 绑定本地端口
|
||||
// - 向指定远端 (host:port) 发送数据报
|
||||
// - 阻塞接收数据报
|
||||
// 不关心协议内容,仅负责字节的收发。
|
||||
//============================================================================
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace ccu
|
||||
|
||||
#endif // PCCU_UDP_ENDPOINT_H
|
||||
@@ -0,0 +1,380 @@
|
||||
#include "SnapshotBuilder.h"
|
||||
#include "../store/DbStore.h"
|
||||
#include "json/json.h"
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
|
||||
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)); }
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 消息描述:根据链路 + 消息 ID + 方向(0=收 1=发),给出中文名与"来源→去向"。
|
||||
// 方向是相对 pCCU 而言:收=对方→CCU,发=CCU→对方。
|
||||
//--------------------------------------------------------------------------
|
||||
std::string describeMessageName(const std::string& link, uint16_t id) {
|
||||
if (link == "fc") {
|
||||
switch (id) {
|
||||
case 0x0001: return "FC控制指令";
|
||||
case 0x0002: return "FC状态反馈";
|
||||
default: break;
|
||||
}
|
||||
} else if (link == "pm") {
|
||||
switch (id) {
|
||||
case 0x0001: return "PM操控指令";
|
||||
case 0x0002: return "PM参数设定";
|
||||
case 0x0003: return "PM参数反馈";
|
||||
case 0x0004: return "PM状态报文";
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "未知(0x%04X)", id);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string describeDirection(const std::string& link, int direction) {
|
||||
const std::string peer = (link == "fc") ? "FC" : "PM";
|
||||
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);
|
||||
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, "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["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, "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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SnapshotBuilder::SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, DbStore* db)
|
||||
: m_sys(sys), m_fc(fc), m_pm(pm), m_db(db) {}
|
||||
|
||||
std::string SnapshotBuilder::build() const {
|
||||
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());
|
||||
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()));
|
||||
} 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);
|
||||
}
|
||||
|
||||
JsonVal links(Json::objectValue);
|
||||
links["fc"] = linkJson(m_fc);
|
||||
links["pm"] = linkJson(m_pm);
|
||||
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
|
||||
@@ -0,0 +1,38 @@
|
||||
#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(最新状态)、两条链路统计、最近原始帧日志,
|
||||
// 序列化为 JSON 字符串(jsoncpp)。串行化逻辑集中在此处,便于维护。
|
||||
//============================================================================
|
||||
|
||||
class SnapshotBuilder {
|
||||
public:
|
||||
SnapshotBuilder(SystemData* sys, LinkManager* fc, LinkManager* pm, 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;
|
||||
DbStore* m_db;
|
||||
};
|
||||
|
||||
} // namespace ccu
|
||||
|
||||
#endif // PCCU_SNAPSHOT_BUILDER_H
|
||||
@@ -0,0 +1,118 @@
|
||||
#ifndef PCCU_SYSTEM_DATA_H
|
||||
#define PCCU_SYSTEM_DATA_H
|
||||
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include "../protocol/FcProtocol.h"
|
||||
#include "../protocol/PmProtocol.h"
|
||||
|
||||
namespace ccu {
|
||||
|
||||
//============================================================================
|
||||
// SystemData:跨线程共享的最新状态快照。
|
||||
//
|
||||
// - 接收线程(FC 链路)写 FcStatus 快照
|
||||
// - CCU 主循环(Iterate)读快照并整合编码为 PM 状态报文发送
|
||||
// - 网页线程读快照展示
|
||||
// 通过互斥锁保护读写。
|
||||
//============================================================================
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
FcStatusValue m_fcStatus;
|
||||
PmControlValue m_pmControl;
|
||||
PmParamSetValue m_pmParamSet;
|
||||
FcControlValue m_fcControl;
|
||||
PmStatusValue m_pmStatus;
|
||||
PmParamSetFbValue m_pmParamFb;
|
||||
unsigned long m_fcStatusCount = 0;
|
||||
unsigned long m_pmControlCount = 0;
|
||||
unsigned long m_fcControlCount = 0;
|
||||
unsigned long m_pmStatusCount = 0;
|
||||
};
|
||||
|
||||
} // namespace ccu
|
||||
|
||||
#endif // PCCU_SYSTEM_DATA_H
|
||||
@@ -0,0 +1,54 @@
|
||||
/************************************************************/
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//============================================================================
|
||||
// pCCU 配置示例
|
||||
//
|
||||
// 链路拓扑:
|
||||
// 燃料电池控制器(FC) 192.168.1.162:7000
|
||||
// 控制主机(pPowerManger) 192.168.0.140:5001
|
||||
// pCCU 本机监听:FC 状态端口 6000、PM 指令端口 7000
|
||||
//============================================================================
|
||||
|
||||
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
|
||||
|
||||
//======== 存储与日志 ========
|
||||
// 数据库路径(SQLite)
|
||||
dbpath = pccu_data.db
|
||||
// 日志文件路径
|
||||
logpath = pCCU.log
|
||||
|
||||
//======== 网页监控 ========
|
||||
// 端口避开 pPowerManger(8090) / pPowerMangerHost(18080)
|
||||
web_port = 8080
|
||||
web_enable = true
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#ifndef PCCU_CHECKSUM_POLICY_H
|
||||
#define PCCU_CHECKSUM_POLICY_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace ccu {
|
||||
|
||||
//============================================================================
|
||||
// ChecksumPolicy:校验和策略(可插拔)。
|
||||
//
|
||||
// 协议联调阶段校验和的实现可能调整,因此对每条消息独立配置策略:
|
||||
// - None : 无校验和字段(如 FC 状态帧文档未列校验行)
|
||||
// - Sum32 : uint32 字节和,从域起始符到校验和之前所有数据的字节和
|
||||
// - Sum16 : uint16 字节和(预留,备用)
|
||||
// 新增策略只需在此类扩展,不影响其它代码。
|
||||
//============================================================================
|
||||
|
||||
enum class ChecksumType {
|
||||
None, // 无校验
|
||||
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::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::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
|
||||
@@ -0,0 +1,318 @@
|
||||
#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 控制指令域编解码
|
||||
// 数据域 14 字节,偏移 0~13(对应文档字节 7~20)
|
||||
//============================================================================
|
||||
|
||||
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, 0, v->mode);
|
||||
FieldCodec::putU8(payload, 1, v->cmd);
|
||||
FieldCodec::putU8(payload, 2, v->outputPower);
|
||||
FieldCodec::putU16(payload, 3, static_cast<uint16_t>(v->pitch)); // 数据1=低位 / 数据2=高位
|
||||
FieldCodec::putU16(payload, 5, static_cast<uint16_t>(v->roll));
|
||||
FieldCodec::putU8(payload, 7, v->emergencyAllow);
|
||||
FieldCodec::putU16(payload, 8, v->depth);
|
||||
FieldCodec::putU8(payload, 10, v->supplyCmd);
|
||||
FieldCodec::putU8(payload, 11, v->reservedCmd5);
|
||||
FieldCodec::putU8(payload, 12, v->reservedCmd6);
|
||||
FieldCodec::putU8(payload, 13, 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 + 0);
|
||||
v->cmd = FieldCodec::getU8(frame, o + 1);
|
||||
v->outputPower = FieldCodec::getU8(frame, o + 2);
|
||||
v->pitch = static_cast<int16_t>(FieldCodec::getU16(frame, o + 3));
|
||||
v->roll = static_cast<int16_t>(FieldCodec::getU16(frame, o + 5));
|
||||
v->emergencyAllow = FieldCodec::getU8(frame, o + 7);
|
||||
v->depth = FieldCodec::getU16(frame, o + 8);
|
||||
v->supplyCmd = FieldCodec::getU8(frame, o + 10);
|
||||
v->reservedCmd5 = FieldCodec::getU8(frame, o + 11);
|
||||
v->reservedCmd6 = FieldCodec::getU8(frame, o + 12);
|
||||
v->heartbeat = FieldCodec::getU8(frame, o + 13);
|
||||
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~12 u16*12 -> 118..141 (字节125-148)
|
||||
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, 12);
|
||||
|
||||
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, 12);
|
||||
|
||||
v->heartbeat = FieldCodec::getU8(frame, o + OFF_HEARTBEAT);
|
||||
v->emergency_cmd = FieldCodec::getU8(frame, o + OFF_EMERGENCY_CMD);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ccu
|
||||
@@ -0,0 +1,174 @@
|
||||
#ifndef PCCU_FC_PROTOCOL_H
|
||||
#define PCCU_FC_PROTOCOL_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "Message.h"
|
||||
|
||||
namespace ccu {
|
||||
|
||||
//============================================================================
|
||||
// FC 协议(燃料电池系统 <-> 复合管控器)
|
||||
// 依据:docs/超滑FC和复合管控器通讯协议 - 0821.docx
|
||||
//
|
||||
// 帧格式统一:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + [校验和]
|
||||
// 注意:FC 协议文档未列出校验和行,故默认采用 None(可插拔,联调可切换)。
|
||||
//
|
||||
// 两条消息:
|
||||
// 0x0001 复合管控器->燃料电池控制器(控制指令域) payload 14B,总长 20B
|
||||
// 0x0002 燃料电池控制器->复合管控器(状态反馈域) payload 144B,总长 150B
|
||||
//============================================================================
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 0x0001 控制指令域(复合管控器 -> 燃料电池)
|
||||
// payload 14 字节,对应文档字节 7~20
|
||||
//--------------------------------------------------------------------------
|
||||
struct FcControlValue {
|
||||
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=低8位 / 数据2=高8位,int16),分辨率0.1°
|
||||
int16_t roll = 0; // 横倾姿态(数据1=低8位 / 数据2=高8位,int16),分辨率0.1°
|
||||
uint8_t emergencyAllow = 0; // 应急允许:Bit0 允许降载 / Bit1 允许排气
|
||||
uint16_t depth = 0; // 潜深深度,单位 m
|
||||
uint8_t supplyCmd = 0; // 补给/排放指令 00~0B
|
||||
uint8_t reservedCmd5 = 0; // 预留指令5
|
||||
uint8_t reservedCmd6 = 0; // 预留指令6
|
||||
uint8_t heartbeat = 0; // 通信心跳,每次+1
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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[12] = {0}; // 预留1~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; }
|
||||
size_t payloadLength() const override { return 14; }
|
||||
|
||||
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
|
||||
@@ -0,0 +1,58 @@
|
||||
#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
|
||||
@@ -0,0 +1,50 @@
|
||||
#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 FieldCodec::putU16(frame, off, static_cast<uint16_t>(sum));
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace ccu
|
||||
@@ -0,0 +1,54 @@
|
||||
#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
|
||||
@@ -0,0 +1,22 @@
|
||||
#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;
|
||||
if (h.length != expectTotalLen) return false;
|
||||
|
||||
// 校验和(长度不足由 verify 内部判断)
|
||||
if (!policy.verify(frame)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ccu
|
||||
@@ -0,0 +1,59 @@
|
||||
#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
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "MessageRegistry.h"
|
||||
|
||||
namespace ccu {
|
||||
|
||||
bool MessageRegistry::registerMessage(std::unique_ptr<Message> msg) {
|
||||
if (!msg) return false;
|
||||
uint16_t id = msg->id();
|
||||
if (m_msgs.count(id) != 0) return false; // id 冲突
|
||||
m_msgs[id] = std::move(msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
Message* MessageRegistry::find(uint16_t id) const {
|
||||
auto it = m_msgs.find(id);
|
||||
return (it != m_msgs.end()) ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef PCCU_MESSAGE_REGISTRY_H
|
||||
#define PCCU_MESSAGE_REGISTRY_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "Message.h"
|
||||
|
||||
namespace ccu {
|
||||
|
||||
//============================================================================
|
||||
// MessageRegistry:消息注册表与收发分发中心。
|
||||
//
|
||||
// 每条链路(FC 链路 / PM 链路)持有各自的注册表实例。
|
||||
// 由于 FC 协议与 PM 协议的域标识符存在重叠(0x0001/0x0002),
|
||||
// 分属不同注册表可完全隔离,互不干扰。
|
||||
//
|
||||
// 协议新增/调整消息时只需 register() 对应 Message 实现,上层无需改动。
|
||||
//============================================================================
|
||||
|
||||
class MessageRegistry {
|
||||
public:
|
||||
// 注册一条消息;返回是否成功(id 冲突时返回 false)
|
||||
bool registerMessage(std::unique_ptr<Message> msg);
|
||||
|
||||
// 按 id 查找消息;未注册返回 nullptr
|
||||
Message* find(uint16_t id) const;
|
||||
|
||||
// 已注册消息数量
|
||||
size_t size() const;
|
||||
|
||||
// 所有已注册消息(按 id 升序)
|
||||
std::vector<Message*> all() const;
|
||||
|
||||
private:
|
||||
std::map<uint16_t, std::unique_ptr<Message>> m_msgs;
|
||||
};
|
||||
|
||||
} // namespace ccu
|
||||
|
||||
#endif // PCCU_MESSAGE_REGISTRY_H
|
||||
@@ -0,0 +1,662 @@
|
||||
#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=低8位/数据2=高8位 (字节18-19)
|
||||
PO_ROLL = 13, // 横倾姿态 数据1=低8位/数据2=高8位 (字节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
|
||||
@@ -0,0 +1,317 @@
|
||||
#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=低8位 / 数据2=高8位,int16),0.1°
|
||||
int16_t roll = 0; // 横倾姿态(数据1=低8位 / 数据2=高8位,int16),0.1°
|
||||
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
|
||||
@@ -0,0 +1,178 @@
|
||||
#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);");
|
||||
|
||||
return prepareInsert();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void DbStore::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 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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace ccu
|
||||
@@ -0,0 +1,70 @@
|
||||
#ifndef PCCU_DB_STORE_H
|
||||
#define PCCU_DB_STORE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "../comm/LinkManager.h"
|
||||
|
||||
struct sqlite3;
|
||||
struct sqlite3_stmt;
|
||||
|
||||
namespace ccu {
|
||||
|
||||
//============================================================================
|
||||
// DbStore:SQLite 存储。
|
||||
//
|
||||
// 设计要点(应对协议变更):
|
||||
// - 以“原始帧日志”为主表(comm_log),收发全部数据按帧落库,
|
||||
// 协议字段变化不会导致建表失败。
|
||||
// - 提供按 link / direction / msg_id 的查询接口供网页展示。
|
||||
// 复用仓库内 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;
|
||||
|
||||
// 查询最近 N 条记录(可限定链路/方向/消息id;limit<=0 表示默认 100)
|
||||
std::vector<CommLogRow> queryRecent(const std::string& link, int direction,
|
||||
uint16_t msgId, 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 ccu
|
||||
|
||||
#endif // PCCU_DB_STORE_H
|
||||
@@ -0,0 +1,132 @@
|
||||
#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) {
|
||||
std::lock_guard<std::mutex> lock(m_wsMutex);
|
||||
m_wsConnections.push_back(c);
|
||||
if (m_onOpen) {
|
||||
std::string welcome = m_onOpen();
|
||||
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) {
|
||||
std::lock_guard<std::mutex> lock(m_wsMutex);
|
||||
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();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_wsMutex);
|
||||
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);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebServer::broadcast(const std::string& text) {
|
||||
std::lock_guard<std::mutex> lock(m_wsMutex);
|
||||
for (auto c : m_wsConnections) {
|
||||
mg_ws_send(c, text.c_str(), text.size(), WEBSOCKET_OP_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ccu
|
||||
@@ -0,0 +1,61 @@
|
||||
#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 客户端广播文本
|
||||
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);
|
||||
|
||||
struct mg_mgr m_mgr;
|
||||
int m_port = 0;
|
||||
std::atomic<bool> m_running{false};
|
||||
std::thread m_thread;
|
||||
|
||||
std::mutex m_wsMutex;
|
||||
std::vector<mg_connection*> m_wsConnections;
|
||||
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
|
||||
@@ -0,0 +1,490 @@
|
||||
#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;}
|
||||
</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>
|
||||
</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="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','fcCmd','pmStatus','pmFb','links','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 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)+')'); }
|
||||
// 带解析的值: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.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('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)+' mm');
|
||||
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);
|
||||
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)));
|
||||
h+=row('动力锂电池启停', parsed(d.dynBatCmd, batCmdText(d.dynBatCmd)));
|
||||
h+=row('动力锂电池功率配置', d.dynBatPower+' kW');
|
||||
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('纵倾姿态', (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('预留指令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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
//------------------ 标签页头部 ------------------
|
||||
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>',
|
||||
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>',
|
||||
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
|
||||
@@ -443,131 +443,6 @@ char LowerCommManager::getBreakerOption(char cmd)
|
||||
return 0x00;
|
||||
}
|
||||
|
||||
void LowerCommManager::coutMsg(const msg_CcuStateFbMsg& p, std::ostream& os){
|
||||
os << std::hex << std::setfill('0') << std::uppercase;
|
||||
|
||||
os << "起始符1 (start1): 0x" << std::setw(2) << static_cast<int>(p.header.start1) << "\n";
|
||||
os << "起始符2 (start2): 0x" << std::setw(2) << static_cast<int>(p.header.start2) << "\n";
|
||||
os << "标识符 (identifier): 0x" << std::setw(4) << p.header.id << "\n";
|
||||
os << std::dec << "数据长度 (length): " << p.header.length << "\n";
|
||||
|
||||
|
||||
os << "运行模式 (fc_mode): 0x" << std::hex << std::setw(2) << static_cast<int>(p.data.fc_mode) << "\n";
|
||||
os << "燃料电池状态 (fc_status): 0x" << std::setw(2) << static_cast<int>(p.data.fc_status) << "\n";
|
||||
os << "故障等级 (fc_fault_level): 0x" << std::setw(2) << static_cast<int>(p.data.fc_fault_level) << "\n";
|
||||
|
||||
os << std::dec;
|
||||
os << "输出功率限制 (output_power_limit): " << p.data.output_power_limit << " W\n";
|
||||
os << "累计发电时间 (total_generation_time): " << p.data.total_generation_time * 0.1 << " 小时\n";
|
||||
os << "发电总功率 (total_generation_power): " << p.data.total_generation_power << " W\n";
|
||||
os << "一级故障字 (fault_level_1): 0x" << std::hex << p.data.fault_level_1 << "\n";
|
||||
os << "二级故障字 (fault_level_2): 0x" << p.data.fault_level_2 << "\n";
|
||||
os << "三级故障字 (fault_level_3): 0x" << p.data.fault_level_3 << "\n";
|
||||
os << "四级故障字 (fault_level_4): 0x" << p.data.fault_level_4 << "\n";
|
||||
|
||||
os << std::dec;
|
||||
os << "储氢剩余容量 (hydrogen_capacity): " << static_cast<int>(p.data.hydrogen_capacity) << "%\n";
|
||||
os << "液氧剩余容量 (liquid_oxygen_capacity): " << static_cast<int>(p.data.liquid_oxygen_capacity) << "%\n";
|
||||
os << "钯膜最高温度 (palladium_temp): " << p.data.palladium_temp << " ℃\n";
|
||||
os << "主水路压力 (main_pipe_pressure): " << p.data.main_pipe_pressure * 0.01 << " kPa\n";
|
||||
os << "辅水路压力 (aux_pipe_pressure): " << p.data.aux_pipe_pressure * 0.01 << " kPa\n";
|
||||
|
||||
os << "舱室压力1 (cabin_pressure1): " << p.data.cabin_pressure1 * 0.01 << " kPa\n";
|
||||
os << "舱室压力2 (cabin_pressure2): " << p.data.cabin_pressure2 * 0.01 << " kPa\n";
|
||||
os << "舱室温度1 (cabin_temp1): " << p.data.cabin_temp1 * 0.01 << " ℃\n";
|
||||
os << "舱室温度2 (cabin_temp2): " << p.data.cabin_temp2 * 0.01 << " ℃\n";
|
||||
os << "舱室湿度1 (cabin_humidity1): " << p.data.cabin_humidity1 * 0.01 << " %RH\n";
|
||||
os << "舱室湿度2 (cabin_humidity2): " << p.data.cabin_humidity2 * 0.01 << " %RH\n";
|
||||
|
||||
os << "火焰探测器1状态 (flame_detector1): 0x" << std::hex << static_cast<int>(p.data.flame_detector1) << "\n";
|
||||
os << "火焰探测器2状态 (flame_detector2): 0x" << static_cast<int>(p.data.flame_detector2) << "\n";
|
||||
|
||||
os << std::dec;
|
||||
os << "氢气浓度1 (h2_concentration1): " << p.data.h2_concentration1 * 0.01 << "% LEL\n";
|
||||
os << "氢气浓度2 (h2_concentration2): " << p.data.h2_concentration2 * 0.01 << "% LEL\n";
|
||||
os << "氢气浓度3 (h2_concentration3): " << p.data.h2_concentration3 * 0.01 << "% LEL\n";
|
||||
os << "氧气浓度1 (o2_concentration1): " << p.data.o2_concentration1 * 0.01 << "% LEL\n";
|
||||
os << "氧气浓度2 (o2_concentration2): " << p.data.o2_concentration2 * 0.01 << "% LEL\n";
|
||||
os << "甲醇浓度1 (ch3oh_concentration1): " << p.data.ch3oh_concentration1 * 0.01 << "% LEL\n";
|
||||
os << "甲醇浓度2 (ch3oh_concentration2): " << p.data.ch3oh_concentration2 * 0.01 << "% LEL\n";
|
||||
|
||||
for (int i = 0; i < 2; ++i)
|
||||
os << "保留字段 reserved1[" << i << "]: 0x" << std::hex << p.data.reserved1[i] << "\n";
|
||||
|
||||
os << "仪表舱 氧气浓度 (cabin_ox_concentration): " << p.data.cabin_ox_concentration * 0.1 << "%\n";
|
||||
os << "仪表舱 温度 (cabin_temperature): " << static_cast<int16_t> (p.data.cabin_temperature) * 0.1 << "°C\n";
|
||||
os << "仪表舱 湿度 (cabin_humidity): " << p.data.cabin_humidity * 0.1 << "% RH\n";
|
||||
os << "仪表舱 大气压 (cabin_pressure): " << p.data.cabin_pressure * 0.2 << " kPa\n";
|
||||
|
||||
os << "动力舱 氧气浓度 (power_cabin_ox_concentration): " << p.data.power_cabin_ox_concentration * 0.1 << "%\n";
|
||||
os << "动力舱 温度 (power_cabin_temperature): " << static_cast<int16_t> (p.data.power_cabin_temperature) * 0.1 << "°C\n";
|
||||
os << "动力舱 湿度 (power_cabin_humidity): " << p.data.power_cabin_humidity * 0.1 << "% RH\n";
|
||||
os << "动力舱 大气压 (power_cabin_pressure): " << p.data.power_cabin_pressure * 0.2 << " kPa\n";
|
||||
|
||||
for (int i = 0; i < 6; ++i)
|
||||
os << "动力电池报警标志 dyn_alarm_flag[" << i << "]: 0x" << std::hex << static_cast<int>(p.data.dyn_alarm_flag[i]) << "\n";
|
||||
for (int i = 0; i < 6; ++i)
|
||||
os << "仪表电池报警标志 ins_alarm_flag[" << i << "]: 0x" << std::hex << static_cast<int>(p.data.ins_alarm_flag[i]) << "\n";
|
||||
|
||||
|
||||
os << "仪表正极/充电继电器状态 (ins_relay_status1): 0x" << std::hex << static_cast<int>(p.data.ins_relay_status1) << "\n";
|
||||
os << "仪表预充/负极继电器状态 (ins_relay_status2): 0x" << static_cast<int>(p.data.ins_relay_status2) << "\n";
|
||||
os << "动力正极/充电继电器状态 (dyn_relay_status1): 0x" << static_cast<int>(p.data.dyn_relay_status1) << "\n";
|
||||
os << "动力预充/负极继电器状态 (dyn_relay_status2): 0x" << static_cast<int>(p.data.dyn_relay_status2) << "\n";
|
||||
|
||||
os << std::dec;
|
||||
os << "仪表最大放电功率 (ins_max_discharge_power): " << p.data.ins_max_discharge_power * 0.05 << " kW\n";
|
||||
os << "动力最大放电功率 (dyn_max_discharge_power): " << p.data.dyn_max_discharge_power * 0.05 << " kW\n";
|
||||
|
||||
os << "仪表SOC (ins_soc): " << static_cast<int>(p.data.ins_soc) << "%\n";
|
||||
os << "动力SOC (dyn_soc): " << static_cast<int>(p.data.dyn_soc) << "%\n";
|
||||
|
||||
os << "仪表电池总能量 (ins_total_energy): " << p.data.ins_total_energy * 0.1 << " kWh\n";
|
||||
os << "动力电池总能量 (dyn_total_energy): " << p.data.dyn_total_energy * 0.1 << " kWh\n";
|
||||
|
||||
os << "仪表输入功率 (ins_power_input): " << p.data.ins_power_input * 0.1 << " kW\n";
|
||||
os << "动力输入功率 (dyn_power_input): " << p.data.dyn_power_input * 0.1 << " kW\n";
|
||||
|
||||
os << "仪表充电状态 (ins_charge_status): 0x" << std::hex << static_cast<int>(p.data.ins_charge_status) << "\n";
|
||||
os << "动力充电状态 (dyn_charge_status): 0x" << static_cast<int>(p.data.dyn_charge_status) << "\n";
|
||||
|
||||
os << std::dec;
|
||||
os << "仪表LINK电压: " << p.data.ins_voltage_link * 0.01 << " V\n";
|
||||
os << "仪表PACK电压: " << p.data.ins_voltage_pack * 0.01 << " V\n";
|
||||
os << "仪表电流: " << static_cast<int16_t> (p.data.ins_current) * 0.05 << " A\n";
|
||||
os << "仪表正端绝缘电阻: " << p.data.ins_resistance_pos << " kΩ\n";
|
||||
os << "仪表负端绝缘电阻: " << p.data.ins_resistance_neg << " kΩ\n";
|
||||
os << "动力LINK电压: " << p.data.dyn_voltage_link * 0.01 << " V\n";
|
||||
os << "动力PACK电压: " << p.data.dyn_voltage_pack * 0.01 << " V\n";
|
||||
os << "动力电流: " << static_cast<int16_t> (p.data.dyn_current) * 0.05 << " A\n";
|
||||
os << "动力正端绝缘电阻: " << p.data.dyn_resistance_pos << " kΩ\n";
|
||||
os << "动力负端绝缘电阻: " << p.data.dyn_resistance_neg << " kΩ\n";
|
||||
|
||||
os << "仪表紧急状态 (ins_emergency_status): 0x" << std::hex << static_cast<int>(p.data.ins_emergency_status) << "\n";
|
||||
os << "动力紧急状态 (dyn_emergency_status): 0x" << static_cast<int>(p.data.dyn_emergency_status) << "\n";
|
||||
|
||||
for (int i = 0; i < 1; ++i)
|
||||
os << "保留字段 reserved2[" << i << "]: 0x" << std::hex << static_cast<int>(p.data.reserved2[i]) << "\n";
|
||||
os << "SOC门限值 ins_soc_threshold1: 0x" << std::hex << static_cast<int>(p.data.ins_soc_threshold1) << "\n";
|
||||
os << "SOC门限值 ins_soc_threshold2: 0x" << std::hex << static_cast<int>(p.data.ins_soc_threshold2) << "\n";
|
||||
os << "SOC门限值 ins_soc_threshold3: 0x" << std::hex << static_cast<int>(p.data.ins_soc_threshold3) << "\n";
|
||||
os << "SOC门限值 dyn_soc_threshold1: 0x" << std::hex << static_cast<int>(p.data.dyn_soc_threshold1) << "\n";
|
||||
os << "SOC门限值 dyn_soc_threshold2: 0x" << std::hex << static_cast<int>(p.data.dyn_soc_threshold2) << "\n";
|
||||
os << "SOC门限值 dyn_soc_threshold3: 0x" << std::hex << static_cast<int>(p.data.dyn_soc_threshold3) << "\n";
|
||||
os << "功率限制 ins_power_limit1: 0x" << std::hex << static_cast<int>(p.data.ins_power_limit1) << "\n";
|
||||
os << "功率限制 ins_power_limit2: 0x" << std::hex << static_cast<int>(p.data.ins_power_limit2) << "\n";
|
||||
os << "功率限制 dyn_power_limit1: 0x" << std::hex << static_cast<int>(p.data.dyn_power_limit1) << "\n";
|
||||
os << "功率限制 dyn_power_limit2: 0x" << std::hex << static_cast<int>(p.data.dyn_power_limit2) << "\n";
|
||||
|
||||
os << "设备在线状态1 (device_online_flag1): 0x" << std::hex << p.data.device_online_flag1 << "\n";
|
||||
os << "设备在线状态2 (device_online_flag2): 0x" << p.data.device_online_flag2 << "\n";
|
||||
|
||||
// os << "保留字段 reserved3: 0x" << p.data.reserved1 << "\n";
|
||||
os << "主机心跳 (reserved4): 0x" << p.data.heartbeat << "\n";
|
||||
os << "校验和 (checksum): 0x" << p.checkCode << "\n";
|
||||
|
||||
os << std::dec;
|
||||
}
|
||||
//================================================================================================================
|
||||
// 高电压母线断路器操作函数
|
||||
bool LowerCommManager::operate_HV_fuelCellCircuit_Breaker(char opt){
|
||||
|
||||
@@ -95,7 +95,6 @@ public:
|
||||
// 启动/停止监听线程
|
||||
void startListenThread();
|
||||
void stopListenThread();
|
||||
void coutMsg(const msg_CcuStateFbMsg& p, std::ostream& os = std::cout);
|
||||
|
||||
//转换函数
|
||||
char getBreakerOption(char cmd);
|
||||
|
||||
@@ -43,6 +43,7 @@ PowerManger::PowerManger()
|
||||
m_leakage = 0;
|
||||
m_thrustRpm = 0;
|
||||
m_thrustEnable = 0;
|
||||
m_insData = {};
|
||||
faultCode.clear();
|
||||
|
||||
m_skew = 10;
|
||||
@@ -259,6 +260,7 @@ void PowerManger::registerVariables()
|
||||
Register(m_upperCommManager->m_secdistStateMsg.key_fb,0);
|
||||
Register(DB_TO_PM_MISSION,0);
|
||||
Register(DB_TO_PM_DEEPTH,0);
|
||||
Register(DB_TO_PM_INSDATA,0);
|
||||
Register(DB_TO_PM_SETOPCONDITION,0);
|
||||
Register(DB_TO_PM_SETOPMODE,0);
|
||||
Register(DB_TO_PM_SETPMSYS,0);
|
||||
|
||||
@@ -97,6 +97,7 @@ class PowerManger : public AppCastingMOOSApp
|
||||
DriverTable m_subDisSysState;
|
||||
DriverTable m_subDisSysFault;
|
||||
int m_depth;
|
||||
InsDataState m_insData;
|
||||
MissionState m_missionState;
|
||||
//===========================STATE=========================================
|
||||
unsigned char workCondition;
|
||||
|
||||
@@ -8,6 +8,7 @@ UpperCommManager::UpperCommManager(PowerManger* pm) :
|
||||
m_secdistStateMsg(),
|
||||
m_setOpConditionMsg(),
|
||||
m_depthMsg(),
|
||||
m_insDataMsg(),
|
||||
m_taskStartMsg(),
|
||||
m_equInsStateMsg(),
|
||||
m_equStateMsg(),
|
||||
@@ -77,6 +78,15 @@ bool UpperCommManager::processUpperMsg(const string& key, const string& strJson)
|
||||
m_pm->m_depth = m_depthMsg.getDepth(key, strJson);
|
||||
return true;
|
||||
}
|
||||
// 惯导姿态与深度获取
|
||||
else if(key == m_insDataMsg.key)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
m_insDataMsg_st_debug = std::make_pair(key, strJson);
|
||||
#endif
|
||||
m_insDataMsg.parseMsg(key, strJson, m_pm->m_insData);
|
||||
return true;
|
||||
}
|
||||
else if(key == m_taskStartMsg.key)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "upmsg/uPower_fcsState.h"
|
||||
#include "upmsg/uPower_liBatState.h"
|
||||
#include "upmsg/uDevice_depth.h"
|
||||
#include "upmsg/uDevice_insData.h"
|
||||
#include "upmsg/uExternComm_setOpCondition.h"
|
||||
#include "upmsg/uExternComm_setOpMode.h"
|
||||
#include "upmsg/uPlan_taskStart.h"
|
||||
@@ -39,6 +40,7 @@ using namespace std;
|
||||
|
||||
//决策消息
|
||||
#define DB_TO_PM_DEEPTH "uDevice_depth_st"
|
||||
#define DB_TO_PM_INSDATA "uDevice_insData_st"
|
||||
// #define DB_TO_PM_MISSION "uPlan_taskStart_cmd"
|
||||
#define DB_TO_PM_TASKSTOP "uPlan_planStop_st"
|
||||
|
||||
@@ -77,6 +79,7 @@ public:
|
||||
|
||||
//上位机订阅消息
|
||||
uDevice_depthMsg m_depthMsg;
|
||||
uDevice_insDataMsg m_insDataMsg;
|
||||
uExternComm_setOpConditionMsg m_setOpConditionMsg;
|
||||
uExternComm_setOpModeMsg m_setOpModeMsg;
|
||||
uPlan_taskStartMsg m_taskStartMsg;
|
||||
@@ -104,6 +107,7 @@ public:
|
||||
std::pair<string, string> m_liBatStateMsg_st_debug;
|
||||
|
||||
std::pair<string, string> m_depthMsg_st_debug;
|
||||
std::pair<string, string> m_insDataMsg_st_debug;
|
||||
std::pair<string, string> m_setOpConditionMsg_st_debug;
|
||||
std::pair<string, string> m_setOpModeMsg_st_debug;
|
||||
std::pair<string, string> m_taskStartMsg_st_debug;
|
||||
|
||||
@@ -524,6 +524,18 @@ void HttpServer::pushSystemData() {
|
||||
//帧尾
|
||||
j["emergency_command"] = Json::Value(m_systemData->emergency_command);
|
||||
|
||||
//惯导姿态/深度数据(uDevice_insData_st)
|
||||
if (m_powerManger) {
|
||||
j["ins_heading_angle"] = Json::Value(m_powerManger->m_insData.headingAngle_int32); // 航向角
|
||||
j["ins_roll_angle"] = Json::Value(m_powerManger->m_insData.rollAngle_int32); // 横摇角
|
||||
j["ins_pitch_angle"] = Json::Value(m_powerManger->m_insData.pitchAngle_int32); // 纵摇角
|
||||
j["ins_depth"] = Json::Value(m_powerManger->m_insData.depth_int32); // 平台深度
|
||||
j["ins_heading_angular_velocity"] = Json::Value(m_powerManger->m_insData.headingAngularVelocity_int32);
|
||||
j["ins_roll_angular_velocity"] = Json::Value(m_powerManger->m_insData.rollAngularVelocity_int32);
|
||||
j["ins_pitch_angular_velocity"] = Json::Value(m_powerManger->m_insData.longAngularVelocity_int32);
|
||||
j["depth_pressure_avg"] = Json::Value(m_powerManger->m_depth); // 深度计压力平均
|
||||
}
|
||||
|
||||
//温度压力参数
|
||||
j["palladium_temp"] = Json::Value(m_systemData->palladium_temp); // 钯膜最高温度,单位:℃
|
||||
j["main_pipe_pressure"] = Json::Value(m_systemData->main_pipe_pressure); // 主水路压力,单位:0.01kPa
|
||||
@@ -871,6 +883,9 @@ void HttpServer::pushSystemData() {
|
||||
j["debug"]["m_depthMsg_st_debug"]["name"] = Json::Value(m_powerManger->m_upperCommManager->m_depthMsg_st_debug.first);
|
||||
j["debug"]["m_depthMsg_st_debug"]["content"] = Json::Value(m_powerManger->m_upperCommManager->m_depthMsg_st_debug.second);
|
||||
|
||||
j["debug"]["m_insDataMsg_st_debug"]["name"] = Json::Value(m_powerManger->m_upperCommManager->m_insDataMsg_st_debug.first);
|
||||
j["debug"]["m_insDataMsg_st_debug"]["content"] = Json::Value(m_powerManger->m_upperCommManager->m_insDataMsg_st_debug.second);
|
||||
|
||||
j["debug"]["m_setOpConditionMsg_st_debug"]["name"] = Json::Value(m_powerManger->m_upperCommManager->m_setOpConditionMsg_st_debug.first);
|
||||
j["debug"]["m_setOpConditionMsg_st_debug"]["content"] = Json::Value(m_powerManger->m_upperCommManager->m_setOpConditionMsg_st_debug.second);
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//决策消息
|
||||
//惯导导航信息 uDevice_insData_st,包含航向角/横摇角/纵摇角及平台深度等数据
|
||||
#ifndef UDEVICE_INSDATA_ST_H
|
||||
#define UDEVICE_INSDATA_ST_H
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include "../logc/loguru.hpp"
|
||||
#include "json/json.h"
|
||||
|
||||
#define DB_TO_PM_INSDATA "uDevice_insData_st"
|
||||
|
||||
typedef struct {
|
||||
int32_t headingAngle_int32; // 航向角,类型:int32_t
|
||||
int32_t rollAngle_int32; // 横摇角,类型:int32_t
|
||||
int32_t pitchAngle_int32; // 纵摇角,类型:int32_t
|
||||
int32_t depth_int32; // 平台深度,类型:int32_t
|
||||
int32_t headingAngularVelocity_int32; // 航向角速度,类型:int32_t
|
||||
int32_t rollAngularVelocity_int32; // 横摇角速度,类型:int32_t
|
||||
int32_t longAngularVelocity_int32; // 纵倾角速度,类型:int32_t
|
||||
int32_t lon_int32; // 经度,类型:int32_t
|
||||
int32_t lat_int32; // 纬度,类型:int32_t
|
||||
int16_t northSpeed_int16; // 北速,类型:int16_t
|
||||
int16_t eastSpeed_int16; // 东速,类型:int16_t
|
||||
int16_t zSpeed_int16; // 垂速,类型:int16_t
|
||||
} InsDataState;
|
||||
|
||||
class uDevice_insDataMsg
|
||||
{
|
||||
private:
|
||||
|
||||
public:
|
||||
std::string key = "uDevice_insData_st";
|
||||
uDevice_insDataMsg(/* args */){};
|
||||
~uDevice_insDataMsg(){};
|
||||
|
||||
int parseMsg(const std::string& msgkey, const std::string& msg, InsDataState& state){
|
||||
if (msgkey != DB_TO_PM_INSDATA)
|
||||
{
|
||||
LOG_F(WARNING, "消息类型错误 %s", msgkey.c_str());
|
||||
return 0;
|
||||
}
|
||||
state = {};
|
||||
Json::Value root;
|
||||
Json::CharReaderBuilder builder;
|
||||
JSONCPP_STRING errs;
|
||||
|
||||
// 解析JSON字符串
|
||||
std::istringstream stream(msg);
|
||||
if (!Json::parseFromStream(builder, stream, &root, &errs)) {
|
||||
std::cerr << "JSON解析失败: " << errs << std::endl;
|
||||
LOG_F(WARNING, "JSON解析失败: %s", errs.c_str());
|
||||
return -1;
|
||||
}
|
||||
// 姿态角与深度(必含)
|
||||
if (root.isMember("headingAngle_int32") && root["headingAngle_int32"].isInt())
|
||||
state.headingAngle_int32 = root["headingAngle_int32"].asInt();
|
||||
if (root.isMember("rollAngle_int32") && root["rollAngle_int32"].isInt())
|
||||
state.rollAngle_int32 = root["rollAngle_int32"].asInt();
|
||||
if (root.isMember("pitchAngle_int32") && root["pitchAngle_int32"].isInt())
|
||||
state.pitchAngle_int32 = root["pitchAngle_int32"].asInt();
|
||||
if (root.isMember("depth_int32") && root["depth_int32"].isInt())
|
||||
state.depth_int32 = root["depth_int32"].asInt();
|
||||
// 角速度(可选)
|
||||
if (root.isMember("headingAngularVelocity_int32") && root["headingAngularVelocity_int32"].isInt())
|
||||
state.headingAngularVelocity_int32 = root["headingAngularVelocity_int32"].asInt();
|
||||
if (root.isMember("rollAngularVelocity_int32") && root["rollAngularVelocity_int32"].isInt())
|
||||
state.rollAngularVelocity_int32 = root["rollAngularVelocity_int32"].asInt();
|
||||
if (root.isMember("longAngularVelocity_int32") && root["longAngularVelocity_int32"].isInt())
|
||||
state.longAngularVelocity_int32 = root["longAngularVelocity_int32"].asInt();
|
||||
// 位置与速度(可选)
|
||||
if (root.isMember("lon_int32") && root["lon_int32"].isInt())
|
||||
state.lon_int32 = root["lon_int32"].asInt();
|
||||
if (root.isMember("lat_int32") && root["lat_int32"].isInt())
|
||||
state.lat_int32 = root["lat_int32"].asInt();
|
||||
if (root.isMember("northSpeed_int16") && root["northSpeed_int16"].isInt())
|
||||
state.northSpeed_int16 = root["northSpeed_int16"].asInt();
|
||||
if (root.isMember("eastSpeed_int16") && root["eastSpeed_int16"].isInt())
|
||||
state.eastSpeed_int16 = root["eastSpeed_int16"].asInt();
|
||||
if (root.isMember("zSpeed_int16") && root["zSpeed_int16"].isInt())
|
||||
state.zSpeed_int16 = root["zSpeed_int16"].asInt();
|
||||
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -363,4 +363,7 @@ void HostSim::registerFeedbackVars()
|
||||
Register("uPower_equInsState_fb", 0); // 设备仪表状态
|
||||
Register("uPower_secdistState_cmd", 0);// 次级配电状态
|
||||
Register("uPower_subDisFault_st", 0); // 次级配电故障状态(_DEBUG 下发布)
|
||||
// 姿态/深度数据(航行控制器→数据中心,供监视)
|
||||
Register("uDevice_depth_st", 0); // 深度信息
|
||||
Register("uDevice_insData_st", 0); // 惯导导航信息(姿态/深度)
|
||||
}
|
||||
|
||||
@@ -275,6 +275,7 @@
|
||||
<nav id="tabs">
|
||||
<button data-tab="home" class="active">主页</button>
|
||||
<button data-tab="equipment">设备控制</button>
|
||||
<button data-tab="attitude">姿态/深度</button>
|
||||
<button data-tab="monitor">反馈监控</button>
|
||||
<button data-tab="chart">曲线</button>
|
||||
<button data-tab="log">日志</button>
|
||||
@@ -344,6 +345,61 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 姿态/深度 -->
|
||||
<div id="page-attitude" class="page">
|
||||
<section>
|
||||
<h2>深度信息 <span class="muted">(uDevice_depth_st · 航行控制器→数据中心)</span></h2>
|
||||
<div class="row">
|
||||
<label>深度计1</label><input id="depthVal1" type="number" min="0" max="1000000" value="0" />
|
||||
<label>深度计2</label><input id="depthVal2" type="number" min="0" max="1000000" value="0" />
|
||||
<label>深度计3</label><input id="depthVal3" type="number" min="0" max="1000000" value="0" />
|
||||
<button id="btnAttDepthSend">下发深度</button>
|
||||
</div>
|
||||
<div class="muted">单位:0.001dbar,范围 0~1000000;约 1000 ≈ 1m 水深</div>
|
||||
<div class="muted" id="depthEcho"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>惯导导航信息 <span class="muted">(uDevice_insData_st · 航行控制器→数据中心)</span></h2>
|
||||
<div class="row">
|
||||
<label>航向角</label><input id="insHeading" type="number" value="0" />
|
||||
<label>横摇角</label><input id="insRoll" type="number" value="0" />
|
||||
<label>纵摇角</label><input id="insPitch" type="number" value="0" />
|
||||
<label>平台深度</label><input id="insDepth" type="number" value="0" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>航向角速度</label><input id="insHeadingRate" type="number" value="0" />
|
||||
<label>横摇角速度</label><input id="insRollRate" type="number" value="0" />
|
||||
<label>纵倾角速度</label><input id="insPitchRate" type="number" value="0" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>经度</label><input id="insLon" type="number" value="0" />
|
||||
<label>纬度</label><input id="insLat" type="number" value="0" />
|
||||
<label>北速</label><input id="insNorth" type="number" value="0" />
|
||||
<label>东速</label><input id="insEast" type="number" value="0" />
|
||||
<label>垂速</label><input id="insZ" type="number" value="0" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<button id="btnInsSend">下发姿态</button>
|
||||
<label><input type="checkbox" id="insAuto" /> 循环发送</label>
|
||||
<label>周期</label>
|
||||
<select id="insAutoPeriod">
|
||||
<option value="500">0.5s</option>
|
||||
<option value="1000" selected>1s</option>
|
||||
<option value="2000">2s</option>
|
||||
</select>
|
||||
<label><input type="checkbox" id="insAutoInc" /> 航向自动递增(0.01°/次)</label>
|
||||
</div>
|
||||
<div class="muted">角度/角速度单位遵循惯导协议(通常 0.01° 或 0.001° 分辨率,整型原始值)</div>
|
||||
<div class="muted" id="insEcho"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>最近下发展示</h2>
|
||||
<div id="attLastSent"><span class="muted">暂无</span></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 反馈监控 -->
|
||||
<div id="page-monitor" class="page">
|
||||
<section>
|
||||
@@ -568,6 +624,37 @@ const FCS_LABELS = {
|
||||
faultCode4_uint16:"四级故障位", fcCommState_uint8:"通信状态"
|
||||
};
|
||||
|
||||
const INS_LABELS = {
|
||||
timeStamp_uint32:"时间戳(ms)", timeStart_uint32:"开机时间", source_int8:"数据源", state_int8:"工作状态",
|
||||
headingAngle_int32:"航向角", rollAngle_int32:"横摇角", pitchAngle_int32:"纵摇角", depth_int32:"平台深度",
|
||||
headingAngularVelocity_int32:"航向角速度", rollAngularVelocity_int32:"横摇角速度", longAngularVelocity_int32:"纵倾角速度",
|
||||
lon_int32:"经度", lat_int32:"纬度", northSpeed_int16:"北速", eastSpeed_int16:"东速", zSpeed_int16:"垂速",
|
||||
frontSpeed_int16:"前速", rightSpeed_int16:"右速", heavenlySpeed_int16:"天速"
|
||||
};
|
||||
|
||||
function renderInsData(d) { return renderKv(d, INS_LABELS); }
|
||||
|
||||
function renderDepth(d) {
|
||||
const arr = Array.isArray(d.value_array_int32) ? d.value_array_int32 : [];
|
||||
let html = '<table class="kv"><tbody>';
|
||||
if (!arr.length) {
|
||||
html += '<tr><td class="k">数据</td><td class="v">—</td></tr>';
|
||||
} else {
|
||||
arr.forEach((v, i) => {
|
||||
html += `<tr><td class="k">深度计${i+1} (0.001dbar)</td><td class="v">${esc(v)} ≈ ${(v * 0.00102).toFixed(2)}m</td></tr>`;
|
||||
});
|
||||
}
|
||||
html += "</tbody></table>";
|
||||
if (arr.length) {
|
||||
const avg = arr.reduce((a, b) => a + b, 0) / arr.length;
|
||||
html += '<div class="stats">' +
|
||||
stat("平均(原始)", avg.toFixed(0), "acc") +
|
||||
stat("约水深", (avg * 0.00102).toFixed(2) + " m", "acc") +
|
||||
"</div>";
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 4. 全局状态
|
||||
// ============================================================
|
||||
@@ -823,6 +910,109 @@ function setAllDevices(v) {
|
||||
updateEquipmentSelects();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 9.5 姿态/深度(uDevice_insData_st / uDevice_depth_st)
|
||||
// ============================================================
|
||||
let lastDepthSent = null;
|
||||
let lastInsSent = null;
|
||||
let insAutoTimer = null;
|
||||
let insHeadingCounter = 0;
|
||||
|
||||
function num(id) { return Number(el(id).value) || 0; }
|
||||
|
||||
function buildDepthJson() {
|
||||
return { value_array_int32: [num("depthVal1"), num("depthVal2"), num("depthVal3")] };
|
||||
}
|
||||
|
||||
function buildInsJson() {
|
||||
return {
|
||||
timeStamp_uint32: Date.now(),
|
||||
source_int8: 1,
|
||||
state_int8: 4,
|
||||
headingAngle_int32: num("insHeading"),
|
||||
rollAngle_int32: num("insRoll"),
|
||||
pitchAngle_int32: num("insPitch"),
|
||||
depth_int32: num("insDepth"),
|
||||
headingAngularVelocity_int32: num("insHeadingRate"),
|
||||
rollAngularVelocity_int32: num("insRollRate"),
|
||||
longAngularVelocity_int32: num("insPitchRate"),
|
||||
lon_int32: num("insLon"),
|
||||
lat_int32: num("insLat"),
|
||||
northSpeed_int16: num("insNorth"),
|
||||
eastSpeed_int16: num("insEast"),
|
||||
zSpeed_int16: num("insZ")
|
||||
};
|
||||
}
|
||||
|
||||
function sendAttDepth() {
|
||||
const obj = buildDepthJson();
|
||||
lastDepthSent = obj;
|
||||
sendMsg("uDevice_depth_st", obj);
|
||||
const avg = obj.value_array_int32.reduce((a, b) => a + b, 0) / 3;
|
||||
el("depthEcho").textContent =
|
||||
"已下发: " + obj.value_array_int32.join(", ") + " (0.001dbar) · 平均约 " + (avg * 0.00102).toFixed(2) + " m";
|
||||
renderLastSent();
|
||||
}
|
||||
|
||||
function sendAttIns() {
|
||||
let obj = buildInsJson();
|
||||
if (el("insAutoInc").checked) {
|
||||
obj.headingAngle_int32 = insHeadingCounter;
|
||||
el("insHeading").value = insHeadingCounter;
|
||||
}
|
||||
lastInsSent = obj;
|
||||
sendMsg("uDevice_insData_st", obj);
|
||||
const t = new Date(obj.timeStamp_uint32).toLocaleTimeString();
|
||||
el("insEcho").textContent =
|
||||
"已下发: 航向=" + obj.headingAngle_int32 + " 横摇=" + obj.rollAngle_int32 +
|
||||
" 纵摇=" + obj.pitchAngle_int32 + " 深度=" + obj.depth_int32 + " @" + t;
|
||||
renderLastSent();
|
||||
}
|
||||
|
||||
function toggleInsAuto() {
|
||||
if (insAutoTimer) { clearInterval(insAutoTimer); insAutoTimer = null; }
|
||||
if (el("insAuto").checked) {
|
||||
insHeadingCounter = num("insHeading");
|
||||
const period = Number(el("insAutoPeriod").value);
|
||||
insAutoTimer = setInterval(() => {
|
||||
if (el("insAutoInc").checked) {
|
||||
insHeadingCounter = (insHeadingCounter + 1) % 36000;
|
||||
}
|
||||
sendAttIns();
|
||||
}, period);
|
||||
el("insEcho").textContent += " · 循环发送已启动";
|
||||
}
|
||||
}
|
||||
|
||||
function renderLastSent() {
|
||||
const box = el("attLastSent");
|
||||
let html = "";
|
||||
if (lastInsSent) {
|
||||
html += "<h3>惯导导航信息 (uDevice_insData_st)</h3>" + renderInsData(lastInsSent);
|
||||
}
|
||||
if (lastDepthSent) {
|
||||
html += "<h3>深度信息 (uDevice_depth_st)</h3>" + renderDepth(lastDepthSent);
|
||||
}
|
||||
if (lastInsSent || lastDepthSent) {
|
||||
html += '<div class="row" style="margin-top:8px"><button class="sec" id="btnShowRaw">查看原始 JSON</button><pre id="attRaw" style="display:none"></pre></div>';
|
||||
}
|
||||
box.innerHTML = html || '<span class="muted">暂无</span>';
|
||||
const btn = el("btnShowRaw");
|
||||
if (btn) {
|
||||
btn.onclick = () => {
|
||||
const pre = el("attRaw");
|
||||
if (pre.style.display === "none") {
|
||||
pre.textContent = JSON.stringify({ uDevice_depth_st: lastDepthSent, uDevice_insData_st: lastInsSent }, null, 2);
|
||||
pre.style.display = "block";
|
||||
btn.textContent = "收起原始 JSON";
|
||||
} else {
|
||||
pre.style.display = "none";
|
||||
btn.textContent = "查看原始 JSON";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 10. 反馈监控页
|
||||
// ============================================================
|
||||
@@ -943,7 +1133,9 @@ const FEEDBACKS = [
|
||||
["uPower_equState_fb","设备状态", renderEquState],
|
||||
["uPower_equInsState_fb","设备仪表状态", renderEquIns],
|
||||
["uPower_secdistState_cmd","次级配电状态", renderSecdist],
|
||||
["uPower_subDisFault_st","次级配电故障", renderSubFault]
|
||||
["uPower_subDisFault_st","次级配电故障", renderSubFault],
|
||||
["uDevice_depth_st","深度信息", renderDepth],
|
||||
["uDevice_insData_st","惯导导航信息", renderInsData]
|
||||
];
|
||||
|
||||
let currentMonitorKey = FEEDBACKS[0][0];
|
||||
@@ -1219,6 +1411,11 @@ function init() {
|
||||
sendMsg(k, v);
|
||||
};
|
||||
|
||||
// 姿态/深度页
|
||||
el("btnAttDepthSend").onclick = sendAttDepth;
|
||||
el("btnInsSend").onclick = sendAttIns;
|
||||
el("insAuto").onchange = toggleInsAuto;
|
||||
|
||||
// 设备控制页
|
||||
el("btnSendTable").onclick = () => {
|
||||
sendMsg("uPower_equState_cmd", deviceCmdState);
|
||||
@@ -1245,6 +1442,7 @@ function init() {
|
||||
renderEquipmentStructure();
|
||||
renderMonitor();
|
||||
renderPmOverview();
|
||||
renderLastSent();
|
||||
renderSeriesBox();
|
||||
drawChart();
|
||||
connect();
|
||||
|
||||
@@ -125,3 +125,37 @@ set_target_properties(fullFeeder PROPERTIES
|
||||
target_link_libraries(fullFeeder
|
||||
pthread
|
||||
)
|
||||
|
||||
################################################pccuTest(pCCU 单元测试)#####
|
||||
# pCCU 协议编解码 / 校验和 / 链路分发 / 数据库 单元测试
|
||||
set(CCU_DIR ${CMAKE_SOURCE_DIR}/src/pCCU)
|
||||
|
||||
add_executable(pccuTest
|
||||
pccu/pccuTest.cpp
|
||||
${CCU_DIR}/protocol/Frame.cpp
|
||||
${CCU_DIR}/protocol/Message.cpp
|
||||
${CCU_DIR}/protocol/MessageRegistry.cpp
|
||||
${CCU_DIR}/protocol/FcProtocol.cpp
|
||||
${CCU_DIR}/protocol/PmProtocol.cpp
|
||||
${CCU_DIR}/comm/UdpEndpoint.cpp
|
||||
${CCU_DIR}/comm/LinkManager.cpp
|
||||
${CCU_DIR}/store/DbStore.cpp
|
||||
${PM_SRC_DIR}/sqlit3/sqlite3.c
|
||||
${PM_SRC_DIR}/logc/loguru.cpp
|
||||
)
|
||||
target_include_directories(pccuTest PRIVATE
|
||||
${CCU_DIR}
|
||||
${PM_SRC_DIR}
|
||||
${PM_SRC_DIR}/sqlit3
|
||||
${PM_SRC_DIR}/logc
|
||||
)
|
||||
set_target_properties(pccuTest PROPERTIES
|
||||
CXX_STANDARD 11
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
)
|
||||
target_link_libraries(pccuTest
|
||||
${MOOS_LIBRARIES}
|
||||
m
|
||||
pthread
|
||||
dl
|
||||
)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# 配电系统调试助手(DisSysTest)
|
||||
|
||||
用于 H100 配电系统(高压母线 / 艏部高压 / 仪表母线 / 艏部低压)调试的
|
||||
桌面工具。可在真实配电板联调与本地模拟两种模式下工作,纯 Python
|
||||
标准库实现(Tkinter + SQLite),无第三方运行依赖。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 协议/配置/数据库自检(无界面,建议先跑)
|
||||
python3 run.py --selftest
|
||||
|
||||
# 启动界面
|
||||
python3 run.py
|
||||
```
|
||||
|
||||
> 旧目录中的 `PowerManagerApp.py`、`PowerManagerApp copy.py`、`udpMsg.py`、
|
||||
> `db.py`、`test.py`、`ccu.py` 等为历史调试脚本,新工程位于 `app/`,
|
||||
> 旧文件保留供参考,未参与运行。
|
||||
|
||||
## 界面与操作
|
||||
|
||||
- **四个状态页**:动力母线 / 艏部高压 / 仪表母线 / 艏部低压,实时显示
|
||||
电压、电流、断路器状态与报警。
|
||||
- **断路器指示灯**:绿=合(0x55) 黑=分(0xAA) 红=故障(0x5A) 灰=无数据。
|
||||
- **配电操作菜单**:打开各母线操作面板,点「合 / 分」下发指令。
|
||||
- **DCDC 状态**:查看 DC/DC 故障字1/2 的 16 个状态位。
|
||||
- **模拟模式**:勾选后无需设备即可联调,配电指令会同步回写模拟反馈,
|
||||
与真实设备共用同一套接收/落库/转发链路。
|
||||
- **转发反馈 / 透传指令**:把收到的反馈转发给上位机;把上位机指令透传
|
||||
给对应配电板。
|
||||
- **数据记录**:反馈与指令自动写入 SQLite(可取消勾选)。
|
||||
|
||||
## 通信配置
|
||||
|
||||
菜单「配置 → 通信配置」可修改并保存到 `config.json`(首次运行自动生成
|
||||
默认值):
|
||||
|
||||
| 端点 | 默认值 | 说明 |
|
||||
|----------|-----------------------|--------------------|
|
||||
| 本机 | 127.0.0.1:7000 | 监听反馈 |
|
||||
| 高压配电板(C1) | 192.168.1.154:6000 | 指令下发目标 |
|
||||
| 仪表配电板(C2) | 192.168.1.158:6000 | 指令下发目标 |
|
||||
| 上位机 | 127.0.0.1:5001 | 反馈转发目标 |
|
||||
|
||||
菜单「配置 → 启动连接」控制 UDP 接收线程(默认开启)。
|
||||
|
||||
## 消息协议
|
||||
|
||||
帧格式:`0x40 0x40 | id(LE u16) | length(LE u16) | body | 和校验(1B)`。
|
||||
|
||||
| 消息 ID | 方向 | 说明 | body 长度 |
|
||||
|---------|------|--------------------------|-----------|
|
||||
| 0x0001 | 指令 | 动力母线配电 | 14 |
|
||||
| 0x0002 | 指令 | 艏部高压配电 | 6 |
|
||||
| 0x0003 | 指令 | 仪表母线配电 | 7 |
|
||||
| 0x0004 | 指令 | 艏部低压配电 | 8 |
|
||||
| 0x0005 | 反馈 | 动力母线状态 | 53 |
|
||||
| 0x0006 | 反馈 | 艏部高压状态 | 25 |
|
||||
| 0x0007 | 反馈 | 仪表母线状态 | 34 |
|
||||
| 0x0008 | 反馈 | 艏部低压状态 | 29 |
|
||||
|
||||
字段定义与顺序对齐 `src/pPMtest/pmSysvariable.h`(注意历史版本 `udpMsg.py`
|
||||
中的 length 字段存在多处错误,`app/protocol.py` 已修正并由字段表自动推导)。
|
||||
|
||||
## 工程结构
|
||||
|
||||
```
|
||||
app/
|
||||
protocol.py 声明式协议定义(pack/unpack/长度自推导,含自检)
|
||||
config.py config.json 加载/保存
|
||||
udp_comm.py UDP 收发线程与按 ID 分发
|
||||
simulator.py 内置模拟器(指令回显)
|
||||
db.py SQLite 存储(表结构由协议自动生成)
|
||||
logger.py 日志(UI/控制台/文件,线程安全)
|
||||
panels.py 数据驱动的状态/操作面板
|
||||
app.py 主窗口
|
||||
ccuMsg.py / ccuWin.py 复合管控 CCU 模块(功能预留,暂未接入)
|
||||
run.py 入口
|
||||
tests/ 协议单元测试
|
||||
build.py PyInstaller 打包脚本
|
||||
```
|
||||
|
||||
## 测试与打包
|
||||
|
||||
```bash
|
||||
python3 run.py --selftest # 协议/配置/数据库自检
|
||||
python3 tests/test_protocol.py
|
||||
python3 build.py # 需先 pip install pyinstaller(在目标平台执行)
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- 现场联调前建议先用模拟模式 + 转发/透传验证链路;
|
||||
- 协议自检保证帧长与 C++ 结构一致,但最终以实际板卡抓包为准;
|
||||
- 数据库文件默认生成在 `config.json` 同目录(源码运行时为 `app/test.db`)。
|
||||
@@ -0,0 +1,3 @@
|
||||
"""配电系统调试助手应用包。"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
@@ -0,0 +1,85 @@
|
||||
"""程序入口:python -m app 或 python run.py。
|
||||
|
||||
支持 --selftest 无 GUI 自检(协议 / 配置 / 数据库)。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def selftest():
|
||||
ok = True
|
||||
print("== 协议自检 ==")
|
||||
proto_ok, report = _protocol_selftest()
|
||||
print(report)
|
||||
ok = ok and proto_ok
|
||||
|
||||
print("== 配置自检 ==")
|
||||
try:
|
||||
from .config import Config
|
||||
c = Config()
|
||||
for key in ("local", "c1", "c2", "host"):
|
||||
assert key in c.data, f"缺少配置节 {key}"
|
||||
print(" [OK] 配置加载/默认值")
|
||||
except Exception as e:
|
||||
print(f" [FAIL] 配置: {e}")
|
||||
ok = False
|
||||
|
||||
print("== 数据库自检 ==")
|
||||
try:
|
||||
from .db import DB, TABLE_NAMES
|
||||
from . import protocol
|
||||
assert len(TABLE_NAMES) == 8, "消息表数量应为 8"
|
||||
fd, path = tempfile.mkstemp(suffix=".db")
|
||||
os.close(fd)
|
||||
db = DB(path)
|
||||
db.connect()
|
||||
for cls in protocol.ALL_MESSAGES.values():
|
||||
assert db.insert_message(cls()), f"{cls.__name__} 插入失败"
|
||||
rows = {name: 0 for name in TABLE_NAMES.values()}
|
||||
for name in TABLE_NAMES.values():
|
||||
rows[name] = db.conn.execute(f'SELECT COUNT(*) FROM "{name}"').fetchone()[0]
|
||||
assert all(v == 1 for v in rows.values()), f"写入行数异常: {rows}"
|
||||
db.disconnect()
|
||||
os.unlink(path)
|
||||
print(" [OK] 建表 / 写入 / disconnect")
|
||||
except Exception as e:
|
||||
print(f" [FAIL] 数据库: {e}")
|
||||
ok = False
|
||||
|
||||
print("SELFTEST:", "PASS" if ok else "FAIL")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def _protocol_selftest():
|
||||
from . import protocol
|
||||
return protocol.self_test()
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="配电系统调试助手")
|
||||
parser.add_argument("--selftest", action="store_true",
|
||||
help="运行协议/配置/数据库自检后退出")
|
||||
parser.add_argument("--config", default=None, help="配置文件路径")
|
||||
parser.add_argument("--version", action="store_true", help="显示版本")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.version:
|
||||
from . import __version__
|
||||
print(f"配电系统调试助手 v{__version__}")
|
||||
return 0
|
||||
if args.selftest:
|
||||
return selftest()
|
||||
|
||||
import tkinter as tk
|
||||
from .app import PowerManagerApp
|
||||
root = tk.Tk()
|
||||
app = PowerManagerApp(root, config_path=args.config)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,440 @@
|
||||
"""电源管理/配电系统调试主程序。
|
||||
|
||||
主窗口组织:
|
||||
- 菜单:通信选项 / 配电操作 / 配置 / DCDC状态 / 复合管控面板(预留) / 软件说明
|
||||
- 四个母线状态页(Notebook),数据驱动刷新
|
||||
- 系统日志区
|
||||
|
||||
数据通路:
|
||||
真实设备/UDP 反馈 -> on_feedback -> 状态缓存 + DB + (可选)转发上位机
|
||||
配电指令(合/分) -> send_breaker_cmd -> UDP 下发 + DB + (可选)模拟回显
|
||||
模拟模式 -> Simulator.update -> 注入同一 on_feedback 通路
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from . import protocol
|
||||
from .config import Config
|
||||
from .db import DB
|
||||
from .logger import AppLogger
|
||||
from .panels import (BUS_SPECS, CMD_SPECS, build_bus_page, update_bus_page,
|
||||
build_control_panel, build_dcdc_display,
|
||||
update_dcdc_display)
|
||||
from .simulator import Simulator
|
||||
from .udp_comm import UdpComm
|
||||
from .protocol import BREAKER_ON
|
||||
|
||||
MSG_ID_BUS = {0x0005: "high_vol", 0x0006: "high_a_vol",
|
||||
0x0007: "low_main", 0x0008: "low_bus"}
|
||||
CMD_ID_DEST = {0x0001: "c1", 0x0002: "c1", 0x0003: "c2", 0x0004: "c2"}
|
||||
|
||||
|
||||
class PowerManagerApp:
|
||||
def __init__(self, root, config_path=None):
|
||||
self.root = root
|
||||
self.config = Config(config_path)
|
||||
|
||||
self.root.title("配电系统调试助手")
|
||||
self.root.tk.call("tk", "scaling", 1.5)
|
||||
self.root.minsize(860, 640)
|
||||
|
||||
# 菜单开关状态
|
||||
self.Forward_Feedback = tk.BooleanVar(value=False)
|
||||
self.Transparent_Command = tk.BooleanVar(value=False)
|
||||
self.Simulation_Mode = tk.BooleanVar(value=False)
|
||||
self.Start_Connection = tk.BooleanVar(value=True)
|
||||
self.Record_Data = tk.BooleanVar(value=True)
|
||||
self.Simulation_Mode.trace_add("write", self._on_sim_toggle)
|
||||
|
||||
self.db = DB(self.config.db_path)
|
||||
try:
|
||||
self.db.connect()
|
||||
except Exception as e:
|
||||
print(f"数据库打开失败: {e}")
|
||||
|
||||
self.sim = Simulator()
|
||||
self._sim_thread = None
|
||||
self._sim_running = False
|
||||
self._data_lock = threading.Lock()
|
||||
# 各母线最新反馈(原始 Message 对象),None 表示尚无数据
|
||||
self.raw_msgs = {key: None for key in BUS_SPECS}
|
||||
self.bus_refs = {}
|
||||
self.dcdc_refs = None # DCDC 弹窗 refs
|
||||
self.dcdc_popup = None
|
||||
|
||||
self._build_ui()
|
||||
|
||||
self.logger = AppLogger(widget=self.log_text)
|
||||
self.udp = UdpComm(logger=self.logger)
|
||||
self.udp.on_feedback = self._on_feedback
|
||||
self.udp.on_command = self._on_command
|
||||
self.udp.on_raw = self._on_raw
|
||||
try:
|
||||
self.udp.bind(self.config.local_ip, self.config.local_port)
|
||||
except OSError as e:
|
||||
self.logger.error(f"绑定本机 {self.config.local_ip}:{self.config.local_port} 失败: {e}")
|
||||
|
||||
if self.Start_Connection.get() and self.udp.bound:
|
||||
try:
|
||||
self.udp.start()
|
||||
self.logger.info("通信连接已启动")
|
||||
except RuntimeError as e:
|
||||
self.logger.error(str(e))
|
||||
|
||||
self.logger.info(f"配电系统调试助手启动(配置文件: {self.config.path})")
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
self.root.after(self.config.refresh_ms, self._refresh_ui)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 界面构建
|
||||
# ------------------------------------------------------------------
|
||||
def _build_ui(self):
|
||||
menubar = tk.Menu(self.root)
|
||||
|
||||
comm_menu = tk.Menu(menubar, tearoff=0)
|
||||
comm_menu.add_checkbutton(label="转发反馈", variable=self.Forward_Feedback)
|
||||
comm_menu.add_checkbutton(label="透传指令", variable=self.Transparent_Command)
|
||||
comm_menu.add_checkbutton(label="模拟模式", variable=self.Simulation_Mode)
|
||||
menubar.add_cascade(label="通信选项", menu=comm_menu)
|
||||
|
||||
op_menu = tk.Menu(menubar, tearoff=0)
|
||||
op_menu.add_command(label="动力母线操作",
|
||||
command=lambda: self.show_control_window("hvb"))
|
||||
op_menu.add_command(label="艏部高压操作",
|
||||
command=lambda: self.show_control_window("hvba"))
|
||||
op_menu.add_command(label="仪表母线操作",
|
||||
command=lambda: self.show_control_window("lvmb"))
|
||||
op_menu.add_command(label="艏部低压操作",
|
||||
command=lambda: self.show_control_window("lvb"))
|
||||
menubar.add_cascade(label="配电操作", menu=op_menu)
|
||||
|
||||
cfg_menu = tk.Menu(menubar, tearoff=0)
|
||||
cfg_menu.add_checkbutton(label="启动连接", variable=self.Start_Connection,
|
||||
command=self._on_connection_toggle)
|
||||
cfg_menu.add_checkbutton(label="记录数据", variable=self.Record_Data)
|
||||
cfg_menu.add_command(label="通信配置", command=self.show_config_window)
|
||||
cfg_menu.add_command(label="数据库配置", command=self.show_db_config_window)
|
||||
menubar.add_cascade(label="配置", menu=cfg_menu)
|
||||
|
||||
menubar.add_command(label="DCDC状态", command=self.show_dcdc_window)
|
||||
menubar.add_command(label="复合管控面板",
|
||||
command=lambda: self.show_placeholder_window("复合管控面板",
|
||||
"CCU 复合管控功能预留,待后续版本实现。"))
|
||||
menubar.add_command(label="软件说明", command=self.show_help_window)
|
||||
self.root.config(menu=menubar)
|
||||
|
||||
main_frame = ttk.Frame(self.root, padding=8)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
main_frame.grid_rowconfigure(0, weight=1)
|
||||
main_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.notebook = ttk.Notebook(main_frame)
|
||||
self.notebook.grid(row=0, column=0, sticky="nsew")
|
||||
for key, spec in BUS_SPECS.items():
|
||||
page = build_bus_page(self.notebook, spec)
|
||||
self.bus_refs[key] = page
|
||||
self.notebook.add(page["frame"], text=spec.title)
|
||||
|
||||
log_frame = ttk.LabelFrame(main_frame, text="系统日志", padding=6)
|
||||
log_frame.grid(row=1, column=0, sticky="nsew", pady=(6, 0))
|
||||
log_frame.grid_rowconfigure(0, weight=1)
|
||||
log_frame.grid_columnconfigure(0, weight=1)
|
||||
self.log_text = tk.Text(log_frame, height=8, state=tk.DISABLED)
|
||||
self.log_text.grid(row=0, column=0, sticky="nsew")
|
||||
sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
|
||||
sb.grid(row=0, column=1, sticky="ns")
|
||||
self.log_text.config(yscrollcommand=sb.set)
|
||||
|
||||
self.status_bar = ttk.Label(main_frame, text="未连接", anchor="w",
|
||||
relief="sunken")
|
||||
self.status_bar.grid(row=2, column=0, sticky="ew", pady=(4, 0))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# UDP 回调(运行在接收线程)
|
||||
# ------------------------------------------------------------------
|
||||
def _on_feedback(self, msg, addr):
|
||||
bus_key = MSG_ID_BUS.get(msg.MSG_ID)
|
||||
if bus_key is None:
|
||||
return
|
||||
with self._data_lock:
|
||||
self.raw_msgs[bus_key] = msg
|
||||
if self.Record_Data.get():
|
||||
self.db.insert_message(msg)
|
||||
|
||||
def _on_command(self, msg, addr):
|
||||
if self.Simulation_Mode.get():
|
||||
self.sim.apply_command(msg)
|
||||
|
||||
def _on_raw(self, data, addr):
|
||||
msg_id = protocol.msg_id_of(data)
|
||||
if msg_id is None:
|
||||
return
|
||||
if self.Forward_Feedback.get() and msg_id in protocol.FB_MESSAGES:
|
||||
self.udp.sendto(data, self.config.host_ip, self.config.host_port)
|
||||
elif self.Transparent_Command.get() and msg_id in protocol.CMD_MESSAGES:
|
||||
self._forward_raw(data, msg_id)
|
||||
|
||||
def _forward_raw(self, data, msg_id):
|
||||
dest = CMD_ID_DEST.get(msg_id)
|
||||
if dest is None:
|
||||
return
|
||||
ip, port = self._dest(dest)
|
||||
self.udp.sendto(data, ip, port)
|
||||
|
||||
def _dest(self, key):
|
||||
if key == "c1":
|
||||
return self.config.c1_ip, self.config.c1_port
|
||||
return self.config.c2_ip, self.config.c2_port
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 指令下发
|
||||
# ------------------------------------------------------------------
|
||||
def send_breaker_cmd(self, spec, field, state):
|
||||
cmd = spec.msg_cls()
|
||||
setattr(cmd, field, state)
|
||||
if self.Simulation_Mode.get():
|
||||
self.sim.apply_command(cmd)
|
||||
ip, port = self._dest(spec.dest)
|
||||
ok = self.udp.send_message(cmd, ip, port)
|
||||
if self.Record_Data.get():
|
||||
self.db.insert_message(cmd)
|
||||
label = next((l for l, f in spec.actions if f == field), field)
|
||||
txt = "合" if state == BREAKER_ON else "分"
|
||||
self.logger.info(f"发送{spec.title}「{label}」{txt} → {ip}:{port}"
|
||||
+ ("" if ok else "(发送失败)"))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 开关回调
|
||||
# ------------------------------------------------------------------
|
||||
def _on_connection_toggle(self):
|
||||
if self.Start_Connection.get():
|
||||
if not self.udp.bound:
|
||||
self.logger.error("本机地址未绑定,无法启动")
|
||||
self.Start_Connection.set(False)
|
||||
return
|
||||
try:
|
||||
self.udp.start()
|
||||
except RuntimeError as e:
|
||||
self.logger.error(str(e))
|
||||
self.Start_Connection.set(False)
|
||||
return
|
||||
self.logger.info("通信连接已启动")
|
||||
else:
|
||||
self.udp.stop()
|
||||
self.logger.info("通信连接已停止")
|
||||
|
||||
def _on_sim_toggle(self, *args):
|
||||
if self.Simulation_Mode.get():
|
||||
self._start_simulation()
|
||||
else:
|
||||
self._stop_simulation()
|
||||
|
||||
def _start_simulation(self):
|
||||
if self._sim_running:
|
||||
return
|
||||
self.sim.reset()
|
||||
self._sim_running = True
|
||||
self._sim_thread = threading.Thread(target=self._sim_loop, daemon=True)
|
||||
self._sim_thread.start()
|
||||
self.logger.info("模拟模式已启动")
|
||||
|
||||
def _stop_simulation(self):
|
||||
was_running = self._sim_running
|
||||
self._sim_running = False
|
||||
if self._sim_thread:
|
||||
self._sim_thread.join(timeout=1.5)
|
||||
self._sim_thread = None
|
||||
if was_running:
|
||||
self.logger.info("模拟模式已停止")
|
||||
|
||||
def _sim_loop(self):
|
||||
while self._sim_running:
|
||||
msgs = self.sim.update(self.config.sim_interval)
|
||||
for msg in msgs:
|
||||
self._on_feedback(msg, ("sim", 0))
|
||||
if self.Forward_Feedback.get():
|
||||
self.udp.sendto(msg.pack(), self.config.host_ip,
|
||||
self.config.host_port)
|
||||
time.sleep(self.config.sim_interval)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 界面刷新(主线程定时器)
|
||||
# ------------------------------------------------------------------
|
||||
def _refresh_ui(self):
|
||||
self.logger.drain()
|
||||
for key, refs in self.bus_refs.items():
|
||||
with self._data_lock:
|
||||
msg = self.raw_msgs[key]
|
||||
update_bus_page(BUS_SPECS[key], refs, msg)
|
||||
# DCDC 弹窗刷新
|
||||
if self.dcdc_refs is not None:
|
||||
with self._data_lock:
|
||||
m = self.raw_msgs.get("high_vol")
|
||||
if m is not None:
|
||||
update_dcdc_display(self.dcdc_refs, m.dcDcFaultWord1,
|
||||
m.dcDcFaultWord2)
|
||||
# 状态栏
|
||||
state = "已连接" if self.udp.running else "未连接"
|
||||
self.status_bar.config(
|
||||
text=f"{state} 收包: {self.udp.rx_count} 发包: {self.udp.tx_count}"
|
||||
f" 模拟: {'开' if self.Simulation_Mode.get() else '关'}"
|
||||
f" 记录: {'开' if self.Record_Data.get() else '关'}")
|
||||
self.root.after(self.config.refresh_ms, self._refresh_ui)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 弹窗
|
||||
# ------------------------------------------------------------------
|
||||
def show_control_window(self, spec_key):
|
||||
spec = CMD_SPECS[spec_key]
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title(spec.title)
|
||||
frame = ttk.Frame(popup, padding=8)
|
||||
frame.pack(fill="both", expand=True)
|
||||
build_control_panel(frame, spec, self.send_breaker_cmd)
|
||||
ttk.Button(popup, text="关闭", command=popup.destroy).pack(pady=8)
|
||||
|
||||
def show_dcdc_window(self):
|
||||
if self.dcdc_popup is not None and self.dcdc_popup.winfo_exists():
|
||||
self.dcdc_popup.lift()
|
||||
return
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title("DC/DC 状态")
|
||||
frame = ttk.Frame(popup, padding=8)
|
||||
frame.pack(fill="both", expand=True, padx=5, pady=5)
|
||||
self.dcdc_refs = build_dcdc_display(frame, "DC/DC 故障字")
|
||||
self.dcdc_refs["widget"].pack(fill="both", expand=True)
|
||||
self.dcdc_popup = popup
|
||||
|
||||
def on_close():
|
||||
self.dcdc_refs = None
|
||||
self.dcdc_popup = None
|
||||
popup.destroy()
|
||||
|
||||
ttk.Button(popup, text="关闭", command=on_close).pack(pady=8)
|
||||
|
||||
def show_placeholder_window(self, title, text):
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title(title)
|
||||
popup.geometry("420x200")
|
||||
frame = ttk.Frame(popup, padding=20)
|
||||
frame.pack(fill="both", expand=True)
|
||||
ttk.Label(frame, text=text, wraplength=360, justify="left").pack()
|
||||
ttk.Button(popup, text="关闭", command=popup.destroy).pack(pady=10)
|
||||
|
||||
def show_help_window(self):
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title("程序说明")
|
||||
popup.geometry("640x460")
|
||||
frame = ttk.Frame(popup, padding=10)
|
||||
frame.pack(fill="both", expand=True)
|
||||
text = tk.Text(frame, wrap="word", font=("Microsoft YaHei", 10))
|
||||
sb = ttk.Scrollbar(frame, orient="vertical", command=text.yview)
|
||||
text.configure(yscrollcommand=sb.set)
|
||||
text.pack(side="left", fill="both", expand=True)
|
||||
sb.pack(side="right", fill="y")
|
||||
help_text = f"""配电系统调试助手
|
||||
|
||||
功能概述:
|
||||
1. 高压母线 / 艏部高压 / 仪表母线 / 艏部低压 四路状态实时监控
|
||||
2. 断路器配电指令远程合/分操作
|
||||
3. 转发反馈:把收到的反馈转发给上位机(信息主机)
|
||||
4. 透传指令:把上位机下发的指令透传给对应配电板
|
||||
5. 模拟模式:无需真实设备即可本地联调
|
||||
6. 数据记录:反馈与指令自动存入 SQLite(可开关)
|
||||
|
||||
通信端点(可在 配置-通信配置 修改):
|
||||
本机监听: {self.config.local_ip}:{self.config.local_port}
|
||||
高压配电板: {self.config.c1_ip}:{self.config.c1_port}
|
||||
仪表配电板: {self.config.c2_ip}:{self.config.c2_port}
|
||||
上位机: {self.config.host_ip}:{self.config.host_port}
|
||||
数据库: {self.config.db_path}
|
||||
|
||||
指示灯说明:
|
||||
绿色=合闸(0x55) 黑色=分闸(0xAA) 红色=故障(0x5A) 灰色=无数据/未知
|
||||
|
||||
操作提示:
|
||||
1. 「启动连接」控制 UDP 接收线程(默认开启)
|
||||
2. 勾选「模拟模式」后,配电操作会同步回写模拟反馈
|
||||
3. 重要操作请以现场实际设备状态为准"""
|
||||
text.insert("1.0", help_text)
|
||||
text.config(state="disabled")
|
||||
ttk.Button(popup, text="关闭", command=popup.destroy).pack(pady=8)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 配置弹窗
|
||||
# ------------------------------------------------------------------
|
||||
def show_config_window(self):
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("通信配置")
|
||||
win.geometry("420x360")
|
||||
rows = [
|
||||
("本机 IP", "local_ip"), ("本机端口", "local_port"),
|
||||
("高压配电板 IP", "c1_ip"), ("高压配电板端口", "c1_port"),
|
||||
("仪表配电板 IP", "c2_ip"), ("仪表配电板端口", "c2_port"),
|
||||
("上位机 IP", "host_ip"), ("上位机端口", "host_port"),
|
||||
]
|
||||
entries = {}
|
||||
for i, (label, key) in enumerate(rows):
|
||||
ttk.Label(win, text=label).grid(row=i, column=0, padx=10, pady=4, sticky="w")
|
||||
e = ttk.Entry(win, width=20)
|
||||
e.grid(row=i, column=1, padx=10, pady=4)
|
||||
val = getattr(self.config, key)
|
||||
e.insert(0, str(val))
|
||||
entries[key] = e
|
||||
|
||||
def save():
|
||||
try:
|
||||
self.config.set_endpoints(
|
||||
entries["local_ip"].get(), int(entries["local_port"].get()),
|
||||
entries["c1_ip"].get(), int(entries["c1_port"].get()),
|
||||
entries["c2_ip"].get(), int(entries["c2_port"].get()),
|
||||
entries["host_ip"].get(), int(entries["host_port"].get()))
|
||||
self.config.save()
|
||||
# 重新绑定本机
|
||||
if self.udp.running:
|
||||
self.udp.stop()
|
||||
self.udp.bind(self.config.local_ip, self.config.local_port)
|
||||
if self.Start_Connection.get():
|
||||
self.udp.start()
|
||||
self.logger.info("通信配置已保存并生效")
|
||||
win.destroy()
|
||||
except Exception as e:
|
||||
self.logger.error(f"配置保存失败: {e}")
|
||||
|
||||
ttk.Button(win, text="保存", command=save).grid(row=8, column=0, columnspan=2, pady=10)
|
||||
ttk.Button(win, text="取消", command=win.destroy).grid(row=9, column=0, columnspan=2)
|
||||
|
||||
def show_db_config_window(self):
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("数据库配置")
|
||||
win.geometry("420x140")
|
||||
ttk.Label(win, text="数据库路径:").grid(row=0, column=0, padx=10, pady=8, sticky="w")
|
||||
e = ttk.Entry(win, width=32)
|
||||
e.grid(row=0, column=1, padx=10, pady=8)
|
||||
e.insert(0, self.config.db_path)
|
||||
|
||||
def save():
|
||||
path = e.get().strip()
|
||||
try:
|
||||
self.db.disconnect()
|
||||
self.db = DB(path)
|
||||
self.db.connect()
|
||||
self.config.db_path = path
|
||||
self.config.save()
|
||||
self.logger.info(f"数据库已切换: {path}")
|
||||
win.destroy()
|
||||
except Exception as ex:
|
||||
self.logger.error(f"数据库切换失败: {ex}")
|
||||
|
||||
ttk.Button(win, text="保存", command=save).grid(row=1, column=0, padx=10, pady=6)
|
||||
ttk.Button(win, text="取消", command=win.destroy).grid(row=1, column=1, pady=6)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def on_closing(self):
|
||||
self._stop_simulation()
|
||||
self.udp.stop()
|
||||
self.db.disconnect()
|
||||
self.root.destroy()
|
||||
@@ -0,0 +1,455 @@
|
||||
import struct
|
||||
|
||||
class msg_ccUFbMsg:
|
||||
def __init__(self):
|
||||
# 消息头 - 对应C++中的m_header
|
||||
self.start1 = 0x20
|
||||
self.start2 = 0x20
|
||||
self.id = 0x0051 # CCU_STATE_FB_ID
|
||||
self.length = 0
|
||||
self.checkCode = 0 # 使用4字节校验码,对应C++中的Checksum (unsigned int)
|
||||
|
||||
# CCU状态数据 - 完全对应C++中的ccuState结构体
|
||||
self.fc_mode = 0x00 # 燃料电池系统运行模式:00H-无效, 01H-调试, 02H-自动, 03H-补给
|
||||
self.fc_status = 0x00 # 燃料电池状态:00H-无效, 01H-初始化, ..., 0DH-维护完成
|
||||
self.fc_fault_level = 0x00 # 燃料电池故障等级:00H-无故障, 10H-一级, 20H-二级, 30H-三级, 40H-四级
|
||||
|
||||
self.output_power_limit = 0 # 系统输出功率限制值,单位:W,范围:0~30000W
|
||||
self.total_generation_time = 0 # 系统累计发电时间,单位:0.1小时,最大值6553.5小时
|
||||
self.total_generation_power = 0 # 系统累计发电总功率,单位:W
|
||||
self.fault_level_1 = 0 # 一级故障位,按位表示不同的故障项
|
||||
self.fault_level_2 = 0 # 二级故障位
|
||||
self.fault_level_3 = 0 # 三级故障位
|
||||
self.fault_level_4 = 0 # 四级故障位
|
||||
|
||||
self.hydrogen_capacity = 0 # 储氢罐剩余容量百分比,单位:%,范围:0~100
|
||||
self.liquid_oxygen_capacity = 0 # 液氧罐剩余容量百分比,单位:%,范围:0~100
|
||||
|
||||
self.palladium_temp = 0 # 钯膜最高温度,单位:℃
|
||||
self.main_pipe_pressure = 0 # 主水路压力,单位:0.01kPa
|
||||
self.aux_pipe_pressure = 0 # 辅水路压力,单位:0.01kPa
|
||||
|
||||
self.cabin_pressure1 = 0 # 舱室1压力,单位:0.01kPa
|
||||
self.cabin_pressure2 = 0 # 舱室2压力,单位:0.01kPa
|
||||
self.cabin_temp1 = 0 # 舱室1温度,单位:0.01℃
|
||||
self.cabin_temp2 = 0 # 舱室2温度,单位:0.01℃
|
||||
self.cabin_humidity1 = 0 # 舱室1湿度,单位:0.01%RH
|
||||
self.cabin_humidity2 = 0 # 舱室2湿度,单位:0.01%RH
|
||||
|
||||
self.flame_detector1 = 0x00 # 火焰探测器1状态:00H-无效, 01H-报警, 02H-故障
|
||||
self.flame_detector2 = 0x00 # 火焰探测器2状态:00H-无效, 01H-报警, 02H-故障
|
||||
|
||||
self.h2_concentration1 = 0 # 舱室H2浓度1,单位:0.01% LEL
|
||||
self.h2_concentration2 = 0 # 舱室H2浓度2,单位:0.01% LEL
|
||||
self.h2_concentration3 = 0 # 舱室H2浓度3,单位:0.01% LEL
|
||||
self.o2_concentration1 = 0 # 舱室O2浓度1,单位:0.01% LEL
|
||||
self.o2_concentration2 = 0 # 舱室O2浓度2,单位:0.01% LEL
|
||||
self.ch3oh_concentration1 = 0 # 舱室甲醇气体浓度1,单位:0.01% LEL
|
||||
self.ch3oh_concentration2 = 0 # 舱室甲醇气体浓度2,单位:0.01% LEL
|
||||
|
||||
self.reserved1 = [0, 0] # 保留字段前2个元素
|
||||
self.emergency_battery1_voltage = 0 # 应急电池1总电压 [V]
|
||||
self.emergency_battery1_current = 0 # 应急电池1总电流 [A]
|
||||
self.emergency_battery1_max_temp = 0 # 应急电池1最高温度 [°C]
|
||||
self.emergency_battery1_fault_word = 0 # 应急电池1故障字
|
||||
self.emergency_battery2_voltage = 0 # 应急电池2总电压 [V]
|
||||
self.emergency_battery2_current = 0 # 应急电池2总电流 [A]
|
||||
self.emergency_battery2_max_temp = 0 # 应急电池2最高温度 [°C]
|
||||
self.emergency_battery2_fault_word = 0 # 应急电池2故障字
|
||||
|
||||
self.cabin_ox_concentration = 0 # 仪表舱内氧气浓度 [%]
|
||||
self.cabin_temperature = 0 # 仪表舱内温度 [°C]
|
||||
self.cabin_humidity = 0 # 仪表舱内湿度 [%RH]
|
||||
self.cabin_pressure = 0 # 仪表舱内大气压 [kPa]
|
||||
|
||||
self.power_cabin_ox_concentration = 0 # 动力舱内氧气浓度 [%]
|
||||
self.power_cabin_temperature = 0 # 动力舱内温度 [°C]
|
||||
self.power_cabin_humidity = 0 # 动力舱内湿度 [%RH]
|
||||
self.power_cabin_pressure = 0 # 动力舱内大气压 [kPa]
|
||||
self.reserved2 = [0] # 保留字段,地址范围54~55,共2个uint8_t,协议预留
|
||||
|
||||
self.dyn_alarm_flag = [0] * 6 # 动力锂电池报警标志字数组,共6字节,按位表示各类报警
|
||||
self.ins_alarm_flag = [0] * 6 # 仪表锂电池报警标志字数组,共6字节,按位表示各类报警
|
||||
|
||||
self.ins_relay_status1 = 0x00 # 仪表电池继电器状态,高4位表示正极继电器,低4位表示充电继电器
|
||||
self.ins_relay_status2 = 0x00 # 仪表电池预充继电器和负极继电器状态
|
||||
self.dyn_relay_status1 = 0x00 # 动力电池正极与充电继电器状态
|
||||
self.dyn_relay_status2 = 0x00 # 动力电池预充与负极继电器状态
|
||||
|
||||
self.ins_max_discharge_power = 0 # 仪表电池最大允许放电功率,单位:kW,精度0.05kW
|
||||
self.dyn_max_discharge_power = 0 # 动力电池最大允许放电功率,单位:kW,精度0.05kW
|
||||
|
||||
self.ins_soc = 0 # 仪表锂电池荷电状态SOC,范围:0~100
|
||||
self.dyn_soc = 0 # 动力锂电池荷电状态SOC,范围:0~100
|
||||
|
||||
self.ins_total_energy = 0 # 仪表电池总能量,单位:kWh
|
||||
self.dyn_total_energy = 0 # 动力电池总能量,单位:kWh
|
||||
|
||||
self.ins_power_input = 0 # 仪表电池输入功率,单位:kW
|
||||
self.dyn_power_input = 0 # 动力电池输入功率,单位:kW
|
||||
|
||||
self.ins_charge_status = 0x00 # 仪表电池充放电状态:00H-无效,10H-充电,20H-放电
|
||||
self.dyn_charge_status = 0x00 # 动力电池充放电状态:00H-无效,10H-充电,20H-放电
|
||||
|
||||
self.ins_voltage_link = 0 # 仪表电池LINK端电压,单位:0.01V
|
||||
self.ins_voltage_pack = 0 # 仪表电池PACK端电压,单位:0.01V
|
||||
self.ins_current = 0 # 仪表电池电流,单位:0.01A(正值放电)
|
||||
self.ins_resistance_pos = 0 # 仪表电池正极绝缘电阻,单位:kΩ
|
||||
self.ins_resistance_neg = 0 # 仪表电池负极绝缘电阻,单位:kΩ
|
||||
|
||||
self.dyn_voltage_link = 0 # 动力电池LINK端电压,单位:0.01V
|
||||
self.dyn_voltage_pack = 0 # 动力电池PACK端电压,单位:0.01V
|
||||
self.dyn_current = 0 # 动力电池电流,单位:0.01A(正值放电)
|
||||
self.dyn_resistance_pos = 0 # 动力电池正极绝缘电阻,单位:kΩ
|
||||
self.dyn_resistance_neg = 0 # 动力电池负极绝缘电阻,单位:kΩ
|
||||
|
||||
self.ins_emergency_status = 0x00 # 仪表电池紧急状态字,按位表示异常或告警状态
|
||||
self.dyn_emergency_status = 0x00 # 动力电池紧急状态字,按位表示异常或告警状态
|
||||
|
||||
self.reserved3 = [0] * 2 # 保留字节,协议预留
|
||||
|
||||
self.ins_soc_threshold1 = 0 # 仪表电池1 SOC门限,范围:0~100
|
||||
self.ins_soc_threshold2 = 0 # 仪表电池2 SOC门限,范围:0~100
|
||||
self.ins_soc_threshold3 = 0 # 仪表电池3 SOC门限,范围:0~100
|
||||
self.dyn_soc_threshold1 = 0 # 动力电池1 SOC门限,范围:0~100
|
||||
self.dyn_soc_threshold2 = 0 # 动力电池2 SOC门限,范围:0~100
|
||||
self.dyn_soc_threshold3 = 0 # 动力电池3 SOC门限,范围:0~100
|
||||
|
||||
self.ins_power_limit1 = 0 # 仪表电池1功率限制值,单位:%
|
||||
self.ins_power_limit2 = 0 # 仪表电池2功率限制值,单位:%
|
||||
self.dyn_power_limit1 = 0 # 动力电池1功率限制值,单位:%
|
||||
self.dyn_power_limit2 = 0 # 动力电池2功率限制值,单位:%
|
||||
|
||||
self.device_online_flag1 = 0 # 设备在线状态标志1,byte0:低压配电板在线标志位 byte1:复合管控主机在线标志位 byte2:复合管控备机在线标志位
|
||||
self.device_online_flag2 = 0 # 设备在线状态标志2,byte0:仪表锂电在线标志位 byte1:动力锂电在线标志位 byte2:燃电锂电在线标志位 byte3:高压配电板在线标志位
|
||||
|
||||
self.dyn_charge_level = 0x00 # 动力电池充电状态:01H-1级(不限), 02H-2级(限功率), 03H-3级(关闭)
|
||||
self.dyn_water_leak_status = 0x00 # 动力漏水状态:0XH-正常, 1XH-异常漏水
|
||||
self.ins_water_leak_dcdc_status = 0x00 # 仪表漏水状态与DCDC状态:低4位-漏水状态(0XH正常,1XH异常),高4位-DCDC状态(X0H正常,X1H故障)
|
||||
self.reserved4 = 0x00
|
||||
self.heartbeat = 0 # 主机心跳计数器或标志值
|
||||
|
||||
def pack(self):
|
||||
"""打包消息为字节数据 - 完全对应C++中的msg_CcuStateFbMsg结构体"""
|
||||
# 重置校验码
|
||||
self.checkCode = 0
|
||||
|
||||
# 首先打包CCU状态数据(不包括消息头和校验码)
|
||||
body_data = b''
|
||||
|
||||
# 打包CCU状态数据 - 完全对应C++中的ccuState结构体
|
||||
# 第一部分:基本状态
|
||||
body_data += struct.pack('<BBB',
|
||||
self.fc_mode,
|
||||
self.fc_status,
|
||||
self.fc_fault_level)
|
||||
|
||||
# 第二部分:功率和时间
|
||||
body_data += struct.pack('<HHHHHHH',
|
||||
self.output_power_limit,
|
||||
self.total_generation_time,
|
||||
self.total_generation_power,
|
||||
self.fault_level_1,
|
||||
self.fault_level_2,
|
||||
self.fault_level_3,
|
||||
self.fault_level_4)
|
||||
|
||||
# 第三部分:容量
|
||||
body_data += struct.pack('<BB',
|
||||
self.hydrogen_capacity,
|
||||
self.liquid_oxygen_capacity)
|
||||
|
||||
# 第四部分:温度和压力
|
||||
body_data += struct.pack('<HHH',
|
||||
self.palladium_temp,
|
||||
self.main_pipe_pressure,
|
||||
self.aux_pipe_pressure)
|
||||
|
||||
# 第五部分:舱室环境
|
||||
body_data += struct.pack('<HHHHHH',
|
||||
self.cabin_pressure1,
|
||||
self.cabin_pressure2,
|
||||
self.cabin_temp1,
|
||||
self.cabin_temp2,
|
||||
self.cabin_humidity1,
|
||||
self.cabin_humidity2)
|
||||
|
||||
# 第六部分:火焰探测器
|
||||
body_data += struct.pack('<BB',
|
||||
self.flame_detector1,
|
||||
self.flame_detector2)
|
||||
|
||||
# 第七部分:气体浓度
|
||||
body_data += struct.pack('<HHHHHHH',
|
||||
self.h2_concentration1,
|
||||
self.h2_concentration2,
|
||||
self.h2_concentration3,
|
||||
self.o2_concentration1,
|
||||
self.o2_concentration2,
|
||||
self.ch3oh_concentration1,
|
||||
self.ch3oh_concentration2)
|
||||
|
||||
# 第八部分:保留字段和应急电池数据
|
||||
body_data += struct.pack('<HHHHHHHH',
|
||||
self.reserved1[0], self.reserved1[1],
|
||||
self.emergency_battery1_voltage,
|
||||
self.emergency_battery1_current,
|
||||
self.emergency_battery1_max_temp,
|
||||
self.emergency_battery1_fault_word,
|
||||
self.emergency_battery2_voltage,
|
||||
self.emergency_battery2_current)
|
||||
|
||||
body_data += struct.pack('<HHHH',
|
||||
self.emergency_battery2_max_temp,
|
||||
self.emergency_battery2_fault_word,
|
||||
self.cabin_ox_concentration,
|
||||
self.cabin_temperature)
|
||||
|
||||
# 第九部分:舱室环境2
|
||||
body_data += struct.pack('<HHHHHHHH',
|
||||
self.cabin_humidity,
|
||||
self.cabin_pressure,
|
||||
self.power_cabin_ox_concentration,
|
||||
self.power_cabin_temperature,
|
||||
self.power_cabin_humidity,
|
||||
self.power_cabin_pressure,
|
||||
self.reserved2[0], 0) # 填充第二个reserved2字节
|
||||
|
||||
# 第十部分:报警标志数组
|
||||
for flag in self.dyn_alarm_flag:
|
||||
body_data += struct.pack('<B', flag)
|
||||
for flag in self.ins_alarm_flag:
|
||||
body_data += struct.pack('<B', flag)
|
||||
|
||||
# 第十一部分:继电器状态
|
||||
body_data += struct.pack('<BBBB',
|
||||
self.ins_relay_status1,
|
||||
self.ins_relay_status2,
|
||||
self.dyn_relay_status1,
|
||||
self.dyn_relay_status2)
|
||||
|
||||
# 第十二部分:功率和SOC数据
|
||||
body_data += struct.pack('<HHBBHHHH',
|
||||
self.ins_max_discharge_power,
|
||||
self.dyn_max_discharge_power,
|
||||
self.ins_soc,
|
||||
self.dyn_soc,
|
||||
self.ins_total_energy,
|
||||
self.dyn_total_energy,
|
||||
self.ins_power_input,
|
||||
self.dyn_power_input)
|
||||
|
||||
# 第十三部分:充电状态
|
||||
body_data += struct.pack('<BB',
|
||||
self.ins_charge_status,
|
||||
self.dyn_charge_status)
|
||||
|
||||
# 第十四部分:电压电流和绝缘电阻
|
||||
body_data += struct.pack('<HHHHHHHHHH',
|
||||
self.ins_voltage_link,
|
||||
self.ins_voltage_pack,
|
||||
self.ins_current,
|
||||
self.ins_resistance_pos,
|
||||
self.ins_resistance_neg,
|
||||
self.dyn_voltage_link,
|
||||
self.dyn_voltage_pack,
|
||||
self.dyn_current,
|
||||
self.dyn_resistance_pos,
|
||||
self.dyn_resistance_neg)
|
||||
|
||||
# 第十五部分:紧急状态和保留字段
|
||||
body_data += struct.pack('<BB',
|
||||
self.ins_emergency_status,
|
||||
self.dyn_emergency_status)
|
||||
|
||||
body_data += struct.pack('<BB',
|
||||
self.reserved3[0], self.reserved3[1])
|
||||
|
||||
# 第十六部分:SOC门限值
|
||||
body_data += struct.pack('<BBBBBB',
|
||||
self.ins_soc_threshold1,
|
||||
self.ins_soc_threshold2,
|
||||
self.ins_soc_threshold3,
|
||||
self.dyn_soc_threshold1,
|
||||
self.dyn_soc_threshold2,
|
||||
self.dyn_soc_threshold3)
|
||||
|
||||
# 第十七部分:功率限制值
|
||||
body_data += struct.pack('<BBBB',
|
||||
self.ins_power_limit1,
|
||||
self.ins_power_limit2,
|
||||
self.dyn_power_limit1,
|
||||
self.dyn_power_limit2)
|
||||
|
||||
# 第十八部分:设备在线状态和心跳
|
||||
body_data += struct.pack('<II',
|
||||
self.device_online_flag1,
|
||||
self.device_online_flag2)
|
||||
|
||||
body_data += struct.pack('<BBBB',
|
||||
self.dyn_charge_level,
|
||||
self.dyn_water_leak_status,
|
||||
self.ins_water_leak_dcdc_status,
|
||||
self.reserved4)
|
||||
|
||||
body_data += struct.pack('<I', self.heartbeat)
|
||||
|
||||
# 计算长度字段:body_data的长度
|
||||
self.length = len(body_data)
|
||||
|
||||
# 打包消息头 - 对应C++中的m_header
|
||||
header_data = struct.pack('<BBHH',
|
||||
self.start1,
|
||||
self.start2,
|
||||
self.id,
|
||||
self.length)
|
||||
|
||||
# 组合头部和body数据
|
||||
data_without_checksum = header_data + body_data
|
||||
|
||||
# 计算校验码 - 根据C++代码,计算所有数据的和(不包括校验码本身)
|
||||
self.checkCode = 0
|
||||
for byte in data_without_checksum:
|
||||
self.checkCode += byte
|
||||
|
||||
# 添加校验码 - 使用4字节,对应C++中的Checksum (unsigned int)
|
||||
checksum_data = struct.pack('<I', self.checkCode)
|
||||
|
||||
# 完整数据:头部 + body + 校验码
|
||||
full_data = data_without_checksum + checksum_data
|
||||
|
||||
return full_data
|
||||
|
||||
def unpack(self, data):
|
||||
"""从字节数据解包消息 - 完全对应C++中的msg_CcuStateFbMsg结构体"""
|
||||
if len(data) < 10: # 最小消息长度检查
|
||||
return False
|
||||
|
||||
# 解析消息头
|
||||
self.start1 = data[0]
|
||||
self.start2 = data[1]
|
||||
self.id = struct.unpack_from('<H', data, 2)[0]
|
||||
self.length = struct.unpack_from('<H', data, 4)[0]
|
||||
|
||||
offset = 6 # 头部结束位置
|
||||
|
||||
# 检查数据长度是否足够(现在校验码是4字节)
|
||||
if len(data) < offset + self.length + 4: # +4 for 4-byte checksum
|
||||
return False
|
||||
|
||||
# 解析CCU状态数据 - 完全对应C++中的ccuState结构体
|
||||
# 第一部分:基本状态
|
||||
(self.fc_mode, self.fc_status, self.fc_fault_level) = struct.unpack_from('<BBB', data, offset); offset += 3
|
||||
|
||||
# 第二部分:功率和时间
|
||||
(self.output_power_limit, self.total_generation_time, self.total_generation_power,
|
||||
self.fault_level_1, self.fault_level_2, self.fault_level_3, self.fault_level_4) = struct.unpack_from('<HHHHHHH', data, offset); offset += 14
|
||||
|
||||
# 第三部分:容量
|
||||
(self.hydrogen_capacity, self.liquid_oxygen_capacity) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第四部分:温度和压力
|
||||
(self.palladium_temp, self.main_pipe_pressure, self.aux_pipe_pressure) = struct.unpack_from('<HHH', data, offset); offset += 6
|
||||
|
||||
# 第五部分:舱室环境
|
||||
(self.cabin_pressure1, self.cabin_pressure2, self.cabin_temp1, self.cabin_temp2,
|
||||
self.cabin_humidity1, self.cabin_humidity2) = struct.unpack_from('<HHHHHH', data, offset); offset += 12
|
||||
|
||||
# 第六部分:火焰探测器
|
||||
(self.flame_detector1, self.flame_detector2) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第七部分:气体浓度
|
||||
(self.h2_concentration1, self.h2_concentration2, self.h2_concentration3,
|
||||
self.o2_concentration1, self.o2_concentration2, self.ch3oh_concentration1,
|
||||
self.ch3oh_concentration2) = struct.unpack_from('<HHHHHHH', data, offset); offset += 14
|
||||
|
||||
# 第八部分:保留字段和应急电池数据
|
||||
(self.reserved1[0], self.reserved1[1], self.emergency_battery1_voltage,
|
||||
self.emergency_battery1_current, self.emergency_battery1_max_temp,
|
||||
self.emergency_battery1_fault_word, self.emergency_battery2_voltage,
|
||||
self.emergency_battery2_current) = struct.unpack_from('<HHHHHHHH', data, offset); offset += 16
|
||||
|
||||
(self.emergency_battery2_max_temp, self.emergency_battery2_fault_word,
|
||||
self.cabin_ox_concentration, self.cabin_temperature) = struct.unpack_from('<HHHH', data, offset); offset += 8
|
||||
|
||||
# 第九部分:舱室环境2
|
||||
(self.cabin_humidity, self.cabin_pressure, self.power_cabin_ox_concentration,
|
||||
self.power_cabin_temperature, self.power_cabin_humidity, self.power_cabin_pressure,
|
||||
reserved2_0, reserved2_1) = struct.unpack_from('<HHHHHHBB', data, offset); offset += 16
|
||||
self.reserved2 = [reserved2_0, reserved2_1]
|
||||
|
||||
# 第十部分:报警标志数组
|
||||
for i in range(6):
|
||||
self.dyn_alarm_flag[i] = data[offset]; offset += 1
|
||||
for i in range(6):
|
||||
self.ins_alarm_flag[i] = data[offset]; offset += 1
|
||||
|
||||
# 第十一部分:继电器状态
|
||||
(self.ins_relay_status1, self.ins_relay_status2,
|
||||
self.dyn_relay_status1, self.dyn_relay_status2) = struct.unpack_from('<BBBB', data, offset); offset += 4
|
||||
|
||||
# 第十二部分:功率和SOC数据
|
||||
(self.ins_max_discharge_power, self.dyn_max_discharge_power,
|
||||
self.ins_soc, self.dyn_soc, self.ins_total_energy, self.dyn_total_energy,
|
||||
self.ins_power_input, self.dyn_power_input) = struct.unpack_from('<HHBBHHHH', data, offset); offset += 14
|
||||
|
||||
# 第十三部分:充电状态
|
||||
(self.ins_charge_status, self.dyn_charge_status) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第十四部分:电压电流和绝缘电阻
|
||||
(self.ins_voltage_link, self.ins_voltage_pack, self.ins_current,
|
||||
self.ins_resistance_pos, self.ins_resistance_neg, self.dyn_voltage_link,
|
||||
self.dyn_voltage_pack, self.dyn_current, self.dyn_resistance_pos,
|
||||
self.dyn_resistance_neg) = struct.unpack_from('<HHHHHHHHHH', data, offset); offset += 20
|
||||
|
||||
# 第十五部分:紧急状态和保留字段
|
||||
(self.ins_emergency_status, self.dyn_emergency_status) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
(self.reserved3[0], self.reserved3[1]) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第十六部分:SOC门限值
|
||||
(self.ins_soc_threshold1, self.ins_soc_threshold2, self.ins_soc_threshold3,
|
||||
self.dyn_soc_threshold1, self.dyn_soc_threshold2, self.dyn_soc_threshold3) = struct.unpack_from('<BBBBBB', data, offset); offset += 6
|
||||
|
||||
# 第十七部分:功率限制值
|
||||
(self.ins_power_limit1, self.ins_power_limit2,
|
||||
self.dyn_power_limit1, self.dyn_power_limit2) = struct.unpack_from('<BBBB', data, offset); offset += 4
|
||||
|
||||
# 第十八部分:设备在线状态和心跳
|
||||
(self.device_online_flag1, self.device_online_flag2) = struct.unpack_from('<II', data, offset); offset += 8
|
||||
|
||||
(self.dyn_charge_level, self.dyn_water_leak_status,
|
||||
self.ins_water_leak_dcdc_status, self.reserved4) = struct.unpack_from('<BBBB', data, offset); offset += 4
|
||||
|
||||
(self.heartbeat,) = struct.unpack_from('<I', data, offset); offset += 4
|
||||
|
||||
# 解析校验码(4字节)
|
||||
(self.checkCode,) = struct.unpack_from('<I', data, offset); offset += 4
|
||||
|
||||
# 校验校验码 - 根据C++代码,计算所有数据的和(不包括校验码本身)
|
||||
checkCode = 0
|
||||
# 计算除校验码外的所有字节的和
|
||||
for i in range(len(data) - 4): # 不包括最后4字节的校验码
|
||||
checkCode += data[i]
|
||||
|
||||
return checkCode == self.checkCode
|
||||
|
||||
def print_info(self):
|
||||
"""打印消息信息"""
|
||||
print(f"消息头: start1=0x{self.start1:02X}, start2=0x{self.start2:02X}, id=0x{self.id:04X}, length={self.length}")
|
||||
print(f"燃料电池模式: 0x{self.fc_mode:02X}")
|
||||
print(f"燃料电池状态: 0x{self.fc_status:02X}")
|
||||
print(f"故障等级: 0x{self.fc_fault_level:02X}")
|
||||
print(f"输出功率限制: {self.output_power_limit}W")
|
||||
print(f"累计发电时间: {self.total_generation_time * 0.1}小时")
|
||||
print(f"发电总功率: {self.total_generation_power}W")
|
||||
print(f"储氢容量: {self.hydrogen_capacity}%")
|
||||
print(f"液氧容量: {self.liquid_oxygen_capacity}%")
|
||||
print(f"钯膜温度: {self.palladium_temp}℃")
|
||||
print(f"主水路压力: {self.main_pipe_pressure * 0.01}kPa")
|
||||
print(f"辅水路压力: {self.aux_pipe_pressure * 0.01}kPa")
|
||||
print(f"仪表SOC: {self.ins_soc}%")
|
||||
print(f"动力SOC: {self.dyn_soc}%")
|
||||
print(f"心跳计数器: {self.heartbeat}")
|
||||
print(f"校验码: 0x{self.checkCode:08X}")
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext
|
||||
|
||||
from . import ccuMsg
|
||||
from datetime import datetime
|
||||
|
||||
class CCUStatusSummaryDialog:
|
||||
"""CCU状态汇总弹窗"""
|
||||
def __init__(self, parent, ccu_status=None, title="CCU状态汇总"):
|
||||
self.parent = parent
|
||||
self.ccu_status = ccu_status or ccuMsg.msg_ccUFbMsg()
|
||||
|
||||
# 创建弹窗
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title(title)
|
||||
self.dialog.geometry("1000x700")
|
||||
self.dialog.transient(parent) # 设置为父窗口的临时窗口
|
||||
self.dialog.grab_set() # 模态对话框
|
||||
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(self.dialog, padding="10")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 标题
|
||||
title_label = ttk.Label(main_frame, text="CCU状态详细信息", font=('Arial', 16, 'bold'))
|
||||
title_label.pack(pady=10)
|
||||
|
||||
# 时间标签
|
||||
self.time_label = ttk.Label(main_frame, text="", font=('Arial', 10))
|
||||
self.time_label.pack(pady=5)
|
||||
|
||||
# 创建滚动文本框
|
||||
self.summary_text = scrolledtext.ScrolledText(main_frame, width=90, height=30, wrap=tk.WORD, font=('Courier', 10))
|
||||
self.summary_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(pady=10)
|
||||
|
||||
# 关闭按钮
|
||||
close_btn = ttk.Button(button_frame, text="关闭", command=self.dialog.destroy)
|
||||
close_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = ttk.Button(button_frame, text="刷新", command=self.update_display)
|
||||
refresh_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 初始更新显示
|
||||
self.update_display()
|
||||
|
||||
def update_display(self):
|
||||
"""更新显示数据"""
|
||||
# 更新时间标签
|
||||
self.time_label.config(text=f"更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 生成汇总信息
|
||||
summary = self.generate_summary()
|
||||
|
||||
# 清空并插入新的汇总信息
|
||||
self.summary_text.delete(1.0, tk.END)
|
||||
self.summary_text.insert(1.0, summary)
|
||||
|
||||
def generate_summary(self):
|
||||
"""生成汇总信息"""
|
||||
return f"""CCU状态汇总 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
燃料电池状态:
|
||||
运行模式: 0x{self.ccu_status.fc_mode:02X}
|
||||
运行状态: 0x{self.ccu_status.fc_status:02X}
|
||||
故障等级: 0x{self.ccu_status.fc_fault_level:02X}
|
||||
输出功率限制: {self.ccu_status.output_power_limit}W
|
||||
累计发电时间: {self.ccu_status.total_generation_time * 0.1:.1f}小时
|
||||
发电总功率: {self.ccu_status.total_generation_power}W
|
||||
故障等级1-4: {self.ccu_status.fault_level_1}, {self.ccu_status.fault_level_2}, {self.ccu_status.fault_level_3}, {self.ccu_status.fault_level_4}
|
||||
储氢容量: {self.ccu_status.hydrogen_capacity}%
|
||||
液氧容量: {self.ccu_status.liquid_oxygen_capacity}%
|
||||
|
||||
传感器数据:
|
||||
钯膜温度: {self.ccu_status.palladium_temp}℃
|
||||
主水路压力: {self.ccu_status.main_pipe_pressure * 0.01:.2f}kPa
|
||||
辅水路压力: {self.ccu_status.aux_pipe_pressure * 0.01:.2f}kPa
|
||||
舱室压力1/2: {self.ccu_status.cabin_pressure1}/{self.ccu_status.cabin_pressure2}Pa
|
||||
舱室温度1/2: {self.ccu_status.cabin_temp1}/{self.ccu_status.cabin_temp2}℃
|
||||
舱室湿度1/2: {self.ccu_status.cabin_humidity1}/{self.ccu_status.cabin_humidity2}%
|
||||
火焰探测器1/2: 0x{self.ccu_status.flame_detector1:02X}/0x{self.ccu_status.flame_detector2:02X}
|
||||
|
||||
浓度检测:
|
||||
H2浓度1/2/3: {self.ccu_status.h2_concentration1}/{self.ccu_status.h2_concentration2}/{self.ccu_status.h2_concentration3}
|
||||
O2浓度1/2: {self.ccu_status.o2_concentration1}/{self.ccu_status.o2_concentration2}
|
||||
CH3OH浓度1/2: {self.ccu_status.ch3oh_concentration1}/{self.ccu_status.ch3oh_concentration2}
|
||||
|
||||
应急电池:
|
||||
电池1电压/电流: {self.ccu_status.emergency_battery1_voltage}V/{self.ccu_status.emergency_battery1_current}A
|
||||
电池1最高温度: {self.ccu_status.emergency_battery1_max_temp}℃
|
||||
电池1故障字: 0x{self.ccu_status.emergency_battery1_fault_word:04X}
|
||||
电池2电压/电流: {self.ccu_status.emergency_battery2_voltage}V/{self.ccu_status.emergency_battery2_current}A
|
||||
电池2最高温度: {self.ccu_status.emergency_battery2_max_temp}℃
|
||||
电池2故障字: 0x{self.ccu_status.emergency_battery2_fault_word:04X}
|
||||
|
||||
舱室环境:
|
||||
舱室氧浓度: {self.ccu_status.cabin_ox_concentration}
|
||||
舱室温度: {self.ccu_status.cabin_temperature}℃
|
||||
舱室湿度: {self.ccu_status.cabin_humidity}%
|
||||
舱室压力: {self.ccu_status.cabin_pressure}Pa
|
||||
动力舱氧浓度: {self.ccu_status.power_cabin_ox_concentration}
|
||||
动力舱温度: {self.ccu_status.power_cabin_temperature}℃
|
||||
动力舱湿度: {self.ccu_status.power_cabin_humidity}%
|
||||
动力舱压力: {self.ccu_status.power_cabin_pressure}Pa
|
||||
|
||||
报警状态:
|
||||
动态报警标志: {[flag for flag in self.ccu_status.dyn_alarm_flag]}
|
||||
绝缘报警标志: {[flag for flag in self.ccu_status.ins_alarm_flag]}
|
||||
动态报警总数: {sum(self.ccu_status.dyn_alarm_flag)}
|
||||
绝缘报警总数: {sum(self.ccu_status.ins_alarm_flag)}
|
||||
|
||||
继电器状态:
|
||||
仪表继电器1/2: 0x{self.ccu_status.ins_relay_status1:02X}/0x{self.ccu_status.ins_relay_status2:02X}
|
||||
动力继电器1/2: 0x{self.ccu_status.dyn_relay_status1:02X}/0x{self.ccu_status.dyn_relay_status2:02X}
|
||||
|
||||
功率信息:
|
||||
仪表最大放电功率: {self.ccu_status.ins_max_discharge_power}W
|
||||
动力最大放电功率: {self.ccu_status.dyn_max_discharge_power}W
|
||||
仪表SOC: {self.ccu_status.ins_soc}%
|
||||
动力SOC: {self.ccu_status.dyn_soc}%
|
||||
仪表总能量: {self.ccu_status.ins_total_energy}Wh
|
||||
动力总能量: {self.ccu_status.dyn_total_energy}Wh
|
||||
仪表功率输入: {self.ccu_status.ins_power_input}W
|
||||
动力功率输入: {self.ccu_status.dyn_power_input}W
|
||||
|
||||
充电状态:
|
||||
仪表充电状态: 0x{self.ccu_status.ins_charge_status:02X}
|
||||
动力充电状态: 0x{self.ccu_status.dyn_charge_status:02X}
|
||||
|
||||
电气参数:
|
||||
仪表链路电压: {self.ccu_status.ins_voltage_link * 0.1:.1f}V
|
||||
仪表电池电压: {self.ccu_status.ins_voltage_pack * 0.1:.1f}V
|
||||
仪表电流: {self.ccu_status.ins_current * 0.1:.1f}A
|
||||
仪表正极绝缘电阻: {self.ccu_status.ins_resistance_pos}Ω
|
||||
仪表负极绝缘电阻: {self.ccu_status.ins_resistance_neg}Ω
|
||||
动力链路电压: {self.ccu_status.dyn_voltage_link * 0.1:.1f}V
|
||||
动力电池电压: {self.ccu_status.dyn_voltage_pack * 0.1:.1f}V
|
||||
动力电流: {self.ccu_status.dyn_current * 0.1:.1f}A
|
||||
动力正极绝缘电阻: {self.ccu_status.dyn_resistance_pos}Ω
|
||||
动力负极绝缘电阻: {self.ccu_status.dyn_resistance_neg}Ω
|
||||
|
||||
紧急状态:
|
||||
仪表紧急状态: 0x{self.ccu_status.ins_emergency_status:02X}
|
||||
动力紧急状态: 0x{self.ccu_status.dyn_emergency_status:02X}
|
||||
|
||||
SOC门限值:
|
||||
仪表SOC门限1/2/3: {self.ccu_status.ins_soc_threshold1}/{self.ccu_status.ins_soc_threshold2}/{self.ccu_status.ins_soc_threshold3}%
|
||||
动力SOC门限1/2/3: {self.ccu_status.dyn_soc_threshold1}/{self.ccu_status.dyn_soc_threshold2}/{self.ccu_status.dyn_soc_threshold3}%
|
||||
|
||||
功率限制值:
|
||||
仪表功率限制1/2: {self.ccu_status.ins_power_limit1}/{self.ccu_status.ins_power_limit2}W
|
||||
动力功率限制1/2: {self.ccu_status.dyn_power_limit1}/{self.ccu_status.dyn_power_limit2}W
|
||||
|
||||
设备在线状态:
|
||||
设备在线标志1: 0x{self.ccu_status.device_online_flag1:08X}
|
||||
设备在线标志2: 0x{self.ccu_status.device_online_flag2:08X}
|
||||
|
||||
其他状态:
|
||||
动力充电等级: 0x{self.ccu_status.dyn_charge_level:02X}
|
||||
动力漏水状态: 0x{self.ccu_status.dyn_water_leak_status:02X}
|
||||
仪表漏水DCDC状态: 0x{self.ccu_status.ins_water_leak_dcdc_status:02X}
|
||||
心跳计数器: {self.ccu_status.heartbeat}
|
||||
"""
|
||||
|
||||
def set_ccu_status(self, ccu_status):
|
||||
"""设置CCU状态数据"""
|
||||
self.ccu_status = ccu_status
|
||||
self.update_display()
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_ccu_summary_dialog():
|
||||
"""测试CCU状态汇总弹窗"""
|
||||
root = tk.Tk()
|
||||
root.withdraw() # 隐藏主窗口
|
||||
|
||||
# 设置一些测试数据
|
||||
test_status = ccuMsg.msg_ccUFbMsg()
|
||||
test_status.fc_mode = 0x02 # 自动模式
|
||||
test_status.fc_status = 0x06 # 运行状态
|
||||
test_status.output_power_limit = 15000
|
||||
test_status.total_generation_time = 100 # 10小时
|
||||
test_status.hydrogen_capacity = 80
|
||||
test_status.liquid_oxygen_capacity = 75
|
||||
test_status.ins_soc = 85
|
||||
test_status.dyn_soc = 90
|
||||
test_status.heartbeat = 12345
|
||||
test_status.palladium_temp = 65
|
||||
test_status.main_pipe_pressure = 1500 # 15.00 kPa
|
||||
test_status.aux_pipe_pressure = 800 # 8.00 kPa
|
||||
|
||||
# 设置一些报警标志
|
||||
test_status.dyn_alarm_flag[0] = 1
|
||||
test_status.ins_alarm_flag[2] = 1
|
||||
|
||||
# 创建弹窗
|
||||
dialog = CCUStatusSummaryDialog(root, test_status)
|
||||
|
||||
# 等待弹窗关闭
|
||||
root.wait_window(dialog.dialog)
|
||||
root.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_ccu_summary_dialog()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""运行配置管理:config.json 的加载 / 保存 / 默认值。
|
||||
|
||||
配置文件位置:
|
||||
- 打包后(exe):与可执行文件同目录
|
||||
- 源码运行:app/ 目录下
|
||||
首次运行不存在时自动生成默认配置。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
DEFAULTS = {
|
||||
"local": {"ip": "127.0.0.1", "port": 7000}, # 本机监听地址
|
||||
"c1": {"ip": "192.168.1.154", "port": 6000}, # 高压配电板(C1)
|
||||
"c2": {"ip": "192.168.1.158", "port": 6000}, # 仪表配电板(C2)
|
||||
"host": {"ip": "127.0.0.1", "port": 5001}, # 上位机(信息主机)
|
||||
"db_path": "test.db",
|
||||
"refresh_ms": 1000,
|
||||
"sim": {"interval": 1.0},
|
||||
}
|
||||
|
||||
|
||||
def default_config_dir():
|
||||
"""返回配置文件所在目录(打包后为 exe 目录,源码为 app/ 目录)。"""
|
||||
if getattr(sys, "frozen", False):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, path=None):
|
||||
self.path = path or os.path.join(default_config_dir(), "config.json")
|
||||
self.data = self._load()
|
||||
|
||||
def _load(self):
|
||||
if os.path.exists(self.path):
|
||||
try:
|
||||
with open(self.path, "r", encoding="utf-8") as f:
|
||||
loaded = json.load(f)
|
||||
merged = {}
|
||||
for key, value in DEFAULTS.items():
|
||||
if isinstance(value, dict):
|
||||
merged[key] = {**value, **loaded.get(key, {})}
|
||||
else:
|
||||
merged[key] = loaded.get(key, value)
|
||||
return merged
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return json.loads(json.dumps(DEFAULTS))
|
||||
|
||||
def save(self):
|
||||
with open(self.path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# ---- 便捷访问 -------------------------------------------------
|
||||
@property
|
||||
def local_ip(self):
|
||||
return self.data["local"]["ip"]
|
||||
|
||||
@local_ip.setter
|
||||
def local_ip(self, v):
|
||||
self.data["local"]["ip"] = v
|
||||
|
||||
@property
|
||||
def local_port(self):
|
||||
return int(self.data["local"]["port"])
|
||||
|
||||
@local_port.setter
|
||||
def local_port(self, v):
|
||||
self.data["local"]["port"] = int(v)
|
||||
|
||||
@property
|
||||
def c1_ip(self):
|
||||
return self.data["c1"]["ip"]
|
||||
|
||||
@property
|
||||
def c1_port(self):
|
||||
return int(self.data["c1"]["port"])
|
||||
|
||||
@property
|
||||
def c2_ip(self):
|
||||
return self.data["c2"]["ip"]
|
||||
|
||||
@property
|
||||
def c2_port(self):
|
||||
return int(self.data["c2"]["port"])
|
||||
|
||||
@property
|
||||
def host_ip(self):
|
||||
return self.data["host"]["ip"]
|
||||
|
||||
@property
|
||||
def host_port(self):
|
||||
return int(self.data["host"]["port"])
|
||||
|
||||
@property
|
||||
def refresh_ms(self):
|
||||
return int(self.data["refresh_ms"])
|
||||
|
||||
@property
|
||||
def db_path(self):
|
||||
p = self.data["db_path"]
|
||||
if os.path.isabs(p):
|
||||
return p
|
||||
return os.path.join(default_config_dir(), p)
|
||||
|
||||
@db_path.setter
|
||||
def db_path(self, v):
|
||||
self.data["db_path"] = v
|
||||
|
||||
@property
|
||||
def sim_interval(self):
|
||||
return float(self.data["sim"]["interval"])
|
||||
|
||||
@sim_interval.setter
|
||||
def sim_interval(self, v):
|
||||
self.data["sim"]["interval"] = float(v)
|
||||
|
||||
# ---- 批量设置(供配置弹窗使用) --------------------------------
|
||||
def set_endpoints(self, local_ip, local_port, c1_ip, c1_port,
|
||||
c2_ip, c2_port, host_ip, host_port):
|
||||
self.data["local"] = {"ip": local_ip, "port": int(local_port)}
|
||||
self.data["c1"] = {"ip": c1_ip, "port": int(c1_port)}
|
||||
self.data["c2"] = {"ip": c2_ip, "port": int(c2_port)}
|
||||
self.data["host"] = {"ip": host_ip, "port": int(host_port)}
|
||||
@@ -0,0 +1,78 @@
|
||||
"""SQLite 存储:表结构与插入字段由协议 schema 自动生成。
|
||||
|
||||
消息类 -> 表名 映射见 TABLE_NAMES;插入时从 msg.to_dict() 取字段,
|
||||
与 protocol.py 保持单一数据源,避免手写列定义漂移。
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import threading
|
||||
|
||||
from . import protocol
|
||||
|
||||
|
||||
def _table_name(msg_cls):
|
||||
name = msg_cls.__name__
|
||||
if name.startswith("MsgDis"):
|
||||
name = name[len("MsgDis"):]
|
||||
# MsgDisHighVolBusFb -> msg_disHighVolBusFbMsg
|
||||
return "msg_dis" + name + "Msg"
|
||||
|
||||
|
||||
TABLE_NAMES = {cls: _table_name(cls) for cls in protocol.ALL_MESSAGES.values()}
|
||||
|
||||
|
||||
class DB:
|
||||
def __init__(self, db_path):
|
||||
self.db_path = db_path
|
||||
self.conn = None
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def connect(self):
|
||||
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self.create_tables()
|
||||
|
||||
def disconnect(self):
|
||||
"""与 close() 等价,兼容历史调用。"""
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self.conn is not None:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
def create_tables(self):
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
for msg_cls, table in TABLE_NAMES.items():
|
||||
cols = ", ".join(f'"{name}" INTEGER' for name in msg_cls.field_names())
|
||||
cursor.execute(
|
||||
f'CREATE TABLE IF NOT EXISTS "{table}" ('
|
||||
f'id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
f'timestamp DATETIME DEFAULT (datetime(\'now\', \'localtime\')), '
|
||||
f'{cols})'
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def insert_message(self, msg):
|
||||
"""按消息类型路由到对应数据表。返回 True/False。"""
|
||||
table = TABLE_NAMES.get(msg.__class__)
|
||||
if table is None:
|
||||
return False
|
||||
return self.insert_dict(table, msg.to_dict())
|
||||
|
||||
def insert_dict(self, table, data_dict):
|
||||
with self.lock:
|
||||
try:
|
||||
if self.conn is None:
|
||||
self.connect()
|
||||
if self.conn is None:
|
||||
return False
|
||||
columns = ", ".join(f'"{c}"' for c in data_dict.keys())
|
||||
placeholders = ", ".join(["?"] * len(data_dict))
|
||||
query = f'INSERT INTO "{table}" ({columns}) VALUES ({placeholders})'
|
||||
self.conn.cursor().execute(query, list(data_dict.values()))
|
||||
self.conn.commit()
|
||||
return True
|
||||
except sqlite3.Error:
|
||||
return False
|
||||
@@ -0,0 +1,65 @@
|
||||
"""日志模块:UI 文本控件 + 控制台/文件输出,线程安全。
|
||||
|
||||
UI 更新不直接在任意线程操作 Tk 控件,而是写入 queue;
|
||||
主线程通过 drain() 周期性取走并写入文本框(见 app._refresh_ui)。
|
||||
"""
|
||||
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class AppLogger:
|
||||
def __init__(self, widget=None, file_path=None, max_lines=200,
|
||||
console=True):
|
||||
self.widget = widget
|
||||
self.file_path = file_path
|
||||
self.max_lines = max_lines
|
||||
self.console = console
|
||||
self._lock = threading.Lock()
|
||||
self._queue = queue.Queue()
|
||||
if file_path:
|
||||
os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)
|
||||
|
||||
def _format(self, level, msg):
|
||||
return f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [{level}] {msg}"
|
||||
|
||||
def info(self, msg):
|
||||
self._write("INFO", msg)
|
||||
|
||||
def warning(self, msg):
|
||||
self._write("WARN", msg)
|
||||
|
||||
def error(self, msg):
|
||||
self._write("ERROR", msg)
|
||||
|
||||
def _write(self, level, msg):
|
||||
line = self._format(level, msg)
|
||||
self._queue.put(line)
|
||||
with self._lock:
|
||||
if self.console:
|
||||
print(line)
|
||||
if self.file_path:
|
||||
try:
|
||||
with open(self.file_path, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def drain(self):
|
||||
"""主线程调用:把队列中的日志写入 UI 文本框。"""
|
||||
if self.widget is None or self._queue.empty():
|
||||
return
|
||||
try:
|
||||
self.widget.config(state="normal")
|
||||
while not self._queue.empty():
|
||||
self.widget.insert("end", self._queue.get_nowait() + "\n")
|
||||
count = int(self.widget.index("end-1c").split(".")[0])
|
||||
if count > self.max_lines:
|
||||
self.widget.delete("1.0", f"{count - self.max_lines}.0")
|
||||
self.widget.see("end")
|
||||
self.widget.config(state="disabled")
|
||||
except Exception:
|
||||
# 窗口销毁等异常静默
|
||||
pass
|
||||
@@ -0,0 +1,440 @@
|
||||
"""数据驱动的状态/操作面板构建器。
|
||||
|
||||
由 BusSpec / CmdSpec 声明式描述每个母线的字段、显示方式与控制项,
|
||||
build_bus_page / update_bus_page 负责创建与刷新界面,
|
||||
避免每个母线重复手写布局与 if/elif 更新逻辑。
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from dataclasses import dataclass
|
||||
from tkinter import ttk
|
||||
|
||||
from . import protocol
|
||||
from .protocol import BREAKER_ON, BREAKER_OFF, BREAKER_FAULT
|
||||
|
||||
# 配色
|
||||
GREEN = "#2e7d32"
|
||||
RED = "#d32f2f"
|
||||
BLACK = "#212121"
|
||||
GRAY = "#9e9e9e"
|
||||
|
||||
|
||||
def breaker_color(raw):
|
||||
if raw == BREAKER_ON:
|
||||
return GREEN
|
||||
if raw == BREAKER_OFF:
|
||||
return BLACK
|
||||
if raw == BREAKER_FAULT:
|
||||
return RED
|
||||
return GRAY
|
||||
|
||||
|
||||
def bool_ok(raw, ok=BREAKER_ON):
|
||||
return raw == ok
|
||||
|
||||
|
||||
def make_light(parent, size=18):
|
||||
canvas = tk.Canvas(parent, width=size, height=size, bg="white",
|
||||
highlightthickness=0)
|
||||
canvas.create_oval(2, 2, size - 2, size - 2, fill=GRAY, outline="black")
|
||||
return canvas
|
||||
|
||||
|
||||
def set_light(canvas, color):
|
||||
canvas.delete("all")
|
||||
canvas.create_oval(2, 2, int(canvas["width"]) - 2,
|
||||
int(canvas["height"]) - 2, fill=color, outline="black")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@dataclass
|
||||
class BusSpec:
|
||||
key: str # 总线标识
|
||||
title: str # 页标题
|
||||
msg_cls: type
|
||||
breakers: list # [(label, field)]
|
||||
voltages: list # [(label, field, scale, unit)]
|
||||
currents: list # [(label, field, scale, unit)]
|
||||
booleans: list # [(label, field)] ok=BREAKER_ON 为正常
|
||||
derived: list # [(label, getter)] getter(msg)->(text, color)
|
||||
|
||||
|
||||
HIGH_VOL_BUS = BusSpec(
|
||||
key="high_vol",
|
||||
title="动力母线状态",
|
||||
msg_cls=protocol.MsgDisHighVolBusFb,
|
||||
breakers=[
|
||||
("燃料电池断路器", "fuelCellCircuitBreaker"),
|
||||
("动力锂电池断路器", "powerLithiumBatteryCircuitBreaker"),
|
||||
("推进电机断路器", "propulsionMotorCircuitBreaker"),
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("DC/DC模块断路器", "dcDc5ModuleCircuitBreaker"),
|
||||
("艏高压配电盒断路器", "bowHighVoltageDistributionBoxCircuitBreaker"),
|
||||
("艉FT设备45断路器", "sternFTDevice45CircuitBreaker"),
|
||||
("艉舵机开关1断路器", "sternRudderSwitch1CircuitBreaker"),
|
||||
("艉舵机开关2断路器", "sternRudderSwitch2CircuitBreaker"),
|
||||
("FB备用断路器", "fbReservedCircuitBreaker"),
|
||||
("TYZ备用断路器", "tyzReservedCircuitBreaker"),
|
||||
("备用断路器", "reservedCircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("动力母线电压", "powerBusVoltage", 0.1, "V"),
|
||||
("A母线电压", "busbarAVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("动力母线电流", "powerBusCurrent", 0.1, "A"),
|
||||
("A汇流排电流", "busbarACurrent", 0.1, "A"),
|
||||
("DC/DC5模块电流", "dcDc5ModuleCurrent", 0.1, "A"),
|
||||
("推进电机控制箱电流", "propulsionMotorControlBoxCurrent", 0.1, "A"),
|
||||
("锂电池组(仪表)电流", "lithiumBatteryGroupMeterCurrent", 0.1, "A"),
|
||||
("艏部高压配电箱电流", "bowHighVoltageDistributionBoxCurrent", 0.1, "A"),
|
||||
("艉部FT装置4/5电流", "sternFTDevice45Current", 0.1, "A"),
|
||||
("TYZ预留开关电流", "tyzReservedSwitchCurrent", 0.1, "A"),
|
||||
("艉舵开关1电流", "sternRudderSwitch1Current", 0.1, "A"),
|
||||
("艉舵开关2电流", "sternRudderSwitch2Current", 0.1, "A"),
|
||||
("FB预留开关电流", "fbReservedSwitchCurrent", 0.1, "A"),
|
||||
("备用电流1", "reservedCurrent1", 0.1, "A"),
|
||||
("备用电流2", "reservedCurrent2", 0.1, "A"),
|
||||
("备用电流3", "reservedCurrent3", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表失电信号", "meterPowerLossSignal"),
|
||||
("应急失电信号", "emergencyPowerLossSignal"),
|
||||
("动力母线绝缘", "powerBusInsulationStatus"),
|
||||
("艏高压绝缘", "aBusInsulationStatus"),
|
||||
],
|
||||
derived=[
|
||||
("冷却水压力", lambda m: (f"{m.coolWaterPressure * 0.1:.1f} kPa", BLACK)),
|
||||
("DC/DC温度", lambda m: (f"{m.dcdcTemperature - 40} ℃", BLACK)),
|
||||
],
|
||||
)
|
||||
|
||||
HIGH_A_VOL_BUS = BusSpec(
|
||||
key="high_a_vol",
|
||||
title="艏部高压状态",
|
||||
msg_cls=protocol.MsgDisHighAVolBusFb,
|
||||
breakers=[
|
||||
("艏FT设备123断路器", "bowFTDevice123CircuitBreaker"),
|
||||
("执行机构断路器", "actuatorCircuitBreaker"),
|
||||
("桅舵机控制箱断路器", "mastSteeringGearControlBoxCircuitBreaker"),
|
||||
("XCZ断路器", "xczCircuitBreaker"),
|
||||
("艏舵控制箱断路器", "bowRudderControlBoxCircuitBreaker"),
|
||||
("开盖启动气缸断路器", "openWaterCoverStartCylinderCircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("汇流排B电压", "busbarBVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("汇流排B电流", "busbarBCurrent", 0.1, "A"),
|
||||
("艏部FT设备123电流", "bowFTDevice123Current", 0.1, "A"),
|
||||
("启闭机构电流", "actuatorCurrent", 0.1, "A"),
|
||||
("桅杆舵机控制箱电流", "mastSteeringGearControlBoxCurrent", 0.1, "A"),
|
||||
("XCZ电流", "xczCurrent", 0.1, "A"),
|
||||
("艏舵控制箱电流", "bowRudderControlBoxCurrent", 0.1, "A"),
|
||||
("敞水盖启动电缸电流", "openWaterCoverStartCylinderCurrent", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表电源失电信号", "instrumentPowerFailureSignal"),
|
||||
("应急电源失电信号", "emergencyPowerFailureSignal"),
|
||||
("进水报警", "waterIngressionAlarm"),
|
||||
],
|
||||
derived=[],
|
||||
)
|
||||
|
||||
LOW_MAIN_BUS = BusSpec(
|
||||
key="low_main",
|
||||
title="仪表母线状态",
|
||||
msg_cls=protocol.MsgDisLowMainBusFb,
|
||||
breakers=[
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("艏低压配电盒断路器", "bowLowVoltageDistributionBoxCircuitBreaker"),
|
||||
("单元4仪表48V断路器", "unit4InstrumentDC48VCircuitBreaker"),
|
||||
("DC/C1直流配电板断路器", "dcC1DCDistributionPanelCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组2断路器", "emergencyLithiumBatteryGroup2CircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("仪表母线电压", "instrumentBusbarVoltage", 0.1, "V"),
|
||||
("应急2汇流排电压", "emergency2BusbarVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("艏部低压配电箱电流", "bowLowVoltageDistributionBoxCurrent", 0.1, "A"),
|
||||
("锂电池组(仪表)电流", "lithiumBatteryGroupInstrumentCurrent", 0.1, "A"),
|
||||
("DC-C1仪表DC48V电流", "dcC1InstrumentDC48VCurrent", 0.1, "A"),
|
||||
("应急锂电池组2电流", "emergencyLithiumBatteryGroup2Current", 0.1, "A"),
|
||||
("单元4仪表48V电流", "unit4InstrumentDC48VCurrent", 0.1, "A"),
|
||||
("复合能源管控应急DC48V电流", "compositeEnergyManagementSystemEmergencyDC48VCurrent", 0.1, "A"),
|
||||
("燃料电池安保应急DC48V电流", "fuelCellSecuritySystemEmergencyDC48VCurrent", 0.1, "A"),
|
||||
("PLC控制电源电流", "plcControlPowerCurrent", 0.1, "A"),
|
||||
("备用电流1", "reservedCurrent1", 0.1, "A"),
|
||||
("备用电流2", "reservedCurrent2", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表电源失电信号", "instrumentPowerFailureSignal"),
|
||||
("应急电源失电信号", "emergencyPowerFailureSignal"),
|
||||
("仪表汇流排绝缘低", "instrumentBusbarInsulationLow"),
|
||||
],
|
||||
derived=[],
|
||||
)
|
||||
|
||||
LOW_BUS = BusSpec(
|
||||
key="low_bus",
|
||||
title="艏部低压状态",
|
||||
msg_cls=protocol.MsgDisLowBusFb,
|
||||
breakers=[
|
||||
("单元1断路器", "unit1CircuitBreaker"),
|
||||
("单元2断路器", "unit2CircuitBreaker"),
|
||||
("单元3断路器", "unit3CircuitBreaker"),
|
||||
("单元5断路器", "unit5CircuitBreaker"),
|
||||
("艏PZ设备断路器", "bowPZDeviceCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组1断路器", "emergencyLithiumBatteryGroup1CircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("仪表母排电压", "instrumentBusbarVoltage", 0.1, "V"),
|
||||
("应急1母线电压", "emergency1BusbarVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("仪表母排电流", "instrumentBusbarCurrent", 0.1, "A"),
|
||||
("单元1电流", "unit1Current", 0.1, "A"),
|
||||
("单元2电流", "unit2Current", 0.1, "A"),
|
||||
("单元3电流", "unit3Current", 0.1, "A"),
|
||||
("单元5电流", "unit5Current", 0.1, "A"),
|
||||
("艏PZ设备电流", "bowPZDeviceCurrent", 0.1, "A"),
|
||||
("应急锂电池组1电流", "emergencyLithiumBatteryGroup1Current", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表电失电信号", "instrumentPowerFailureSignal"),
|
||||
("应急电失电信号", "emergencyPowerFailureSignal"),
|
||||
("浸水报警", "waterIngressionAlarm"),
|
||||
],
|
||||
derived=[],
|
||||
)
|
||||
|
||||
BUS_SPECS = {spec.key: spec for spec in (HIGH_VOL_BUS, HIGH_A_VOL_BUS,
|
||||
LOW_MAIN_BUS, LOW_BUS)}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 状态页构建 / 刷新
|
||||
# ------------------------------------------------------------------
|
||||
def build_bus_page(parent, spec):
|
||||
"""构建一个母线状态页,返回用于刷新的 refs 字典。"""
|
||||
page = ttk.Frame(parent, padding=6)
|
||||
page.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# ---- 顶部:电压 + 断路器 ----
|
||||
top = ttk.LabelFrame(page, text="电压与断路器状态", padding=8)
|
||||
top.grid(row=0, column=0, sticky="nsew", pady=(0, 6))
|
||||
for c in range(3):
|
||||
top.grid_columnconfigure(c, weight=1)
|
||||
|
||||
voltage_widgets = {}
|
||||
for i, (label, field, scale, unit) in enumerate(spec.voltages):
|
||||
sub = ttk.Frame(top)
|
||||
sub.grid(row=0, column=i, padx=10, pady=4, sticky="w")
|
||||
ttk.Label(sub, text=f"{label}({unit}):", width=20, anchor="w").pack(side="left")
|
||||
value_lbl = ttk.Label(sub, text="--", width=10, anchor="e", relief="groove")
|
||||
value_lbl.pack(side="right")
|
||||
voltage_widgets[field] = {"widget": value_lbl, "scale": scale, "unit": unit}
|
||||
|
||||
ttk.Label(top, text="断路器状态:",
|
||||
font=("Arial", 10, "bold")).grid(row=1, column=0,
|
||||
columnspan=6, sticky="w", pady=(10, 4))
|
||||
|
||||
breaker_lights = {}
|
||||
field_for_label = {label: f for label, f in spec.breakers}
|
||||
for i, (label, _f) in enumerate(spec.breakers):
|
||||
bf = tk.Frame(top, relief="solid", borderwidth=1, padx=6, pady=2)
|
||||
bf.grid(row=2 + i // 3, column=i % 3, padx=6, pady=4, sticky="w")
|
||||
ttk.Label(bf, text=label, width=20, anchor="w").pack(side="left")
|
||||
light = make_light(bf)
|
||||
light.pack(side="right")
|
||||
breaker_lights[label] = light
|
||||
|
||||
# ---- 下部:电流 / 布尔量 / 派生量 ----
|
||||
group = ttk.LabelFrame(page, text="系统状态", padding=8)
|
||||
group.grid(row=1, column=0, sticky="nsew")
|
||||
page.grid_rowconfigure(1, weight=1)
|
||||
for c in range(3):
|
||||
group.grid_columnconfigure(c, weight=1)
|
||||
|
||||
values = {}
|
||||
booleans = {}
|
||||
derived = {}
|
||||
|
||||
items = []
|
||||
for it in spec.currents:
|
||||
items.append(("value", it))
|
||||
for it in spec.booleans:
|
||||
items.append(("bool", it))
|
||||
for it in spec.derived:
|
||||
items.append(("derived", it))
|
||||
|
||||
for i, (kind, it) in enumerate(items):
|
||||
f = ttk.Frame(group)
|
||||
f.grid(row=i // 3, column=i % 3, sticky="w", padx=8, pady=3)
|
||||
if kind == "value":
|
||||
label, field, scale, unit = it
|
||||
ttk.Label(f, text=f"{label}({unit}):", width=26, anchor="w").pack(side="left")
|
||||
v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove")
|
||||
v.pack(side="right")
|
||||
values[field] = {"widget": v, "scale": scale, "unit": unit}
|
||||
elif kind == "bool":
|
||||
label, field = it
|
||||
ttk.Label(f, text=f"{label}:", width=26, anchor="w").pack(side="left")
|
||||
v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove")
|
||||
v.pack(side="right")
|
||||
booleans[field] = {"widget": v}
|
||||
else:
|
||||
label, getter = it
|
||||
ttk.Label(f, text=f"{label}:", width=26, anchor="w").pack(side="left")
|
||||
v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove")
|
||||
v.pack(side="right")
|
||||
derived[label] = {"widget": v, "getter": getter}
|
||||
|
||||
return {
|
||||
"frame": page,
|
||||
"field_for_label": field_for_label,
|
||||
"breaker_lights": breaker_lights,
|
||||
"voltage_widgets": voltage_widgets,
|
||||
"values": values,
|
||||
"booleans": booleans,
|
||||
"derived": derived,
|
||||
}
|
||||
|
||||
|
||||
def update_bus_page(spec, refs, msg):
|
||||
"""用最新反馈消息刷新状态页。msg 为 None 时跳过。"""
|
||||
if msg is None:
|
||||
return
|
||||
for label, light in refs["breaker_lights"].items():
|
||||
field = refs["field_for_label"][label]
|
||||
set_light(light, breaker_color(getattr(msg, field)))
|
||||
for field, info in refs["voltage_widgets"].items():
|
||||
v = getattr(msg, field) * info["scale"]
|
||||
info["widget"].config(text=f"{v:.1f} {info['unit']}")
|
||||
for field, info in refs["values"].items():
|
||||
v = getattr(msg, field) * info["scale"]
|
||||
info["widget"].config(text=f"{v:.1f} {info['unit']}")
|
||||
for field, info in refs["booleans"].items():
|
||||
ok = bool_ok(getattr(msg, field))
|
||||
info["widget"].config(text="正常" if ok else "故障",
|
||||
foreground=GREEN if ok else RED)
|
||||
for label, info in refs["derived"].items():
|
||||
text, color = info["getter"](msg)
|
||||
info["widget"].config(text=text, foreground=color)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 控制面板
|
||||
# ------------------------------------------------------------------
|
||||
@dataclass
|
||||
class CmdSpec:
|
||||
key: str
|
||||
title: str
|
||||
msg_cls: type
|
||||
dest: str # 'c1' / 'c2'
|
||||
actions: list # [(label, field)]
|
||||
|
||||
|
||||
HVB_CMD = CmdSpec("hvb", "动力母线配电指令", protocol.MsgDisHVBCmd, "c1", [
|
||||
("燃料电池断路器", "fuelCellCircuitBreaker"),
|
||||
("动力锂电池断路器", "powerLithiumBatteryCircuitBreaker"),
|
||||
("推进电机断路器", "propulsionMotorCircuitBreaker"),
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("DC/DC模块断路器", "dcDc5ModuleCircuitBreaker"),
|
||||
("艏高压配电盒断路器", "bowHighVoltageDistributionBoxCircuitBreaker"),
|
||||
("艉FT设备45断路器", "sternFTDevice45CircuitBreaker"),
|
||||
("艉舵机开关1断路器", "sternRudderSwitch1CircuitBreaker"),
|
||||
("艉舵机开关2断路器", "sternRudderSwitch2CircuitBreaker"),
|
||||
("FB备用断路器", "fbReservedCircuitBreaker"),
|
||||
("TYZ备用断路器", "tyzReservedCircuitBreaker"),
|
||||
("备用断路器", "reservedCircuitBreaker"),
|
||||
("DC/DC模块启动", "dcdc5Module"),
|
||||
("冷却系统启动", "coolingSystem"),
|
||||
])
|
||||
|
||||
HVBA_CMD = CmdSpec("hvba", "艏部高压配电指令", protocol.MsgDisHVBACmd, "c1", [
|
||||
("艏FT设备123断路器", "bowFTDevice123CircuitBreaker"),
|
||||
("执行机构断路器", "actuatorCircuitBreaker"),
|
||||
("桅舵机控制箱断路器", "mastSteeringGearControlBoxCircuitBreaker"),
|
||||
("XCZ断路器", "xczCircuitBreaker"),
|
||||
("艏舵控制箱断路器", "bowRudderControlBoxCircuitBreaker"),
|
||||
("开盖启动气缸断路器", "openWaterCoverStartCylinderCircuitBreaker"),
|
||||
])
|
||||
|
||||
LVMB_CMD = CmdSpec("lvmb", "仪表母线配电指令", protocol.MsgDisLVMBCmd, "c2", [
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("艏低压配电盒断路器", "bowLowVoltageDistributionBoxCircuitBreaker"),
|
||||
("单元4仪表48V断路器", "unit4InstrumentDC48VCircuitBreaker"),
|
||||
("DC/C1直流配电板断路器", "dcC1DCDistributionPanelCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组2断路器", "emergencyLithiumBatteryGroup2CircuitBreaker"),
|
||||
])
|
||||
|
||||
LVB_CMD = CmdSpec("lvb", "艏部低压配电指令", protocol.MsgDisLVBCmd, "c2", [
|
||||
("单元1断路器", "unit1CircuitBreaker"),
|
||||
("单元2断路器", "unit2CircuitBreaker"),
|
||||
("单元3断路器", "unit3CircuitBreaker"),
|
||||
("单元5断路器", "unit5CircuitBreaker"),
|
||||
("艏PZ设备断路器", "bowPZDeviceCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组1断路器", "emergencyLithiumBatteryGroup1CircuitBreaker"),
|
||||
])
|
||||
|
||||
CMD_SPECS = {spec.key: spec for spec in (HVB_CMD, HVBA_CMD, LVMB_CMD, LVB_CMD)}
|
||||
|
||||
|
||||
def build_control_panel(parent, spec, on_cmd):
|
||||
"""构建控制面板。on_cmd(spec, field, state)。"""
|
||||
frame = ttk.LabelFrame(parent, text=spec.title, padding=10)
|
||||
frame.pack(fill="x", pady=5)
|
||||
for i, (label, field) in enumerate(spec.actions):
|
||||
row, col = i // 3, i % 3
|
||||
bf = ttk.Frame(frame)
|
||||
bf.grid(row=row, column=col, padx=10, pady=2, sticky="w")
|
||||
ttk.Label(bf, text=label, width=20).pack(side="left")
|
||||
ttk.Button(bf, text="合", width=3,
|
||||
command=lambda f=field: on_cmd(spec, f, BREAKER_ON)).pack(side="left", padx=2)
|
||||
ttk.Button(bf, text="分", width=3,
|
||||
command=lambda f=field: on_cmd(spec, f, BREAKER_OFF)).pack(side="left", padx=2)
|
||||
return frame
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DC/DC 故障字显示(供状态页与独立弹窗复用)
|
||||
# ------------------------------------------------------------------
|
||||
FAULT1_LABELS = ["过温关机", "输出过流", "输出过压", "输出欠压",
|
||||
"输入过压", "输入欠压", "输出短路保护", "内部故障"]
|
||||
FAULT2_LABELS = ["通信故障", "工作状态", "冷却泵1故障", "冷却泵2故障",
|
||||
"冷却水泄漏", "冷却泵1运行状态", "冷却泵2运行状态", "保留"]
|
||||
|
||||
|
||||
def build_dcdc_display(parent, title="DC/DC 故障字"):
|
||||
"""构建 DC/DC 故障字灯组,返回 refs={"words": {1: [canvas...], 2: [...]}}。"""
|
||||
frame = ttk.LabelFrame(parent, text=title, padding=8)
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
words = {1: [], 2: []}
|
||||
for wi, labels in ((1, FAULT1_LABELS), (2, FAULT2_LABELS)):
|
||||
ttk.Label(frame, text=f"DC/DC故障字{wi}:",
|
||||
font=("Microsoft YaHei", 9, "bold")).grid(
|
||||
row=(wi - 1) * 2, column=0, sticky="w", pady=(6, 2))
|
||||
for i, label in enumerate(labels):
|
||||
pf = tk.Frame(frame, borderwidth=1, relief="solid", padx=4, pady=2)
|
||||
pf.grid(row=(wi - 1) * 2 + 1 + i // 4, column=i % 4, padx=5, pady=3, sticky="w")
|
||||
ttk.Label(pf, text=label).pack(side="left", padx=(0, 6))
|
||||
light = make_light(pf, 18)
|
||||
light.pack(side="right")
|
||||
words[wi].append(light)
|
||||
return {"widget": frame, "words": words}
|
||||
|
||||
|
||||
def update_dcdc_display(refs, word1, word2):
|
||||
for i in range(8):
|
||||
set_light(refs["words"][1][i], RED if (word1 & (1 << i)) else GREEN)
|
||||
set_light(refs["words"][2][i], RED if (word2 & (1 << i)) else GREEN)
|
||||
@@ -0,0 +1,374 @@
|
||||
"""配电系统 UDP 协议封装(声明式消息定义)。
|
||||
|
||||
字段顺序、大小与 C++ 侧 pmSysvariable.h / ccuUdpMsg.h 严格对齐。
|
||||
|
||||
帧格式(配电消息,单字节校验):
|
||||
start1(0x40) start2(0x40) | id(uint16 LE) | length(uint16 LE) | body | checkCode(和校验 &0xFF)
|
||||
|
||||
说明:
|
||||
- 字段表(FIELDS)是唯一数据源,length、pack/unpack 均由此推导,
|
||||
避免手工维护长度字段与 C++ 结构体大小不一致。
|
||||
- unpack 采用固定偏移解析 + 起始字节/最小长度/校验和校验。
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
DIS_START1 = 0x40
|
||||
DIS_START2 = 0x40
|
||||
|
||||
# 断路器状态定义(与 C++ 一致)
|
||||
BREAKER_ON = 0x55 # 合
|
||||
BREAKER_OFF = 0xAA # 分
|
||||
BREAKER_FAULT = 0x5A # 故障
|
||||
|
||||
|
||||
class Field:
|
||||
"""协议字段定义。fmt 支持 'B'(uchar) / 'H'(ushort LE)。"""
|
||||
|
||||
__slots__ = ("name", "fmt", "size")
|
||||
|
||||
def __init__(self, name, fmt):
|
||||
self.name = name
|
||||
self.fmt = fmt
|
||||
self.size = struct.calcsize(fmt)
|
||||
|
||||
|
||||
class Message:
|
||||
"""声明式消息基类:由 FIELDS 自动生成 pack/unpack 与长度。"""
|
||||
|
||||
MSG_ID = 0x0000
|
||||
CN = "未知消息"
|
||||
FIELDS = () # tuple[Field]
|
||||
|
||||
def __init__(self):
|
||||
self.start1 = DIS_START1
|
||||
self.start2 = DIS_START2
|
||||
self.msg_id = self.MSG_ID
|
||||
self.length = self.body_size()
|
||||
self.checkCode = 0
|
||||
for f in self.FIELDS:
|
||||
setattr(self, f.name, 0)
|
||||
|
||||
# ---- 派生属性 -------------------------------------------------
|
||||
@classmethod
|
||||
def body_size(cls):
|
||||
return sum(f.size for f in cls.FIELDS)
|
||||
|
||||
@classmethod
|
||||
def field_names(cls):
|
||||
return [f.name for f in cls.FIELDS]
|
||||
|
||||
def to_dict(self):
|
||||
return {f.name: getattr(self, f.name) for f in self.FIELDS}
|
||||
|
||||
def __repr__(self):
|
||||
parts = [f"{self.__class__.__name__}(id=0x{self.MSG_ID:04X}, len={self.length}"]
|
||||
for f in self.FIELDS:
|
||||
parts.append(f"{f.name}={getattr(self, f.name)}")
|
||||
return ", ".join(parts) + ")"
|
||||
|
||||
# ---- 打包/解包 ------------------------------------------------
|
||||
def pack(self):
|
||||
body = b""
|
||||
for f in self.FIELDS:
|
||||
body += struct.pack("<" + f.fmt, getattr(self, f.name))
|
||||
self.length = self.body_size()
|
||||
header = struct.pack("<BBHH", DIS_START1, DIS_START2, self.MSG_ID, self.length)
|
||||
data = header + body
|
||||
self.checkCode = sum(data) & 0xFF
|
||||
return data + struct.pack("<B", self.checkCode)
|
||||
|
||||
def unpack(self, data):
|
||||
if len(data) < 7:
|
||||
return False
|
||||
if data[0] != DIS_START1 or data[1] != DIS_START2:
|
||||
return False
|
||||
if struct.unpack_from("<H", data, 2)[0] != self.MSG_ID:
|
||||
return False
|
||||
expected = 6 + self.body_size() + 1
|
||||
if len(data) < expected:
|
||||
return False
|
||||
|
||||
self.start1, self.start2 = data[0], data[1]
|
||||
self.msg_id = struct.unpack_from("<H", data, 2)[0]
|
||||
self.length = struct.unpack_from("<H", data, 4)[0]
|
||||
|
||||
offset = 6
|
||||
for f in self.FIELDS:
|
||||
if f.size == 1:
|
||||
setattr(self, f.name, data[offset])
|
||||
offset += 1
|
||||
else:
|
||||
setattr(self, f.name, struct.unpack_from("<H", data, offset)[0])
|
||||
offset += 2
|
||||
self.checkCode = data[-1]
|
||||
|
||||
checksum = sum(data[:-1]) & 0xFF
|
||||
return checksum == data[-1]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 高压母线反馈 0x0005 body=53
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHighVolBusFb(Message):
|
||||
MSG_ID = 0x0005
|
||||
CN = "动力母线反馈"
|
||||
FIELDS = (
|
||||
Field("fuelCellCircuitBreaker", "B"),
|
||||
Field("powerLithiumBatteryCircuitBreaker", "B"),
|
||||
Field("propulsionMotorCircuitBreaker", "B"),
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("dcDc5ModuleCircuitBreaker", "B"),
|
||||
Field("bowHighVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("sternFTDevice45CircuitBreaker", "B"),
|
||||
Field("fbReservedCircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch1CircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch2CircuitBreaker", "B"),
|
||||
Field("tyzReservedCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker", "B"),
|
||||
Field("dcDcFaultWord1", "B"),
|
||||
Field("dcDcFaultWord2", "B"),
|
||||
Field("powerBusInsulationStatus", "B"),
|
||||
Field("aBusInsulationStatus", "B"),
|
||||
Field("meterPowerLossSignal", "B"),
|
||||
Field("emergencyPowerLossSignal", "B"),
|
||||
Field("dcdcTemperature", "B"),
|
||||
Field("powerBusVoltage", "H"),
|
||||
Field("dcDc5ModuleCurrent", "H"),
|
||||
Field("powerBusCurrent", "H"),
|
||||
Field("busbarAVoltage", "H"),
|
||||
Field("propulsionMotorControlBoxCurrent", "H"),
|
||||
Field("busbarACurrent", "H"),
|
||||
Field("lithiumBatteryGroupMeterCurrent", "H"),
|
||||
Field("bowHighVoltageDistributionBoxCurrent", "H"),
|
||||
Field("sternFTDevice45Current", "H"),
|
||||
Field("tyzReservedSwitchCurrent", "H"),
|
||||
Field("sternRudderSwitch1Current", "H"),
|
||||
Field("reservedCurrent1", "H"),
|
||||
Field("sternRudderSwitch2Current", "H"),
|
||||
Field("reservedCurrent2", "H"),
|
||||
Field("fbReservedSwitchCurrent", "H"),
|
||||
Field("reservedCurrent3", "H"),
|
||||
Field("coolWaterPressure", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 高压A母线反馈 0x0006 body=25
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHighAVolBusFb(Message):
|
||||
MSG_ID = 0x0006
|
||||
CN = "艏部高压反馈"
|
||||
FIELDS = (
|
||||
Field("bowFTDevice123CircuitBreaker", "B"),
|
||||
Field("actuatorCircuitBreaker", "B"),
|
||||
Field("mastSteeringGearControlBoxCircuitBreaker", "B"),
|
||||
Field("xczCircuitBreaker", "B"),
|
||||
Field("bowRudderControlBoxCircuitBreaker", "B"),
|
||||
Field("openWaterCoverStartCylinderCircuitBreaker", "B"),
|
||||
Field("instrumentPowerFailureSignal", "B"),
|
||||
Field("emergencyPowerFailureSignal", "B"),
|
||||
Field("waterIngressionAlarm", "B"),
|
||||
Field("busbarBVoltage", "H"),
|
||||
Field("busbarBCurrent", "H"),
|
||||
Field("bowFTDevice123Current", "H"),
|
||||
Field("actuatorCurrent", "H"),
|
||||
Field("mastSteeringGearControlBoxCurrent", "H"),
|
||||
Field("xczCurrent", "H"),
|
||||
Field("bowRudderControlBoxCurrent", "H"),
|
||||
Field("openWaterCoverStartCylinderCurrent", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表主母线反馈 0x0007 body=34
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLowMainBusFb(Message):
|
||||
MSG_ID = 0x0007
|
||||
CN = "仪表主母线反馈"
|
||||
FIELDS = (
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("bowLowVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("unit4InstrumentDC48VCircuitBreaker", "B"),
|
||||
Field("dcC1DCDistributionPanelCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup2CircuitBreaker", "B"),
|
||||
Field("instrumentPowerFailureSignal", "B"),
|
||||
Field("emergencyPowerFailureSignal", "B"),
|
||||
Field("instrumentBusbarInsulationLow", "B"),
|
||||
Field("instrumentBusbarVoltage", "H"),
|
||||
Field("bowLowVoltageDistributionBoxCurrent", "H"),
|
||||
Field("dcC1InstrumentDC48VCurrent", "H"),
|
||||
Field("reservedCurrent1", "H"),
|
||||
Field("emergencyLithiumBatteryGroup2Current", "H"),
|
||||
Field("lithiumBatteryGroupInstrumentCurrent", "H"),
|
||||
Field("unit4InstrumentDC48VCurrent", "H"),
|
||||
Field("reservedCurrent2", "H"),
|
||||
Field("emergency2BusbarVoltage", "H"),
|
||||
Field("compositeEnergyManagementSystemEmergencyDC48VCurrent", "H"),
|
||||
Field("fuelCellSecuritySystemEmergencyDC48VCurrent", "H"),
|
||||
Field("plcControlPowerCurrent", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表汇流排反馈 0x0008 body=29
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLowBusFb(Message):
|
||||
MSG_ID = 0x0008
|
||||
CN = "艏部低压反馈"
|
||||
FIELDS = (
|
||||
Field("unit1CircuitBreaker", "B"),
|
||||
Field("unit2CircuitBreaker", "B"),
|
||||
Field("unit3CircuitBreaker", "B"),
|
||||
Field("unit5CircuitBreaker", "B"),
|
||||
Field("bowPZDeviceCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup1CircuitBreaker", "B"),
|
||||
Field("instrumentPowerFailureSignal", "B"),
|
||||
Field("emergencyPowerFailureSignal", "B"),
|
||||
Field("waterIngressionAlarm", "B"),
|
||||
Field("instrumentBusbarVoltage", "H"),
|
||||
Field("instrumentBusbarCurrent", "H"),
|
||||
Field("unit1Current", "H"),
|
||||
Field("unit2Current", "H"),
|
||||
Field("unit3Current", "H"),
|
||||
Field("unit5Current", "H"),
|
||||
Field("bowPZDeviceCurrent", "H"),
|
||||
Field("emergencyLithiumBatteryGroup1Current", "H"),
|
||||
Field("emergency1BusbarVoltage", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 动力母线配电指令 0x0001 body=14
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHVBCmd(Message):
|
||||
MSG_ID = 0x0001
|
||||
CN = "动力母线配电指令"
|
||||
FIELDS = (
|
||||
Field("fuelCellCircuitBreaker", "B"),
|
||||
Field("powerLithiumBatteryCircuitBreaker", "B"),
|
||||
Field("propulsionMotorCircuitBreaker", "B"),
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("dcDc5ModuleCircuitBreaker", "B"),
|
||||
Field("bowHighVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("sternFTDevice45CircuitBreaker", "B"),
|
||||
Field("fbReservedCircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch1CircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch2CircuitBreaker", "B"),
|
||||
Field("tyzReservedCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker", "B"),
|
||||
Field("dcdc5Module", "B"),
|
||||
Field("coolingSystem", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 动力汇流排A配电指令 0x0002 body=6
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHVBACmd(Message):
|
||||
MSG_ID = 0x0002
|
||||
CN = "艏部高压配电指令"
|
||||
FIELDS = (
|
||||
Field("bowFTDevice123CircuitBreaker", "B"),
|
||||
Field("actuatorCircuitBreaker", "B"),
|
||||
Field("mastSteeringGearControlBoxCircuitBreaker", "B"),
|
||||
Field("xczCircuitBreaker", "B"),
|
||||
Field("bowRudderControlBoxCircuitBreaker", "B"),
|
||||
Field("openWaterCoverStartCylinderCircuitBreaker", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表主母线配电指令 0x0003 body=7
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLVMBCmd(Message):
|
||||
MSG_ID = 0x0003
|
||||
CN = "仪表主母线配电指令"
|
||||
FIELDS = (
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("bowLowVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("unit4InstrumentDC48VCircuitBreaker", "B"),
|
||||
Field("dcC1DCDistributionPanelCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup2CircuitBreaker", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表汇流排配电指令 0x0004 body=8
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLVBCmd(Message):
|
||||
MSG_ID = 0x0004
|
||||
CN = "艏部低压配电指令"
|
||||
FIELDS = (
|
||||
Field("unit1CircuitBreaker", "B"),
|
||||
Field("unit2CircuitBreaker", "B"),
|
||||
Field("unit3CircuitBreaker", "B"),
|
||||
Field("unit5CircuitBreaker", "B"),
|
||||
Field("bowPZDeviceCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup1CircuitBreaker", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 消息注册表
|
||||
# ---------------------------------------------------------------
|
||||
FB_MESSAGES = {c.MSG_ID: c for c in (MsgDisHighVolBusFb, MsgDisHighAVolBusFb,
|
||||
MsgDisLowMainBusFb, MsgDisLowBusFb)}
|
||||
CMD_MESSAGES = {c.MSG_ID: c for c in (MsgDisHVBCmd, MsgDisHVBACmd,
|
||||
MsgDisLVMBCmd, MsgDisLVBCmd)}
|
||||
ALL_MESSAGES = {**FB_MESSAGES, **CMD_MESSAGES}
|
||||
|
||||
|
||||
def msg_id_of(data):
|
||||
"""从原始帧提取消息 ID。数据不合法时返回 None。"""
|
||||
if len(data) < 6:
|
||||
return None
|
||||
if data[0] != DIS_START1 or data[1] != DIS_START2:
|
||||
return None
|
||||
return struct.unpack_from("<H", data, 2)[0]
|
||||
|
||||
|
||||
def make_message(msg_id):
|
||||
cls = ALL_MESSAGES.get(msg_id)
|
||||
return cls() if cls else None
|
||||
|
||||
|
||||
def self_test():
|
||||
"""pack/unpack 往返 + 长度断言。返回 (ok, 报告)。"""
|
||||
lines = []
|
||||
ok = True
|
||||
for cls in (MsgDisHighVolBusFb, MsgDisHighAVolBusFb, MsgDisLowMainBusFb,
|
||||
MsgDisLowBusFb, MsgDisHVBCmd, MsgDisHVBACmd, MsgDisLVMBCmd,
|
||||
MsgDisLVBCmd):
|
||||
m = cls()
|
||||
data = m.pack()
|
||||
expected = 6 + cls.body_size() + 1
|
||||
frame_ok = len(data) == expected
|
||||
len_ok = m.length == cls.body_size()
|
||||
m2 = cls()
|
||||
round_ok = m2.unpack(data)
|
||||
# 翻转一个字节应导致校验失败
|
||||
bad = bytearray(data)
|
||||
bad[6] ^= 0xFF
|
||||
neg_ok = not m2.unpack(bytes(bad))
|
||||
status = "OK" if (frame_ok and len_ok and round_ok and neg_ok) else "FAIL"
|
||||
if status == "FAIL":
|
||||
ok = False
|
||||
lines.append(f" [{status}] {cls.__name__}: id=0x{cls.MSG_ID:04X} "
|
||||
f"body={cls.body_size()}B frame={len(data)}B "
|
||||
f"length_field={m.length} roundtrip={round_ok} neg={neg_ok}")
|
||||
return ok, "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ok, report = self_test()
|
||||
print(report)
|
||||
print("PROTOCOL SELF-TEST:", "PASS" if ok else "FAIL")
|
||||
@@ -0,0 +1,189 @@
|
||||
"""内置模拟器:产生四路母线反馈,支持配电指令回显。
|
||||
|
||||
设计:模拟器状态(反馈消息 + 最近指令)独立维护;周期性调用 update()
|
||||
返回需要注入解析管线的反馈消息列表。配电指令通过 apply_command()
|
||||
回写对应断路器状态,从而与真实设备的收发链路完全一致。
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from . import protocol
|
||||
from .protocol import (MsgDisHighVolBusFb, MsgDisHighAVolBusFb,
|
||||
MsgDisLowMainBusFb, MsgDisLowBusFb,
|
||||
BREAKER_ON)
|
||||
|
||||
|
||||
class Simulator:
|
||||
# 指令消息 ID -> 对应反馈消息 ID
|
||||
CMD_TO_FB = {0x0001: 0x0005, 0x0002: 0x0006,
|
||||
0x0003: 0x0007, 0x0004: 0x0008}
|
||||
|
||||
def __init__(self):
|
||||
self.fb = {
|
||||
0x0005: MsgDisHighVolBusFb(),
|
||||
0x0006: MsgDisHighAVolBusFb(),
|
||||
0x0007: MsgDisLowMainBusFb(),
|
||||
0x0008: MsgDisLowBusFb(),
|
||||
}
|
||||
# 最近一次配电指令(用于回显)
|
||||
self.last_cmd = {
|
||||
0x0001: protocol.MsgDisHVBCmd(),
|
||||
0x0002: protocol.MsgDisHVBACmd(),
|
||||
0x0003: protocol.MsgDisLVMBCmd(),
|
||||
0x0004: protocol.MsgDisLVBCmd(),
|
||||
}
|
||||
self.fault_animation = True
|
||||
self.noise = True
|
||||
self._fault_counter = 0
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self._init_high_vol()
|
||||
self._init_high_a_vol()
|
||||
self._init_low_main()
|
||||
self._init_low_bus()
|
||||
# 复位指令状态
|
||||
for cmd in self.last_cmd.values():
|
||||
for name in cmd.field_names():
|
||||
setattr(cmd, name, 0)
|
||||
|
||||
# ---- 初始值(与 test.py 一致) ---------------------------------
|
||||
def _init_high_vol(self):
|
||||
m = self.fb[0x0005]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
for name in ("fuelCellCircuitBreaker", "powerLithiumBatteryCircuitBreaker",
|
||||
"propulsionMotorCircuitBreaker", "lithiumBatteryGroupInstrumentCircuitBreaker",
|
||||
"dcDc5ModuleCircuitBreaker", "bowHighVoltageDistributionBoxCircuitBreaker",
|
||||
"sternFTDevice45CircuitBreaker", "sternRudderSwitch1CircuitBreaker",
|
||||
"sternRudderSwitch2CircuitBreaker", "fbReservedCircuitBreaker",
|
||||
"tyzReservedCircuitBreaker", "reservedCircuitBreaker"):
|
||||
setattr(m, name, BREAKER_ON)
|
||||
m.dcDcFaultWord1 = 0x01
|
||||
m.dcDcFaultWord2 = 0x01
|
||||
m.powerBusInsulationStatus = BREAKER_ON
|
||||
m.aBusInsulationStatus = BREAKER_ON
|
||||
m.meterPowerLossSignal = BREAKER_ON
|
||||
m.emergencyPowerLossSignal = BREAKER_ON
|
||||
m.dcdcTemperature = 0x08
|
||||
m.powerBusVoltage = 6000
|
||||
m.dcDc5ModuleCurrent = 500
|
||||
m.powerBusCurrent = 1000
|
||||
m.busbarAVoltage = 6000
|
||||
m.propulsionMotorControlBoxCurrent = 800
|
||||
m.busbarACurrent = 500
|
||||
m.lithiumBatteryGroupMeterCurrent = 300
|
||||
m.bowHighVoltageDistributionBoxCurrent = 200
|
||||
m.sternFTDevice45Current = 150
|
||||
m.tyzReservedSwitchCurrent = 0
|
||||
m.sternRudderSwitch1Current = 100
|
||||
m.reservedCurrent1 = 1000
|
||||
m.sternRudderSwitch2Current = 100
|
||||
m.reservedCurrent2 = 1000
|
||||
m.fbReservedSwitchCurrent = 10000
|
||||
m.reservedCurrent3 = 1000
|
||||
m.coolWaterPressure = 1000
|
||||
|
||||
def _init_high_a_vol(self):
|
||||
m = self.fb[0x0006]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
for name in ("bowFTDevice123CircuitBreaker", "actuatorCircuitBreaker",
|
||||
"mastSteeringGearControlBoxCircuitBreaker", "xczCircuitBreaker",
|
||||
"bowRudderControlBoxCircuitBreaker", "openWaterCoverStartCylinderCircuitBreaker"):
|
||||
setattr(m, name, BREAKER_ON)
|
||||
m.instrumentPowerFailureSignal = BREAKER_ON
|
||||
m.emergencyPowerFailureSignal = BREAKER_ON
|
||||
m.waterIngressionAlarm = BREAKER_ON
|
||||
m.busbarBVoltage = 6000
|
||||
m.busbarBCurrent = 300
|
||||
m.bowFTDevice123Current = 200
|
||||
m.actuatorCurrent = 150
|
||||
m.mastSteeringGearControlBoxCurrent = 100
|
||||
m.xczCurrent = 50
|
||||
m.bowRudderControlBoxCurrent = 80
|
||||
m.openWaterCoverStartCylinderCurrent = 0
|
||||
|
||||
def _init_low_main(self):
|
||||
m = self.fb[0x0007]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
m.lithiumBatteryGroupInstrumentCircuitBreaker = BREAKER_ON
|
||||
m.bowLowVoltageDistributionBoxCircuitBreaker = BREAKER_ON
|
||||
m.unit4InstrumentDC48VCircuitBreaker = BREAKER_ON
|
||||
m.dcC1DCDistributionPanelCircuitBreaker = BREAKER_ON
|
||||
m.reservedCircuitBreaker1 = BREAKER_ON
|
||||
m.reservedCircuitBreaker2 = BREAKER_ON
|
||||
m.emergencyLithiumBatteryGroup2CircuitBreaker = BREAKER_ON
|
||||
m.instrumentPowerFailureSignal = BREAKER_ON
|
||||
m.emergencyPowerFailureSignal = BREAKER_ON
|
||||
m.instrumentBusbarInsulationLow = BREAKER_ON
|
||||
m.instrumentBusbarVoltage = 480
|
||||
m.bowLowVoltageDistributionBoxCurrent = 100
|
||||
m.dcC1InstrumentDC48VCurrent = 50
|
||||
m.reservedCurrent1 = 1100
|
||||
m.emergencyLithiumBatteryGroup2Current = 1000
|
||||
m.lithiumBatteryGroupInstrumentCurrent = 200
|
||||
m.unit4InstrumentDC48VCurrent = 50
|
||||
m.reservedCurrent2 = 1000
|
||||
m.emergency2BusbarVoltage = 480
|
||||
m.compositeEnergyManagementSystemEmergencyDC48VCurrent = 20
|
||||
m.fuelCellSecuritySystemEmergencyDC48VCurrent = 20
|
||||
m.plcControlPowerCurrent = 10
|
||||
|
||||
def _init_low_bus(self):
|
||||
m = self.fb[0x0008]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
for name in ("unit1CircuitBreaker", "unit2CircuitBreaker",
|
||||
"unit3CircuitBreaker", "unit5CircuitBreaker",
|
||||
"bowPZDeviceCircuitBreaker", "reservedCircuitBreaker1",
|
||||
"reservedCircuitBreaker2", "emergencyLithiumBatteryGroup1CircuitBreaker"):
|
||||
setattr(m, name, BREAKER_ON)
|
||||
m.instrumentPowerFailureSignal = BREAKER_ON
|
||||
m.emergencyPowerFailureSignal = BREAKER_ON
|
||||
m.waterIngressionAlarm = BREAKER_ON
|
||||
m.instrumentBusbarVoltage = 480
|
||||
m.instrumentBusbarCurrent = 5000
|
||||
m.unit1Current = 1000
|
||||
m.unit2Current = 1000
|
||||
m.unit3Current = 1000
|
||||
m.unit5Current = 1000
|
||||
m.bowPZDeviceCurrent = 500
|
||||
m.emergencyLithiumBatteryGroup1Current = 1000
|
||||
m.emergency1BusbarVoltage = 480
|
||||
|
||||
# ---- 对外接口 ------------------------------------------------
|
||||
def apply_command(self, cmd):
|
||||
"""收到配电指令后回写对应母线反馈的断路器状态。"""
|
||||
fb = self.fb.get(self.CMD_TO_FB.get(cmd.MSG_ID))
|
||||
if fb is None:
|
||||
return
|
||||
self.last_cmd[cmd.MSG_ID] = cmd
|
||||
for name in cmd.field_names():
|
||||
if hasattr(fb, name):
|
||||
setattr(fb, name, getattr(cmd, name))
|
||||
|
||||
def _jitter(self, fb):
|
||||
"""给电流/电压叠加轻微随机波动,模拟真实读数。"""
|
||||
for name in fb.field_names():
|
||||
if name.endswith("Current") or name.endswith("Voltage") or name.endswith("Pressure"):
|
||||
v = getattr(fb, name)
|
||||
if v > 0:
|
||||
setattr(fb, name, max(0, v + random.randint(-int(v * 0.05), int(v * 0.05))))
|
||||
|
||||
def _step_fault(self):
|
||||
m = self.fb[0x0005]
|
||||
w1 = ((m.dcDcFaultWord1 << 1) | (m.dcDcFaultWord1 >> 7)) & 0xFF
|
||||
w2 = ((m.dcDcFaultWord2 >> 1) | (m.dcDcFaultWord2 << 7)) & 0xFF
|
||||
m.dcDcFaultWord1 = w1
|
||||
m.dcDcFaultWord2 = w2
|
||||
|
||||
def update(self, interval=1.0):
|
||||
"""推进一帧模拟。返回需要注入解析管线的反馈消息列表。"""
|
||||
if self.fault_animation:
|
||||
self._step_fault()
|
||||
if self.noise:
|
||||
for fb in self.fb.values():
|
||||
self._jitter(fb)
|
||||
return list(self.fb.values())
|
||||
@@ -0,0 +1,117 @@
|
||||
"""UDP 通信层:socket 管理、收发线程、按消息 ID 分发。
|
||||
|
||||
线程安全:start/stop/reconfigure 可随时调用;
|
||||
收包在独立线程解析,解析成功后回调 on_feedback / on_command,
|
||||
原始帧经 on_raw 回调(用于转发/透传)。
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from . import protocol
|
||||
|
||||
|
||||
class UdpComm:
|
||||
def __init__(self, logger=None):
|
||||
self.logger = logger
|
||||
self.sock = None
|
||||
self.running = False
|
||||
self._thread = None
|
||||
self._lock = threading.Lock()
|
||||
# 回调
|
||||
self.on_feedback = None # callable(msg, addr)
|
||||
self.on_command = None # callable(msg, addr)
|
||||
self.on_raw = None # callable(data, addr)
|
||||
self.on_unknown = None # callable(data, addr) 解析失败/未知ID
|
||||
# 统计
|
||||
self.rx_count = 0
|
||||
self.tx_count = 0
|
||||
self.last_rx_time = None
|
||||
|
||||
def bind(self, ip, port):
|
||||
"""创建并绑定监听 socket(带超时 + 地址复用)。失败抛 OSError。"""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.settimeout(0.1)
|
||||
s.bind((ip, int(port)))
|
||||
with self._lock:
|
||||
old = self.sock
|
||||
self.sock = s
|
||||
if old:
|
||||
old.close()
|
||||
return self
|
||||
|
||||
@property
|
||||
def bound(self):
|
||||
return self.sock is not None
|
||||
|
||||
def start(self):
|
||||
if self.running:
|
||||
return
|
||||
if self.sock is None:
|
||||
raise RuntimeError("socket 未绑定")
|
||||
self.running = True
|
||||
self._thread = threading.Thread(target=self._recv_loop, daemon=True,
|
||||
name="udp-recv")
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
t = self._thread
|
||||
self._thread = None
|
||||
if t:
|
||||
t.join(timeout=1.5)
|
||||
|
||||
def _recv_loop(self):
|
||||
while self.running:
|
||||
try:
|
||||
data, addr = self.sock.recvfrom(2048)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
self.rx_count += 1
|
||||
self.last_rx_time = time.time()
|
||||
try:
|
||||
self._dispatch(data, addr)
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"分发消息异常: {e}")
|
||||
|
||||
def _dispatch(self, data, addr):
|
||||
if self.on_raw:
|
||||
self.on_raw(data, addr)
|
||||
msg_id = protocol.msg_id_of(data)
|
||||
cls = protocol.ALL_MESSAGES.get(msg_id)
|
||||
msg = None
|
||||
if cls:
|
||||
msg = cls()
|
||||
if not msg.unpack(data):
|
||||
msg = None
|
||||
if msg is None:
|
||||
if self.on_unknown:
|
||||
self.on_unknown(data, addr)
|
||||
return
|
||||
if msg_id in protocol.CMD_MESSAGES:
|
||||
if self.on_command:
|
||||
self.on_command(msg, addr)
|
||||
else:
|
||||
if self.on_feedback:
|
||||
self.on_feedback(msg, addr)
|
||||
|
||||
def sendto(self, data, ip, port):
|
||||
"""发送原始字节。返回 True/False。"""
|
||||
if self.sock is None:
|
||||
return False
|
||||
try:
|
||||
self.sock.sendto(data, (ip, int(port)))
|
||||
self.tx_count += 1
|
||||
return True
|
||||
except OSError as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"发送到 {ip}:{port} 失败: {e}")
|
||||
return False
|
||||
|
||||
def send_message(self, msg, ip, port):
|
||||
return self.sendto(msg.pack(), ip, port)
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PyInstaller 打包脚本(可选)。
|
||||
|
||||
用法:
|
||||
pip install pyinstaller
|
||||
python3 build.py
|
||||
产物:dist/DisSysTest(.exe) —— 单文件免安装,可在目标平台直接运行。
|
||||
|
||||
注意:需在目标平台(如 Windows)上执行,PyInstaller 不支持跨平台交叉打包。
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def main():
|
||||
if shutil.which("pyinstaller") is None and not os.path.exists(
|
||||
os.path.join(sys.prefix, "Scripts", "pyinstaller.exe")):
|
||||
print("未检测到 PyInstaller,请先安装: pip install pyinstaller")
|
||||
return 1
|
||||
|
||||
cmd = [
|
||||
sys.executable, "-m", "PyInstaller",
|
||||
"--onefile", # 单文件
|
||||
"--windowed", # 无控制台窗口
|
||||
"--name", "DisSysTest",
|
||||
"--paths", HERE,
|
||||
"--clean", "--noconfirm",
|
||||
os.path.join(HERE, "run.py"),
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
print("打包完成:dist/DisSysTest(.exe)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,4 @@
|
||||
# 运行时无第三方依赖(Tkinter/SQLite 均为 Python 标准库)。
|
||||
|
||||
# 仅打包时需要:
|
||||
pyinstaller>=5.0
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""配电系统调试助手入口。
|
||||
|
||||
用法:
|
||||
python3 run.py 启动界面
|
||||
python3 run.py --selftest 协议/配置/数据库自检
|
||||
python3 run.py --version 显示版本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.__main__ import main # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,92 @@
|
||||
"""协议层单元测试(无第三方依赖,直接运行)。
|
||||
|
||||
用法:python3 tests/test_protocol.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import protocol # noqa: E402
|
||||
from app.protocol import (MsgDisHighVolBusFb, MsgDisHighAVolBusFb, # noqa: E402
|
||||
MsgDisLowMainBusFb, MsgDisLowBusFb,
|
||||
MsgDisHVBCmd, MsgDisHVBACmd, MsgDisLVMBCmd,
|
||||
MsgDisLVBCmd)
|
||||
|
||||
|
||||
def test_expected_sizes():
|
||||
# 与 C++ pmSysvariable.h 结构体大小对齐
|
||||
expected = {
|
||||
MsgDisHighVolBusFb: 53,
|
||||
MsgDisHighAVolBusFb: 25,
|
||||
MsgDisLowMainBusFb: 34,
|
||||
MsgDisLowBusFb: 29,
|
||||
MsgDisHVBCmd: 14,
|
||||
MsgDisHVBACmd: 6,
|
||||
MsgDisLVMBCmd: 7,
|
||||
MsgDisLVBCmd: 8,
|
||||
}
|
||||
for cls, size in expected.items():
|
||||
assert cls.body_size() == size, f"{cls.__name__} 应为 {size},实际 {cls.body_size()}"
|
||||
|
||||
|
||||
def test_roundtrip():
|
||||
for cls in (MsgDisHighVolBusFb, MsgDisHighAVolBusFb, MsgDisLowMainBusFb,
|
||||
MsgDisLowBusFb, MsgDisHVBCmd, MsgDisHVBACmd, MsgDisLVMBCmd,
|
||||
MsgDisLVBCmd):
|
||||
m = cls()
|
||||
for i, name in enumerate(m.field_names()):
|
||||
setattr(m, name, (i + 1) % 256)
|
||||
data = m.pack()
|
||||
assert len(data) == 6 + cls.body_size() + 1
|
||||
assert protocol.msg_id_of(data) == cls.MSG_ID
|
||||
m2 = cls()
|
||||
assert m2.unpack(data)
|
||||
for name in m.field_names():
|
||||
assert getattr(m2, name) == getattr(m, name), f"{cls.__name__}.{name}"
|
||||
|
||||
|
||||
def test_checksum_reject():
|
||||
for cls in (MsgDisHighVolBusFb, MsgDisHVBACmd):
|
||||
data = bytearray(cls().pack())
|
||||
data[6] ^= 0xFF
|
||||
assert not cls().unpack(bytes(data))
|
||||
|
||||
|
||||
def test_bad_start_byte_reject():
|
||||
data = bytearray(MsgDisHVBCmd().pack())
|
||||
data[0] = 0x00
|
||||
assert protocol.msg_id_of(bytes(data)) is None
|
||||
assert not MsgDisHVBCmd().unpack(bytes(data))
|
||||
|
||||
|
||||
def test_unknown_id():
|
||||
data = protocol.MsgDisHVBCmd().pack()
|
||||
data = data[0:2] + b'\x00\x00' + data[4:] # 把 id 改为 0x0000
|
||||
assert protocol.msg_id_of(data) == 0x0000
|
||||
assert protocol.make_message(0x0000) is None
|
||||
assert protocol.make_message(MsgDisHVBCmd.MSG_ID).MSG_ID == MsgDisHVBCmd.MSG_ID
|
||||
|
||||
|
||||
def test_short_frame_reject():
|
||||
data = MsgDisHVBCmd().pack()[:10]
|
||||
assert not MsgDisHVBCmd().unpack(data)
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||
failed = 0
|
||||
for t in tests:
|
||||
try:
|
||||
t()
|
||||
print(f" [OK] {t.__name__}")
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
print(f" [FAIL] {t.__name__}: {e}")
|
||||
print("TEST:", "PASS" if failed == 0 else f"FAIL ({failed})")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_all())
|
||||
@@ -0,0 +1,301 @@
|
||||
//============================================================================
|
||||
// pccuTest:pCCU 协议编解码 / 校验和 / 帧长度 / 链路分发 / 数据库 单元测试
|
||||
// 无第三方测试库,独立可执行。参照 test/until/testSqlitdb.cpp 的轻量风格。
|
||||
//============================================================================
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
#include "../../src/pCCU/protocol/FieldCodec.h"
|
||||
#include "../../src/pCCU/protocol/Frame.h"
|
||||
#include "../../src/pCCU/protocol/ChecksumPolicy.h"
|
||||
#include "../../src/pCCU/protocol/MessageRegistry.h"
|
||||
#include "../../src/pCCU/protocol/FcProtocol.h"
|
||||
#include "../../src/pCCU/protocol/PmProtocol.h"
|
||||
#include "../../src/pCCU/comm/LinkManager.h"
|
||||
#include "../../src/pCCU/comm/FcLinkManager.h"
|
||||
#include "../../src/pCCU/comm/PmLinkManager.h"
|
||||
#include "../../src/pCCU/store/DbStore.h"
|
||||
|
||||
using namespace ccu;
|
||||
|
||||
static int g_fail = 0;
|
||||
static int g_pass = 0;
|
||||
|
||||
#define CHECK(cond) do { \
|
||||
if (cond) { ++g_pass; } \
|
||||
else { ++g_fail; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
|
||||
} while (0)
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 1. 帧长度必须与协议文档一致
|
||||
static void testFrameLengths() {
|
||||
FcControlMessage fcm;
|
||||
FcStatusMessage fsm;
|
||||
PmControlMessage pcm;
|
||||
PmParamSetMessage pps;
|
||||
PmParamSetFbMessage ppf;
|
||||
PmStatusMessage psm;
|
||||
|
||||
CHECK(fcm.totalLength() == 20); // FC 控制指令 24B 文档表,实际 20B(域字节数0x0014)
|
||||
CHECK(fsm.totalLength() == 150); // FC 状态反馈 150B
|
||||
CHECK(pcm.totalLength() == 45); // PM 操控指令 45B
|
||||
CHECK(pps.totalLength() == 28); // PM 参数设定 28B
|
||||
CHECK(ppf.totalLength() == 12); // PM 参数反馈 12B
|
||||
CHECK(psm.totalLength() == 248); // PM 状态报文 248B
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 2. FC 控制指令编解码往返
|
||||
static void testFcControlRoundTrip() {
|
||||
FcControlValue v;
|
||||
v.mode = 2; v.cmd = 2; v.outputPower = 100; v.pitch = 35; v.roll = -12;
|
||||
v.emergencyAllow = 0x03; v.depth = 150;
|
||||
v.supplyCmd = 1; v.reservedCmd5 = 5; v.reservedCmd6 = 6; v.heartbeat = 7;
|
||||
|
||||
FcControlMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
|
||||
CHECK(frame.size() == 20);
|
||||
CHECK(frame[0] == 0x40 && frame[1] == 0x40);
|
||||
|
||||
// 字节位置校验:纵倾数据1=低位(字节10=0x23=35), 数据2=高位(字节11=0x00)
|
||||
CHECK(frame[6 + 3] == 0x23); // 姿态数据1(低位)
|
||||
CHECK(frame[6 + 4] == 0x00); // 姿态数据2(高位)
|
||||
CHECK(frame[6 + 5] == 0xF4); // 横倾数据1(低位, -12 = 0xFFF4 小端)
|
||||
CHECK(frame[6 + 6] == 0xFF); // 横倾数据2(高位)
|
||||
|
||||
FcControlValue out;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&out)));
|
||||
CHECK(out.mode == 2);
|
||||
CHECK(out.cmd == 2);
|
||||
CHECK(out.outputPower == 100);
|
||||
CHECK(out.pitch == 35);
|
||||
CHECK(out.roll == -12);
|
||||
CHECK(out.emergencyAllow == 0x03);
|
||||
CHECK(out.depth == 150);
|
||||
CHECK(out.supplyCmd == 1);
|
||||
CHECK(out.reservedCmd5 == 5 && out.reservedCmd6 == 6);
|
||||
CHECK(out.heartbeat == 7);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 3. FC 状态编解码往返(关键字段抽查)
|
||||
static void testFcStatusRoundTrip() {
|
||||
FcStatusValue v;
|
||||
v.fc_mode = 2; v.fc_status = 6; v.fc_fault_level = 0;
|
||||
v.fault_level_1 = 0x0102; v.fault_level_2 = 0x0304;
|
||||
v.fault_level_3 = 0x0506; v.fault_level_4 = 0x0708;
|
||||
v.total_generation_time = 12345;
|
||||
v.generation_power = 23456;
|
||||
v.hydrogen_capacity = 80; v.liquid_oxygen_capacity = 70;
|
||||
v.palladium_temp = 350; v.buffer_tank_pressure = 200;
|
||||
v.dcdc1_in_voltage = 12; v.dcdc_out_current = 33;
|
||||
v.methanol_total_use = 100;
|
||||
v.cabin_temp1 = 25; v.h2_concentration1 = 500;
|
||||
v.heartbeat = 9; v.emergency_cmd = 0x01;
|
||||
v.reserved[0] = 0xAAAA; v.reserved[11] = 0xBBBB;
|
||||
|
||||
FcStatusMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
CHECK(frame.size() == 150);
|
||||
|
||||
// 字节位置校验(对照 0821 docx):发电功率在字节21-22(offset 14),心跳在字节149(offset 142)
|
||||
CHECK(frame[6 + 12] == 0); // 故障等级 offset12
|
||||
CHECK(frame[6 + 14] == 0xA0 && frame[6 + 15] == 0x5B); // 发电功率 23456 -> 0x5BA0 小端
|
||||
CHECK(frame[6 + 142] == 9); // 心跳 offset142
|
||||
CHECK(frame[6 + 143] == 0x01);// 应急指令 offset143
|
||||
|
||||
FcStatusValue out;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&out)));
|
||||
CHECK(out.fc_mode == 2 && out.fc_status == 6);
|
||||
CHECK(out.fault_level_1 == 0x0102 && out.fault_level_4 == 0x0708);
|
||||
CHECK(out.total_generation_time == 12345);
|
||||
CHECK(out.generation_power == 23456);
|
||||
CHECK(out.hydrogen_capacity == 80 && out.liquid_oxygen_capacity == 70);
|
||||
CHECK(out.palladium_temp == 350);
|
||||
CHECK(out.dcdc1_in_voltage == 12 && out.dcdc_out_current == 33);
|
||||
CHECK(out.methanol_total_use == 100);
|
||||
CHECK(out.cabin_temp1 == 25 && out.h2_concentration1 == 500);
|
||||
CHECK(out.heartbeat == 9 && out.emergency_cmd == 0x01);
|
||||
CHECK(out.reserved[0] == 0xAAAA && out.reserved[11] == 0xBBBB);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 4. PM 操控指令编解码往返
|
||||
static void testPmControlRoundTrip() {
|
||||
PmControlValue v;
|
||||
v.year = 2026; v.month = 8; v.day = 26; v.hour = 17; v.minute = 30; v.second = 5;
|
||||
v.millisecond10 = 3;
|
||||
v.mode = 2; v.cmd = 2; v.outputPower = 120;
|
||||
v.pitch = 35; v.roll = -12;
|
||||
v.emergencyAllow = 0x03; v.depth = 200;
|
||||
v.supplyCmd = 1; v.reservedCmd5 = 5; v.reservedCmd6 = 6;
|
||||
v.insBatCmd = 0x10; v.dynBatCmd = 0x20; v.dynBatPower = 300;
|
||||
v.heartbeat = 11; v.hostState = 0xAA;
|
||||
v.reserved1 = 0x11223344; v.reserved2 = 0x55667788;
|
||||
|
||||
PmControlMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
CHECK(frame.size() == 45);
|
||||
|
||||
PmControlValue out;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&out)));
|
||||
CHECK(out.year == 2026 && out.month == 8 && out.day == 26);
|
||||
CHECK(out.hour == 17 && out.minute == 30 && out.second == 5);
|
||||
CHECK(out.millisecond10 == 3);
|
||||
CHECK(out.mode == 2 && out.cmd == 2 && out.outputPower == 120);
|
||||
CHECK(out.pitch == 35 && out.roll == -12);
|
||||
CHECK(out.depth == 200);
|
||||
CHECK(out.insBatCmd == 0x10 && out.dynBatCmd == 0x20 && out.dynBatPower == 300);
|
||||
CHECK(out.heartbeat == 11 && out.hostState == 0xAA);
|
||||
CHECK(out.reserved1 == 0x11223344 && out.reserved2 == 0x55667788);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 5. PM 状态报文编解码往返(抽查关键字段,覆盖各分组)
|
||||
static void testPmStatusRoundTrip() {
|
||||
PmStatusValue v;
|
||||
v.fc_mode = 2; v.fc_status = 6;
|
||||
v.fault_level_1 = 1; v.fault_level_2 = 2; v.fault_level_3 = 3; v.fault_level_4 = 4;
|
||||
v.total_generation_time = 500; v.fc_fault_level = 0;
|
||||
v.output_power_limit = 30000; v.generation_power = 100;
|
||||
v.hydrogen_capacity = 80; v.liquid_oxygen_capacity = 70;
|
||||
v.main_pipe_pressure = 500; v.aux_pipe_pressure = 400;
|
||||
v.dcdc_out_voltage = 240; v.dcdc_out_current = 100;
|
||||
v.cabin_temp1 = 2500; v.cabin_temp2 = 2400;
|
||||
v.h2_concentration1 = 300; v.o2_concentration1 = 200;
|
||||
v.emergency_battery1_voltage = 24; v.emergency_battery2_voltage = 26;
|
||||
v.ins_soc = 60; v.dyn_soc = 55;
|
||||
v.ins_voltage_link = 4800; v.dyn_voltage_link = 4900;
|
||||
v.ins_current = 120; v.dyn_current = 130;
|
||||
v.device_online_flag1 = 0x0F; v.device_online_flag2 = 0x07;
|
||||
v.heartbeat = 42; v.emergency_cmd = 0x01;
|
||||
v.dyn_alarm_flag[0] = 0xAA; v.ins_alarm_flag[5] = 0x55;
|
||||
|
||||
PmStatusMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
CHECK(frame.size() == 248);
|
||||
|
||||
PmStatusValue out;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&out)));
|
||||
CHECK(out.fc_mode == 2 && out.fc_status == 6);
|
||||
CHECK(out.fault_level_1 == 1 && out.fault_level_4 == 4);
|
||||
CHECK(out.total_generation_time == 500 && out.fc_fault_level == 0);
|
||||
CHECK(out.output_power_limit == 30000 && out.generation_power == 100);
|
||||
CHECK(out.hydrogen_capacity == 80 && out.liquid_oxygen_capacity == 70);
|
||||
CHECK(out.main_pipe_pressure == 500 && out.aux_pipe_pressure == 400);
|
||||
CHECK(out.dcdc_out_voltage == 240 && out.dcdc_out_current == 100);
|
||||
CHECK(out.cabin_temp1 == 2500 && out.h2_concentration1 == 300);
|
||||
CHECK(out.emergency_battery1_voltage == 24 && out.emergency_battery2_voltage == 26);
|
||||
CHECK(out.ins_soc == 60 && out.dyn_soc == 55);
|
||||
CHECK(out.ins_voltage_link == 4800 && out.dyn_voltage_link == 4900);
|
||||
CHECK(out.device_online_flag1 == 0x0F && out.device_online_flag2 == 0x07);
|
||||
CHECK(out.heartbeat == 42 && out.emergency_cmd == 0x01);
|
||||
CHECK(out.dyn_alarm_flag[0] == 0xAA && out.ins_alarm_flag[5] == 0x55);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 6. PM 校验和:正确帧通过,篡改字节校验失败
|
||||
static void testChecksum() {
|
||||
PmControlValue v;
|
||||
v.mode = 2; v.cmd = 3; v.heartbeat = 1;
|
||||
|
||||
PmControlMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
PmControlValue tmp;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&tmp))); // 校验通过
|
||||
|
||||
// 篡改数据域中间字节
|
||||
std::vector<uint8_t> bad = frame;
|
||||
bad[10] ^= 0xFF; // 数据域某字节
|
||||
CHECK(!msg.decode(bad, static_cast<void*>(&tmp)));
|
||||
|
||||
// 篡改帧头长度
|
||||
std::vector<uint8_t> badLen = frame;
|
||||
badLen[4] = 0x01; // length 低字节改小 -> 0x012D != 45
|
||||
CHECK(!msg.decode(badLen, static_cast<void*>(&tmp)));
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 7. 链路分发:FC 链路收到状态帧 -> 回调解码
|
||||
static void testLinkDispatch() {
|
||||
// 注册表按链路隔离:FC 与 PM 共用 0x0001/0x0002 不应冲突
|
||||
MessageRegistry fcReg;
|
||||
registerFcMessages(fcReg);
|
||||
MessageRegistry pmReg;
|
||||
registerPmMessages(pmReg);
|
||||
|
||||
CHECK(fcReg.size() == 2);
|
||||
CHECK(pmReg.size() == 4);
|
||||
CHECK(fcReg.find(0x0001)->id() == 0x0001);
|
||||
CHECK(pmReg.find(0x0001)->id() == 0x0001); // id 重叠但隔离
|
||||
|
||||
// 用 LinkManager 的 handleIncoming 模拟收帧
|
||||
FcStatusValue fv;
|
||||
fv.fc_status = 6; fv.heartbeat = 8;
|
||||
FcStatusMessage fsMsg;
|
||||
std::vector<uint8_t> frame = fsMsg.encode(fv);
|
||||
|
||||
LinkManager lm("test_fc", 0);
|
||||
registerFcMessages(lm.registry());
|
||||
|
||||
bool got = false;
|
||||
lm.setOnMessage([&](Message* m, const std::vector<uint8_t>& f) {
|
||||
if (m && m->id() == 0x0002) {
|
||||
FcStatusValue out;
|
||||
if (m->decode(f, static_cast<void*>(&out))) {
|
||||
got = (out.fc_status == 6 && out.heartbeat == 8);
|
||||
}
|
||||
}
|
||||
});
|
||||
CHECK(lm.handleIncoming(frame));
|
||||
CHECK(got);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 8. 数据库落库与查询
|
||||
static void testDbStore() {
|
||||
std::string path = "test_pccu.db";
|
||||
std::remove(path.c_str());
|
||||
{
|
||||
DbStore db(path);
|
||||
CHECK(db.open());
|
||||
|
||||
std::vector<uint8_t> frame = {0x40,0x40,0x01,0x00,0x14,0x00, 1,2,3,4,5,6,7,8,9,10,11,12,13,14};
|
||||
db.onRawFrame(0, 0x0001, "fc", frame, true);
|
||||
db.onRawFrame(1, 0x0004, "pm", frame, false);
|
||||
|
||||
CHECK(db.count() == 2);
|
||||
|
||||
auto rows = db.queryRecent("", -1, 0, 10);
|
||||
CHECK(rows.size() == 2);
|
||||
CHECK(rows[0].msgId == 0x0004 || rows[0].msgId == 0x0001);
|
||||
CHECK(rows[0].checksumOk == 0); // 最近一条是 pm 的 false
|
||||
|
||||
auto fcRows = db.queryRecent("fc", 0, 0, 10);
|
||||
CHECK(fcRows.size() == 1);
|
||||
CHECK(fcRows[0].msgId == 0x0001 && fcRows[0].direction == 0);
|
||||
|
||||
db.close();
|
||||
}
|
||||
std::remove(path.c_str());
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
int main() {
|
||||
testFrameLengths();
|
||||
testFcControlRoundTrip();
|
||||
testFcStatusRoundTrip();
|
||||
testPmControlRoundTrip();
|
||||
testPmStatusRoundTrip();
|
||||
testChecksum();
|
||||
testLinkDispatch();
|
||||
testDbStore();
|
||||
|
||||
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pCCU 集成测试:验证真实 UDP 链路上的完整数据流。
|
||||
|
||||
拓扑:
|
||||
测试脚本(FC模拟) --16000--> pCCU(fc_local) [FC状态 0x0002]
|
||||
测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002]
|
||||
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
|
||||
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003]
|
||||
|
||||
校验:
|
||||
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
|
||||
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 20B)到 16001
|
||||
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
|
||||
4. SQLite 记录了收/发原始帧
|
||||
5. Web 页面可访问
|
||||
"""
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
# ---- 配置 ----
|
||||
FC_STATUS_PORT = 16000
|
||||
FC_CTRL_RECV_PORT = 16001
|
||||
PM_CTRL_PORT = 16002
|
||||
PM_STATUS_RECV_PORT = 16003
|
||||
WEB_PORT = 18081
|
||||
DB_PATH = "/tmp/pccu_it_test.db"
|
||||
MOOSDB_EXE = "/usr/local/bin/MOOSDB"
|
||||
PCCU_EXE = "/home/zjk/project/H100PowerManger/bin/pCCU"
|
||||
MISSION = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pccu_it.moos")
|
||||
|
||||
# 帧头
|
||||
START = bytes([0x40, 0x40])
|
||||
|
||||
|
||||
def frame(msg_id, payload, checksum=True):
|
||||
total = 6 + len(payload) + (4 if checksum else 0)
|
||||
hdr = START + struct.pack("<HH", msg_id, total)
|
||||
body = hdr + payload
|
||||
if checksum:
|
||||
cks = sum(body) & 0xFFFFFFFF
|
||||
return body + struct.pack("<I", cks)
|
||||
return body
|
||||
|
||||
|
||||
def fc_status_frame():
|
||||
# payload 144B,仅填关键字段,其余为0
|
||||
p = bytearray(144)
|
||||
p[0] = 2 # 运行模式 自动
|
||||
p[1] = 6 # 运行状态 运行
|
||||
struct.pack_into("<H", p, 2, 0x0102) # 一级故障码
|
||||
struct.pack_into("<H", p, 10, 1234) # 累积发电时间
|
||||
p[12] = 0 # 故障等级
|
||||
struct.pack_into("<H", p, 14, 2000) # 系统发电功率 0.01kW
|
||||
p[16] = 80 # 储氢
|
||||
p[17] = 70 # 液氧
|
||||
struct.pack_into("<H", p, 24, 350) # 钯膜温度
|
||||
struct.pack_into("<H", p, 68, 2500) # 舱室温度1
|
||||
p[142] = 5 # 心跳
|
||||
p[143] = 0x01 # 应急指令
|
||||
return frame(0x0002, bytes(p), checksum=False)
|
||||
|
||||
|
||||
def pm_control_frame():
|
||||
p = bytearray(35)
|
||||
struct.pack_into("<H", p, 0, 2026)
|
||||
p[2], p[3], p[4], p[5], p[6], p[7] = 8, 26, 10, 30, 0, 0
|
||||
p[8] = 2 # 模式 自动
|
||||
p[9] = 2 # 操控 启动
|
||||
p[10] = 120 # 输出功率 0.1kW
|
||||
p[15] = 0x03 # 应急允许
|
||||
struct.pack_into("<H", p, 16, 150) # 潜深
|
||||
p[21] = 0x10 # 仪表锂电启动
|
||||
p[22] = 0x20 # 动力锂电关闭
|
||||
struct.pack_into("<H", p, 23, 300) # 动力电池功率
|
||||
p[25] = 7 # 心跳
|
||||
p[26] = 0xAA # 主机状态
|
||||
return frame(0x0001, bytes(p))
|
||||
|
||||
|
||||
def pm_paramset_frame():
|
||||
p = bytearray(18)
|
||||
p[0], p[1], p[2] = 75, 85, 93
|
||||
p[5], p[6], p[7] = 75, 85, 93
|
||||
return frame(0x0002, bytes(p))
|
||||
|
||||
|
||||
def recv_loop(sock, label, results, msg_ids, timeout):
|
||||
"""持续接收指定 msg_id 的帧,记录到 results"""
|
||||
sock.settimeout(timeout)
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
try:
|
||||
data, addr = sock.recvfrom(2048)
|
||||
except socket.timeout:
|
||||
break
|
||||
except OSError:
|
||||
break
|
||||
if len(data) < 6:
|
||||
continue
|
||||
mid = struct.unpack_from("<H", data, 2)[0]
|
||||
if mid in msg_ids:
|
||||
results.append(data)
|
||||
|
||||
|
||||
def main():
|
||||
# 清理
|
||||
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log"):
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
|
||||
procs = []
|
||||
try:
|
||||
# 1. 启动 MOOSDB
|
||||
moosdb = subprocess.Popen([MOOSDB_EXE, "--moos", "--port", "9000"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
procs.append(moosdb)
|
||||
time.sleep(1.0)
|
||||
|
||||
# 2. 启动 pCCU
|
||||
pccu = subprocess.Popen([PCCU_EXE, MISSION],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
procs.append(pccu)
|
||||
time.sleep(2.0)
|
||||
if pccu.poll() is not None:
|
||||
print("FAIL: pCCU exited early, code", pccu.returncode)
|
||||
return 1
|
||||
|
||||
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003)
|
||||
fc_ctl_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
fc_ctl_recv.bind(("127.0.0.1", FC_CTRL_RECV_PORT))
|
||||
pm_status_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
pm_status_recv.bind(("127.0.0.1", PM_STATUS_RECV_PORT))
|
||||
|
||||
fc_ctl_results, pm_status_results = [], []
|
||||
t1 = threading.Thread(target=recv_loop,
|
||||
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 12))
|
||||
t2 = threading.Thread(target=recv_loop,
|
||||
args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12))
|
||||
t1.start(); t2.start()
|
||||
|
||||
# 4. 发送 FC 状态(周期模拟)
|
||||
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
for _ in range(3):
|
||||
fc_send.sendto(fc_status_frame(), ("127.0.0.1", FC_STATUS_PORT))
|
||||
time.sleep(0.5)
|
||||
|
||||
# 5. 发送 PM 操控指令 + 参数设定
|
||||
pm_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
pm_send.sendto(pm_control_frame(), ("127.0.0.1", PM_CTRL_PORT))
|
||||
time.sleep(0.5)
|
||||
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT))
|
||||
|
||||
t1.join(); t2.join()
|
||||
|
||||
ok = True
|
||||
|
||||
# 校验 1:PM 状态报文(248B)周期发送
|
||||
pm_status = [d for d in pm_status_results if len(d) == 248]
|
||||
print(f"[PM 状态报文] 收到 {len(pm_status)} 帧 (期望>=1, 每帧248B)")
|
||||
if len(pm_status) >= 1:
|
||||
d = pm_status[0]
|
||||
total = struct.unpack_from("<H", d, 4)[0]
|
||||
mode = d[6]; status = d[7]
|
||||
print(f" -> length={total}, mode={mode}, status={status}, size={len(d)}")
|
||||
ok = ok and (total == 248) and (mode == 2) and (status == 6)
|
||||
else:
|
||||
ok = False
|
||||
|
||||
# 校验 2:PM 操控转发为 FC 控制(20B)
|
||||
fc_ctl = [d for d in fc_ctl_results if len(d) == 20]
|
||||
print(f"[FC 控制转发] 收到 {len(fc_ctl)} 帧 (期望>=1, 每帧20B)")
|
||||
if fc_ctl:
|
||||
d = fc_ctl[0]
|
||||
cmd = d[7]; power = d[8]; hb = d[19]
|
||||
print(f" -> cmd={cmd}, outputPower={power}, heartbeat={hb}")
|
||||
ok = ok and (cmd == 2) and (power == 120) and (hb == 7)
|
||||
else:
|
||||
ok = False
|
||||
|
||||
# 校验 3:PM 参数反馈(12B)
|
||||
fb = [d for d in pm_status_results if len(d) == 12]
|
||||
print(f"[PM 参数反馈] 收到 {len(fb)} 帧 (期望>=1, 每帧12B)")
|
||||
if fb:
|
||||
flag = fb[0][6]
|
||||
print(f" -> flag=0x{flag:02X}")
|
||||
ok = ok and (flag == 0x10)
|
||||
else:
|
||||
ok = False
|
||||
|
||||
# 校验 4:数据库有记录
|
||||
time.sleep(1.0)
|
||||
db_count = -1
|
||||
if os.path.exists(DB_PATH):
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0]
|
||||
conn.close()
|
||||
print(f"[数据库] comm_log 记录数 = {db_count}")
|
||||
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
|
||||
|
||||
# 校验 5:Web 页面
|
||||
web_ok = False
|
||||
try:
|
||||
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)
|
||||
html = resp.read().decode()
|
||||
web_ok = ("pCCU" in html)
|
||||
print(f"[Web] 页面可访问, 长度={len(html)}")
|
||||
except Exception as e:
|
||||
print(f"[Web] 访问失败: {e}")
|
||||
ok = ok and web_ok
|
||||
|
||||
print("\n==== 集成测试 " + ("PASSED" if ok else "FAILED") + " ====")
|
||||
return 0 if ok else 1
|
||||
|
||||
finally:
|
||||
for p in reversed(procs):
|
||||
try:
|
||||
p.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
for p in procs:
|
||||
try:
|
||||
p.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,24 @@
|
||||
//============================================================================
|
||||
// 测试用 pCCU 配置(集成测试专用,端口避开生产默认值)
|
||||
//============================================================================
|
||||
|
||||
ProcessConfig = pCCU
|
||||
{
|
||||
AppTick = 4
|
||||
CommsTick = 4
|
||||
|
||||
// 测试端口
|
||||
fc_local_port = 16000 // 监听 FC 状态
|
||||
fc_remote_ip = 127.0.0.1
|
||||
fc_remote_port = 16001 // FC 控制器(测试监听此端口收转发指令)
|
||||
|
||||
pm_local_port = 16002 // 监听 PM 指令
|
||||
pm_remote_ip = 127.0.0.1
|
||||
pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文)
|
||||
|
||||
dbpath = /tmp/pccu_it_test.db
|
||||
logpath = /tmp/pccu_it_test.log
|
||||
|
||||
web_port = 18081 // 避开 pPowerMangerHost(18080)
|
||||
web_enable = true
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pCCU 演示模拟器:模拟燃料电池(FC) 与 控制主机(PM) 两端。
|
||||
|
||||
- 每 1s 向 pCCU 的 fc_local 端口发送 FC 状态帧(0x0002, 150B),数据带波动
|
||||
- 每 5s 向 pCCU 的 pm_local 端口发送 PM 操控指令(0x0001) 与参数设定(0x0002)
|
||||
- 监听 pCCU 的 fc_remote / pm_remote 端口,打印 pCCU 转发的 FC 控制与 PM 状态反馈
|
||||
|
||||
用法: python3 pccu_sim.py [fc_status_port] [pm_cmd_port] [fc_ctl_listen] [pm_status_listen]
|
||||
默认端口与 test/pccu/pccu_it.moos 一致。
|
||||
"""
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import math
|
||||
import sys
|
||||
|
||||
# 默认端口(与 pccu_it.moos 一致)
|
||||
FC_STATUS_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 16000
|
||||
PM_CMD_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 16002
|
||||
FC_CTL_LISTEN = int(sys.argv[3]) if len(sys.argv) > 3 else 16001
|
||||
PM_STAT_LISTEN = int(sys.argv[4]) if len(sys.argv) > 4 else 16003
|
||||
|
||||
START = bytes([0x40, 0x40])
|
||||
|
||||
|
||||
def frame(msg_id, payload, checksum=True):
|
||||
total = 6 + len(payload) + (4 if checksum else 0)
|
||||
body = START + struct.pack("<HH", msg_id, total) + payload
|
||||
if checksum:
|
||||
return body + struct.pack("<I", sum(body) & 0xFFFFFFFF)
|
||||
return body
|
||||
|
||||
|
||||
class FcSim:
|
||||
"""燃料电池控制器:周期发送带波动的状态帧"""
|
||||
|
||||
def __init__(self, port):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.port = port
|
||||
self.t = 0.0
|
||||
self.hydrogen = 85.0
|
||||
self.lo2 = 70.0
|
||||
self.gen_time = 0.0
|
||||
self.heartbeat = 0
|
||||
self.fc_status = 4 # 从就绪开始
|
||||
self.status_timer = 0.0
|
||||
|
||||
def build(self):
|
||||
p = bytearray(144)
|
||||
p[0] = 2 # 运行模式: 自动
|
||||
p[1] = self.fc_status # 运行状态
|
||||
struct.pack_into("<H", p, 10, int(self.gen_time)) # 累积发电时间 0.1h
|
||||
p[12] = 0 # 故障等级 无 (字节19)
|
||||
# 字节20 隐式保留
|
||||
# 发电功率 0.01kW:500kW + 正弦波动 (字节21-22)
|
||||
struct.pack_into("<H", p, 14, int(50000 + 3000 * math.sin(self.t / 5)))
|
||||
p[16] = int(self.hydrogen) # 储氢 % (字节23)
|
||||
p[17] = int(self.lo2) # 液氧 % (字节24)
|
||||
# FC 单片电压 (10mV) (字节25-30)
|
||||
p[18] = 75; p[20] = 74; p[21] = 73; p[22] = 72; p[24] = 72
|
||||
p[19] = 12; p[20] = 13; p[23] = 11 # 电压位置
|
||||
# 钯膜温度 0.1℃:350℃ + 波动 (字节31-32)
|
||||
struct.pack_into("<H", p, 24, int(3500 + 20 * math.sin(self.t / 3)))
|
||||
# 缓冲罐压力 0.1kPa (字节33-34)
|
||||
struct.pack_into("<H", p, 26, int(1800 + 100 * math.sin(self.t / 7)))
|
||||
# 反应器压力 0.001MPa (字节39-40)
|
||||
struct.pack_into("<H", p, 32, int(1200 + 50 * math.sin(self.t / 4)))
|
||||
# 电动阀开度 0.1% (字节41-42)
|
||||
struct.pack_into("<H", p, 34, int(500 + 100 * math.sin(self.t / 2)))
|
||||
# DC/DC 输出电压 0.1V: 240V (字节51-52)
|
||||
struct.pack_into("<H", p, 44, int(2400 + 20 * math.sin(self.t / 6)))
|
||||
# DC/DC 输出电流 0.1A (字节53-54)
|
||||
struct.pack_into("<H", p, 46, int(1200 + 400 * math.sin(self.t / 5)))
|
||||
p[48] = 55 # 控制电源电压 0.25V (字节55)
|
||||
p[49] = 30 # 辅电输出 0.125V (字节56)
|
||||
# 甲醇累计使用量 0.1kg:缓慢增加 (字节57-58)
|
||||
struct.pack_into("<H", p, 50, int(self.gen_time * 3))
|
||||
struct.pack_into("<H", p, 52, int(120 + 20 * math.sin(self.t / 4))) # 进料量 mL/min (字节59-60)
|
||||
# 氧/氢/配重水箱液位 0.01mm (字节61-66)
|
||||
struct.pack_into("<H", p, 54, int(5000 + 200 * math.sin(self.t / 5)))
|
||||
struct.pack_into("<H", p, 56, int(4200 + 150 * math.sin(self.t / 6)))
|
||||
struct.pack_into("<H", p, 58, int(3000 + 100 * math.sin(self.t / 7)))
|
||||
# 尾气装置进气/排气压力 0.01MPa (字节67-70)
|
||||
struct.pack_into("<H", p, 60, int(200 + 10 * math.sin(self.t / 3)))
|
||||
struct.pack_into("<H", p, 62, int(150 + 8 * math.sin(self.t / 4)))
|
||||
# 舱室压力 0.01kPa:101.3kPa + 波动 (字节71-74)
|
||||
struct.pack_into("<H", p, 64, int(10130 + 20 * math.sin(self.t / 8)))
|
||||
struct.pack_into("<H", p, 66, int(10120 + 20 * math.sin(self.t / 9)))
|
||||
# 舱室温度 0.01℃:25℃ + 波动 (字节75-78)
|
||||
struct.pack_into("<H", p, 68, int(2500 + 30 * math.sin(self.t / 10)))
|
||||
struct.pack_into("<H", p, 70, int(2480 + 25 * math.sin(self.t / 11)))
|
||||
# 舱室湿度 0.01%RH (字节79-82)
|
||||
struct.pack_into("<H", p, 72, int(4500 + 300 * math.sin(self.t / 12)))
|
||||
struct.pack_into("<H", p, 74, int(4400 + 300 * math.sin(self.t / 13)))
|
||||
# H2 浓度 0.01%LEL:0.5% 波动 (字节83-88)
|
||||
struct.pack_into("<H", p, 76, int(50 + 20 * math.sin(self.t / 3)))
|
||||
struct.pack_into("<H", p, 78, int(45 + 15 * math.sin(self.t / 4)))
|
||||
struct.pack_into("<H", p, 80, int(40 + 10 * math.sin(self.t / 5)))
|
||||
# O2 浓度 0.01%Vol:21% (字节89-92)
|
||||
struct.pack_into("<H", p, 82, int(2100 + 50 * math.sin(self.t / 6)))
|
||||
struct.pack_into("<H", p, 84, int(2050 + 40 * math.sin(self.t / 7)))
|
||||
# 甲醇浓度 0.01%LEL (字节93-96)
|
||||
struct.pack_into("<H", p, 86, int(30 + 10 * math.sin(self.t / 8)))
|
||||
struct.pack_into("<H", p, 88, int(25 + 8 * math.sin(self.t / 9)))
|
||||
# 火焰探测器 (字节97-98)
|
||||
p[90] = 0; p[91] = 0
|
||||
# 应急上浮深度/时间 (字节99-102)
|
||||
struct.pack_into("<H", p, 92, 100)
|
||||
struct.pack_into("<H", p, 94, 60)
|
||||
# 尾气运行频率 0.01Hz (字节103-104)
|
||||
struct.pack_into("<H", p, 96, int(5000 + 300 * math.sin(self.t / 4)))
|
||||
# 一体化罐液氧罐压力 0.01MPa (字节113-114)
|
||||
struct.pack_into("<H", p, 106, int(300 + 20 * math.sin(self.t / 5)))
|
||||
struct.pack_into("<H", p, 108, int(200 + 15 * math.sin(self.t / 6)))
|
||||
struct.pack_into("<H", p, 110, int(8000 + 200 * math.sin(self.t / 7)))
|
||||
# 供氢/供氧流量 0.01 L/min (字节119-124)
|
||||
struct.pack_into("<H", p, 112, int(1500 + 200 * math.sin(self.t / 4)))
|
||||
struct.pack_into("<H", p, 114, int(1800 + 250 * math.sin(self.t / 4)))
|
||||
struct.pack_into("<H", p, 116, int(900 + 100 * math.sin(self.t / 4)))
|
||||
p[142] = self.heartbeat & 0xFF # 通信心跳 (字节149)
|
||||
p[143] = 0 # 应急指令 (字节150)
|
||||
return frame(0x0002, bytes(p), checksum=False)
|
||||
|
||||
def tick(self, dt):
|
||||
self.t += dt
|
||||
self.gen_time += dt / 360.0 # 每秒累积 0.1h/360 ≈ 1s 的发电时间
|
||||
self.hydrogen -= 0.0005 # 缓慢消耗
|
||||
self.lo2 -= 0.0004
|
||||
self.heartbeat += 1
|
||||
# 状态推进: 就绪(4) -> 启动(5) -> 运行(6)
|
||||
self.status_timer += dt
|
||||
if self.fc_status == 4 and self.status_timer > 3:
|
||||
self.fc_status = 5; self.status_timer = 0
|
||||
elif self.fc_status == 5 and self.status_timer > 3:
|
||||
self.fc_status = 6; self.status_timer = 0
|
||||
return self.build()
|
||||
|
||||
|
||||
class PmSim:
|
||||
"""控制主机:周期发送操控指令,监听 pCCU 状态反馈"""
|
||||
|
||||
def __init__(self, cmd_port, stat_listen):
|
||||
self.cmd_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.cmd_port = cmd_port
|
||||
self.stat_listen = stat_listen
|
||||
self.power = 100
|
||||
self.heartbeat = 0
|
||||
|
||||
def control_frame(self):
|
||||
p = bytearray(35)
|
||||
now = time.localtime()
|
||||
struct.pack_into("<H", p, 0, now.tm_year)
|
||||
p[2] = now.tm_mon; p[3] = now.tm_mday; p[4] = now.tm_hour
|
||||
p[5] = now.tm_min; p[6] = now.tm_sec; p[7] = (int(time.time() * 100) % 100)
|
||||
p[8] = 2 # 模式: 自动
|
||||
p[9] = 2 # 操控: 启动
|
||||
p[10] = self.power # 输出功率 (0.1kW)
|
||||
# 纵倾姿态 (数据1=低8位/数据2=高8位, int16, 0.1°) / 横倾姿态
|
||||
pitch = int((3.0 + 1.5 * math.sin(time.time() / 8)) * 10)
|
||||
roll = int((-1.0 + 0.5 * math.cos(time.time() / 6)) * 10)
|
||||
struct.pack_into("<h", p, 11, pitch)
|
||||
struct.pack_into("<h", p, 13, roll)
|
||||
p[15] = 0x03 # 应急允许: 降载+排气
|
||||
struct.pack_into("<H", p, 16, 150) # 潜深 m
|
||||
p[18] = 0 # 补给/排放
|
||||
p[21] = 0x10 # 仪表锂电启动
|
||||
p[22] = 0x10 # 动力锂电启动
|
||||
struct.pack_into("<H", p, 23, 300) # 动力电池功率配置 kW
|
||||
p[25] = self.heartbeat & 0xFF # 通信心跳
|
||||
p[26] = 0xAA # 主机状态: 运行
|
||||
return frame(0x0001, bytes(p))
|
||||
|
||||
def param_frame(self):
|
||||
p = bytearray(18)
|
||||
p[0], p[1], p[2] = 75, 85, 93 # 仪表 SOC 门限
|
||||
p[3] = 80 # 仪表输出功率
|
||||
p[5], p[6], p[7] = 75, 85, 93 # 动力 SOC 门限
|
||||
p[8] = 80 # 动力输出功率
|
||||
return frame(0x0002, bytes(p))
|
||||
|
||||
def send_cmd(self):
|
||||
self.cmd_sock.sendto(self.control_frame(), ("127.0.0.1", self.cmd_port))
|
||||
self.heartbeat += 1
|
||||
self.power = 100 + ((self.power - 100 + 20) % 80) # 100->120->140 循环
|
||||
|
||||
|
||||
def listen_loop(port, label, msg_ids, name_of):
|
||||
"""监听 pCCU 输出端口,打印收到的帧关键字段"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
except OSError as e:
|
||||
print(f"[{label}] 监听端口 {port} 失败: {e}")
|
||||
return
|
||||
sock.settimeout(1.0)
|
||||
print(f"[{label}] 监听 {label} 输出端口 {port}")
|
||||
while True:
|
||||
try:
|
||||
data, addr = sock.recvfrom(2048)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
if len(data) < 6:
|
||||
continue
|
||||
mid = struct.unpack_from("<H", data, 2)[0]
|
||||
if mid not in msg_ids:
|
||||
continue
|
||||
total = struct.unpack_from("<H", data, 4)[0]
|
||||
if mid == 0x0001 and len(data) >= 20: # FC 控制转发
|
||||
cmd = data[7]; power = data[8]; hb = data[19]
|
||||
print(f"[{label}] ← FC控制转发 0x0001: cmd={cmd} power={power} 心跳={hb} ({len(data)}B)")
|
||||
elif mid == 0x0004 and len(data) >= 248: # PM 状态报文
|
||||
mode = data[6]; status = data[7]; hb = data[242]
|
||||
genp = struct.unpack_from("<H", data, 6 + 15)[0] # 发电功率 payload偏移15
|
||||
print(f"[{label}] ← PM状态 0x0004: mode={mode} status={status} 心跳={hb} 功率={genp*0.01:.2f}kW ({len(data)}B)")
|
||||
elif mid == 0x0003 and len(data) >= 12: # 参数反馈
|
||||
flag = data[6]
|
||||
print(f"[{label}] ← 参数反馈 0x0003: flag=0x{flag:02X} ({len(data)}B)")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 64)
|
||||
print(f"pCCU 演示模拟器启动")
|
||||
print(f" FC 状态 -> pCCU:{FC_STATUS_PORT} (0x0002 150B)")
|
||||
print(f" PM 指令 -> pCCU:{PM_CMD_PORT} (0x0001 45B / 0x0002 28B)")
|
||||
print(f" 监听 FC 控制转发 于 {FC_CTL_LISTEN}")
|
||||
print(f" 监听 PM 状态报文 于 {PM_STAT_LISTEN}")
|
||||
print("=" * 64)
|
||||
|
||||
fc = FcSim(FC_STATUS_PORT)
|
||||
pm = PmSim(PM_CMD_PORT, PM_STAT_LISTEN)
|
||||
|
||||
threading.Thread(target=listen_loop,
|
||||
args=(FC_CTL_LISTEN, "FC", {0x0001}, None), daemon=True).start()
|
||||
threading.Thread(target=listen_loop,
|
||||
args=(PM_STAT_LISTEN, "PM", {0x0004, 0x0003}, None), daemon=True).start()
|
||||
|
||||
last_cmd_t = 0
|
||||
last_print_t = 0
|
||||
try:
|
||||
while True:
|
||||
# FC 状态 1Hz
|
||||
f = fc.tick(1.0)
|
||||
fc.sock.sendto(f, ("127.0.0.1", FC_STATUS_PORT))
|
||||
# PM 指令每 5s
|
||||
if time.time() - last_cmd_t >= 5.0:
|
||||
pm.send_cmd()
|
||||
pm.cmd_sock.sendto(pm.param_frame(), ("127.0.0.1", PM_CMD_PORT))
|
||||
print(f"[PM] → 发送操控指令 power={pm.power}, 参数设定")
|
||||
last_cmd_t = time.time()
|
||||
# 每秒打印一行 FC 发送摘要
|
||||
if time.time() - last_print_t >= 5.0:
|
||||
last_print_t = time.time()
|
||||
print(f"[FC] → 状态: status={fc.fc_status} 心跳={fc.heartbeat} "
|
||||
f"储氢={fc.hydrogen:.1f}% 液氧={fc.lo2:.1f}% 发电时间={fc.gen_time:.1f}h")
|
||||
time.sleep(1.0)
|
||||
except KeyboardInterrupt:
|
||||
print("\n模拟器退出")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""验证 pCCU WebSocket 推送的 JSON 快照格式与分组完整性。"""
|
||||
import socket, base64, os, struct, time, json, sys
|
||||
|
||||
HOST, PORT = "127.0.0.1", 18081
|
||||
|
||||
class WsClient:
|
||||
def __init__(self):
|
||||
self.s = socket.create_connection((HOST, PORT), timeout=5)
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {HOST}:{PORT}\r\n"
|
||||
f"Upgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n")
|
||||
self.s.sendall(req.encode())
|
||||
self.buf = b""
|
||||
# 读取 HTTP 101 响应头
|
||||
while b"\r\n\r\n" not in self.buf:
|
||||
chunk = self.s.recv(4096)
|
||||
if not chunk:
|
||||
raise RuntimeError("连接关闭")
|
||||
self.buf += chunk
|
||||
head, self.buf = self.buf.split(b"\r\n\r\n", 1)
|
||||
if b"101" not in head.split(b"\r\n")[0]:
|
||||
raise RuntimeError("WebSocket 升级失败: " + head.decode(errors="replace"))
|
||||
|
||||
def _read(self, n):
|
||||
while len(self.buf) < n:
|
||||
chunk = self.s.recv(4096)
|
||||
if not chunk:
|
||||
raise RuntimeError("连接关闭")
|
||||
self.buf += chunk
|
||||
out, self.buf = self.buf[:n], self.buf[n:]
|
||||
return out
|
||||
|
||||
def recv_text(self, timeout=6):
|
||||
self.s.settimeout(timeout)
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
if len(self.buf) < 2:
|
||||
try:
|
||||
chunk = self.s.recv(4096)
|
||||
except socket.timeout:
|
||||
return None
|
||||
if not chunk:
|
||||
return None
|
||||
self.buf += chunk
|
||||
continue
|
||||
b1, b2 = self.buf[0], self.buf[1]
|
||||
opcode = b1 & 0x0F
|
||||
length = b2 & 0x7F
|
||||
hdr_len = 2
|
||||
self.buf = self.buf[2:] # 消费帧头前2字节
|
||||
if length == 126:
|
||||
length = struct.unpack(">H", self._read(2))[0]
|
||||
hdr_len += 2
|
||||
elif length == 127:
|
||||
length = struct.unpack(">Q", self._read(8))[0]
|
||||
hdr_len += 8
|
||||
masked = (b2 & 0x80) != 0
|
||||
mask = self._read(4) if masked else b""
|
||||
payload = self._read(length)
|
||||
if opcode == 1: # text
|
||||
if masked:
|
||||
payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
|
||||
return payload.decode("utf-8", "replace")
|
||||
# ping/pong/close 跳过,继续
|
||||
return None
|
||||
|
||||
def main():
|
||||
try:
|
||||
ws = WsClient()
|
||||
except Exception as e:
|
||||
print("FAIL: WS 连接失败:", e)
|
||||
return 1
|
||||
text = ws.recv_text()
|
||||
if not text:
|
||||
print("FAIL: 未收到快照")
|
||||
return 1
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except Exception as e:
|
||||
print("FAIL: 快照非合法JSON:", text[:200])
|
||||
return 1
|
||||
ok = True
|
||||
for key in ("fc", "pmCmd", "links", "fcStatusCount", "pmControlCount", "logs"):
|
||||
ok = ok and key in data
|
||||
print(f"PASS: 快照 JSON 有效, 分组keys={sorted(data.keys())}")
|
||||
print(f" fc.fc_status={data['fc']['fc_status']}, fc.fc_mode={data['fc']['fc_mode']}, "
|
||||
f"heartbeat={data['fc']['heartbeat']}")
|
||||
print(f" links={json.dumps(data['links'], ensure_ascii=False)}")
|
||||
print(f" logs条数={len(data['logs'])}, fcStatusCount={data['fcStatusCount']}")
|
||||
ws.s.close()
|
||||
return 0 if ok else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "upmsg/uPlan_planStop.h"
|
||||
#include "upmsg/uDetect_xcVoltage_cmd.h"
|
||||
#include "upmsg/uDetect_zdpVoltage_cmd.h"
|
||||
#include "upmsg/uDevice_insData.h"
|
||||
|
||||
TEST(UpmsgTest, PowerSysStateParse)
|
||||
{
|
||||
@@ -105,3 +106,29 @@ TEST(UpmsgTest, ZdpVoltageRoundTrip)
|
||||
EXPECT_TRUE(cmd2.fromJsonString(json));
|
||||
EXPECT_EQ(cmd2.zdpVoltageAdd_int32, 2);
|
||||
}
|
||||
|
||||
TEST(UpmsgTest, InsDataParse)
|
||||
{
|
||||
uDevice_insDataMsg msg;
|
||||
InsDataState st;
|
||||
EXPECT_EQ(msg.parseMsg("uDevice_insData_st",
|
||||
R"({"headingAngle_int32":1200,"rollAngle_int32":-50,"pitchAngle_int32":30,"depth_int32":1500,
|
||||
"headingAngularVelocity_int32":10,"rollAngularVelocity_int32":-1,"longAngularVelocity_int32":2,
|
||||
"lon_int32":113123,"lat_int32":22123,"northSpeed_int16":12,"eastSpeed_int16":-3,"zSpeed_int16":1})",
|
||||
st), 1);
|
||||
EXPECT_EQ(st.headingAngle_int32, 1200);
|
||||
EXPECT_EQ(st.rollAngle_int32, -50);
|
||||
EXPECT_EQ(st.pitchAngle_int32, 30);
|
||||
EXPECT_EQ(st.depth_int32, 1500);
|
||||
EXPECT_EQ(st.headingAngularVelocity_int32, 10);
|
||||
EXPECT_EQ(st.northSpeed_int16, 12);
|
||||
// key 不匹配 → 0
|
||||
EXPECT_EQ(msg.parseMsg("wrong_key", R"({"headingAngle_int32":1})", st), 0);
|
||||
// 非法 JSON → -1
|
||||
EXPECT_EQ(msg.parseMsg("uDevice_insData_st", "{bad json", st), -1);
|
||||
// 缺字段 → 解析成功但保持缺省值
|
||||
InsDataState st2;
|
||||
EXPECT_EQ(msg.parseMsg("uDevice_insData_st", R"({"headingAngle_int32":99})", st2), 1);
|
||||
EXPECT_EQ(st2.headingAngle_int32, 99);
|
||||
EXPECT_EQ(st2.depth_int32, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user