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 0000000..2ffdcaa Binary files /dev/null and b/docs/信息中心-复合管控双锂电阶段协议20231006.docx differ diff --git a/docs/控制主机与复合管控器通信协议- 0824.xlsx b/docs/控制主机与复合管控器通信协议- 0824.xlsx new file mode 100644 index 0000000..5ff1dc4 Binary files /dev/null and b/docs/控制主机与复合管控器通信协议- 0824.xlsx differ diff --git a/docs/超滑FC和复合管控器通讯协议 - 0821.docx b/docs/超滑FC和复合管控器通讯协议 - 0821.docx new file mode 100644 index 0000000..7356d48 Binary files /dev/null and b/docs/超滑FC和复合管控器通讯协议 - 0821.docx differ diff --git a/docs/锂电池协议20230324(1).docx b/docs/锂电池协议20230324(1).docx new file mode 100644 index 0000000..75d089c Binary files /dev/null and b/docs/锂电池协议20230324(1).docx differ 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 @@