0827协议修订:姿态数据2改预留 + 状态帧新增剩余发电量 + CCU地址可配置
build-test-deploy / ci (push) Successful in 1h13m13s

FC协议(0827 docx):
- 控制指令域:字节12,13/16,17由纵倾/横倾姿态数据2改为预留(编码恒0、解码忽略)
- 状态反馈域:字节147,148由预留12改为剩余发电量(MWh),贯通JSON/网页显示
- PM→CCU链路:姿态字段4xuchar改为2xshort(数据1),结构体大小与线上布局不变
- 同步更新:CCU转发映射、SnapshotBuilder、pCCU网页、fuelcell网页、SQLite列描述、单测/模拟器
- 修正集成测试遗留断言(20B→24B、心跳d[19]→d[23])并增强预留字节校验

CCU地址可配置:
- pPowerManger 支持 moos 配置项 ccuhost/ccuport(默认127.0.0.1:7000)
- build-board.sh 增加 --ccu-host/--ccu-port 参数

文档:FC协议文档更新为0827版,清理过期协议docx
This commit is contained in:
zjk
2026-08-28 00:09:17 +08:00
parent c8becd1f6c
commit 2590282aed
28 changed files with 199 additions and 91 deletions
Binary file not shown.
+3
View File
@@ -16,6 +16,9 @@ ProcessConfig = pPowerManger
logpath = /root/work/h100/data/pPowerManger.log
// 数据库存储路径(目录需已存在,由 deploy.sh 创建)
dbpath = /root/work/h100/data/power_data.db
// CCU 地址(pPowerManger -> pCCU 的 UDP 链路);不配置则默认 127.0.0.1:7000
ccuhost = 127.0.0.1
ccuport = 7000
}
ProcessConfig = pPowerMangerHost
+3
View File
@@ -2,4 +2,7 @@ ProcessConfig = pPowerManger
{
AppTick = 4
CommsTick = 4
// CCU 地址;不配置则默认 127.0.0.1:7000
// ccuhost = 127.0.0.1
// ccuport = 7000
}
+41 -4
View File
@@ -19,6 +19,8 @@
# --jobs <n> 编译并行度(默认板卡 nproc)
# --start 编译部署后重启服务
# --clean 先重新 cmake(配置变更时用)
# --ccu-host <ip> 覆盖 mission 中 pPowerManger 的 CCU 地址 (ccuhost)
# --ccu-port <n> 覆盖 mission 中 pPowerManger 的 CCU 端口 (ccuport)
# -h, --help 显示帮助
#
# 前置: 板卡已配免密 SSH,且有编译环境(cmake/g++/MOOS/jsoncpp),
@@ -39,7 +41,8 @@
# 关联端口:
# FC 状态: FC:7000 -> 板卡:6000 (pCCU 监听)
# FC 控制: 板卡:6000 -> FC:7000 (pCCU 发送)
# PM 指令: pPowerManger:5001 -> pCCU:7000 (127.0.0.1,见 PowerManger.h CCUHOST)
# PM 指令: pPowerManger:5001 -> pCCU:7000 (127.0.0.1,
# 见 missions/h100.moos ccuhost/ccuport,默认值在 PowerManger.h CCUHOST)
# PM 状态: pCCU:7000 -> pPowerManger:5001 (127.0.0.1)
#
# 排障提示:
@@ -62,6 +65,8 @@ BOARD_BIN="/root/work/h100/bin"
JOBS=""
DO_START=0
DO_CLEAN=0
CCU_HOST=""
CCU_PORT=""
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
@@ -71,7 +76,7 @@ warn() { printf '\033[1;33m[build-board]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[build-board]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,29p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,31p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
@@ -87,6 +92,8 @@ while [ $# -gt 0 ]; do
--jobs) [ $# -ge 2 ] || { err "--jobs 需要参数"; exit 1; }; JOBS="$2"; shift 2 ;;
--start) DO_START=1; shift ;;
--clean) DO_CLEAN=1; shift ;;
--ccu-host) [ $# -ge 2 ] || { err "--ccu-host 需要参数"; exit 1; }; CCU_HOST="$2"; shift 2 ;;
--ccu-port) [ $# -ge 2 ] || { err "--ccu-port 需要参数"; exit 1; }; CCU_PORT="$2"; shift 2 ;;
-h|--help) usage ;;
*) err "未知参数: $1(见 --help)"; exit 1 ;;
esac
@@ -95,6 +102,10 @@ done
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
case "${CCU_PORT}" in
""|*[!0-9]*) [ -z "${CCU_PORT}" ] || { err "--ccu-port 必须是数字端口: ${CCU_PORT}"; exit 1; } ;;
esac
#-------------------------------------------------------------------
# 1. 前置检查
#-------------------------------------------------------------------
@@ -105,6 +116,21 @@ if ! ${SSH} "${SSH_TARGET}" 'true' 2>/dev/null; then
fi
ok "SSH 连接正常: ${SSH_TARGET}"
#-------------------------------------------------------------------
# mission 中插入/替换 "key = value"(键已存在则替换,否则插到第一个
# 顶层 '}' 之前,即 pPowerManger 配置块末尾)
#-------------------------------------------------------------------
upsert_moos_param() {
local f="$1" key="$2" val="$3"
if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${f}"; then
sed -i -E "s|^([[:space:]]*)${key}[[:space:]]*=.*|\1${key} = ${val}|" "${f}"
else
awk -v kv=" ${key} = ${val}" \
'!d && /^[[:space:]]*}[[:space:]]*$/ { print kv; d=1 } { print }' \
"${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}"
fi
}
#-------------------------------------------------------------------
# 2. 同步源码到板卡(排除构建产物 / git / 数据)
#-------------------------------------------------------------------
@@ -129,8 +155,19 @@ ok "源码已同步"
if [ -f "${PROJECT_ROOT}/missions/h100.moos" ]; then
info "同步 mission h100.moos ..."
rsync -az "${PROJECT_ROOT}/missions/h100.moos" \
"${SSH_TARGET}:/root/work/h100/missions/" || { err "rsync mission 失败"; exit 1; }
local_mission="${PROJECT_ROOT}/missions/h100.moos"
tmp_moos=""
if [ -n "${CCU_HOST}" ] || [ -n "${CCU_PORT}" ]; then
tmp_moos="$(mktemp)"
cp "${local_mission}" "${tmp_moos}"
[ -n "${CCU_HOST}" ] && upsert_moos_param "${tmp_moos}" ccuhost "${CCU_HOST}"
[ -n "${CCU_PORT}" ] && upsert_moos_param "${tmp_moos}" ccuport "${CCU_PORT}"
local_mission="${tmp_moos}"
info "CCU 地址覆盖: ccuhost=${CCU_HOST:-<保持原值>} ccuport=${CCU_PORT:-<保持原值>}"
fi
rsync -az "${local_mission}" \
"${SSH_TARGET}:/root/work/h100/missions/h100.moos" || { err "rsync mission 失败"; [ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"; exit 1; }
[ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"
ok "mission 已同步"
fi
+39 -3
View File
@@ -20,6 +20,8 @@
# --user <user> 登录用户(默认 root)
# --board-dir <dir> 板卡部署根目录(默认 /root/work/h100)
# --start 部署后立即启动服务
# --ccu-host <ip> 覆盖 mission 中 pPowerManger 的 CCU 地址 (ccuhost)
# --ccu-port <n> 覆盖 mission 中 pPowerManger 的 CCU 端口 (ccuport)
#
# 板卡布局:
# /root/work/h100/bin/ aarch64 二进制
@@ -37,6 +39,8 @@ HOST="192.168.0.223"
USER="root"
BOARD_DIR="/root/work/h100"
DO_START=0
CCU_HOST=""
CCU_PORT=""
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
@@ -46,7 +50,7 @@ warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*"; }
err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*"; }
usage() {
sed -n '2,29p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
sed -n '2,31p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
}
@@ -60,6 +64,8 @@ while [ $# -gt 0 ]; do
--user) [ $# -ge 2 ] || { err "--user 需要参数"; exit 1; }; USER="$2"; shift 2 ;;
--board-dir)[ $# -ge 2 ] || { err "--board-dir 需要参数"; exit 1; }; BOARD_DIR="$2"; shift 2 ;;
--start) DO_START=1; shift ;;
--ccu-host) [ $# -ge 2 ] || { err "--ccu-host 需要参数"; exit 1; }; CCU_HOST="$2"; shift 2 ;;
--ccu-port) [ $# -ge 2 ] || { err "--ccu-port 需要参数"; exit 1; }; CCU_PORT="$2"; shift 2 ;;
setup-ssh|status|stop|help|-h|--help) ACTION="$1"; shift ;;
"") shift ;;
*) err "未知参数: $1(见 help)"; exit 1 ;;
@@ -67,6 +73,10 @@ while [ $# -gt 0 ]; do
done
[ -n "${ACTION}" ] || ACTION="deploy"
case "${CCU_PORT}" in
""|*[!0-9]*) [ -z "${CCU_PORT}" ] || { err "--ccu-port 必须是数字端口: ${CCU_PORT}"; exit 1; } ;;
esac
SSH_TARGET="${USER}@${HOST}"
SSH="ssh -o BatchMode=yes -o ConnectTimeout=5"
SCP="scp -o BatchMode=yes -o ConnectTimeout=5"
@@ -164,6 +174,21 @@ WantedBy=multi-user.target
EOF
}
#-------------------------------------------------------------------
# mission 中插入/替换 "key = value"(键已存在则替换,否则插到第一个
# 顶层 '}' 之前,即 pPowerManger 配置块末尾)
#-------------------------------------------------------------------
upsert_moos_param() {
local f="$1" key="$2" val="$3"
if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${f}"; then
sed -i -E "s|^([[:space:]]*)${key}[[:space:]]*=.*|\1${key} = ${val}|" "${f}"
else
awk -v kv=" ${key} = ${val}" \
'!d && /^[[:space:]]*}[[:space:]]*$/ { print kv; d=1 } { print }' \
"${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}"
fi
}
#-------------------------------------------------------------------
# 命令: deploy
#-------------------------------------------------------------------
@@ -182,8 +207,19 @@ cmd_deploy() {
"${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; }
local mission_src="${PROJECT_ROOT}/missions/h100.moos"
local mission_push="${mission_src}" tmp_moos=""
if [ -n "${CCU_HOST}" ] || [ -n "${CCU_PORT}" ]; then
tmp_moos="$(mktemp)"
cp "${mission_src}" "${tmp_moos}"
[ -n "${CCU_HOST}" ] && upsert_moos_param "${tmp_moos}" ccuhost "${CCU_HOST}"
[ -n "${CCU_PORT}" ] && upsert_moos_param "${tmp_moos}" ccuport "${CCU_PORT}"
mission_push="${tmp_moos}"
info "CCU 地址覆盖: ccuhost=${CCU_HOST:-<保持原值>} ccuport=${CCU_PORT:-<保持原值>}"
fi
${SCP} -q "${mission_push}" \
"${SSH_TARGET}:${BOARD_DIR}/missions/" || { err "推送 mission 失败"; [ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"; return 1; }
[ -n "${tmp_moos}" ] && rm -f "${tmp_moos}"
${SSH} "${SSH_TARGET}" "chmod +x ${BOARD_DIR}/bin/pPowerManger ${BOARD_DIR}/bin/pPowerMangerHost ${BOARD_DIR}/bin/pCCU"
ok "二进制与 mission 已推送"
+1 -3
View File
@@ -218,15 +218,13 @@ void CCU::handlePmMessage(Message* msg, const std::vector<uint8_t>& frame) {
LOG_F(INFO, "[PM] control: mode=%d cmd=%d power=%d",
c.mode, c.cmd, c.outputPower);
// 映射并转发到 FC(1.1.1:姿态为双通道 int16,PM 单通道填数据1)
// 映射并转发到 FC(0827协议:姿态仅数据1有效,数据2字节预留填0)
FcControlValue f;
f.mode = c.mode;
f.cmd = c.cmd;
f.outputPower = c.outputPower;
f.pitch1 = c.pitch;
f.pitch2 = 0;
f.roll1 = c.roll;
f.roll2 = 0;
f.emergencyAllow = c.emergencyAllow;
f.depth = c.depth;
f.supplyCmd = c.supplyCmd;
+1 -2
View File
@@ -133,6 +133,7 @@ JsonVal fcStatusJson(const FcStatusValue& v) {
put(j, "alloy_h2_flow", v.alloy_h2_flow);
put(j, "fc_h2_flow", v.fc_h2_flow);
put(j, "fc_o2_flow", v.fc_o2_flow);
put(j, "remaining_generation", v.remaining_generation);
put(j, "heartbeat", v.heartbeat);
put(j, "emergency_cmd", v.emergency_cmd);
return j;
@@ -173,9 +174,7 @@ JsonVal fcControlJson(const FcControlValue& v) {
put(j, "cmd", v.cmd);
put(j, "outputPower", v.outputPower);
j["pitch1"] = JsonVal(static_cast<int>(v.pitch1)); // 有符号 int16,×0.1°
j["pitch2"] = JsonVal(static_cast<int>(v.pitch2));
j["roll1"] = JsonVal(static_cast<int>(v.roll1));
j["roll2"] = JsonVal(static_cast<int>(v.roll2));
put(j, "emergencyAllow", v.emergencyAllow);
put(j, "depth", v.depth);
put(j, "supplyCmd", v.supplyCmd);
+12 -9
View File
@@ -22,9 +22,9 @@ enum : size_t {
CO_CMD = 1, // 操控指令 (字节8)
CO_OUT_POWER = 2, // 输出功率指令 (字节9)
CO_PITCH1 = 3, // 纵倾姿态数据1 int16 (字节10,11)
CO_PITCH2 = 5, // 纵倾姿态数据2 int16 (字节12,13)
CO_RESERVED1 = 5, // 预留 (字节12,13),0827协议由纵倾姿态数据2改为预留
CO_ROLL1 = 7, // 横倾姿态数据1 int16 (字节14,15)
CO_ROLL2 = 9, // 横倾姿态数据2 int16 (字节16,17)
CO_RESERVED2 = 9, // 预留 (字节16,17),0827协议由横倾姿态数据2改为预留
CO_EMER_ALLOW = 11, // 应急允许 (字节18)
CO_DEPTH = 12, // 潜深深度 u16 (字节19,20)
CO_SUPPLY_CMD = 14, // 补给/排放指令 (字节21)
@@ -42,9 +42,9 @@ std::vector<uint8_t> FcControlMessage::encode(const void* obj) const {
FieldCodec::putU8(payload, CO_CMD, v->cmd);
FieldCodec::putU8(payload, CO_OUT_POWER, v->outputPower);
FieldCodec::putU16(payload, CO_PITCH1, static_cast<uint16_t>(v->pitch1));
FieldCodec::putU16(payload, CO_PITCH2, static_cast<uint16_t>(v->pitch2));
FieldCodec::putU16(payload, CO_RESERVED1, 0); // 预留恒填0
FieldCodec::putU16(payload, CO_ROLL1, static_cast<uint16_t>(v->roll1));
FieldCodec::putU16(payload, CO_ROLL2, static_cast<uint16_t>(v->roll2));
FieldCodec::putU16(payload, CO_RESERVED2, 0); // 预留恒填0
FieldCodec::putU8(payload, CO_EMER_ALLOW, v->emergencyAllow);
FieldCodec::putU16(payload, CO_DEPTH, v->depth);
FieldCodec::putU8(payload, CO_SUPPLY_CMD, v->supplyCmd);
@@ -64,9 +64,9 @@ bool FcControlMessage::decode(const std::vector<uint8_t>& frame, void* obj) cons
v->cmd = FieldCodec::getU8(frame, o + CO_CMD);
v->outputPower = FieldCodec::getU8(frame, o + CO_OUT_POWER);
v->pitch1 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_PITCH1));
v->pitch2 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_PITCH2));
// 字节12,13 预留,忽略
v->roll1 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_ROLL1));
v->roll2 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_ROLL2));
// 字节16,17 预留,忽略
v->emergencyAllow = FieldCodec::getU8(frame, o + CO_EMER_ALLOW);
v->depth = FieldCodec::getU16(frame, o + CO_DEPTH);
v->supplyCmd = FieldCodec::getU8(frame, o + CO_SUPPLY_CMD);
@@ -153,7 +153,8 @@ enum : size_t {
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_RESERVED = 118, // 预留1~11 u16*11 -> 118..138 (字节125-146)
OFF_REMAIN_GEN = 140, // 剩余发电量 u16 (字节147,148),0827协议由预留12改为
OFF_HEARTBEAT = 142, // 通信心跳 (字节149)
OFF_EMERGENCY_CMD = 143, // 应急指令 (字节150)
};
@@ -244,7 +245,8 @@ std::vector<uint8_t> FcStatusMessage::encode(const void* obj) const {
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);
putU16Arr(p, OFF_RESERVED, v->reserved, 11);
FieldCodec::putU16(p, OFF_REMAIN_GEN, v->remaining_generation);
FieldCodec::putU8(p, OFF_HEARTBEAT, v->heartbeat);
FieldCodec::putU8(p, OFF_EMERGENCY_CMD, v->emergency_cmd);
@@ -331,7 +333,8 @@ bool FcStatusMessage::decode(const std::vector<uint8_t>& frame, void* obj) const
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);
getU16Arr(frame, o + OFF_RESERVED, v->reserved, 11);
v->remaining_generation = FieldCodec::getU16(frame, o + OFF_REMAIN_GEN);
v->heartbeat = FieldCodec::getU8(frame, o + OFF_HEARTBEAT);
v->emergency_cmd = FieldCodec::getU8(frame, o + OFF_EMERGENCY_CMD);
+7 -6
View File
@@ -10,28 +10,28 @@ namespace ccu {
//============================================================================
// FC 协议(燃料电池系统 <-> 复合管控器)
// 依据:docs/超滑FC和复合管控器通讯协议 - 0821.docx
// 依据:docs/超滑FC和复合管控器通讯协议 - 0827.docx
//
// 帧格式统一:0x40 0x40 + 域标识符(2B) + 域字节数(2B) + 数据域 + [校验和]
// 注意:FC 协议文档未列出校验和行,故默认采用 None(可插拔,联调可切换)。
//
// 两条消息:
// 0x0001 复合管控器->燃料电池控制器(控制指令域) payload 14B,总长 20B
// 0x0001 复合管控器->燃料电池控制器(控制指令域) payload 18B,总长 24B
// 0x0002 燃料电池控制器->复合管控器(状态反馈域) payload 144B,总长 150B
//============================================================================
//--------------------------------------------------------------------------
// 0x0001 控制指令域(复合管控器 -> 燃料电池)
// payload 14 字节,对应文档字节 7~20
// payload 18 字节,对应文档字节 7~24
//--------------------------------------------------------------------------
struct FcControlValue {
uint8_t mode = 0; // 模式设定 00~03
uint8_t cmd = 0; // 操控指令 00~0B
uint8_t outputPower = 0; // 输出功率指令 0~250,分辨率0.1kW
int16_t pitch1 = 0; // 纵倾姿态数据1 int16,分辨率0.1°(字节10,11)
int16_t pitch2 = 0; // 纵倾姿态数据2 int16,分辨率0.1°(字节12,13)
uint16_t reserved1 = 0; // 预留(字节12,13),0827协议由纵倾姿态数据2改为预留
int16_t roll1 = 0; // 横倾姿态数据1 int16,分辨率0.1°(字节14,15)
int16_t roll2 = 0; // 横倾姿态数据2 int16,分辨率0.1°(字节16,17)
uint16_t reserved2 = 0; // 预留(字节16,17),0827协议由横倾姿态数据2改为预留
uint8_t emergencyAllow = 0; // 应急允许:Bit0 允许降载 / Bit1 允许排气(字节18)
uint16_t depth = 0; // 潜深深度,单位 m(字节19,20)
uint8_t supplyCmd = 0; // 补给/排放指令 00~0B(字节21)
@@ -119,7 +119,8 @@ struct FcStatusValue {
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
uint16_t reserved[11] = {0}; // 预留1~11(字节125~146)
uint16_t remaining_generation = 0; // 剩余发电量,MWh(字节147,148),0827协议由预留12改为
uint8_t heartbeat = 0; // 通信心跳,每次+1
uint8_t emergency_cmd = 0; // 应急指令:Bit0 降载/Bit1 上浮/...
+3 -3
View File
@@ -29,8 +29,8 @@ enum : size_t {
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_PITCH = 11, // 纵倾姿态数据1 int16 (字节18-19)
PO_ROLL = 13, // 横倾姿态数据1 int16 (字节20-21)
PO_EMER_ALLOW = 15,
PO_DEPTH = 16, // u16
PO_SUPPLY_CMD = 18,
@@ -60,7 +60,7 @@ std::vector<uint8_t> PmControlMessage::encode(const void* obj) const {
FieldCodec::putU8(p, PO_MODE, v->mode);
FieldCodec::putU8(p, PO_CMD, v->cmd);
FieldCodec::putU8(p, PO_OUTPOWER, v->outputPower);
FieldCodec::putU16(p, PO_PITCH, static_cast<uint16_t>(v->pitch)); // 数据1=低位/数据2=高位
FieldCodec::putU16(p, PO_PITCH, static_cast<uint16_t>(v->pitch)); // 姿态数据1,数据2字节预留
FieldCodec::putU16(p, PO_ROLL, static_cast<uint16_t>(v->roll));
FieldCodec::putU8(p, PO_EMER_ALLOW, v->emergencyAllow);
FieldCodec::putU16(p, PO_DEPTH, v->depth);
+2 -2
View File
@@ -38,8 +38,8 @@ struct PmControlValue {
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°
int16_t pitch = 0; // 纵倾姿态数据1 int16,分辨率0.1°(字节18,19)
int16_t roll = 0; // 横倾姿态数据1 int16,分辨率0.1°(字节20,21)
uint8_t emergencyAllow = 0; // 应急允许
uint16_t depth = 0; // 潜深深度 m
uint8_t supplyCmd = 0; // 补给/排放指令
+1 -2
View File
@@ -154,6 +154,7 @@ function fcCard(d){
h+=row('四级故障码', faultHex(d.fault_level_4));
h+=row('累积发电时间', (d.total_generation_time*0.1).toFixed(1)+' h');
h+=row('系统发电功率', (d.generation_power*0.01).toFixed(2)+' kW');
h+=row('剩余发电量', d.remaining_generation+' MWh');
h+=row('储氢剩余容量', d.hydrogen_capacity+'%');
h+=row('液氧剩余容量', d.liquid_oxygen_capacity+'%');
h+=row('通信心跳', d.heartbeat);
@@ -325,9 +326,7 @@ function fcCmdCard(d){
h+=row('操控指令', parsed(d.cmd, cmdText(d.cmd)));
h+=row('输出功率指令', d.outputPower+' (×0.1kW)');
h+=row('纵倾姿态数据1', (d.pitch1*0.1).toFixed(1)+'°');
h+=row('纵倾姿态数据2', (d.pitch2*0.1).toFixed(1)+'°');
h+=row('横倾姿态数据1', (d.roll1*0.1).toFixed(1)+'°');
h+=row('横倾姿态数据2', (d.roll2*0.1).toFixed(1)+'°');
h+=row('应急允许', parsed(d.emergencyAllow, emerAllowText(d.emergencyAllow)));
h+=row('潜深深度', d.depth+' m');
h+=row('补给/排放指令', parsed(d.supplyCmd, supplyCmdText(d.supplyCmd)));
+9
View File
@@ -75,6 +75,15 @@ bool LowerCommManager::initUdpComm(){
return true;
}
void LowerCommManager::setCcuAddress(const std::string& host, long port){
m_ccuHost = host;
m_ccuPort = port;
if (m_udpComm) {
m_udpComm->ccuHost = host;
m_udpComm->ccuPort = port;
}
}
void LowerCommManager::setCommLogger(ICommLogger* logger)
{
if (m_udpComm) m_udpComm->setCommLogger(logger);
+3
View File
@@ -32,6 +32,9 @@ public:
// 初始化UDP通信
bool initUdpComm();
// 设置 CCU 地址(moos 配置读取后调用;须在监听/发送线程启动前调用)
void setCcuAddress(const std::string& host, long port);
// 设置原始帧日志回调(转发给 udpComm,可为空)
void setCommLogger(ICommLogger* logger);
+23 -4
View File
@@ -48,6 +48,10 @@ PowerManger::PowerManger()
m_skew = 10;
// CCU 地址默认值,可在 moos 文件中用 ccuhost/ccuport 覆盖
m_ccuHost = CCUHOST;
m_ccuPort = CCUPORT;
m_pmCurrentState = {};
m_currentDisBreakerState = {};
m_targetDisBreakerState = {};
@@ -64,7 +68,7 @@ PowerManger::PowerManger()
initControlCmd();
//初始化下位机通信
m_lowerCommManager = new LowerCommManager(this,IPORT,CCUPORT,CCUHOST);
m_lowerCommManager = new LowerCommManager(this,IPORT,m_ccuPort,m_ccuHost);
m_lowerCommManager->initUdpComm();
//初始化上位机通信
@@ -189,6 +193,8 @@ bool PowerManger::OnStartUp()
std::string dbPath; // 数据库路径,不配置则默认当前目录 power_data.db
std::string logPath; // 日志文件路径,不配置则默认当前目录 pPowerManger.log
std::string ccuHost; // CCU 地址,不配置则沿用默认值
long ccuPort = 0; // CCU 端口,不配置则沿用默认值
STRING_LIST::iterator p;
for(p=sParams.begin(); p!=sParams.end(); p++) {
@@ -212,6 +218,14 @@ bool PowerManger::OnStartUp()
logPath = value;
handled = true;
}
else if(param == "ccuhost") {
ccuHost = value;
handled = true;
}
else if(param == "ccuport") {
ccuPort = atoi(value.c_str());
handled = true;
}
if(!handled)
reportUnhandledConfigWarning(orig);
@@ -230,6 +244,13 @@ bool PowerManger::OnStartUp()
LOG_F(INFO, "数据库存储路径: %s", dbPath.c_str());
m_db = new SQLite(dbPath);
m_db->createTable();
// 应用 moos 文件中的 CCU 地址配置(在监听/通信线程启动前生效)
if (!ccuHost.empty()) m_ccuHost = ccuHost;
if (ccuPort > 0) m_ccuPort = ccuPort;
m_lowerCommManager->setCcuAddress(m_ccuHost, m_ccuPort);
LOG_F(INFO, "CCU 地址: %s:%ld", m_ccuHost.c_str(), m_ccuPort);
// 注入原始帧日志回调(RX/TX 双向落库)
m_lowerCommManager->setCommLogger(m_db);
// 数据库就绪后再启动下位机监听线程
@@ -365,10 +386,8 @@ void PowerManger::initControlCmd()
m_CcuColCmd.fcmode = 0x00; //默认无效
m_CcuColCmd.fcuCmd = 0x00; //无效指令
m_CcuColCmd.fcuPowerEfficiency = 0; //输出功率指令,0~250,分辨率0.1kW
m_CcuColCmd.pitchData1 = 0; //纵倾姿态数据1
m_CcuColCmd.pitchData2 = 0; //纵倾姿态数据2
m_CcuColCmd.pitchData1 = 0; //纵倾姿态数据1(int16,分辨率0.1°;0824协议:姿态数据2已删除)
m_CcuColCmd.rollData1 = 0; //横倾姿态数据1
m_CcuColCmd.rollData2 = 0; //横倾姿态数据2
m_CcuColCmd.emergencyAllow = 0; //应急允许
m_CcuColCmd.depth = 0; //潜深深度
m_CcuColCmd.supplyExhaustCmd = 0x00; //补给/排放指令
+3
View File
@@ -42,6 +42,7 @@
#define CCUPORT 7000
// #define CCUHOST "192.168.0.140"
// pCCU 与本程序同机部署时用回环地址;127.0.0.0 是网络号,内核不路由
// 默认值,可在 moos 文件中用 ccuhost/ccuport 覆盖
#define CCUHOST "127.0.0.1"
#define MAX_UDP_PKT_SIZE 65535
#define SKEW_TOLERANCE 5
@@ -124,6 +125,8 @@ class PowerManger : public AppCastingMOOSApp
bool isDeviceCmdEmpty() const;
//===========================CONFIG========================================
long m_lPort;
std::string m_ccuHost; // CCU 地址,moos 配置项 ccuhost
long m_ccuPort; // CCU 端口,moos 配置项 ccuport
unsigned int m_nReceiveBufferSizeKB;
unsigned int m_nSendBufferSizeKB;
double m_current_time;
+5 -9
View File
@@ -541,19 +541,15 @@ body {
</div>
<div class="control-group">
<label class="control-label">姿态数据(分辨率0.1°)</label>
<label class="control-label">姿态数据(int16,分辨率0.1°,数据2字段已删除)</label>
<div class="two-col">
<div class="control-group">
<label class="control-label">纵倾姿态1</label>
<input type="number" name="pitch1" min="0" max="255" value="0" class="select-control">
<label class="control-label">纵倾姿态2</label>
<input type="number" name="pitch2" min="0" max="255" value="0" class="select-control">
<label class="control-label">纵倾姿态数据1</label>
<input type="number" name="pitch" min="-32768" max="32767" value="0" class="select-control">
</div>
<div class="control-group">
<label class="control-label">横倾姿态1</label>
<input type="number" name="roll1" min="0" max="255" value="0" class="select-control">
<label class="control-label">横倾姿态2</label>
<input type="number" name="roll2" min="0" max="255" value="0" class="select-control">
<label class="control-label">横倾姿态数据1</label>
<input type="number" name="roll" min="-32768" max="32767" value="0" class="select-control">
</div>
</div>
</div>
+12 -22
View File
@@ -107,10 +107,8 @@ void HttpServer::handleWebSocketEvent(struct mg_connection* c, int ev, void* ev_
unsigned char fcMode = 0;
unsigned char fcCommand = 0;
unsigned char fcPower = 0;
unsigned char pitch1 = 0;
unsigned char pitch2 = 0;
unsigned char roll1 = 0;
unsigned char roll2 = 0;
short pitch = 0;
short roll = 0;
unsigned char emergencyAllow = 0;
unsigned short depth = 0;
unsigned char supplyExhaustCmd = 0;
@@ -125,14 +123,10 @@ void HttpServer::handleWebSocketEvent(struct mg_connection* c, int ev, void* ev_
fcCommand = (unsigned char)strtoul(tmp, NULL, 16);
mg_http_get_var(&body, "fcPower", tmp, sizeof(tmp));
fcPower = (unsigned char)atoi(tmp);
mg_http_get_var(&body, "pitch1", tmp, sizeof(tmp));
pitch1 = (unsigned char)atoi(tmp);
mg_http_get_var(&body, "pitch2", tmp, sizeof(tmp));
pitch2 = (unsigned char)atoi(tmp);
mg_http_get_var(&body, "roll1", tmp, sizeof(tmp));
roll1 = (unsigned char)atoi(tmp);
mg_http_get_var(&body, "roll2", tmp, sizeof(tmp));
roll2 = (unsigned char)atoi(tmp);
mg_http_get_var(&body, "pitch", tmp, sizeof(tmp));
pitch = (short)atoi(tmp);
mg_http_get_var(&body, "roll", tmp, sizeof(tmp));
roll = (short)atoi(tmp);
mg_http_get_var(&body, "emergencyAllow", tmp, sizeof(tmp));
emergencyAllow = (unsigned char)strtoul(tmp, NULL, 16);
mg_http_get_var(&body, "depth", tmp, sizeof(tmp));
@@ -148,8 +142,8 @@ void HttpServer::handleWebSocketEvent(struct mg_connection* c, int ev, void* ev_
mg_http_get_var(&body, "hostStatus", tmp, sizeof(tmp));
hostStatus = (unsigned char)strtoul(tmp, NULL, 16);
LOG_F(INFO, "Received fuel cell control config: fcMode=%02XH fcCommand=%02XH fcPower=%d pitch1=%d pitch2=%d roll1=%d roll2=%d emergencyAllow=%02XH depth=%d supplyExhaustCmd=%02XH insBatCmd=%02XH powerBatCmd=%02XH powerBatPower=%d hostStatus=%02XH",
fcMode, fcCommand, fcPower, pitch1, pitch2, roll1, roll2,
LOG_F(INFO, "Received fuel cell control config: fcMode=%02XH fcCommand=%02XH fcPower=%d pitch=%d roll=%d emergencyAllow=%02XH depth=%d supplyExhaustCmd=%02XH insBatCmd=%02XH powerBatCmd=%02XH powerBatPower=%d hostStatus=%02XH",
fcMode, fcCommand, fcPower, pitch, roll,
emergencyAllow, depth, supplyExhaustCmd, insBatCmd, powerBatCmd, powerBatPower, hostStatus);
{
@@ -157,10 +151,8 @@ void HttpServer::handleWebSocketEvent(struct mg_connection* c, int ev, void* ev_
m_powerManger->m_CcuColCmd.fcmode = fcMode;
m_powerManger->m_CcuColCmd.fcuCmd = fcCommand;
m_powerManger->m_CcuColCmd.fcuPowerEfficiency = fcPower;
m_powerManger->m_CcuColCmd.pitchData1 = pitch1;
m_powerManger->m_CcuColCmd.pitchData2 = pitch2;
m_powerManger->m_CcuColCmd.rollData1 = roll1;
m_powerManger->m_CcuColCmd.rollData2 = roll2;
m_powerManger->m_CcuColCmd.pitchData1 = pitch;
m_powerManger->m_CcuColCmd.rollData1 = roll;
m_powerManger->m_CcuColCmd.emergencyAllow = emergencyAllow;
m_powerManger->m_CcuColCmd.depth = depth;
m_powerManger->m_CcuColCmd.supplyExhaustCmd = supplyExhaustCmd;
@@ -936,10 +928,8 @@ void HttpServer::pushSystemData() {
j["ccuColCmd"]["fcmode"] = Json::Value(ccuCmd.fcmode);
j["ccuColCmd"]["fcuCmd"] = Json::Value(ccuCmd.fcuCmd);
j["ccuColCmd"]["fcuPowerEfficiency"] = Json::Value(ccuCmd.fcuPowerEfficiency);
j["ccuColCmd"]["pitchData1"] = Json::Value(ccuCmd.pitchData1);
j["ccuColCmd"]["pitchData2"] = Json::Value(ccuCmd.pitchData2);
j["ccuColCmd"]["rollData1"] = Json::Value(ccuCmd.rollData1);
j["ccuColCmd"]["rollData2"] = Json::Value(ccuCmd.rollData2);
j["ccuColCmd"]["pitchData1"] = Json::Value(static_cast<int>(ccuCmd.pitchData1));
j["ccuColCmd"]["rollData1"] = Json::Value(static_cast<int>(ccuCmd.rollData1));
j["ccuColCmd"]["emergencyAllow"] = Json::Value(ccuCmd.emergencyAllow);
j["ccuColCmd"]["depth"] = Json::Value(ccuCmd.depth);
j["ccuColCmd"]["supplyExhaustCmd"] = Json::Value(ccuCmd.supplyExhaustCmd);
+3
View File
@@ -8,4 +8,7 @@ ProcessConfig = pPowerManger
// logpath = /data/pPowerManger.log
// 数据库存储路径;不配置则默认当前目录 power_data.db
// dbpath = /data/power_data.db
// CCU 地址;不配置则默认 127.0.0.1:7000
// ccuhost = 127.0.0.1
// ccuport = 7000
}
+2 -4
View File
@@ -212,10 +212,8 @@ typedef struct {
unsigned char fcmode; // 模式设定:00H-无效, 01H-调试, 02H-自动, 03H-补给/排放
unsigned char fcuCmd; // 操控指令:00H-无效, 01H-自检, 02H-启动, 03H-停机, 04H-复位, 05H-紧急停机, 06H-补给, 07H-制氢预热, 08H-停机维护, 0AH-开机, 0BH-关机
unsigned char fcuPowerEfficiency; // 输出功率指令:0~250,分辨率0.1,单位kW,非净输出
unsigned char pitchData1; // 纵倾姿态数据1:分辨率0.1,单位°
unsigned char pitchData2; // 纵倾姿态数据2:分辨率0.1,单位°
unsigned char rollData1; // 横倾姿态数据1:分辨率0.1,单位°
unsigned char rollData2; // 横倾姿态数据2:分辨率0.1,单位°
short pitchData1; // 纵倾姿态数据1:int16,分辨率0.1,单位°(0824协议:姿态数据2已删除)
short rollData1; // 横倾姿态数据1:int16,分辨率0.1,单位°
unsigned char emergencyAllow; // 应急允许:Bit0允许降载, Bit1允许排气, Bit2~Bit7预留
unsigned short depth; // 潜深深度,单位:m
unsigned char supplyExhaustCmd; // 补给/排放指令:00H-无效, 01H-液氧加注, 02H-甲醇加注, 03H-液氧排空, 04H-甲醇排空, 05H-CO2排空, 06H-/, 07H-合金加注, 08H-氮气加注, 09H-纯水加注, 0AH-备用, 0BH-备用
-2
View File
@@ -286,9 +286,7 @@ const std::vector<SQLite::TableDesc>& SQLite::allTables()
COL(ccuColCmd, fcuCmd),
COL(ccuColCmd, fcuPowerEfficiency),
COL(ccuColCmd, pitchData1),
COL(ccuColCmd, pitchData2),
COL(ccuColCmd, rollData1),
COL(ccuColCmd, rollData2),
COL(ccuColCmd, emergencyAllow),
COL(ccuColCmd, depth),
COL(ccuColCmd, supplyExhaustCmd),
+7 -9
View File
@@ -52,8 +52,8 @@ static void testFrameLengths() {
// 2. FC 控制指令编解码往返
static void testFcControlRoundTrip() {
FcControlValue v;
v.mode = 2; v.cmd = 2; v.outputPower = 100; v.pitch1 = 35; v.pitch2 = -21;
v.roll1 = -12; v.roll2 = 48;
v.mode = 2; v.cmd = 2; v.outputPower = 100; v.pitch1 = 35;
v.roll1 = -12;
v.emergencyAllow = 0x03; v.depth = 150;
v.supplyCmd = 1; v.reservedCmd5 = 5; v.reservedCmd6 = 6; v.heartbeat = 7;
@@ -64,11 +64,11 @@ static void testFcControlRoundTrip() {
CHECK(frame[0] == 0x40 && frame[1] == 0x40);
CHECK(frame[4] == 0x18 && frame[5] == 0x00); // 域字节数 0x0018 小端
// 字节位置校验(数据域起点=帧头6):姿态各 int16 小端
// 字节位置校验(数据域起点=帧头6):姿态数据1 int16 小端,数据2字节预留恒0
CHECK(frame[6 + 3] == 0x23 && frame[6 + 4] == 0x00); // 纵倾数据1 = 35
CHECK(frame[6 + 5] == 0xEB && frame[6 + 6] == 0xFF); // 纵倾数据2 = -21 (0xFFEB)
CHECK(frame[6 + 5] == 0x00 && frame[6 + 6] == 0x00); // 预留(0827协议,原纵倾数据2)
CHECK(frame[6 + 7] == 0xF4 && frame[6 + 8] == 0xFF); // 横倾数据1 = -12 (0xFFF4)
CHECK(frame[6 + 9] == 0x30 && frame[6 + 10] == 0x00); // 横倾数据2 = 48
CHECK(frame[6 + 9] == 0x00 && frame[6 + 10] == 0x00); // 预留(0827协议,原横倾数据2)
FcControlValue out;
CHECK(msg.decode(frame, static_cast<void*>(&out)));
@@ -76,9 +76,7 @@ static void testFcControlRoundTrip() {
CHECK(out.cmd == 2);
CHECK(out.outputPower == 100);
CHECK(out.pitch1 == 35);
CHECK(out.pitch2 == -21);
CHECK(out.roll1 == -12);
CHECK(out.roll2 == 48);
CHECK(out.emergencyAllow == 0x03);
CHECK(out.depth == 150);
CHECK(out.supplyCmd == 1);
@@ -101,7 +99,7 @@ static void testFcStatusRoundTrip() {
v.methanol_total_use = 100;
v.cabin_temp1 = 25; v.h2_concentration1 = 500;
v.heartbeat = 9; v.emergency_cmd = 0x01;
v.reserved[0] = 0xAAAA; v.reserved[11] = 0xBBBB;
v.reserved[0] = 0xAAAA; v.remaining_generation = 0xBBBB;
FcStatusMessage msg;
std::vector<uint8_t> frame = msg.encode(v);
@@ -125,7 +123,7 @@ static void testFcStatusRoundTrip() {
CHECK(out.methanol_total_use == 100);
CHECK(out.cabin_temp1 == 25 && out.h2_concentration1 == 500);
CHECK(out.heartbeat == 9 && out.emergency_cmd == 0x01);
CHECK(out.reserved[0] == 0xAAAA && out.reserved[11] == 0xBBBB);
CHECK(out.reserved[0] == 0xAAAA && out.remaining_generation == 0xBBBB);
}
//--------------------------------------------------------------------------
+16 -6
View File
@@ -9,7 +9,7 @@
校验:
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 20B)到 16001
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 24B)到 16001
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
4. SQLite 记录了收/发原始帧
5. Web 页面可访问
@@ -62,6 +62,7 @@ def fc_status_frame():
p[17] = 70 # 液氧
struct.pack_into("<H", p, 24, 350) # 钯膜温度
struct.pack_into("<H", p, 68, 2500) # 舱室温度1
struct.pack_into("<H", p, 140, 4321) # 剩余发电量 MWh (字节147,148)
p[142] = 5 # 心跳
p[143] = 0x01 # 应急指令
return frame(0x0002, bytes(p), checksum=False)
@@ -74,6 +75,8 @@ def pm_control_frame():
p[8] = 2 # 模式 自动
p[9] = 2 # 操控 启动
p[10] = 120 # 输出功率 0.1kW
struct.pack_into("<h", p, 11, 123) # 纵倾姿态数据1 12.3°
struct.pack_into("<h", p, 13, -45) # 横倾姿态数据1 -4.5°
p[15] = 0x03 # 应急允许
struct.pack_into("<H", p, 16, 150) # 潜深
p[21] = 0x10 # 仪表锂电启动
@@ -173,14 +176,21 @@ def main():
else:
ok = False
# 校验 2:PM 操控转发为 FC 控制(20B)
fc_ctl = [d for d in fc_ctl_results if len(d) == 20]
print(f"[FC 控制转发] 收到 {len(fc_ctl)} 帧 (期望>=1, 每帧20B)")
# 校验 2:PM 操控转发为 FC 控制(24B,0827协议:姿态数据1转发,数据2字节预留恒0)
fc_ctl = [d for d in fc_ctl_results if len(d) == 24]
print(f"[FC 控制转发] 收到 {len(fc_ctl)} 帧 (期望>=1, 每帧24B)")
if fc_ctl:
d = fc_ctl[0]
cmd = d[7]; power = d[8]; hb = d[19]
print(f" -> cmd={cmd}, outputPower={power}, heartbeat={hb}")
cmd = d[7]; power = d[8]; hb = d[23]
pitch = struct.unpack_from("<h", d, 9)[0] # 字节10,11
roll = struct.unpack_from("<h", d, 13)[0] # 字节14,15
resv1 = struct.unpack_from("<H", d, 11)[0] # 字节12,13 预留
resv2 = struct.unpack_from("<H", d, 15)[0] # 字节16,17 预留
print(f" -> cmd={cmd}, outputPower={power}, pitch={pitch}, roll={roll}, "
f"resv1={resv1}, resv2={resv2}, heartbeat={hb}")
ok = ok and (cmd == 2) and (power == 120) and (hb == 7)
ok = ok and (pitch == 123) and (roll == -45)
ok = ok and (resv1 == 0) and (resv2 == 0)
else:
ok = False
+3 -1
View File
@@ -120,6 +120,8 @@ class FcSim:
struct.pack_into("<H", p, 116, int(900 + 100 * math.sin(self.t / 4)))
p[142] = self.heartbeat & 0xFF # 通信心跳 (字节149)
p[143] = 0 # 应急指令 (字节150)
# 剩余发电量 MWh (字节147-148, 0827协议由预留12改为):随发电缓慢消耗
struct.pack_into("<H", p, 140, int(500 - self.gen_time / 360.0))
return frame(0x0002, bytes(p), checksum=False)
def tick(self, dt):
@@ -156,7 +158,7 @@ class PmSim:
p[8] = 2 # 模式: 自动
p[9] = 2 # 操控: 启动
p[10] = self.power # 输出功率 (0.1kW)
# 纵倾姿态 (数据1=低8位/数据2=高8位, int16, 0.1°) / 横倾姿态
# 纵倾姿态数据1 / 横倾姿态数据1 (int16, 0.1°, 0827协议数据2字节预留)
pitch = int((3.0 + 1.5 * math.sin(time.time() / 8)) * 10)
roll = int((-1.0 + 0.5 * math.cos(time.time() / 6)) * 10)
struct.pack_into("<h", p, 11, pitch)