From 54c32a180a3845a9765e821cf3fbabe0d7f345b9 Mon Sep 17 00:00:00 2001 From: zjk <1553836110@qq.com> Date: Wed, 26 Aug 2026 22:43:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=A4=8D=E5=90=88=E7=AE=A1?= =?UTF-8?q?=E6=8E=A7=E5=99=A8(pCCU)=20+=20=E6=83=AF=E5=AF=BC=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=A7=A3=E6=9E=90=20+=20=E8=AE=BE=E5=A4=87=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E9=A1=B5=E5=A2=9E=E5=BC=BA=20+=200824=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E6=96=87=E6=A1=A3=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 补充编译产物/临时文件忽略 --- .gitignore | 14 + docs/上位机下位机通信协议.md | 1240 ----------------- ...息中心-复合管控双锂电阶段协议20231006.docx | Bin 0 -> 53314 bytes docs/控制主机与复合管控器通信协议- 0824.xlsx | Bin 0 -> 28474 bytes docs/超滑FC和复合管控器通讯协议 - 0821.docx | Bin 0 -> 53265 bytes docs/锂电池协议20230324(1).docx | Bin 0 -> 43079 bytes missions/h100.moos | 26 +- scripts/clean-data.sh | 14 +- scripts/deploy.sh | 41 +- scripts/fetch-data.sh | 8 +- src/CMakeLists.txt | 1 + src/pCCU/CCU.cpp | 409 ++++++ src/pCCU/CCU.h | 79 ++ src/pCCU/CCU_Info.cpp | 102 ++ src/pCCU/CCU_Info.h | 15 + src/pCCU/CCU软件需求.txt | 7 + src/pCCU/CMakeLists.txt | 76 + src/pCCU/comm/FcLinkManager.h | 42 + src/pCCU/comm/LinkManager.cpp | 108 ++ src/pCCU/comm/LinkManager.h | 112 ++ src/pCCU/comm/PmLinkManager.h | 42 + src/pCCU/comm/UdpEndpoint.cpp | 57 + src/pCCU/comm/UdpEndpoint.h | 46 + src/pCCU/core/SnapshotBuilder.cpp | 380 +++++ src/pCCU/core/SnapshotBuilder.h | 38 + src/pCCU/core/SystemData.h | 118 ++ src/pCCU/main.cpp | 54 + src/pCCU/pCCU.moos | 39 + src/pCCU/protocol/ChecksumPolicy.h | 82 ++ src/pCCU/protocol/FcProtocol.cpp | 318 +++++ src/pCCU/protocol/FcProtocol.h | 174 +++ src/pCCU/protocol/FieldCodec.h | 58 + src/pCCU/protocol/Frame.cpp | 50 + src/pCCU/protocol/Frame.h | 54 + src/pCCU/protocol/Message.cpp | 22 + src/pCCU/protocol/Message.h | 59 + src/pCCU/protocol/MessageRegistry.cpp | 29 + src/pCCU/protocol/MessageRegistry.h | 42 + src/pCCU/protocol/PmProtocol.cpp | 662 +++++++++ src/pCCU/protocol/PmProtocol.h | 317 +++++ src/pCCU/store/DbStore.cpp | 178 +++ src/pCCU/store/DbStore.h | 70 + src/pCCU/web/WebServer.cpp | 132 ++ src/pCCU/web/WebServer.h | 61 + src/pCCU/web/pages/index.h | 490 +++++++ src/pPowerManger/LowerCommManager.cpp | 125 -- src/pPowerManger/LowerCommManager.h | 1 - src/pPowerManger/PowerManger.cpp | 2 + src/pPowerManger/PowerManger.h | 1 + src/pPowerManger/UpperCommManager.cpp | 10 + src/pPowerManger/UpperCommManager.h | 4 + src/pPowerManger/httpserver/httpserver.cpp | 15 + src/pPowerManger/upmsg/uDevice_insData.h | 86 ++ src/pPowerMangerHost/HostSim.cpp | 3 + src/pPowerMangerHost/web/index.html | 200 ++- test/CMakeLists.txt | 34 + test/disSysTest/README.md | 96 ++ test/disSysTest/app/__init__.py | 3 + test/disSysTest/app/__main__.py | 85 ++ test/disSysTest/app/app.py | 440 ++++++ test/disSysTest/app/ccuMsg.py | 455 ++++++ test/disSysTest/app/ccuWin.py | 209 +++ test/disSysTest/app/config.py | 126 ++ test/disSysTest/app/db.py | 78 ++ test/disSysTest/app/logger.py | 65 + test/disSysTest/app/panels.py | 440 ++++++ test/disSysTest/app/protocol.py | 374 +++++ test/disSysTest/app/simulator.py | 189 +++ test/disSysTest/app/udp_comm.py | 117 ++ test/disSysTest/build.py | 41 + test/disSysTest/requirements.txt | 4 + test/disSysTest/run.py | 18 + test/disSysTest/tests/test_protocol.py | 92 ++ test/pccu/pccuTest.cpp | 301 ++++ test/pccu/pccu_integration_test.py | 237 ++++ test/pccu/pccu_it.moos | 24 + test/pccu/pccu_sim.py | 265 ++++ test/pccu/pccu_ws_check.py | 96 ++ test/until/UpmsgTest.cpp | 27 + 79 files changed, 8731 insertions(+), 1398 deletions(-) delete mode 100644 docs/上位机下位机通信协议.md create mode 100644 docs/信息中心-复合管控双锂电阶段协议20231006.docx create mode 100644 docs/控制主机与复合管控器通信协议- 0824.xlsx create mode 100644 docs/超滑FC和复合管控器通讯协议 - 0821.docx create mode 100644 docs/锂电池协议20230324(1).docx create mode 100644 src/pCCU/CCU.cpp create mode 100644 src/pCCU/CCU.h create mode 100644 src/pCCU/CCU_Info.cpp create mode 100644 src/pCCU/CCU_Info.h create mode 100644 src/pCCU/CCU软件需求.txt create mode 100644 src/pCCU/CMakeLists.txt create mode 100644 src/pCCU/comm/FcLinkManager.h create mode 100644 src/pCCU/comm/LinkManager.cpp create mode 100644 src/pCCU/comm/LinkManager.h create mode 100644 src/pCCU/comm/PmLinkManager.h create mode 100644 src/pCCU/comm/UdpEndpoint.cpp create mode 100644 src/pCCU/comm/UdpEndpoint.h create mode 100644 src/pCCU/core/SnapshotBuilder.cpp create mode 100644 src/pCCU/core/SnapshotBuilder.h create mode 100644 src/pCCU/core/SystemData.h create mode 100644 src/pCCU/main.cpp create mode 100644 src/pCCU/pCCU.moos create mode 100644 src/pCCU/protocol/ChecksumPolicy.h create mode 100644 src/pCCU/protocol/FcProtocol.cpp create mode 100644 src/pCCU/protocol/FcProtocol.h create mode 100644 src/pCCU/protocol/FieldCodec.h create mode 100644 src/pCCU/protocol/Frame.cpp create mode 100644 src/pCCU/protocol/Frame.h create mode 100644 src/pCCU/protocol/Message.cpp create mode 100644 src/pCCU/protocol/Message.h create mode 100644 src/pCCU/protocol/MessageRegistry.cpp create mode 100644 src/pCCU/protocol/MessageRegistry.h create mode 100644 src/pCCU/protocol/PmProtocol.cpp create mode 100644 src/pCCU/protocol/PmProtocol.h create mode 100644 src/pCCU/store/DbStore.cpp create mode 100644 src/pCCU/store/DbStore.h create mode 100644 src/pCCU/web/WebServer.cpp create mode 100644 src/pCCU/web/WebServer.h create mode 100644 src/pCCU/web/pages/index.h create mode 100644 src/pPowerManger/upmsg/uDevice_insData.h create mode 100644 test/disSysTest/README.md create mode 100644 test/disSysTest/app/__init__.py create mode 100644 test/disSysTest/app/__main__.py create mode 100644 test/disSysTest/app/app.py create mode 100644 test/disSysTest/app/ccuMsg.py create mode 100644 test/disSysTest/app/ccuWin.py create mode 100644 test/disSysTest/app/config.py create mode 100644 test/disSysTest/app/db.py create mode 100644 test/disSysTest/app/logger.py create mode 100644 test/disSysTest/app/panels.py create mode 100644 test/disSysTest/app/protocol.py create mode 100644 test/disSysTest/app/simulator.py create mode 100644 test/disSysTest/app/udp_comm.py create mode 100644 test/disSysTest/build.py create mode 100644 test/disSysTest/requirements.txt create mode 100644 test/disSysTest/run.py create mode 100644 test/disSysTest/tests/test_protocol.py create mode 100644 test/pccu/pccuTest.cpp create mode 100644 test/pccu/pccu_integration_test.py create mode 100644 test/pccu/pccu_it.moos create mode 100644 test/pccu/pccu_sim.py create mode 100644 test/pccu/pccu_ws_check.py diff --git a/.gitignore b/.gitignore index bbb4681..80c0ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -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* diff --git a/docs/上位机下位机通信协议.md b/docs/上位机下位机通信协议.md deleted file mode 100644 index 44b1e65..0000000 --- a/docs/上位机下位机通信协议.md +++ /dev/null @@ -1,1240 +0,0 @@ -# H100PowerManger 上位机 / 下位机通信协议 - -> 本文档整理自 `src/pPowerManger` 源码,完整描述能源管理程序 `pPowerManger` 与 **上位机**(任务规划 / 决策 / 外部通信 / XC 阵 / ZDP 等 MOOS 应用)以及与 **下位机**(复合管控器 CCU、高压母线、高压汇流排 A、仪表母线、仪表汇流排等配电设备)之间的通信协议。 -> -> 主要源码依据: -> - 上位机:`UpperCommManager.h/.cpp`、`PowerManger.h/.cpp`、`driver.h/.cpp`、`upmsg/*.h` -> - 下位机:`LowerCommManager.h/.cpp`、`udpcomm/udpComm.h/.cpp`、`udpcomm/ccuUdpMsg.h`、`pmSysvariable.h` - -## 目录 - -1. [总体架构](#1-总体架构) -2. [上位机通信协议(MOOS + JSON)](#2-上位机通信协议moos--json) - - 2.1 通信方式与参数 - - 2.2 消息总览 - - 2.3 上位机 → pPowerManger(订阅消息) - - 2.4 pPowerManger → 上位机(发布消息) - - 2.5 设备表(DriverTable)字段 - - 2.6 次级配电(secdist)字段 - - 2.7 设备 ID 枚举 -3. [下位机通信协议(UDP 二进制)](#3-下位机通信协议udp-二进制) - - 3.1 通信方式与参数 - - 3.2 通用帧格式与校验 - - 3.3 消息 ID 汇总 - - 3.4 复合管控器(CCU)报文 - - 3.5 配电系统报文 -4. [附录:常量与枚举](#4-附录常量与枚举) - ---- - -## 1. 总体架构 - -``` - ┌─────────────────────────────────────────────────────┐ - │ 上 位 机 │ - │ 任务规划 / 决策 / 外部通信 / XC阵 / ZDP / 推进器 │ - └─────────────────────┬───────────────────────────────┘ - │ MOOS 消息(JSON 字符串) - │ MOOSDB ServerPort=9000 AppTick=4 - ┌─────────────────────▼───────────────────────────────┐ - │ pPowerManger(能源管理程序) │ - │ UpperCommManager ⇄ 状态机 FSM ⇄ LowerCommManager │ - └─────────────────────┬───────────────────────────────┘ - │ UDP 二进制帧(小端) - │ 本机 5001 ⇄ CCU 192.168.0.140:7000 - ┌─────────────────────▼───────────────────────────────┐ - │ 下 位 机 │ - │ 复合管控器 CCU / 高压母线 / 高压汇流排A / 仪表母线 / 仪表汇流排 │ - └─────────────────────────────────────────────────────┘ -``` - -- **上位机通信**:MOOS 变量承载 **JSON 字符串**,由 `UpperCommManager` 处理。订阅注册于 `PowerManger::registerVariables()`,发布通过 `PowerManger::sendNotification()`。 -- **下位机通信**:**UDP** 承载 **二进制结构体帧**(`#pragma pack(1)` 紧凑、小端),由 `LowerCommManager` + `udpComm` 处理。 -- 关键配置(`PowerManger.h`): - - ```c - #define IPORT 5001 // 本机 UDP 监听端口 - #define CCUPORT 7000 // CCU 目标端口 - #define CCUHOST "192.168.0.140" // CCU 目标地址 - #define MAX_UDP_PKT_SIZE 65535 // UDP 最大报文 - ``` - ---- - -## 2. 上位机通信协议(MOOS + JSON) - -### 2.1 通信方式与参数 - -| 项目 | 说明 | -|------|------| -| 传输载体 | MOOS 消息(`CMOOSMsg`),值为 `std::string` 型 JSON | -| 报文格式 | JSON(jsoncpp),键名带类型后缀,如 `xxx_uint8`、`xxx_fp32`、`xxx_array` | -| 运行频率 | `AppTick = 4`(状态机主循环 4Hz)、`CommsTick = 4` | -| 消息倾斜校验 | `OnNewMail` 中,消息时标与本地时差 > `m_skew`(默认 10s)则丢弃 | -| 周期发送 | `commLoop()` 每 1s 调用 `processPeriodicTasks()` 发布周期反馈 | - -### 2.2 消息总览 - -#### 2.2.1 订阅消息(上位机 → pPowerManger) - -| 序号 | MOOS 变量名 | 含义 | 触发动作 | -|------|-------------|------|----------| -| 1 | `uPower_sysState_cmd` | 系统主控指令 | `MasterCommandEvent` | -| 2 | `uPower_equState_cmd` | 设备控制指令(设备表) | `DevCmdExt`(逐设备) | -| 3 | `uDevice_secdistState_st` | 次级配电状态反馈 | 更新 `m_subDisSysState` | -| 4 | `uDevice_depth_st` | 深度(三路取均值) | 更新 `m_depth` | -| 5 | `uPlan_taskStart_st` | 任务开始 | `TaskStartEvent` | -| 6 | `uPlan_planStop_st` | 任务停止 | — | -| 7 | `uExternComm_setOpCondition_cmd` | 设置工况 | `MasterCommandEvent` | -| 8 | `uExternComm_setOpMode_cmd` | 设置模式 | `MasterCommandEvent` | -| 9 | `uExternComm_setPowerSystem_cmd` | 设置电源系统 | `MasterCommandEvent` | -| 10 | `uExternComm_setDeviceSwitch_cmd` | 单个设备开关 | `DevCmdExt` | -| 11 | `uDetect_xcVoltage_cmd` | XC 阵电压申请 | 舷侧声呐电压指令 | -| 12 | `uDetect_zdpVoltage_cmd` | ZDP 电压申请 | 低频声呐开关 | -| 13 | `uDevice_thruster_st` | 推进器状态 | 更新转速/使能 | - -> `uExternComm_setThrustSpeed_cmd`(字段 `rpm_int16`、`enable_int16`)已在 `upmsg` 中定义,但 `registerVariables()` 中已注释,当前**未订阅**。 - -#### 2.2.2 发布消息(pPowerManger → 上位机) - -| 序号 | MOOS 变量名 | 含义 | -|------|-------------|------| -| 1 | `uPower_pmState_fb` | 能源管理整体状态 | -| 2 | `uPower_fcsState_st` | 燃料电池系统状态 | -| 3 | `uPower_liBatState_st` | 锂电池状态(仪表 / 动力 / 应急) | -| 4 | `uPower_disSysState_st` | 配电系统状态(断路器位图 + 故障码) | -| 5 | `uPower_currentState_st` | 各母线电压 / 电流 | -| 6 | `uPower_equState_fb` | 设备状态反馈(设备表) | -| 7 | `uPower_secdistState_cmd` | 次级配电指令(UNIT1~5 设备开关) | -| 8 | `uPower_equInsState_fb` | 设备在线状态(位图) | -| 9 | `uPower_subDisFault_st` | (仅 `_DEBUG`)次级配电故障表 | - ---- - -### 2.3 上位机 → pPowerManger(订阅消息) - -#### 2.3.1 `uPower_sysState_cmd` — 系统主控指令 - -解析结果写入 `m_msCmd`(`MSControlCmd`),并触发 `MasterCommandEvent`。 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `powerCMD` | uint | 电源系统指令 | -| `modCMD` | uint | 模式指令 | -| `workCMD` | uint | 工况指令 | - -示例: - -```json -{"powerCMD":1,"modCMD":2,"workCMD":3} -``` - -#### 2.3.2 `uPower_equState_cmd` — 设备控制指令(设备表) - -解析 `DriverTable`,逐条派发 `DevCmdExt{id, cmd}`(`id` 为设备 ID,见 2.7)。字段名 → 设备 ID 映射见 `Driver::loadDriverIdFromJson()`。 - -JSON 字段为 `<设备名>_uint8`,取值含义见 `EquipmentCommand`(0=断电,1=上电,2=仪表电上电,3=动力电上电,4=仪表电断电,5=动力电断电)。 - -示例: - -```json -{"thruster_uint8":1,"depth1_uint8":1,"buoyage_uint8":0} -``` - -> 完整字段清单见 [2.5 设备表字段](#25-设备表drivertable字段)。 - -#### 2.3.3 `uDevice_secdistState_st` — 次级配电状态反馈 - -解析到 `m_subDisSysState`(`DriverTable`)。字段为 `<设备名>_uint8`,按 UNIT1~5 分组。 - -> 完整字段清单见 [2.6 次级配电字段](#26-次级配电secdist字段)。 - -#### 2.3.4 `uDevice_depth_st` — 深度 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `value_array_int32` | int[] | 深度数组,取前 3 个元素平均值 | - -示例: - -```json -{"value_array_int32":[1200,1210,1190]} -``` - -#### 2.3.5 `uPlan_taskStart_st` — 任务开始 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `taskType_str` | string | 任务类型(见 `TaskType::fromString`) | -| `balanceEnabled_bool` | bool | 使能浮力调节 | -| `bowRudderUnfold_bool` | bool | 艏舵展开 | - -示例: - -```json -{"taskType_str":"sail","balanceEnabled_bool":true,"bowRudderUnfold_bool":false} -``` - -#### 2.3.6 `uPlan_planStop_st` — 任务停止 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `op_int32` | int | 1 = 停止 | - -示例: - -```json -{"op_int32":1} -``` - -#### 2.3.7 `uExternComm_setOpCondition_cmd` — 设置工况 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `state_uint8` | uint | 工况值(见 `WorkCondition`) | - -#### 2.3.8 `uExternComm_setOpMode_cmd` — 设置模式 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `state_uint8` | uint | 模式值(见 `RunMode`) | - -#### 2.3.9 `uExternComm_setPowerSystem_cmd` — 设置电源系统 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `powerCmd_uint8` | uint | 电源系统指令 | - -#### 2.3.10 `uExternComm_setDeviceSwitch_cmd` — 单个设备开关 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `id_uint8` | uint | 设备 ID(见 2.7) | -| `cmd_uint8` | uint | 开关指令(`EquipmentCommand`) | - -示例: - -```json -{"id_uint8":53,"cmd_uint8":1} -``` - -#### 2.3.11 `uDetect_xcVoltage_cmd` — XC 阵电压申请 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `xcLowVoltageAdd_int32` | int | 低压申请(0=无命令,1=申请仪表上电,2=申请仪表断电) | -| `xcHighVoltageAdd_int32` | int | 高压申请(0=无命令,1=申请高压上电,2=申请高压断电) | - -#### 2.3.12 `uDetect_zdpVoltage_cmd` — ZDP 电压申请 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `zdpVoltageAdd_int32` | int | 电压申请(同 XC 定义) | - -#### 2.3.13 `uDevice_thruster_st` — 推进器状态 - -| 字段 | 类型 | 单位 | 说明 | -|------|------|------|------| -| `rpm_int16` | int16 | rpm | 实际转速 | -| `voltage_uint16` | uint16 | V | 电压 | -| `current_uint16` | uint16 | A | 电流 | -| `ctrlTemp_uint16` | uint16 | ℃ | 控制器温度 | -| `motorTemp_uint16` | uint16 | ℃ | 电机温度 | -| `motorTorque_uint16` | uint16 | N·m | 电机扭矩 | -| `maxTorque_uint16` | uint16 | N·m | 电机最大扭矩(0=未使能) | -| `error_int64` | int64 | — | 错误码 | - ---- - -### 2.4 pPowerManger → 上位机(发布消息) - -#### 2.4.1 `uPower_pmState_fb` — 能源管理整体状态 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `workCondition_uint8` | uint8 | 当前工况(`WorkCondition`) | -| `currentMod_uint8` | uint8 | 当前模式 | -| `powerState_uint8` | uint8 | 电源状态 | -| `faultLevel_uint8` | uint8 | 故障等级(取 `getFaultCodeLevel()`) | -| `faultCode_array` | uint[] | 故障码数组 | -| `soc_uint16` | uint16 | 荷电状态 SOC | -| `sustainableTime_int64` | int64 | 可持续时间 | -| `leakage_uint8` | uint8 | 漏水标志(`generateLeakageCode()`) | -| `info_str` | string | 系统信息描述 | - -#### 2.4.2 `uPower_fcsState_st` — 燃料电池系统状态 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `backpressure1_fp32` | float | 背压 1 | -| `backpressure2_fp32` | float | 背压 2 | -| `ch4oConcent1_fp32` | float | 甲醇浓度 1 | -| `ch4oConcent2_fp32` | float | 甲醇浓度 2 | -| `ch4o_fp32` | float | 备用 | -| `excTemp_fp32` | float | 钯膜温度 | -| `fcuCurIns_fp32` | float | 备用 | -| `fcuCurPower_fp32` | float | 备用 | -| `fcuCur_fp32` | float | 备用 | -| `fcuFaultLeve_uint8` | uint8 | 故障等级 | -| `fcuOutIns_fp32` | float | 备用 | -| `fcuOutPower_fp32` | float | 备用 | -| `fcuState_uint8` | uint8 | 燃料电池状态(`FcuState`) | -| `fcuVolIns_fp32` | float | 备用 | -| `fcuVolPower_fp32` | float | 备用 | -| `fcuVol_fp32` | float | 备用 | -| `h2Concent1_fp32` | float | H2 浓度 1 | -| `h2Concent2_fp32` | float | H2 浓度 2 | -| `o2Concent1_fp32` | float | O2 浓度 1 | -| `o2Concent2_fp32` | float | O2 浓度 2 | -| `o2Level_fp32` | float | 备用 | -| `o2_fp32` | float | 备用 | -| `powLimit_fp32` | float | 输出功率限制 | -| `powerGen_fp32` | float | 累计发电功率 | -| `flameDet_uint8` | uint8 | 火焰探测器状态 | -| `faultCode1_uint16` | uint16 | 一级故障字 | -| `faultCode2_uint16` | uint16 | 二级故障字 | -| `faultCode3_uint16` | uint16 | 三级故障字 | -| `faultCode4_uint16` | uint16 | 四级故障字 | -| `fcCommState_uint8` | uint8 | 通信状态(当前固定为 0) | - -#### 2.4.3 `uPower_liBatState_st` — 锂电池状态 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `insBatCurElect_uint16` | uint16 | 仪表电池电流电量 | -| `insBatLimit_uint16` | uint16 | 仪表电池限功率 | -| `insBatSoc_uint8` | uint8 | 仪表电池 SOC | -| `insBatTotalElect_uint16` | uint16 | 仪表电池总能量 | -| `insChargeState_uint8` | uint8 | 仪表电池充放电状态 | -| `insCurSocHold1_uint8` | uint8 | 备用 | -| `insCurSocHold2_uint8` | uint8 | 备用 | -| `insCurSocHold3_uint8` | uint8 | 备用 | -| `insCurrent_uint16` | uint16 | 仪表电池电流 | -| `insLinkVol_uint16` | uint16 | 仪表电池 LINK 电压 | -| `insNegRelayState_uint8` | uint8 | 仪表电池负极继电器状态(固定 0) | -| `insNegRes_uint16` | uint16 | 仪表电池负极绝缘电阻 | -| `insPackVol_uint16` | uint16 | 仪表电池 PACK 电压 | -| `insPosRelayState_uint8` | uint8 | 仪表电池正极继电器状态(固定 0) | -| `insPosRes_uint16` | uint16 | 仪表电池正极绝缘电阻 | -| `powBatCurElect_uint16` | uint16 | 动力电池电流电量(固定 0) | -| `powBatSoc_uint8` | uint8 | 动力电池 SOC | -| `powBatTotalElect_uint16` | uint16 | 动力电池总能量 | -| `powChargeState_uint8` | uint8 | 动力电池充放电状态 | -| `powCurSocHold1_uint8` | uint8 | 备用 | -| `powCurSocHold2_uint8` | uint8 | 备用 | -| `powCurSocHold3_uint8` | uint8 | 备用 | -| `powCurToInsLimit1_uint8` | uint8 | 动力→仪表限功率 1 | -| `powCurToInsLimit2_uint8` | uint8 | 动力→仪表限功率 2 | -| `powCur_uint16` | uint16 | 动力电池电流 | -| `powLinkVol_uint16` | uint16 | 动力电池 LINK 电压 | -| `powNegRelayState_uint8` | uint8 | 动力电池负极继电器状态(固定 0) | -| `powNegRes_uint16` | uint16 | 动力电池负极绝缘电阻 | -| `powPosRelayState_uint8` | uint8 | 动力电池正极继电器状态(固定 0) | -| `powPosRes_uint16` | uint16 | 动力电池正极绝缘电阻 | -| `powerBatLimit_uint16` | uint16 | 备用 | -| `powerPackVol_uint16` | uint16 | 动力电池 PACK 电压 | -| `powFaultCode_uint8` | uint8 | 动力电池故障码 | -| `insFaultCode_uint8` | uint8 | 仪表电池故障码 | -| `insCommState_uint8` | uint8 | 仪表电池通信状态(固定 0) | -| `powCommState_uint8` | uint8 | 动力电池通信状态(固定 0) | -| `emergencyBattery1Voltage_int16` | int16 | 应急电池 1 电压 | -| `emergencyBattery1Current_int16` | int16 | 应急电池 1 电流 | -| `emergencyBattery1MaxTemp_int16` | int16 | 应急电池 1 最高温度 | -| `emergencyBattery1FaultWord_uint16` | uint16 | 应急电池 1 故障字 | -| `emergencyBattery2Voltage_int16` | int16 | 应急电池 2 电压 | -| `emergencyBattery2Current_int16` | int16 | 应急电池 2 电流 | -| `emergencyBattery2MaxTemp_int16` | int16 | 应急电池 2 最高温度 | -| `emergencyBattery2FaultWord_uint16` | uint16 | 应急电池 2 故障字 | - -#### 2.4.4 `uPower_disSysState_st` — 配电系统状态 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `HVolABusBreaker_uint16` | uint16 | 高压母线(BUS1)断路器位图 | -| `HVolBBusBreaker_uint16` | uint16 | 高压汇流排 A(BUS2)断路器位图 | -| `HVolBusBreaker_uint16` | uint16 | 仪表母线(BUS3)断路器位图 | -| `LVolBusBreaker_uint16` | uint16 | 仪表汇流排(BUS4)断路器位图 | -| `faultCode_array` | uint[] | 配电故障码数组 | - -位图定义(bit0 起始): - -| 位 | HVolABusBreaker | HVolBBusBreaker | HVolBusBreaker | LVolBusBreaker | -|----|-----------------|-----------------|----------------|----------------| -| bit0 | 燃料电池断路器 | 艏部FT装置1-3 | 锂电池组(仪表)断路器 | UNIT1断路器 | -| bit1 | 动力锂电池断路器 | 启闭机构断路器 | 艏部低压配电箱断路器 | UNIT2断路器 | -| bit2 | 推进电机断路器 | 桅杆舵机控制箱断路器 | UNIT4仪表DC48V断路器 | UNIT3断路器 | -| bit3 | 锂电池组仪表断路器 | XCZ断路器 | DC-C1直流配电板断路器 | UNIT5断路器 | -| bit4 | DC/DC5模块断路器 | 艏舵控制箱断路器 | 预留断路器1 | 艏部PZ装置断路器 | -| bit5 | 艏部高压配电箱断路器 | 敞水盖启动电缸断路器 | 预留断路器2 | 预留断路器1 | -| bit6 | 艉部FT装置4/5断路器 | — | 应急锂电池组2断路器 | 预留断路器2 | -| bit7 | 艉舵开关1断路器 | — | — | 应急锂电池组1断路器 | -| bit8 | 艉舵开关2断路器 | — | — | — | -| bit9 | FB预留断路器 | — | — | — | -| bit10 | TYZ预留断路器 | — | — | — | -| bit11 | 预留断路器 | — | — | — | - -#### 2.4.5 `uPower_currentState_st` — 各母线电压/电流 - -**高压母线(HVB,BUS1)** - -| 字段 | 类型 | 来源 | -|------|------|------| -| `HVBpowerBusVol_uint16` | uint16 | 动力汇流排电压 | -| `HVBdcDc5Cur_uint16` | uint16 | DC/DC5 模块电流 | -| `HVBpowerBusCur_uint16` | uint16 | 动力汇流排电流 | -| `HVBbusAVol_uint16` | uint16 | A 汇流排电压 | -| `HVBmotorCur_uint16` | uint16 | 推进电机控制箱电流 | -| `HVBbusACur_uint16` | uint16 | A 汇流排电流 | -| `HVBbatGroupCur_uint16` | uint16 | 锂电池组(仪表)电流 | -| `HVBbowHVolDisBoxCur_uint16` | uint16 | 艏部高压配电箱电流 | -| `HVBFTDev45Cur_uint16` | uint16 | 艉部FT装置4/5电流 | -| `HVBtyzCur_uint16` | uint16 | TYZ预留开关电流 | -| `HVBsternRud1Cur_uint16` | uint16 | 艉舵开关1电流 | -| `HVBreservedCur1_uint16` | uint16 | 预留电流1 | -| `HVBsternRud2Cur_uint16` | uint16 | 艉舵开关2电流 | -| `HVBreservedCur2_uint16` | uint16 | 预留电流2 | -| `HVBfbCur_uint16` | uint16 | FB预留电流 | -| `HVBreservedCur3_uint16` | uint16 | 预留电流3 | - -**高压汇流排 A(HVAB,BUS2)** - -| 字段 | 类型 | 来源 | -|------|------|------| -| `HVABbusBVol_uint16` | uint16 | 汇流排 B 电压 | -| `HVABbusBCur_uint16` | uint16 | 汇流排 B 电流 | -| `HVABFTDev123Cur_uint16` | uint16 | 艏部FT装置1/2/3电流 | -| `HVABactCur_uint16` | uint16 | 启闭机构电流 | -| `HVABmastSteGeaCur_uint16` | uint16 | 桅杆舵机控制箱电流 | -| `HVABxczCur_uint16` | uint16 | XCZ电流 | -| `HVABbowRudCur_uint16` | uint16 | 艏舵控制箱电流 | -| `HVABopenWaterCur_uint16` | uint16 | 敞水盖启动电缸电流 | - -**仪表母线(HVBB,BUS3)** - -| 字段 | 类型 | 来源 | -|------|------|------| -| `HVBBinsBusVol_uint16` | uint16 | 仪表母排电压 | -| `HVBBbowLowVolDisBoxCur_uint16` | uint16 | 艏部低压配电箱电流 | -| `HVBBdcC1InsCur_uint16` | uint16 | DC-C1 仪表DC48V电流 | -| `HVBBreservedCur1_uint16` | uint16 | 预留电流1 | -| `HVBBemerBatCur_uint16` | uint16 | 应急锂电池组2电流 | -| `HVBBbatInsCur_uint16` | uint16 | 锂电池组(仪表)电流 | -| `HVBBunit4Cur_uint16` | uint16 | UNIT4 仪表DC48V电流 | -| `HVBBreservedCur2_uint16` | uint16 | 预留电流2 | -| `HVBBemer2BusVol_uint16` | uint16 | 应急2汇流排电压 | -| `HVBBCCUCur_uint16` | uint16 | 复合能源管控系统应急DC48V电流 | -| `HVBBFCSSecurSysCur_uint16` | uint16 | 燃料电池安全系统应急DC48V电流 | -| `HVBBplcCur_uint16` | uint16 | PLC控制电源电流 | - -**仪表汇流排(LVB,BUS4)** - -| 字段 | 类型 | 来源 | -|------|------|------| -| `LVBinsBusVol_uint16` | uint16 | 仪表母排电压 | -| `LVBBinsBusCur_uint16` | uint16 | 仪表母排电流 | -| `LVBBunit1Cur_uint16` | uint16 | UNIT1 电流 | -| `LVBBunit2Cur_uint16` | uint16 | UNIT2 电流 | -| `LVBBunit3Cur_uint16` | uint16 | UNIT3 电流 | -| `LVBBunit5Cur_uint16` | uint16 | UNIT5 电流 | -| `LVBBbowPZDevCur_uint16` | uint16 | 艏部PZ装置电流 | -| `LVBBemerBatCur_uint16` | uint16 | 应急锂电池组1电流 | -| `LVBBemerBusVol_uint16` | uint16 | 应急1汇流排电压 | - -#### 2.4.6 `uPower_equState_fb` — 设备状态反馈 - -设备表 JSON(字段同 2.5),反映当前设备状态。 - -#### 2.4.7 `uPower_secdistState_cmd` — 次级配电指令 - -次级配电 JSON(字段同 2.6),向次级配电系统下发 UNIT1~5 设备开关指令。 - -#### 2.4.8 `uPower_equInsState_fb` — 设备在线状态(位图) - -| 字段 | 类型 | 说明 | -|------|------|------| -| `sensor_int16` | uint16 | 传感器状态位图 | -| `actuator_int16` | uint16 | 执行机构状态位图 | -| `motion_int16` | uint16 | 运动状态位图 | -| `loadcomm_int16` | uint16 | 负载通信状态位图 | -| `throwpower_int16` | uint16 | 抛载/电源状态位图 | - -位图定义: - -| 位 | sensor_int16 | actuator_int16 | motion_int16 | loadcomm_int16 | throwpower_int16 | -|----|--------------|----------------|--------------|----------------|------------------| -| bit0 | 单波束声呐1 | 启闭机构1 | 艉部舵机1 | 载荷发控 | 艏部抛载 | -| bit1 | 单波束声呐2 | 启闭机构2 | 艉部舵机2 | 光电桅杆 | 保留 | -| bit2 | 单波束声呐3 | 启闭机构3 | 艉部舵机3 | 电磁桅杆 | 保留 | -| bit3 | 单波束声呐4 | 启闭机构4 | 艉部舵机4 | 保留 | 保留 | -| bit4 | 单波束声呐5 | 启闭机构5 | 艏舵电缸 | 舷侧声纳 | 保留 | -| bit5 | 单波束声呐6 | 启闭机构6 | 注水机构 | 拖曳声纳 | 保留 | -| bit6 | 单波束声呐7 | 保留 | 推进电机 | 保留 | 保留 | -| bit7 | 单波束声呐8 | 保留 | 浮调机构1 | 保留 | 保留 | -| bit8 | 前视声呐 | 敞水机构1 | 浮调机构2 | 通信桅杆 | 燃料电池控制器 | -| bit9 | CTD1 | 敞水机构2 | 浮调机构3 | 中低频声呐 | 仪表配电板 | -| bit10 | CTD2 | 敞水机构3 | 浮调机构4 | 高频声通机 | 动力配电板 | -| bit11 | 声速仪 | 敞水机构4 | 浮调机构5 | 浮标系统 | 仪表锂电池 | -| bit12 | NTP | 敞水机构5 | DVL | 通信桅杆舵机 | 动力锂电 | -| bit13 | 深度计1 | 敞水机构6 | 惯导系统 | 光电桅杆舵机 | 艉部抛载 | -| bit14 | 深度计2 | 敞水机构7 | 艏舵左舵机 | 电磁桅杆舵机 | 保留 | -| bit15 | 深度计3 | 敞水机构8 | 艏右舵舵机 | 保留 | 保留 | - ---- - -### 2.5 设备表(DriverTable)字段 - -以下字段用于 `uPower_equState_cmd` 与 `uPower_equState_fb`,全部为 `<设备名>_uint8`: - -| 序号 | 字段 | 对应设备 ID | -|------|------|-------------| -| 1 | `depth1_uint8` | 18 | -| 2 | `depth2_uint8` | 19 | -| 3 | `depth3_uint8` | 20 | -| 4 | `overDeep1_uint8` | 21 | -| 5 | `overDeep2_uint8` | 22 | -| 6 | `ctd1_uint8` | 23 | -| 7 | `ctd2_uint8` | 24 | -| 8 | `soundVeloc_uint8` | 25 | -| 9 | `commDev_uint8` | 40 | -| 10 | `ins_uint8` | 57 | -| 11 | `ntp_uint8` | 58 | -| 12 | `inputWaterSensor1_uint8` | 59 | -| 13 | `inputWaterSensor2_uint8` | 60 | -| 14 | `inputWaterSensor3_uint8` | 61 | -| 15 | `singleBeamSonar1_uint8` | 0 | -| 16 | `singleBeamSonar2_uint8` | 1 | -| 17 | `singleBeamSonar3_uint8` | 2 | -| 18 | `singleBeamSonar4_uint8` | 3 | -| 19 | `singleBeamSonar5_uint8` | 4 | -| 20 | `singleBeamSonar6_uint8` | 5 | -| 21 | `singleBeamSonar7_uint8` | 6 | -| 22 | `singleBeamSonar8_uint8` | 7 | -| 23 | `forwardSonar_uint8` | 8 | -| 24 | `DVL_uint8` | 43 | -| 25 | `lowFreqSonar_uint8` | 36 | -| 26 | `USBL_uint8` | 42 | -| 27 | `bowCover1_uint8` | 9 | -| 28 | `bowCover2_uint8` | 10 | -| 29 | `bowCover3_uint8` | 11 | -| 30 | `bowCover4_uint8` | 12 | -| 31 | `bowCover5_uint8` | 13 | -| 32 | `bowCover6_uint8` | 14 | -| 33 | `bowRudderDeploy_uint8` | 15 | -| 34 | `bowRudderServo1_uint8` | 16 | -| 35 | `bowRudderServo2_uint8` | 17 | -| 36 | `bowWaterInet1_uint8` | 27 | -| 37 | `bowWaterInet2_uint8` | 28 | -| 38 | `bowWaterInet3_uint8` | 29 | -| 39 | `bowWaterInet4_uint8` | 30 | -| 40 | `bowWaterInet5_uint8` | 31 | -| 41 | `bowWaterInet6_uint8` | 32 | -| 42 | `bowWaterInet7_uint8` | 33 | -| 43 | `bowWaterInet8_uint8` | 34 | -| 44 | `waterInject_uint8` | 26 | -| 45 | `mastLiftingServo_uint8` | 41 | -| 46 | `buoyancyAdjust1_uint8` | 44 | -| 47 | `buoyancyAdjust2_uint8` | 45 | -| 48 | `buoyancyAdjust3_uint8` | 46 | -| 49 | `buoyancyAdjust4_uint8` | 47 | -| 50 | `buoyancyAdjust5_uint8` | 48 | -| 51 | `sternRudderServo1_uint8` | 49 | -| 52 | `sternRudderServo2_uint8` | 50 | -| 53 | `sternRudderServo3_uint8` | 51 | -| 54 | `sternRudderServo4_uint8` | 52 | -| 55 | `thruster_uint8` | 53 | -| 56 | `bowThrow_uint8` | 54 | -| 57 | `sternThrow_uint8` | 55 | -| 58 | `photoSensor_uint8` | 38 | -| 59 | `electSensor_uint8` | 39 | -| 60 | `launchControler_uint8` | 37 | -| 61 | `sideSonar_uint8` | 35 | -| 62 | `buoyage_uint8` | 56 | - ---- - -### 2.6 次级配电(secdist)字段 - -以下字段用于 `uPower_secdistState_cmd` 与 `uDevice_secdistState_st`,全部为 `<设备名>_uint8`: - -**UNIT1(19 项)** - -| 字段 | 字段 | 字段 | -|------|------|------| -| `bowCover1_uint8` | `bowCover2_uint8` | `bowCover3_uint8` | -| `bowCover4_uint8` | `bowCover5_uint8` | `bowCover6_uint8` | -| `singleBeamSonar1_uint8` | `singleBeamSonar2_uint8` | `singleBeamSonar3_uint8` | -| `singleBeamSonar4_uint8` | `singleBeamSonar5_uint8` | `singleBeamSonar6_uint8` | -| `forwardSonar_uint8` | `depth3_uint8` | `soundVeloc_uint8` | -| `bowRudderServo_uint8` | `bowRudderDeploy_uint8` | `ctd1_uint8` | -| `waterInject_uint8` | | | - -**UNIT2(11 项)** - -| 字段 | 字段 | 字段 | -|------|------|------| -| `sideSonar_uint8` | `bowWaterInet1_uint8` | `bowWaterInet2_uint8` | -| `bowWaterInet3_uint8` | `bowWaterInet4_uint8` | `bowWaterInet5_uint8` | -| `bowWaterInet6_uint8` | `bowWaterInet7_uint8` | `bowWaterInet8_uint8` | -| `launchControler_uint8` | `electSensor_uint8` | `lowFreqSonar_uint8` | - -**UNIT3(11 项)** - -| 字段 | 字段 | 字段 | -|------|------|------| -| `buoyancyAdjust1_uint8` | `buoyancyAdjust2_uint8` | `buoyancyAdjust3_uint8` | -| `USBL_uint8` | `depth1_uint8` | `commDev_uint8` | -| `mastLiftingServo_uint8` | `DVL_uint8` | `ADCP_uint8` | -| `inOutLetWater1_uint8` | `inOutLetWater2_uint8` | | - -**UNIT4(16 项)** - -| 字段 | 字段 | 字段 | -|------|------|------| -| `singleBeamSonar7_uint8` | `singleBeamSonar8_uint8` | `buoyage_uint8` | -| `bowThrow_uint8` | `depth2_uint8` | `thruster_uint8` | -| `dragSonar_uint8` | `haulingMachine_uint8` | `buoyancyAdjust4_uint8` | -| `buoyancyAdjust5_uint8` | `sternRudderServo1_uint8` | `sternRudderServo2_uint8` | -| `sternRudderServo3_uint8` | `sternRudderServo4_uint8` | `ctd2_uint8` | -| `commDevStern_uint8` | `inOutLetWater3_uint8` | | - -**UNIT5(4 项)** - -| 字段 | 字段 | -|------|------| -| `secINS_uint8` | `photoSensor_uint8` | -| `sternThrow_uint8` | `mainINS_uint8` | - ---- - -### 2.7 设备 ID 枚举(DeviceId) - -| ID | 名称 | ID | 名称 | ID | 名称 | -|----|------|----|------|----|------| -| 0 | singleBeamSonar1 | 21 | overDeep1 | 42 | USBL | -| 1 | singleBeamSonar2 | 22 | overDeep2 | 43 | DVL | -| 2 | singleBeamSonar3 | 23 | ctd1 | 44 | buoyancyAdjust1 | -| 3 | singleBeamSonar4 | 24 | ctd2 | 45 | buoyancyAdjust2 | -| 4 | singleBeamSonar5 | 25 | soundVeloc | 46 | buoyancyAdjust3 | -| 5 | singleBeamSonar6 | 26 | waterInject | 47 | buoyancyAdjust4 | -| 6 | singleBeamSonar7 | 27 | bowWaterInet1 | 48 | buoyancyAdjust5 | -| 7 | singleBeamSonar8 | 28 | bowWaterInet2 | 49 | sternRudderServo1 | -| 8 | forwardSonar | 29 | bowWaterInet3 | 50 | sternRudderServo2 | -| 9 | bowCover1 | 30 | bowWaterInet4 | 51 | sternRudderServo3 | -| 10 | bowCover2 | 31 | bowWaterInet5 | 52 | sternRudderServo4 | -| 11 | bowCover3 | 32 | bowWaterInet6 | 53 | thruster | -| 12 | bowCover4 | 33 | bowWaterInet7 | 54 | bowThrow | -| 13 | bowCover5 | 34 | bowWaterInet8 | 55 | sternThrow | -| 14 | bowCover6 | 35 | sideSonar | 56 | buoyage | -| 15 | bowRudderDeploy | 36 | lowFreqSonar | 57 | ins | -| 16 | bowRudderServo1 | 37 | launchControler | 58 | ntp | -| 17 | bowRudderServo2 | 38 | photoSensor | 59 | inputWaterSensor1 | -| 18 | depth1 | 39 | electSensor | 60 | inputWaterSensor2 | -| 19 | depth2 | 40 | commMast | 61 | inputWaterSensor3 | -| 20 | depth3 | 41 | mastLiftingServo | — | — | - ---- - -## 3. 下位机通信协议(UDP 二进制) - -### 3.1 通信方式与参数 - -| 项目 | 说明 | -|------|------| -| 传输 | UDP 单播 | -| 本机地址 | 绑定端口 `5001` | -| 下位机地址 | `192.168.0.140:7000` | -| 字节序 | 小端(little-endian) | -| 对齐 | `#pragma pack(1)` 紧凑,无填充 | -| 监听 | `LowerCommManager::listenThreadFunc()` 独立线程接收 | -| 分拣 | `udpComm::pushMsgToQueue()` 按消息 ID 入队,队列上限 `QUENUE_SIZE = 100` | -| 周期发送 | `commLoop()` 每 1s:`sendCcuCommand`(CCU 操作指令)+ `processPeriodicTasks` | - -### 3.2 通用帧格式与校验 - -所有报文统一为:**消息头(6B)+ 数据区 + 校验字段**。以下帧结构表中,`偏移(字节)` 均为相对整帧起始的绝对偏移。 - -#### 3.2.1 消息头 `m_header`(6 字节) - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 1 | -| 1 | start2 | u8 | 起始符 2 | -| 2~3 | id | u16 | 消息标识(小端) | -| 4~5 | length | u16 | 数据区长度 | - -- 起始符分类: - - CCU 报文:`start1 = start2 = 0x20` - - 配电报文:`start1 = start2 = 0x40` - -#### 3.2.2 日期 `m_date`(8 字节,仅 CCU 操作指令携带) - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | year | u16 | 年 | -| 2 | month | u8 | 月 | -| 3 | day | u8 | 日 | -| 4 | hour | u8 | 时 | -| 5 | minute | u8 | 分 | -| 6 | second | u8 | 秒 | -| 7 | milliseconds | u8 | 毫秒(每 10ms 一档) | - -#### 3.2.3 校验字段宽度 - -| 报文类别 | 校验字段类型 | 宽度 | -|----------|--------------|------| -| CCU 报文 | `Checksum`(unsigned int) | 4 字节 | -| 配电报文 | `cChecksum`(unsigned char) | 1 字节 | - -#### 3.2.4 校验算法 - -``` -校验和 = 从 start1 起至校验字段之前的所有字节累加求和(不含校验字段本身) -``` - -- CCU 报文:校验字段 4 字节,实际发送 / 比较取累加和**低 8 位**(`cc & 0xFF`)。 -- 配电报文:校验字段 1 字节,`sum = 累加和 & 0xFF`。 -- 接收端校验不符则丢弃该帧并记录 `Check error`。 - -```c -Checksum calculateChecksum(const unsigned char* data, int size) { - Checksum checksum = 0; - int size_data = size - sizeof(Checksum); // 去掉末尾校验字段 - for (int i = 0; i < size_data; i++) checksum += data[i]; - return checksum; -} -``` - -### 3.3 消息 ID 汇总 - -| 分类 | ID | 方向 | 报文结构体 | 说明 | -|------|-----|------|------------|------| -| CCU | `0x0040` | 发送 | `msg_CcuColCmdMsg` | 复合管控器操作指令 | -| CCU | `0x0041` | 发送 | `msg_CcuSetParmMsg` | 复合管控器参数设定指令 | -| CCU | `0x0051` | 接收 | `msg_CcuStateFbMsg` | 复合管控器状态反馈 | -| CCU | `0x0040` | 接收 | `msg_CcuSetParmFbMsg` | 复合管控器参数设定反馈(与操作指令 ID 复用) | -| 配电 | `0x0001` | 发送 | `msg_disHVBCmd` | 高压母线配电指令 | -| 配电 | `0x0002` | 发送 | `msg_disHVBACmd` | 高压汇流排A配电指令 | -| 配电 | `0x0003` | 发送 | `msg_disHVBBCmd` | 仪表母线配电指令 | -| 配电 | `0x0004` | 发送 | `msg_disLVBCmd` | 仪表汇流排配电指令 | -| 配电 | `0x0005` | 接收 | `msg_disHighVolBusFbMsg` | 高压母线配电反馈 | -| 配电 | `0x0006` | 接收 | `msg_disHighAVolBusFbMsg` | 高压汇流排A配电反馈 | -| 配电 | `0x0007` | 接收 | `msg_disLowMainBusFbMsg` | 仪表母线配电反馈 | -| 配电 | `0x0008` | 接收 | `msg_disLowBusFbMsg` | 仪表汇流排配电反馈 | - -> 说明:`CCU_SET_PARM_FB_ID` 与 `CCU_CMD_ID` 同为 `0x0040`。接收端以起始符 `0x20 0x20` 判定为 CCU 报文后,将 `0x0040` 一律按“参数设定反馈”处理(操作指令只发送不接收,实际不冲突)。 - -### 3.4 复合管控器(CCU)报文 - -#### 3.4.1 操作指令 `msg_CcuColCmdMsg`(0x0040,发送) - -总长度 36 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x20 | -| 1 | start2 | u8 | 起始符 0x20 | -| 2~3 | id | u16 | 消息标识 0x0040 | -| 4~5 | length | u16 | 数据区长度(=18) | -| 6~7 | year | u16 | 年 | -| 8 | month | u8 | 月 | -| 9 | day | u8 | 日 | -| 10 | hour | u8 | 时 | -| 11 | minute | u8 | 分 | -| 12 | second | u8 | 秒 | -| 13 | milliseconds | u8 | 毫秒(每 10ms 一档) | -| 14 | fcmode | u8 | 燃料电池运行模式 | -| 15 | fcuCmd | u8 | 燃料电池控制指令(`FcuCommand`) | -| 16 | fcuPowerEfficiency | u8 | 燃料电池输出功率(默认 8kW) | -| 17 | insChargeEnable | u8 | 仪表电池充电使能(0x30=不允许充电) | -| 18 | insBatCmd | u8 | 仪表电池指令(`BatteryCommand`) | -| 19 | powerBatCmd | u8 | 动力电池指令 | -| 20~21 | powerBatEfficiency | u16 | 动力电池效率(0=最大) | -| 22 | heartbeat | u8 | 心跳计数(周期自增) | -| 23 | masterState | u8 | 主站状态(0xAA=运行中) | -| 24~27 | paceholder1 | u32 | 预留 | -| 28~31 | paceholder2 | u32 | 预留 | -| 32~35 | checkCode | u32 | 校验和(累加和低 8 位) | - -#### 3.4.2 参数设定指令 `msg_CcuSetParmMsg`(0x0041,发送) - -总长度 20 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x20 | -| 1 | start2 | u8 | 起始符 0x20 | -| 2~3 | id | u16 | 消息标识 0x0041 | -| 4~5 | length | u16 | 数据区长度(=10) | -| 6 | insSocHold1 | u8 | 仪表 SOC 保持门限 1 | -| 7 | insSocHold2 | u8 | 仪表 SOC 保持门限 2 | -| 8 | insSocHold3 | u8 | 仪表 SOC 保持门限 3 | -| 9 | fcuToInsLimit1 | u8 | FCU→仪表限功率 1 | -| 10 | fcuToinsLimit2 | u8 | FCU→仪表限功率 2 | -| 11 | powerSocHold1 | u8 | 动力 SOC 保持门限 1 | -| 12 | powerSocHold2 | u8 | 动力 SOC 保持门限 2 | -| 13 | powerSocHold3 | u8 | 动力 SOC 保持门限 3 | -| 14 | fcuToPowerLimit1 | u8 | FCU→动力限功率 1 | -| 15 | fcuToPowerLimit2 | u8 | FCU→动力限功率 2 | -| 16~19 | checkCode | u32 | 校验和(累加和低 8 位) | - -#### 3.4.3 状态反馈 `msg_CcuStateFbMsg`(0x0051,接收) - -总长度 182 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x20 | -| 1 | start2 | u8 | 起始符 0x20 | -| 2~3 | id | u16 | 消息标识 0x0051 | -| 4~5 | length | u16 | 数据区长度 | -| 6 | fc_mode | u8 | 燃料电池运行模式(00=无效,01=调试,02=自动,03=补给) | -| 7 | fc_status | u8 | 燃料电池状态(`FcuState`) | -| 8 | fc_fault_level | u8 | 故障等级(00=无,10=一级,20=二级,30=三级,40=四级) | -| 9~10 | output_power_limit | u16 | 输出功率限制 [W],0~30000 | -| 11~12 | total_generation_time | u16 | 累计发电时间 [0.1h],最大 6553.5h | -| 13~14 | total_generation_power | u16 | 累计发电总功率 [W] | -| 15~16 | fault_level_1 | u16 | 一级故障位 | -| 17~18 | fault_level_2 | u16 | 二级故障位 | -| 19~20 | fault_level_3 | u16 | 三级故障位 | -| 21~22 | fault_level_4 | u16 | 四级故障位 | -| 23 | hydrogen_capacity | u8 | 储氢罐剩余容量 [%] | -| 24 | liquid_oxygen_capacity | u8 | 液氧罐剩余容量 [%] | -| 25~26 | palladium_temp | u16 | 钯膜最高温度 [℃] | -| 27~28 | main_pipe_pressure | u16 | 主水路压力 [0.01kPa] | -| 29~30 | aux_pipe_pressure | u16 | 辅水路压力 [0.01kPa] | -| 31~32 | cabin_pressure1 | u16 | 舱室1压力 [0.01kPa] | -| 33~34 | cabin_pressure2 | u16 | 舱室2压力 [0.01kPa] | -| 35~36 | cabin_temp1 | u16 | 舱室1温度 [0.01℃] | -| 37~38 | cabin_temp2 | u16 | 舱室2温度 [0.01℃] | -| 39~40 | cabin_humidity1 | u16 | 舱室1湿度 [0.01%RH] | -| 41~42 | cabin_humidity2 | u16 | 舱室2湿度 [0.01%RH] | -| 43 | flame_detector1 | u8 | 火焰探测器1(00=无效,01=报警,02=故障) | -| 44 | flame_detector2 | u8 | 火焰探测器2 | -| 45~46 | h2_concentration1 | u16 | 舱室H2浓度1 [0.01% LEL] | -| 47~48 | h2_concentration2 | u16 | 舱室H2浓度2 [0.01% LEL] | -| 49~50 | h2_concentration3 | u16 | 舱室H2浓度3 [0.01% LEL] | -| 51~52 | o2_concentration1 | u16 | 舱室O2浓度1 [0.01% LEL] | -| 53~54 | o2_concentration2 | u16 | 舱室O2浓度2 [0.01% LEL] | -| 55~56 | ch3oh_concentration1 | u16 | 舱室甲醇浓度1 [0.01% LEL] | -| 57~58 | ch3oh_concentration2 | u16 | 舱室甲醇浓度2 [0.01% LEL] | -| 59~62 | reserved1[2] | u16×2 | 保留 | -| 63~64 | emergency_battery1_voltage | u16 | 应急电池1总电压 [V] | -| 65~66 | emergency_battery1_current | u16 | 应急电池1总电流 [A] | -| 67~68 | emergency_battery1_max_temp | u16 | 应急电池1最高温度 [℃] | -| 69~70 | emergency_battery1_fault_word | u16 | 应急电池1故障字 | -| 71~72 | emergency_battery2_voltage | u16 | 应急电池2总电压 [V] | -| 73~74 | emergency_battery2_current | u16 | 应急电池2总电流 [A] | -| 75~76 | emergency_battery2_max_temp | u16 | 应急电池2最高温度 [℃] | -| 77~78 | emergency_battery2_fault_word | u16 | 应急电池2故障字 | -| 79~80 | cabin_ox_concentration | u16 | 仪表舱氧气浓度 [%] | -| 81~82 | cabin_temperature | u16 | 仪表舱温度 [℃] | -| 83~84 | cabin_humidity | u16 | 仪表舱湿度 [%RH] | -| 85~86 | cabin_pressure | u16 | 仪表舱大气压 [kPa] | -| 87~88 | power_cabin_ox_concentration | u16 | 动力舱氧气浓度 [%] | -| 89~90 | power_cabin_temperature | u16 | 动力舱温度 [℃] | -| 91~92 | power_cabin_humidity | u16 | 动力舱湿度 [%RH] | -| 93~94 | power_cabin_pressure | u16 | 动力舱大气压 [kPa] | -| 95 | reserved2[1] | u8 | 保留 | -| 96~101 | dyn_alarm_flag[6] | u8×6 | 动力锂电池报警标志 | -| 102~107 | ins_alarm_flag[6] | u8×6 | 仪表锂电池报警标志 | -| 108 | ins_relay_status1 | u8 | 仪表电池正极(高4)/充电(低4)继电器 | -| 109 | ins_relay_status2 | u8 | 仪表电池预充/负极继电器 | -| 110 | dyn_relay_status1 | u8 | 动力电池正极/充电继电器 | -| 111 | dyn_relay_status2 | u8 | 动力电池预充/负极继电器 | -| 112~113 | ins_max_discharge_power | u16 | 仪表电池最大放电功率 [kW, 0.05] | -| 114~115 | dyn_max_discharge_power | u16 | 动力电池最大放电功率 [kW, 0.05] | -| 116 | ins_soc | u8 | 仪表电池 SOC [%] | -| 117 | dyn_soc | u8 | 动力电池 SOC [%] | -| 118~119 | ins_total_energy | u16 | 仪表电池总能量 [kWh] | -| 120~121 | dyn_total_energy | u16 | 动力电池总能量 [kWh] | -| 122~123 | ins_power_input | u16 | 仪表电池输入功率 [kW] | -| 124~125 | dyn_power_input | u16 | 动力电池输入功率 [kW] | -| 126 | ins_charge_status | u8 | 仪表电池充放电(00=无效,10=充电,20=放电) | -| 127 | dyn_charge_status | u8 | 动力电池充放电状态 | -| 128~129 | ins_voltage_link | u16 | 仪表电池 LINK 电压 [0.01V] | -| 130~131 | ins_voltage_pack | u16 | 仪表电池 PACK 电压 [0.01V] | -| 132~133 | ins_current | u16 | 仪表电池电流 [0.01A],正值放电 | -| 134~135 | ins_resistance_pos | u16 | 仪表电池正极绝缘电阻 [kΩ] | -| 136~137 | ins_resistance_neg | u16 | 仪表电池负极绝缘电阻 [kΩ] | -| 138~139 | dyn_voltage_link | u16 | 动力电池 LINK 电压 [0.01V] | -| 140~141 | dyn_voltage_pack | u16 | 动力电池 PACK 电压 [0.01V] | -| 142~143 | dyn_current | u16 | 动力电池电流 [0.01A],正值放电 | -| 144~145 | dyn_resistance_pos | u16 | 动力电池正极绝缘电阻 [kΩ] | -| 146~147 | dyn_resistance_neg | u16 | 动力电池负极绝缘电阻 [kΩ] | -| 148 | ins_emergency_status | u8 | 仪表电池紧急状态(0x00=无,0x3A=4级…0x3D=1级) | -| 149 | dyn_emergency_status | u8 | 动力电池紧急状态 | -| 150~151 | reserved3[2] | u8×2 | 保留 | -| 152 | ins_soc_threshold1 | u8 | 仪表电池1 SOC 门限 | -| 153 | ins_soc_threshold2 | u8 | 仪表电池2 SOC 门限 | -| 154 | ins_soc_threshold3 | u8 | 仪表电池3 SOC 门限 | -| 155 | dyn_soc_threshold1 | u8 | 动力电池1 SOC 门限 | -| 156 | dyn_soc_threshold2 | u8 | 动力电池2 SOC 门限 | -| 157 | dyn_soc_threshold3 | u8 | 动力电池3 SOC 门限 | -| 158 | ins_power_limit1 | u8 | 仪表电池1功率限制 [%] | -| 159 | ins_power_limit2 | u8 | 仪表电池2功率限制 [%] | -| 160 | dyn_power_limit1 | u8 | 动力电池1功率限制 [%] | -| 161 | dyn_power_limit2 | u8 | 动力电池2功率限制 [%] | -| 162~165 | device_online_flag1 | u32 | 设备在线标志1(byte0=低压配电板,byte1=CCU主机,byte2=CCU备机) | -| 166~169 | device_online_flag2 | u32 | 设备在线标志2(byte0=仪表锂电,byte1=动力锂电,byte2=燃电锂电,byte3=高压配电板) | -| 170 | dyn_charge_level | u8 | 动力电池充电(01=1级,02=2级限功率,03=3级关闭) | -| 171 | dyn_water_leak_status | u8 | 动力漏水(0X=正常,1X=异常) | -| 172 | ins_water_leak_dcdc_status | u8 | 仪表漏水(低4)/DCDC(高4)状态 | -| 173 | reserved4 | u8 | 保留 | -| 174~177 | heartbeat | u32 | 主机心跳 | -| 178~181 | checkCode | u32 | 校验和 | - -#### 3.4.4 参数设定反馈 `msg_CcuSetParmFbMsg`(0x0040,接收) - -总长度 12 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x20 | -| 1 | start2 | u8 | 起始符 0x20 | -| 2~3 | id | u16 | 消息标识 0x0040 | -| 4~5 | length | u16 | 数据区长度 | -| 6 | flag | u8 | 应答标志 | -| 7 | failureCode | u8 | 失败码 | -| 8~11 | checkCode | u32 | 校验和 | - -### 3.5 配电系统报文 - -断路器字段取值: - -| 值 | 含义 | -|----|------| -| `0x55` | 合闸(ON) | -| `0xAA` | 分闸(OFF) | -| `0x00` | 无操作 / 保持 | -| `0x5A` | 故障脱扣(仅反馈) | - -#### 3.5.1 高压母线(BUS1) - -**指令 `msg_disHVBCmd`(0x0001,发送)** - -总长度 21 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0001 | -| 4~5 | length | u16 | 数据区长度(固定 18) | -| 6 | fuelCellCircuitBreaker | u8 | 燃料电池断路器 | -| 7 | powerLithiumBatteryCircuitBreaker | u8 | 动力锂电池断路器 | -| 8 | propulsionMotorCircuitBreaker | u8 | 推进电机断路器 | -| 9 | lithiumBatteryGroupInstrumentCircuitBreaker | u8 | 锂电池组仪表断路器 | -| 10 | dcDc5ModuleCircuitBreaker | u8 | DC/DC5模块断路器 | -| 11 | bowHighVoltageDistributionBoxCircuitBreaker | u8 | 艏部高压配电箱断路器 | -| 12 | sternFTDevice45CircuitBreaker | u8 | 艉部FT装置4/5断路器 | -| 13 | fbReservedCircuitBreaker | u8 | FB预留断路器 | -| 14 | sternRudderSwitch1CircuitBreaker | u8 | 艉舵开关1断路器 | -| 15 | sternRudderSwitch2CircuitBreaker | u8 | 艉舵开关2断路器 | -| 16 | tyzReservedCircuitBreaker | u8 | TYZ预留断路器 | -| 17 | reservedCircuitBreaker | u8 | 预留断路器 | -| 18 | dcdc5Module | u8 | DC/DC5模块启动 | -| 19 | coolingSystem | u8 | 冷却系统启动 | -| 20 | checkCode | u8 | 校验和(累加和低 8 位) | - -**反馈 `msg_disHighVolBusFbMsg`(0x0005,接收)** - -总长度 60 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0005 | -| 4~5 | length | u16 | 数据区长度 | -| 6 | fuelCellCircuitBreaker | u8 | 燃料电池断路器状态 | -| 7 | powerLithiumBatteryCircuitBreaker | u8 | 动力锂电池断路器状态 | -| 8 | propulsionMotorCircuitBreaker | u8 | 推进电机断路器状态 | -| 9 | lithiumBatteryGroupInstrumentCircuitBreaker | u8 | 锂电池组仪表断路器状态 | -| 10 | dcDc5ModuleCircuitBreaker | u8 | DC/DC5模块断路器状态 | -| 11 | bowHighVoltageDistributionBoxCircuitBreaker | u8 | 艏部高压配电箱断路器状态 | -| 12 | sternFTDevice45CircuitBreaker | u8 | 艉部FT装置4/5断路器状态 | -| 13 | fbReservedCircuitBreaker | u8 | FB预留断路器状态 | -| 14 | sternRudderSwitch1CircuitBreaker | u8 | 艉舵开关1断路器状态 | -| 15 | sternRudderSwitch2CircuitBreaker | u8 | 艉舵开关2断路器状态 | -| 16 | tyzReservedCircuitBreaker | u8 | TYZ预留断路器状态 | -| 17 | reservedCircuitBreaker | u8 | 预留断路器状态 | -| 18 | dcDcFaultWord1 | u8 | DC/DC5故障字低(Bit0过温关机…Bit7内部故障) | -| 19 | dcDcFaultWord2 | u8 | DC/DC5故障字高(Bit0通讯故障,Bit1工作状态,Bit2/3冷却泵1/2故障,Bit5/6冷却泵1/2运行) | -| 20 | powerBusInsulationStatus | u8 | 动力母排绝缘状态(0x55正常,0xAA绝缘低) | -| 21 | aBusInsulationStatus | u8 | A汇流排绝缘状态(0x55正常,0xAA绝缘低) | -| 22 | meterPowerLossSignal | u8 | 仪表电源状态(0x55正常,0xAA故障) | -| 23 | emergencyPowerLossSignal | u8 | 应急电源状态(0x55正常,0xAA故障) | -| 24 | dcDcTemperature | u8 | DC/DC温度 | -| 25~26 | powerBusVoltage | u16 | 动力汇流排电压 | -| 27~28 | dcDc5ModuleCurrent | u16 | DC/DC5模块电流 | -| 29~30 | powerBusCurrent | u16 | 动力汇流排电流 | -| 31~32 | busbarAVoltage | u16 | A汇流排电压 | -| 33~34 | propulsionMotorControlBoxCurrent | u16 | 推进电机控制箱电流 | -| 35~36 | busbarACurrent | u16 | A汇流排电流 | -| 37~38 | lithiumBatteryGroupMeterCurrent | u16 | 锂电池组(仪表)电流 | -| 39~40 | bowHighVoltageDistributionBoxCurrent | u16 | 艏部高压配电箱电流 | -| 41~42 | sternFTDevice45Current | u16 | 艉部FT装置4/5电流 | -| 43~44 | tyzReservedSwitchCurrent | u16 | TYZ预留开关电流 | -| 45~46 | sternRudderSwitch1Current | u16 | 艉舵开关1电流 | -| 47~48 | reservedCurrent1 | u16 | 预留电流1 | -| 49~50 | sternRudderSwitch2Current | u16 | 艉舵开关2电流 | -| 51~52 | reservedCurrent2 | u16 | 预留电流2 | -| 53~54 | fbReservedSwitchCurrent | u16 | FB预留电流 | -| 55~56 | reservedCurrent3 | u16 | 预留电流3 | -| 57~58 | coolWaterPressure | u16 | 冷却水压力 | -| 59 | checkCode | u8 | 校验和(累加和低 8 位) | - -#### 3.5.2 高压汇流排 A(BUS2) - -**指令 `msg_disHVBACmd`(0x0002,发送)** - -总长度 13 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0002 | -| 4~5 | length | u16 | 数据区长度(固定 6) | -| 6 | bowFTDevice123CircuitBreaker | u8 | 艏部FT装置1/2/3断路器 | -| 7 | actuatorCircuitBreaker | u8 | 启闭机构断路器 | -| 8 | mastSteeringGearControlBoxCircuitBreaker | u8 | 桅杆舵机控制箱断路器 | -| 9 | xczCircuitBreaker | u8 | XCZ断路器 | -| 10 | bowRudderControlBoxCircuitBreaker | u8 | 艏舵控制箱断路器 | -| 11 | openWaterCoverStartCylinderCircuitBreaker | u8 | 敞水盖启动电缸断路器 | -| 12 | checkCode | u8 | 校验和(累加和低 8 位) | - -**反馈 `msg_disHighAVolBusFbMsg`(0x0006,接收)** - -总长度 32 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0006 | -| 4~5 | length | u16 | 数据区长度 | -| 6 | bowFTDevice123CircuitBreaker | u8 | 艏部FT装置1/2/3断路器状态 | -| 7 | actuatorCircuitBreaker | u8 | 启闭机构断路器状态 | -| 8 | mastSteeringGearControlBoxCircuitBreaker | u8 | 桅杆舵机控制箱断路器状态 | -| 9 | xczCircuitBreaker | u8 | XCZ断路器状态 | -| 10 | bowRudderControlBoxCircuitBreaker | u8 | 艏舵控制箱断路器状态 | -| 11 | openWaterCoverStartCylinderCircuitBreaker | u8 | 敞水盖启动电缸断路器状态 | -| 12 | instrumentPowerFailureSignal | u8 | 仪表电源失电信号(0x55正常,0xAA故障) | -| 13 | emergencyPowerFailureSignal | u8 | 应急电源失电信号(0x55正常,0xAA故障) | -| 14 | waterIngressionAlarm | u8 | 浸水报警 | -| 15~16 | busbarBVoltage | u16 | 汇流排B电压 | -| 17~18 | busbarBCurrent | u16 | 汇流排B电流 | -| 19~20 | bowFTDevice123Current | u16 | 艏部FT装置1/2/3电流 | -| 21~22 | actuatorCurrent | u16 | 启闭机构电流 | -| 23~24 | mastSteeringGearControlBoxCurrent | u16 | 桅杆舵机控制箱电流 | -| 25~26 | xczCurrent | u16 | XCZ电流 | -| 27~28 | bowRudderControlBoxCurrent | u16 | 艏舵控制箱电流 | -| 29~30 | openWaterCoverStartCylinderCurrent | u16 | 敞水盖启动电缸电流 | -| 31 | checkCode | u8 | 校验和(累加和低 8 位) | - -#### 3.5.3 仪表母线(BUS3,低压主母线) - -**指令 `msg_disHVBBCmd`(0x0003,发送)** - -总长度 14 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0003 | -| 4~5 | length | u16 | 数据区长度(固定 8) | -| 6 | lithiumBatteryGroupInstrumentCircuitBreaker | u8 | 锂电池组(仪表)断路器 | -| 7 | bowLowVoltageDistributionBoxCircuitBreaker | u8 | 艏部低压配电箱断路器 | -| 8 | unit4InstrumentDC48VCircuitBreaker | u8 | UNIT4仪表DC48V断路器 | -| 9 | dcC1DCDistributionPanelCircuitBreaker | u8 | DC-C1直流配电板断路器 | -| 10 | reservedCircuitBreaker1 | u8 | 预留断路器1 | -| 11 | reservedCircuitBreaker2 | u8 | 预留断路器2 | -| 12 | emergencyLithiumBatteryGroup2CircuitBreaker | u8 | 应急锂电池组2断路器 | -| 13 | checkCode | u8 | 校验和(累加和低 8 位) | - -**反馈 `msg_disLowMainBusFbMsg`(0x0007,接收)** - -总长度 41 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0007 | -| 4~5 | length | u16 | 数据区长度 | -| 6 | lithiumBatteryGroupInstrumentCircuitBreaker | u8 | 锂电池组(仪表)断路器状态 | -| 7 | bowLowVoltageDistributionBoxCircuitBreaker | u8 | 艏部低压配电箱断路器状态 | -| 8 | unit4InstrumentDC48VCircuitBreaker | u8 | UNIT4仪表DC48V断路器状态 | -| 9 | dcC1DCDistributionPanelCircuitBreaker | u8 | DC-C1直流配电板断路器状态 | -| 10 | reservedCircuitBreaker1 | u8 | 预留断路器1状态 | -| 11 | reservedCircuitBreaker2 | u8 | 预留断路器2状态 | -| 12 | emergencyLithiumBatteryGroup2CircuitBreaker | u8 | 应急锂电池组2断路器状态 | -| 13 | instrumentPowerFailureSignal | u8 | 仪表电源失电信号(0x55正常,0xAA故障) | -| 14 | emergencyPowerFailureSignal | u8 | 应急电源失电信号(0x55正常,0xAA故障) | -| 15 | instrumentBusbarInsulationLow | u8 | 仪表汇流排绝缘低(0x55正常,0x55绝缘低) | -| 16~17 | instrumentBusbarVoltage | u16 | 仪表母排电压 | -| 18~19 | bowLowVoltageDistributionBoxCurrent | u16 | 艏部低压配电箱电流 | -| 20~21 | dcC1InstrumentDC48VCurrent | u16 | DC-C1仪表DC48V电流 | -| 22~23 | reservedCurrent1 | u16 | 预留电流1 | -| 24~25 | emergencyLithiumBatteryGroup2Current | u16 | 应急锂电池组2电流 | -| 26~27 | lithiumBatteryGroupInstrumentCurrent | u16 | 锂电池组(仪表)电流 | -| 28~29 | unit4InstrumentDC48VCurrent | u16 | UNIT4仪表DC48V电流 | -| 30~31 | reservedCurrent2 | u16 | 预留电流2 | -| 32~33 | emergency2BusbarVoltage | u16 | 应急2汇流排电压 | -| 34~35 | compositeEnergyManagementSystemEmergencyDC48VCurrent | u16 | 复合能源管控系统应急DC48V电流 | -| 36~37 | fuelCellSecuritySystemEmergencyDC48VCurrent | u16 | 燃料电池安全系统应急DC48V电流 | -| 38~39 | plcControlPowerCurrent | u16 | PLC控制电源电流 | -| 40 | checkCode | u8 | 校验和(累加和低 8 位) | - -#### 3.5.4 仪表汇流排(BUS4) - -**指令 `msg_disLVBCmd`(0x0004,发送)** - -总长度 15 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0004 | -| 4~5 | length | u16 | 数据区长度(固定 8) | -| 6 | unit1CircuitBreaker | u8 | UNIT1断路器 | -| 7 | unit2CircuitBreaker | u8 | UNIT2断路器 | -| 8 | unit3CircuitBreaker | u8 | UNIT3断路器 | -| 9 | unit5CircuitBreaker | u8 | UNIT5断路器 | -| 10 | bowPZDeviceCircuitBreaker | u8 | 艏部PZ装置断路器 | -| 11 | reservedCircuitBreaker1 | u8 | 预留断路器1 | -| 12 | reservedCircuitBreaker2 | u8 | 预留断路器2 | -| 13 | emergencyLithiumBatteryGroup1CircuitBreaker | u8 | 应急锂电池组1断路器 | -| 14 | checkCode | u8 | 校验和(累加和低 8 位) | - -**反馈 `msg_disLowBusFbMsg`(0x0008,接收)** - -总长度 36 字节。 - -| 偏移(字节) | 字段 | 类型 | 说明 | -|------------|------|------|------| -| 0 | start1 | u8 | 起始符 0x40 | -| 1 | start2 | u8 | 起始符 0x40 | -| 2~3 | id | u16 | 消息标识 0x0008 | -| 4~5 | length | u16 | 数据区长度 | -| 6 | unit1CircuitBreaker | u8 | UNIT1断路器状态 | -| 7 | unit2CircuitBreaker | u8 | UNIT2断路器状态 | -| 8 | unit3CircuitBreaker | u8 | UNIT3断路器状态 | -| 9 | unit5CircuitBreaker | u8 | UNIT5断路器状态 | -| 10 | bowPZDeviceCircuitBreaker | u8 | 艏部PZ装置断路器状态 | -| 11 | reservedCircuitBreaker1 | u8 | 预留断路器1状态 | -| 12 | reservedCircuitBreaker2 | u8 | 预留断路器2状态 | -| 13 | emergencyLithiumBatteryGroup1CircuitBreaker | u8 | 应急锂电池组1断路器状态 | -| 14 | powerFailureSignal | u8 | 仪表电源失电信号(0x55正常,0xAA故障) | -| 15 | emergencyPowerFailureSignal | u8 | 应急电源失电信号(0x55正常,0xAA故障) | -| 16 | waterIngressionAlarm | u8 | 浸水报警(0x55正常,0xAA故障) | -| 17~18 | instrumentBusbarVoltage | u16 | 仪表母排电压 | -| 19~20 | instrumentBusbarCurrent | u16 | 仪表母排电流 | -| 21~22 | unit1Current | u16 | UNIT1电流 | -| 23~24 | unit2Current | u16 | UNIT2电流 | -| 25~26 | unit3Current | u16 | UNIT3电流 | -| 27~28 | unit5Current | u16 | UNIT5电流 | -| 29~30 | bowPZDeviceCurrent | u16 | 艏部PZ装置电流 | -| 31~32 | emergencyLithiumBatteryGroup1Current | u16 | 应急锂电池组1电流 | -| 33~34 | emergency1BusbarVoltage | u16 | 应急1汇流排电压 | -| 35 | checkCode | u8 | 校验和(累加和低 8 位) | - ---- - -## 4. 附录:常量与枚举 - -### 4.1 断路器 / 信号常量 - -```c -#define BREAK_OFF 0xAA // 分闸 -#define BREAK_ON 0x55 // 合闸 -#define BREAK_FALSE 0x5A // 故障脱扣(仅反馈) -``` - -### 4.2 燃料电池控制指令 `FcuCommand` - -| 值 | 含义 | -|----|------| -| 0x01 | 自检 SelfCheck | -| 0x02 | 启动 Start | -| 0x03 | 停机 Stop | -| 0x04 | 复位 Reset | -| 0x05 | 急停 EmergencyStop | -| 0x06 | 补给 Supply | -| 0x07 | 预热 WarmUp | -| 0x08 | 维护 Maintenance | - -### 4.3 燃料电池状态 `FcuState` - -| 值 | 含义 | 值 | 含义 | -|----|------|----|------| -| 0x01 | 初始化 Init | 0x08 | 故障 Fault | -| 0x02 | 待机 Standby | 0x09 | 补给 Supply | -| 0x03 | 自检异常 SelfCheckError | 0x0A | 预热 WarmUp | -| 0x04 | 就绪 Ready | 0x0B | 预热完成 WarmUpComplete | -| 0x05 | 启动 Start | 0x0C | 维护 Maintenance | -| 0x06 | 运行 Running | 0x0D | 维护完成 MaintenanceComplete | -| 0x07 | 停机 Stop | — | — | - -### 4.4 电池指令 `BatteryCommand` - -| 值 | 含义 | -|----|------| -| 0x00 | 保持 Hold | -| 0x10 | 启动 Start | -| 0x20 | 停机 Stop | - -### 4.5 设备控制 `EquipmentCommand` - -| 值 | 含义 | -|----|------| -| 0 | 断电 OFF | -| 1 | 上电 ON | -| 2 | 仪表电上电 METER_ON | -| 3 | 动力电上电 POWER_ON | -| 4 | 仪表电断电 METER_OFF | -| 5 | 动力电断电 POWER_OFF | - -### 4.6 任务类型 `TaskType` - -| 值 | 含义 | 字符串 | -|----|------|--------| -| 1 | 抛载 | throwLoad | -| 2 | 运动上浮 | floatUp | -| 3 | 回收 | recycle | -| 4 | 悬停 | hover | -| 5 | 上浮通信 | satComm | -| 6 | 上浮校准 | satCalibrate | -| 7 | 游曳 | tour | -| 8 | 航行 | sail | -| 9 | 紧急上浮通信 | emerSatComm | -| 10 | 紧急航行 | emerSail | - -### 4.7 工况 `WorkCondition` - -| 值 | 含义 | 值 | 含义 | -|----|------|----|------| -| 0 | 工况切换中 | 7 | 上浮下潜 | -| 1 | 待机 | 8 | 浮调模式 | -| 2 | 岸基备航 | 9 | 水下侦查 | -| 3 | 水中备航 | 10 | 水面侦查 | -| 4 | 遥控工况 | 11 | DJ模式 | -| 5 | 巡航模式 | 12 | 调试模式 | -| 6 | 高速模式 | — | — | - -### 4.8 运行模式 `RunMode` - -| 值 | 含义 | -|----|------| -| 0 | 正常模式 Normal | -| 1 | 测试模式 Test | -| 2 | 调试模式 Debug | - -### 4.9 故障码格式 - -- 系统故障码:`faultCode` 中每个值为 `0xabcd`,`b` 位(右移 8 位取低 4 位)表示故障等级(`PowerManger::getFaultLevel()`)。 -- 锂电池故障码映射表:`src/pPowerManger/faultCode.h` 中 `batfaultMap`(1~4 级,0x01~0xC9)。 -- 配电故障码映射表:`src/pPowerManger/faultCode.h` 中 `disfaultMap`(1~45 项断路器脱扣/绝缘/失电/浸水故障)。 diff --git a/docs/信息中心-复合管控双锂电阶段协议20231006.docx b/docs/信息中心-复合管控双锂电阶段协议20231006.docx new file mode 100644 index 0000000000000000000000000000000000000000..2ffdcaab436fe74df4863e8f2086e7e714850cd8 GIT binary patch literal 53314 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 XYx>A*lhHIP6+s6bF4TEcd=&L+0bddeR5CQdqZ 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 I=x^WtA15A(OaK4? literal 0 HcmV?d00001 diff --git a/docs/锂电池协议20230324(1).docx b/docs/锂电池协议20230324(1).docx new file mode 100644 index 0000000000000000000000000000000000000000..75d089c29e84721df8ae582d774a3b10d3c78a10 GIT binary patch literal 43079 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 CnAk!9 literal 0 HcmV?d00001 diff --git a/missions/h100.moos b/missions/h100.moos index 3d481f8..3420367 100644 --- a/missions/h100.moos +++ b/missions/h100.moos @@ -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 +} diff --git a/scripts/clean-data.sh b/scripts/clean-data.sh index e83e4d6..d0141a4 100755 --- a/scripts/clean-data.sh +++ b/scripts/clean-data.sh @@ -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 "" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index edae95f..f70f974 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -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 "停止服务失败" } diff --git a/scripts/fetch-data.sh b/scripts/fetch-data.sh index e654432..15efd9c 100755 --- a/scripts/fetch-data.sh +++ b/scripts/fetch-data.sh @@ -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 登录用户(默认 root) # --board-dir

板卡部署根目录(默认 /root/work/h100) # --out-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' "$*"; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 40c1d62..ea82536 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ ADD_SUBDIRECTORY(pPowerManger) # ADD_SUBDIRECTORY(appPowerMangerWebUI) add_subdirectory(pPMtest) add_subdirectory(pPowerMangerHost) +add_subdirectory(pCCU) ############################################################################## # END of CMakeLists.txt ############################################################################## diff --git a/src/pCCU/CCU.cpp b/src/pCCU/CCU.cpp new file mode 100644 index 0000000..441c1fd --- /dev/null +++ b/src/pCCU/CCU.cpp @@ -0,0 +1,409 @@ +#include "CCU.h" +#include "MBUtils.h" +#include "../pPowerManger/logc/loguru.hpp" +#include "protocol/Frame.h" +#include + +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& frame) { + handleFcMessage(msg, frame); + }); + m_fcLink->setOnRawFrame([](int dir, const std::vector&, 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& frame) { + handlePmMessage(msg, frame); + }); + m_pmLink->setOnRawFrame([](int dir, const std::vector&, 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& frame) { + if (!msg) return; + switch (msg->id()) { + case 0x0002: { // FC 状态反馈 + FcStatusValue v; + if (msg->decode(frame, static_cast(&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& frame) { + if (!msg) return; + switch (msg->id()) { + case 0x0001: { // PM 操控指令 -> 转发给 FC + PmControlValue c; + if (msg->decode(frame, static_cast(&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(&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; +} \ No newline at end of file diff --git a/src/pCCU/CCU.h b/src/pCCU/CCU.h new file mode 100644 index 0000000..322a276 --- /dev/null +++ b/src/pCCU/CCU.h @@ -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& frame); + void handlePmMessage(Message* msg, const std::vector& 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 \ No newline at end of file diff --git a/src/pCCU/CCU_Info.cpp b/src/pCCU/CCU_Info.cpp new file mode 100644 index 0000000..e4a3407 --- /dev/null +++ b/src/pCCU/CCU_Info.cpp @@ -0,0 +1,102 @@ +/****************************************************************/ +/* NAME: CCU_Info */ +/* FILE: CCU_Info.cpp */ +/****************************************************************/ + +#include +#include +#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","= "); + 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); +} \ No newline at end of file diff --git a/src/pCCU/CCU_Info.h b/src/pCCU/CCU_Info.h new file mode 100644 index 0000000..122b795 --- /dev/null +++ b/src/pCCU/CCU_Info.h @@ -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 \ No newline at end of file diff --git a/src/pCCU/CCU软件需求.txt b/src/pCCU/CCU软件需求.txt new file mode 100644 index 0000000..c370c84 --- /dev/null +++ b/src/pCCU/CCU软件需求.txt @@ -0,0 +1,7 @@ +1.接收来自燃料电池的消息,消息协议格式见docs/超滑FC和复合管控器通讯协议 - 0821.docx(备注:日期会随着时间进行更新) +2.将消息进行整合处理后,发送到pPowerManger,消息协议见docs/控制主机与复合管控器通信协议- 0824.xlsx +3.接收来自pPowermanger的指令,解析和编码后转发给燃料电池 +4.具备sqlit数据库存储功能,可以把接收和发送的数据都存储到sqlit数据库中 +5.各端口配置、IP配置、日志及数据库配置可以通过.moos文件进行配置 +6.具备网页显示功能,可以显示接收、发送的数据、各数据状态等,数据展示要按照类型进行归类,要直观、简介 +7.编译部署与仓库内其他程序一样,可以跟随整个仓库一块部署 \ No newline at end of file diff --git a/src/pCCU/CMakeLists.txt b/src/pCCU/CMakeLists.txt new file mode 100644 index 0000000..3ea4d8b --- /dev/null +++ b/src/pCCU/CMakeLists.txt @@ -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} +) \ No newline at end of file diff --git a/src/pCCU/comm/FcLinkManager.h b/src/pCCU/comm/FcLinkManager.h new file mode 100644 index 0000000..6c8318f --- /dev/null +++ b/src/pCCU/comm/FcLinkManager.h @@ -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 \ No newline at end of file diff --git a/src/pCCU/comm/LinkManager.cpp b/src/pCCU/comm/LinkManager.cpp new file mode 100644 index 0000000..3d8f486 --- /dev/null +++ b/src/pCCU/comm/LinkManager.cpp @@ -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 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& 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 frame(buf, buf + n); + handleIncoming(frame); + } else { + // 无数据,短暂休眠避免忙等 + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } +} + +} // namespace ccu \ No newline at end of file diff --git a/src/pCCU/comm/LinkManager.h b/src/pCCU/comm/LinkManager.h new file mode 100644 index 0000000..da23b6f --- /dev/null +++ b/src/pCCU/comm/LinkManager.h @@ -0,0 +1,112 @@ +#ifndef PCCU_LINK_MANAGER_H +#define PCCU_LINK_MANAGER_H + +#include +#include +#include +#include +#include +#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& 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& frame)> cb) { + m_onMessage = std::move(cb); + } + + // 原始帧回调(用于 Web/调试展示原始报文) + void setOnRawFrame(std::function& 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& 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 m_running{false}; + std::thread m_rxThread; + + std::function&)> m_onMessage; + std::function&, 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 \ No newline at end of file diff --git a/src/pCCU/comm/PmLinkManager.h b/src/pCCU/comm/PmLinkManager.h new file mode 100644 index 0000000..231227f --- /dev/null +++ b/src/pCCU/comm/PmLinkManager.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 \ No newline at end of file diff --git a/src/pCCU/comm/UdpEndpoint.cpp b/src/pCCU/comm/UdpEndpoint.cpp new file mode 100644 index 0000000..20d0ddb --- /dev/null +++ b/src/pCCU/comm/UdpEndpoint.cpp @@ -0,0 +1,57 @@ +#include "UdpEndpoint.h" +#include + +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& data) { + if (!m_socket || data.empty()) return false; + try { + m_socket->iSendMessageTo(const_cast(data.data()), + static_cast(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(len)); + } catch (...) { + return 0; + } +} + +} // namespace ccu \ No newline at end of file diff --git a/src/pCCU/comm/UdpEndpoint.h b/src/pCCU/comm/UdpEndpoint.h new file mode 100644 index 0000000..259cb99 --- /dev/null +++ b/src/pCCU/comm/UdpEndpoint.h @@ -0,0 +1,46 @@ +#ifndef PCCU_UDP_ENDPOINT_H +#define PCCU_UDP_ENDPOINT_H + +#define UNIX +#include "MOOS/libMOOS/Comms/XPCUdpSocket.h" +#include +#include +#include + +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& 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 \ No newline at end of file diff --git a/src/pCCU/core/SnapshotBuilder.cpp b/src/pCCU/core/SnapshotBuilder.cpp new file mode 100644 index 0000000..da26839 --- /dev/null +++ b/src/pCCU/core/SnapshotBuilder.cpp @@ -0,0 +1,380 @@ +#include "SnapshotBuilder.h" +#include "../store/DbStore.h" +#include "json/json.h" +#include +#include + +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(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(v.pitch)); // 有符号 int16,×0.1° + j["roll"] = JsonVal(static_cast(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(v.pitch)); // 有符号 int16,×0.1° + j["roll"] = JsonVal(static_cast(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(lm->rxCount())); + j["tx"] = JsonVal(static_cast(lm->txCount())); + j["err"] = JsonVal(static_cast(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(m_sys->fcStatusCount())); + root["pmControlCount"] = JsonVal(static_cast(m_sys->pmControlCount())); + root["fcControlCount"] = JsonVal(static_cast(m_sys->fcControlCount())); + root["pmStatusCount"] = JsonVal(static_cast(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 \ No newline at end of file diff --git a/src/pCCU/core/SnapshotBuilder.h b/src/pCCU/core/SnapshotBuilder.h new file mode 100644 index 0000000..a3f9ab4 --- /dev/null +++ b/src/pCCU/core/SnapshotBuilder.h @@ -0,0 +1,38 @@ +#ifndef PCCU_SNAPSHOT_BUILDER_H +#define PCCU_SNAPSHOT_BUILDER_H + +#include +#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 \ No newline at end of file diff --git a/src/pCCU/core/SystemData.h b/src/pCCU/core/SystemData.h new file mode 100644 index 0000000..2c12133 --- /dev/null +++ b/src/pCCU/core/SystemData.h @@ -0,0 +1,118 @@ +#ifndef PCCU_SYSTEM_DATA_H +#define PCCU_SYSTEM_DATA_H + +#include +#include +#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 lock(m_mutex); + m_fcStatus = v; + m_fcStatusCount++; + } + FcStatusValue fcStatus() const { + std::lock_guard lock(m_mutex); + return m_fcStatus; + } + unsigned long fcStatusCount() const { + std::lock_guard lock(m_mutex); + return m_fcStatusCount; + } + + // 更新/获取最新 PM 操控指令(用于转发到 FC) + void updatePmControl(const PmControlValue& v) { + std::lock_guard lock(m_mutex); + m_pmControl = v; + m_pmControlCount++; + } + PmControlValue pmControl() const { + std::lock_guard lock(m_mutex); + return m_pmControl; + } + unsigned long pmControlCount() const { + std::lock_guard lock(m_mutex); + return m_pmControlCount; + } + + // 更新/获取最新 PM 参数设定(预留,用于未来转发/应用) + void updatePmParamSet(const PmParamSetValue& v) { + std::lock_guard lock(m_mutex); + m_pmParamSet = v; + } + PmParamSetValue pmParamSet() const { + std::lock_guard lock(m_mutex); + return m_pmParamSet; + } + + // 更新/获取最新转发给 FC 的控制指令 + void updateFcControl(const FcControlValue& v) { + std::lock_guard lock(m_mutex); + m_fcControl = v; + m_fcControlCount++; + } + FcControlValue fcControl() const { + std::lock_guard lock(m_mutex); + return m_fcControl; + } + unsigned long fcControlCount() const { + std::lock_guard lock(m_mutex); + return m_fcControlCount; + } + + // 更新/获取最新发送给 PM 的状态报文(整合后的) + void updatePmStatus(const PmStatusValue& v) { + std::lock_guard lock(m_mutex); + m_pmStatus = v; + m_pmStatusCount++; + } + PmStatusValue pmStatus() const { + std::lock_guard lock(m_mutex); + return m_pmStatus; + } + unsigned long pmStatusCount() const { + std::lock_guard lock(m_mutex); + return m_pmStatusCount; + } + + // 更新/获取最新发送给 PM 的参数反馈 + void updatePmParamFb(const PmParamSetFbValue& v) { + std::lock_guard lock(m_mutex); + m_pmParamFb = v; + } + PmParamSetFbValue pmParamFb() const { + std::lock_guard 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 \ No newline at end of file diff --git a/src/pCCU/main.cpp b/src/pCCU/main.cpp new file mode 100644 index 0000000..7278886 --- /dev/null +++ b/src/pCCU/main.cpp @@ -0,0 +1,54 @@ +/************************************************************/ +/* NAME: pCCU */ +/* FILE: main.cpp */ +/************************************************************/ + +#include +#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; +} \ No newline at end of file diff --git a/src/pCCU/pCCU.moos b/src/pCCU/pCCU.moos new file mode 100644 index 0000000..afed511 --- /dev/null +++ b/src/pCCU/pCCU.moos @@ -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 +} \ No newline at end of file diff --git a/src/pCCU/protocol/ChecksumPolicy.h b/src/pCCU/protocol/ChecksumPolicy.h new file mode 100644 index 0000000..70f42be --- /dev/null +++ b/src/pCCU/protocol/ChecksumPolicy.h @@ -0,0 +1,82 @@ +#ifndef PCCU_CHECKSUM_POLICY_H +#define PCCU_CHECKSUM_POLICY_H + +#include +#include +#include + +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& 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& frame) const { + size_t cs = size(); + if (cs == 0) return true; // 无校验,视为通过 + if (frame.size() < cs) return false; + + std::vector 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(frame[offset]) | + (static_cast(frame[offset + 1]) << 8) | + (static_cast(frame[offset + 2]) << 16) | + (static_cast(frame[offset + 3]) << 24); + return expect == actual; + } + // Sum16 + actual = static_cast(frame[offset]) | + (static_cast(frame[offset + 1]) << 8); + return (expect & 0xFFFF) == actual; + } + +private: + ChecksumType m_type; +}; + +} // namespace ccu + +#endif // PCCU_CHECKSUM_POLICY_H diff --git a/src/pCCU/protocol/FcProtocol.cpp b/src/pCCU/protocol/FcProtocol.cpp new file mode 100644 index 0000000..b0f3184 --- /dev/null +++ b/src/pCCU/protocol/FcProtocol.cpp @@ -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(new FcControlMessage())); + reg.registerMessage(std::unique_ptr(new FcStatusMessage())); +} + +//============================================================================ +// 0x0001 控制指令域编解码 +// 数据域 14 字节,偏移 0~13(对应文档字节 7~20) +//============================================================================ + +std::vector FcControlMessage::encode(const void* obj) const { + const FcControlValue* v = static_cast(obj); + std::vector 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(v->pitch)); // 数据1=低位 / 数据2=高位 + FieldCodec::putU16(payload, 5, static_cast(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& frame, void* obj) const { + if (!validateFrame(frame, id(), checksum(), totalLength())) return false; + FcControlValue* v = static_cast(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(FieldCodec::getU16(frame, o + 3)); + v->roll = static_cast(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& 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& 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 FcStatusMessage::encode(const void* obj) const { + const FcStatusValue* v = static_cast(obj); + std::vector 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& frame, void* obj) const { + if (!validateFrame(frame, id(), checksum(), totalLength())) return false; + FcStatusValue* v = static_cast(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 diff --git a/src/pCCU/protocol/FcProtocol.h b/src/pCCU/protocol/FcProtocol.h new file mode 100644 index 0000000..d42c111 --- /dev/null +++ b/src/pCCU/protocol/FcProtocol.h @@ -0,0 +1,174 @@ +#ifndef PCCU_FC_PROTOCOL_H +#define PCCU_FC_PROTOCOL_H + +#include +#include +#include +#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 encode(const void* obj) const override; + bool decode(const std::vector& frame, void* obj) const override; + + // 便捷重载 + std::vector encode(const FcControlValue& v) const { return encode(&v); } + bool decode(const std::vector& frame, FcControlValue& v) const { + return decode(frame, static_cast(&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 encode(const void* obj) const override; + bool decode(const std::vector& frame, void* obj) const override; + + std::vector encode(const FcStatusValue& v) const { return encode(&v); } + bool decode(const std::vector& frame, FcStatusValue& v) const { + return decode(frame, static_cast(&v)); + } + +private: + ChecksumPolicy m_checksum{ChecksumType::None}; +}; + +// 注册 FC 消息到给定注册表(见 MessageRegistry.h) +void registerFcMessages(class MessageRegistry& reg); + +} // namespace ccu + +#endif // PCCU_FC_PROTOCOL_H diff --git a/src/pCCU/protocol/FieldCodec.h b/src/pCCU/protocol/FieldCodec.h new file mode 100644 index 0000000..971908a --- /dev/null +++ b/src/pCCU/protocol/FieldCodec.h @@ -0,0 +1,58 @@ +#ifndef PCCU_FIELD_CODEC_H +#define PCCU_FIELD_CODEC_H + +#include +#include +#include +#include +#include + +namespace ccu { + +//============================================================================ +// FieldCodec:小端序(Little-Endian)编解码原语。 +// +// 两条协议(FC 协议 / PM 协议)均采用: +// - 多字节数据小端序 +// - 域起始符 0x40 0x40 +// - 域字节数 = 整个协议包的数据长度 +// 本类提供对裸缓冲区的字段读写,不关心具体业务含义。 +//============================================================================ + +class FieldCodec { +public: + // 在 dst 偏移 offset 处写入单字节 + static void putU8(std::vector& dst, size_t offset, uint8_t v) { + dst[offset] = v; + } + // 小端写入 2 字节 + static void putU16(std::vector& dst, size_t offset, uint16_t v) { + dst[offset] = static_cast(v & 0xFF); + dst[offset + 1] = static_cast((v >> 8) & 0xFF); + } + // 小端写入 4 字节 + static void putU32(std::vector& dst, size_t offset, uint32_t v) { + dst[offset] = static_cast(v & 0xFF); + dst[offset + 1] = static_cast((v >> 8) & 0xFF); + dst[offset + 2] = static_cast((v >> 16) & 0xFF); + dst[offset + 3] = static_cast((v >> 24) & 0xFF); + } + + static uint8_t getU8 (const std::vector& src, size_t offset) { + return src[offset]; + } + static uint16_t getU16(const std::vector& src, size_t offset) { + return static_cast(src[offset]) | + (static_cast(src[offset + 1]) << 8); + } + static uint32_t getU32(const std::vector& src, size_t offset) { + return static_cast(src[offset]) | + (static_cast(src[offset + 1]) << 8) | + (static_cast(src[offset + 2]) << 16) | + (static_cast(src[offset + 3]) << 24); + } +}; + +} // namespace ccu + +#endif // PCCU_FIELD_CODEC_H diff --git a/src/pCCU/protocol/Frame.cpp b/src/pCCU/protocol/Frame.cpp new file mode 100644 index 0000000..8ddbfb9 --- /dev/null +++ b/src/pCCU/protocol/Frame.cpp @@ -0,0 +1,50 @@ +#include "Frame.h" +#include "FieldCodec.h" + +namespace ccu { + +bool Frame::parseId(const std::vector& 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& 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 Frame::build( + uint16_t id, + const std::vector& payload, + const ChecksumPolicy& checksum) { + + size_t csSize = checksum.size(); + size_t total = FRAME_HEADER_LEN + payload.size() + csSize; + + std::vector frame(total, 0); + frame[0] = FRAME_START1; + frame[1] = FRAME_START2; + FieldCodec::putU16(frame, 2, id); + FieldCodec::putU16(frame, 4, static_cast(total)); // 域字节数=整个包长度 + + std::copy(payload.begin(), payload.end(), frame.begin() + FRAME_HEADER_LEN); + + if (csSize > 0) { + // 校验和:对起始符到校验和之前所有字节求和 + std::vector 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(sum)); + } + return frame; +} + +} // namespace ccu diff --git a/src/pCCU/protocol/Frame.h b/src/pCCU/protocol/Frame.h new file mode 100644 index 0000000..5b810d7 --- /dev/null +++ b/src/pCCU/protocol/Frame.h @@ -0,0 +1,54 @@ +#ifndef PCCU_FRAME_H +#define PCCU_FRAME_H + +#include +#include +#include +#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& buf, uint16_t& outId); + + // 解析帧头;返回是否成功且起始符正确 + static bool parseHeader(const std::vector& buf, FrameHeader& h); + + // 构造一个完整帧:data 为数据域(不含起始符与校验和),校验和自动追加 + // 调用方需保证 data.size() >= 帧头之前已经包含起始符+id+length 部分 + static std::vector build( + uint16_t id, + const std::vector& payload, + const ChecksumPolicy& checksum); +}; + +} // namespace ccu + +#endif // PCCU_FRAME_H diff --git a/src/pCCU/protocol/Message.cpp b/src/pCCU/protocol/Message.cpp new file mode 100644 index 0000000..2af0ec5 --- /dev/null +++ b/src/pCCU/protocol/Message.cpp @@ -0,0 +1,22 @@ +#include "Message.h" +#include "Frame.h" + +namespace ccu { + +bool Message::validateFrame(const std::vector& 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 diff --git a/src/pCCU/protocol/Message.h b/src/pCCU/protocol/Message.h new file mode 100644 index 0000000..0377d61 --- /dev/null +++ b/src/pCCU/protocol/Message.h @@ -0,0 +1,59 @@ +#ifndef PCCU_MESSAGE_H +#define PCCU_MESSAGE_H + +#include +#include +#include +#include +#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 encode(const void* obj) const = 0; + + // 从完整帧解析业务值;成功返回 true 并填充 obj + virtual bool decode(const std::vector& frame, void* obj) const = 0; + + // 供实现类解码前做通用校验:起始符 / id / 总长度 / 校验和 + static bool validateFrame(const std::vector& frame, + uint16_t expectId, + const ChecksumPolicy& policy, + size_t expectTotalLen); +}; + +} // namespace ccu + +#endif // PCCU_MESSAGE_H diff --git a/src/pCCU/protocol/MessageRegistry.cpp b/src/pCCU/protocol/MessageRegistry.cpp new file mode 100644 index 0000000..fbe79ae --- /dev/null +++ b/src/pCCU/protocol/MessageRegistry.cpp @@ -0,0 +1,29 @@ +#include "MessageRegistry.h" + +namespace ccu { + +bool MessageRegistry::registerMessage(std::unique_ptr 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 MessageRegistry::all() const { + std::vector out; + out.reserve(m_msgs.size()); + for (const auto& kv : m_msgs) out.push_back(kv.second.get()); + return out; +} + +} // namespace ccu \ No newline at end of file diff --git a/src/pCCU/protocol/MessageRegistry.h b/src/pCCU/protocol/MessageRegistry.h new file mode 100644 index 0000000..6dbf13e --- /dev/null +++ b/src/pCCU/protocol/MessageRegistry.h @@ -0,0 +1,42 @@ +#ifndef PCCU_MESSAGE_REGISTRY_H +#define PCCU_MESSAGE_REGISTRY_H + +#include +#include +#include +#include +#include "Message.h" + +namespace ccu { + +//============================================================================ +// MessageRegistry:消息注册表与收发分发中心。 +// +// 每条链路(FC 链路 / PM 链路)持有各自的注册表实例。 +// 由于 FC 协议与 PM 协议的域标识符存在重叠(0x0001/0x0002), +// 分属不同注册表可完全隔离,互不干扰。 +// +// 协议新增/调整消息时只需 register() 对应 Message 实现,上层无需改动。 +//============================================================================ + +class MessageRegistry { +public: + // 注册一条消息;返回是否成功(id 冲突时返回 false) + bool registerMessage(std::unique_ptr msg); + + // 按 id 查找消息;未注册返回 nullptr + Message* find(uint16_t id) const; + + // 已注册消息数量 + size_t size() const; + + // 所有已注册消息(按 id 升序) + std::vector all() const; + +private: + std::map> m_msgs; +}; + +} // namespace ccu + +#endif // PCCU_MESSAGE_REGISTRY_H \ No newline at end of file diff --git a/src/pCCU/protocol/PmProtocol.cpp b/src/pCCU/protocol/PmProtocol.cpp new file mode 100644 index 0000000..667b6f7 --- /dev/null +++ b/src/pCCU/protocol/PmProtocol.cpp @@ -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(new PmControlMessage())); + reg.registerMessage(std::unique_ptr(new PmParamSetMessage())); + reg.registerMessage(std::unique_ptr(new PmParamSetFbMessage())); + reg.registerMessage(std::unique_ptr(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 PmControlMessage::encode(const void* obj) const { + const PmControlValue* v = static_cast(obj); + std::vector 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(v->pitch)); // 数据1=低位/数据2=高位 + FieldCodec::putU16(p, PO_ROLL, static_cast(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& frame, void* obj) const { + if (!validateFrame(frame, id(), checksum(), totalLength())) return false; + PmControlValue* v = static_cast(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(FieldCodec::getU16(frame, o + PO_PITCH)); + v->roll = static_cast(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 PmParamSetMessage::encode(const void* obj) const { + const PmParamSetValue* v = static_cast(obj); + std::vector 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& frame, void* obj) const { + if (!validateFrame(frame, id(), checksum(), totalLength())) return false; + PmParamSetValue* v = static_cast(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 PmParamSetFbMessage::encode(const void* obj) const { + const PmParamSetFbValue* v = static_cast(obj); + std::vector 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& frame, void* obj) const { + if (!validateFrame(frame, id(), checksum(), totalLength())) return false; + PmParamSetFbValue* v = static_cast(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& 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& 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 PmStatusMessage::encode(const void* obj) const { + const PmStatusValue* v = static_cast(obj); + std::vector 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& frame, void* obj) const { + if (!validateFrame(frame, id(), checksum(), totalLength())) return false; + PmStatusValue* v = static_cast(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 \ No newline at end of file diff --git a/src/pCCU/protocol/PmProtocol.h b/src/pCCU/protocol/PmProtocol.h new file mode 100644 index 0000000..74c6adb --- /dev/null +++ b/src/pCCU/protocol/PmProtocol.h @@ -0,0 +1,317 @@ +#ifndef PCCU_PM_PROTOCOL_H +#define PCCU_PM_PROTOCOL_H + +#include +#include +#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 encode(const void* obj) const override; + bool decode(const std::vector& frame, void* obj) const override; + std::vector encode(const PmControlValue& v) const { return encode(&v); } + bool decode(const std::vector& frame, PmControlValue& v) const { + return decode(frame, static_cast(&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 encode(const void* obj) const override; + bool decode(const std::vector& frame, void* obj) const override; + std::vector encode(const PmParamSetValue& v) const { return encode(&v); } + bool decode(const std::vector& frame, PmParamSetValue& v) const { + return decode(frame, static_cast(&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 encode(const void* obj) const override; + bool decode(const std::vector& frame, void* obj) const override; + std::vector encode(const PmParamSetFbValue& v) const { return encode(&v); } + bool decode(const std::vector& frame, PmParamSetFbValue& v) const { + return decode(frame, static_cast(&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 encode(const void* obj) const override; + bool decode(const std::vector& frame, void* obj) const override; + std::vector encode(const PmStatusValue& v) const { return encode(&v); } + bool decode(const std::vector& frame, PmStatusValue& v) const { + return decode(frame, static_cast(&v)); + } +private: + ChecksumPolicy m_checksum{ChecksumType::Sum32}; +}; + +// 注册 PM 消息到给定注册表 +void registerPmMessages(class MessageRegistry& reg); + +} // namespace ccu + +#endif // PCCU_PM_PROTOCOL_H diff --git a/src/pCCU/store/DbStore.cpp b/src/pCCU/store/DbStore.cpp new file mode 100644 index 0000000..96e8b74 --- /dev/null +++ b/src/pCCU/store/DbStore.cpp @@ -0,0 +1,178 @@ +#include "DbStore.h" +#include "../protocol/Message.h" +#include "sqlite3.h" +#include +#include +#include +#include + +namespace ccu { + +namespace { +std::string toHex(const std::vector& 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 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& data, bool checksumOk) { + std::lock_guard 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(::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(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 DbStore::queryRecent(const std::string& link, int direction, + uint16_t msgId, int limit) { + std::lock_guard lock(m_mutex); + std::vector 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(sqlite3_column_text(stmt, 2)); + r.direction = sqlite3_column_int(stmt, 3); + r.msgId = static_cast(sqlite3_column_int(stmt, 4)); + r.msgName = reinterpret_cast(sqlite3_column_text(stmt, 5)); + r.length = sqlite3_column_int(stmt, 6); + r.checksumOk = sqlite3_column_int(stmt, 7); + r.hex = reinterpret_cast(sqlite3_column_text(stmt, 8)); + rows.push_back(std::move(r)); + } + sqlite3_finalize(stmt); + return rows; +} + +long long DbStore::count() const { + std::lock_guard 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 \ No newline at end of file diff --git a/src/pCCU/store/DbStore.h b/src/pCCU/store/DbStore.h new file mode 100644 index 0000000..8291426 --- /dev/null +++ b/src/pCCU/store/DbStore.h @@ -0,0 +1,70 @@ +#ifndef PCCU_DB_STORE_H +#define PCCU_DB_STORE_H + +#include +#include +#include +#include +#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& data, bool checksumOk) override; + + // 查询最近 N 条记录(可限定链路/方向/消息id;limit<=0 表示默认 100) + std::vector 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 \ No newline at end of file diff --git a/src/pCCU/web/WebServer.cpp b/src/pCCU/web/WebServer.cpp new file mode 100644 index 0000000..4720b53 --- /dev/null +++ b/src/pCCU/web/WebServer.cpp @@ -0,0 +1,132 @@ +#include "WebServer.h" +#include "pages/index.h" +#include + +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 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 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 handler) { + m_onOpen = std::move(handler); +} + +void WebServer::setApiHandler(std::function 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(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 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(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 lock(m_wsMutex); + for (auto c : m_wsConnections) { + mg_ws_send(c, text.c_str(), text.size(), WEBSOCKET_OP_TEXT); + } +} + +} // namespace ccu \ No newline at end of file diff --git a/src/pCCU/web/WebServer.h b/src/pCCU/web/WebServer.h new file mode 100644 index 0000000..4a4dc7a --- /dev/null +++ b/src/pCCU/web/WebServer.h @@ -0,0 +1,61 @@ +#ifndef PCCU_WEB_SERVER_H +#define PCCU_WEB_SERVER_H + +#define UNIX +#include "mongoose.h" +#include +#include +#include +#include +#include +#include + +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 handler); + + // 处理 /api/* 请求,返回 JSON 响应体(空串表示 404) + void setApiHandler(std::function 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 m_running{false}; + std::thread m_thread; + + std::mutex m_wsMutex; + std::vector m_wsConnections; + std::function m_onOpen; + std::function m_apiHandler; +}; + +} // namespace ccu + +#endif // PCCU_WEB_SERVER_H \ No newline at end of file diff --git a/src/pCCU/web/pages/index.h b/src/pCCU/web/pages/index.h new file mode 100644 index 0000000..965cf4c --- /dev/null +++ b/src/pCCU/web/pages/index.h @@ -0,0 +1,490 @@ +#ifndef PCCU_PAGE_INDEX_H +#define PCCU_PAGE_INDEX_H + +#include + +//============================================================================ +// pCCU 监控页面(纯前端,内嵌 HTML)。 +// +// - 通过 WebSocket /ws 接收服务器推送的 JSON 快照(每秒) +// - 每条消息一个标签页(接收/发送/系统分组),数据按消息归类展示 +// - 故障码/故障等级突出高亮;每条协议数据单独一行 +// - 纯监控展示,不提供反向控制 +//============================================================================ + +namespace ccu { +const std::string INDEX_HTML = R"HTML( + + + + +pCCU 复合管控器监控 + + + +

⚡ pCCU 复合管控器监控 连接中...

+
+
接收 + + + +
+
发送 + + + +
+
系统 + + +
+
+
加载中...
+ + + + +)HTML"; +} // namespace ccu + +#endif // PCCU_PAGE_INDEX_H \ No newline at end of file diff --git a/src/pPowerManger/LowerCommManager.cpp b/src/pPowerManger/LowerCommManager.cpp index b9e0a33..658a241 100644 --- a/src/pPowerManger/LowerCommManager.cpp +++ b/src/pPowerManger/LowerCommManager.cpp @@ -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(p.header.start1) << "\n"; - os << "起始符2 (start2): 0x" << std::setw(2) << static_cast(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(p.data.fc_mode) << "\n"; - os << "燃料电池状态 (fc_status): 0x" << std::setw(2) << static_cast(p.data.fc_status) << "\n"; - os << "故障等级 (fc_fault_level): 0x" << std::setw(2) << static_cast(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(p.data.hydrogen_capacity) << "%\n"; - os << "液氧剩余容量 (liquid_oxygen_capacity): " << static_cast(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(p.data.flame_detector1) << "\n"; - os << "火焰探测器2状态 (flame_detector2): 0x" << static_cast(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 (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 (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(p.data.dyn_alarm_flag[i]) << "\n"; - for (int i = 0; i < 6; ++i) - os << "仪表电池报警标志 ins_alarm_flag[" << i << "]: 0x" << std::hex << static_cast(p.data.ins_alarm_flag[i]) << "\n"; - - - os << "仪表正极/充电继电器状态 (ins_relay_status1): 0x" << std::hex << static_cast(p.data.ins_relay_status1) << "\n"; - os << "仪表预充/负极继电器状态 (ins_relay_status2): 0x" << static_cast(p.data.ins_relay_status2) << "\n"; - os << "动力正极/充电继电器状态 (dyn_relay_status1): 0x" << static_cast(p.data.dyn_relay_status1) << "\n"; - os << "动力预充/负极继电器状态 (dyn_relay_status2): 0x" << static_cast(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(p.data.ins_soc) << "%\n"; - os << "动力SOC (dyn_soc): " << static_cast(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(p.data.ins_charge_status) << "\n"; - os << "动力充电状态 (dyn_charge_status): 0x" << static_cast(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 (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 (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(p.data.ins_emergency_status) << "\n"; - os << "动力紧急状态 (dyn_emergency_status): 0x" << static_cast(p.data.dyn_emergency_status) << "\n"; - - for (int i = 0; i < 1; ++i) - os << "保留字段 reserved2[" << i << "]: 0x" << std::hex << static_cast(p.data.reserved2[i]) << "\n"; - os << "SOC门限值 ins_soc_threshold1: 0x" << std::hex << static_cast(p.data.ins_soc_threshold1) << "\n"; - os << "SOC门限值 ins_soc_threshold2: 0x" << std::hex << static_cast(p.data.ins_soc_threshold2) << "\n"; - os << "SOC门限值 ins_soc_threshold3: 0x" << std::hex << static_cast(p.data.ins_soc_threshold3) << "\n"; - os << "SOC门限值 dyn_soc_threshold1: 0x" << std::hex << static_cast(p.data.dyn_soc_threshold1) << "\n"; - os << "SOC门限值 dyn_soc_threshold2: 0x" << std::hex << static_cast(p.data.dyn_soc_threshold2) << "\n"; - os << "SOC门限值 dyn_soc_threshold3: 0x" << std::hex << static_cast(p.data.dyn_soc_threshold3) << "\n"; - os << "功率限制 ins_power_limit1: 0x" << std::hex << static_cast(p.data.ins_power_limit1) << "\n"; - os << "功率限制 ins_power_limit2: 0x" << std::hex << static_cast(p.data.ins_power_limit2) << "\n"; - os << "功率限制 dyn_power_limit1: 0x" << std::hex << static_cast(p.data.dyn_power_limit1) << "\n"; - os << "功率限制 dyn_power_limit2: 0x" << std::hex << static_cast(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){ diff --git a/src/pPowerManger/LowerCommManager.h b/src/pPowerManger/LowerCommManager.h index 1a7ccaf..6a8a406 100644 --- a/src/pPowerManger/LowerCommManager.h +++ b/src/pPowerManger/LowerCommManager.h @@ -95,7 +95,6 @@ public: // 启动/停止监听线程 void startListenThread(); void stopListenThread(); - void coutMsg(const msg_CcuStateFbMsg& p, std::ostream& os = std::cout); //转换函数 char getBreakerOption(char cmd); diff --git a/src/pPowerManger/PowerManger.cpp b/src/pPowerManger/PowerManger.cpp index 339836a..c75a949 100644 --- a/src/pPowerManger/PowerManger.cpp +++ b/src/pPowerManger/PowerManger.cpp @@ -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); diff --git a/src/pPowerManger/PowerManger.h b/src/pPowerManger/PowerManger.h index e5cd59f..aee5f25 100644 --- a/src/pPowerManger/PowerManger.h +++ b/src/pPowerManger/PowerManger.h @@ -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; diff --git a/src/pPowerManger/UpperCommManager.cpp b/src/pPowerManger/UpperCommManager.cpp index 4d108a1..ff23947 100644 --- a/src/pPowerManger/UpperCommManager.cpp +++ b/src/pPowerManger/UpperCommManager.cpp @@ -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 diff --git a/src/pPowerManger/UpperCommManager.h b/src/pPowerManger/UpperCommManager.h index 3d3edcb..365dd6f 100644 --- a/src/pPowerManger/UpperCommManager.h +++ b/src/pPowerManger/UpperCommManager.h @@ -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 m_liBatStateMsg_st_debug; std::pair m_depthMsg_st_debug; + std::pair m_insDataMsg_st_debug; std::pair m_setOpConditionMsg_st_debug; std::pair m_setOpModeMsg_st_debug; std::pair m_taskStartMsg_st_debug; diff --git a/src/pPowerManger/httpserver/httpserver.cpp b/src/pPowerManger/httpserver/httpserver.cpp index bb25116..854c255 100644 --- a/src/pPowerManger/httpserver/httpserver.cpp +++ b/src/pPowerManger/httpserver/httpserver.cpp @@ -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); diff --git a/src/pPowerManger/upmsg/uDevice_insData.h b/src/pPowerManger/upmsg/uDevice_insData.h new file mode 100644 index 0000000..23cb671 --- /dev/null +++ b/src/pPowerManger/upmsg/uDevice_insData.h @@ -0,0 +1,86 @@ +//决策消息 +//惯导导航信息 uDevice_insData_st,包含航向角/横摇角/纵摇角及平台深度等数据 +#ifndef UDEVICE_INSDATA_ST_H +#define UDEVICE_INSDATA_ST_H +#include +#include +#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 \ No newline at end of file diff --git a/src/pPowerMangerHost/HostSim.cpp b/src/pPowerMangerHost/HostSim.cpp index b34a643..0a0edaa 100644 --- a/src/pPowerMangerHost/HostSim.cpp +++ b/src/pPowerMangerHost/HostSim.cpp @@ -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); // 惯导导航信息(姿态/深度) } diff --git a/src/pPowerMangerHost/web/index.html b/src/pPowerMangerHost/web/index.html index 70bf976..cbd4dfc 100644 --- a/src/pPowerMangerHost/web/index.html +++ b/src/pPowerMangerHost/web/index.html @@ -275,6 +275,7 @@