2026-08-27 调试完成版本

- FC 控制指令域修正:payload 14→18 字节,纵倾/横倾姿态拆分为数据1/数据2
- WebServer 广播改为待发队列模式,修复 mongoose 跨线程调用问题
- build-board.sh 增加板卡组网说明(/23 掩码方案)、pCCU 服务部署与 mission 同步
- 同步 0824 协议文档更新
This commit is contained in:
zjk
2026-08-27 21:51:36 +08:00
parent 54c32a180a
commit c8becd1f6c
13 changed files with 162 additions and 67 deletions
+38 -2
View File
@@ -23,6 +23,31 @@
#
# 前置: 板卡已配免密 SSH,且有编译环境(cmake/g++/MOOS/jsoncpp),
# 源码已能本地编译(本脚本只负责同步+板卡编译)。
#
# !!!! 板卡网络配置(2026-08-26 联调定型,勿随意修改) !!!!
#
# 板卡与 FC 的组网为"单网卡 + /23 掩码"方案(依据《超滑FC和复合管控器
# 通讯协议-0821》1.1.1:CCU 192.168.0.140/23 <-> FC 192.168.1.162):
#
# enP3p49s0 192.168.0.223/23 (255.255.254.0) <- 唯一业务网口,接交换机
# /23 使 192.168.0.x 与 192.168.1.x 同子网直达,
# FC(192.168.1.162) 无需网关即可互通。
# NM 配置档案: "有线连接 1"(静态 manual,已持久化)
# enP4p65s0 已弃用(旧双网卡方案的 FC 专口,物理链路已断)
# 回环 lo pPowerManger(5001) <-> pCCU(7000) 同机 UDP 互通
#
# 关联端口:
# FC 状态: FC:7000 -> 板卡:6000 (pCCU 监听)
# FC 控制: 板卡:6000 -> FC:7000 (pCCU 发送)
# PM 指令: pPowerManger:5001 -> pCCU:7000 (127.0.0.1,见 PowerManger.h CCUHOST)
# PM 状态: pCCU:7000 -> pPowerManger:5001 (127.0.0.1)
#
# 排障提示:
# - 若收不到 FC 状态,先在板卡上 `ethtool enP3p49s0` 看 Link detected,
# 再 `ping 192.168.1.162`;ping 不通通常是物理链路/交换机问题,
# 不要改回 /24 掩码或启用 enP4p65s0 来"修"网络。
# - 改 IP/掩码须用 `nmcli con mod "有线连接 1" ...` 持久化,
# 并保持掩码 /23。
#=======================================================================
set -uo pipefail
@@ -38,7 +63,7 @@ JOBS=""
DO_START=0
DO_CLEAN=0
SERVICES=(moosdb pPowerManger pPowerMangerHost)
SERVICES=(moosdb pPowerManger pPowerMangerHost pCCU)
info() { printf '\033[1;36m[build-board]\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m[build-board]\033[0m %s\n' "$*"; }
@@ -92,6 +117,9 @@ rsync -az --delete \
--exclude='bin/' \
--exclude='.git/' \
--exclude='.cache/' \
--exclude='.kilo/' \
--exclude='.vscode/' \
--exclude='docs/' \
--exclude='reports/' \
--exclude='*.log' \
--exclude='*.db' \
@@ -99,6 +127,13 @@ rsync -az --delete \
"${PROJECT_ROOT}/" "${SSH_TARGET}:${BOARD_SRC}/" || { err "rsync 源码失败"; exit 1; }
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; }
ok "mission 已同步"
fi
#-------------------------------------------------------------------
# 3. 板卡原生编译
#-------------------------------------------------------------------
@@ -137,7 +172,8 @@ info "部署产物到 ${BOARD_BIN}/ ..."
${SSH} "${SSH_TARGET}" "mkdir -p ${BOARD_BIN} && \
cp -f ${BOARD_SRC}/bin/pPowerManger ${BOARD_BIN}/pPowerManger && \
cp -f ${BOARD_SRC}/bin/pPowerMangerHost ${BOARD_BIN}/pPowerMangerHost && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost" \
cp -f ${BOARD_SRC}/bin/pCCU ${BOARD_BIN}/pCCU && \
chmod +x ${BOARD_BIN}/pPowerManger ${BOARD_BIN}/pPowerMangerHost ${BOARD_BIN}/pCCU" \
|| { err "部署产物失败"; exit 1; }
ok "产物已部署"
+5 -3
View File
@@ -218,13 +218,15 @@ 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
// 映射并转发到 FC(1.1.1:姿态为双通道 int16,PM 单通道填数据1)
FcControlValue f;
f.mode = c.mode;
f.cmd = c.cmd;
f.outputPower = c.outputPower;
f.pitch = c.pitch;
f.roll = c.roll;
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;
+4 -2
View File
@@ -172,8 +172,10 @@ JsonVal fcControlJson(const FcControlValue& v) {
put(j, "mode", v.mode);
put(j, "cmd", v.cmd);
put(j, "outputPower", v.outputPower);
j["pitch"] = JsonVal(static_cast<int>(v.pitch)); // 有符号 int16,×0.1°
j["roll"] = JsonVal(static_cast<int>(v.roll));
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);
+46 -23
View File
@@ -12,24 +12,45 @@ void registerFcMessages(MessageRegistry& reg) {
//============================================================================
// 0x0001 控制指令域编解码
// 数据域 14 字节,偏移 0~13(对应文档字节 7~20)
// 数据域 18 字节(字节7~24),偏移 0~17;整包 24 字节
//============================================================================
namespace {
// 数据域内偏移(相对数据域起点,offset = 文档字节号 - 7)
enum : size_t {
CO_MODE = 0, // 模式设定 (字节7)
CO_CMD = 1, // 操控指令 (字节8)
CO_OUT_POWER = 2, // 输出功率指令 (字节9)
CO_PITCH1 = 3, // 纵倾姿态数据1 int16 (字节10,11)
CO_PITCH2 = 5, // 纵倾姿态数据2 int16 (字节12,13)
CO_ROLL1 = 7, // 横倾姿态数据1 int16 (字节14,15)
CO_ROLL2 = 9, // 横倾姿态数据2 int16 (字节16,17)
CO_EMER_ALLOW = 11, // 应急允许 (字节18)
CO_DEPTH = 12, // 潜深深度 u16 (字节19,20)
CO_SUPPLY_CMD = 14, // 补给/排放指令 (字节21)
CO_RESERVED5 = 15, // 预留指令5 (字节22)
CO_RESERVED6 = 16, // 预留指令6 (字节23)
CO_HEARTBEAT = 17, // 通信心跳 (字节24)
};
} // namespace
std::vector<uint8_t> FcControlMessage::encode(const void* obj) const {
const FcControlValue* v = static_cast<const FcControlValue*>(obj);
std::vector<uint8_t> payload(payloadLength(), 0);
FieldCodec::putU8(payload, 0, v->mode);
FieldCodec::putU8(payload, 1, v->cmd);
FieldCodec::putU8(payload, 2, v->outputPower);
FieldCodec::putU16(payload, 3, static_cast<uint16_t>(v->pitch)); // 数据1=低位 / 数据2=高位
FieldCodec::putU16(payload, 5, static_cast<uint16_t>(v->roll));
FieldCodec::putU8(payload, 7, v->emergencyAllow);
FieldCodec::putU16(payload, 8, v->depth);
FieldCodec::putU8(payload, 10, v->supplyCmd);
FieldCodec::putU8(payload, 11, v->reservedCmd5);
FieldCodec::putU8(payload, 12, v->reservedCmd6);
FieldCodec::putU8(payload, 13, v->heartbeat);
FieldCodec::putU8(payload, CO_MODE, v->mode);
FieldCodec::putU8(payload, CO_CMD, v->cmd);
FieldCodec::putU8(payload, CO_OUT_POWER, v->outputPower);
FieldCodec::putU16(payload, CO_PITCH1, static_cast<uint16_t>(v->pitch1));
FieldCodec::putU16(payload, CO_PITCH2, static_cast<uint16_t>(v->pitch2));
FieldCodec::putU16(payload, CO_ROLL1, static_cast<uint16_t>(v->roll1));
FieldCodec::putU16(payload, CO_ROLL2, static_cast<uint16_t>(v->roll2));
FieldCodec::putU8(payload, CO_EMER_ALLOW, v->emergencyAllow);
FieldCodec::putU16(payload, CO_DEPTH, v->depth);
FieldCodec::putU8(payload, CO_SUPPLY_CMD, v->supplyCmd);
FieldCodec::putU8(payload, CO_RESERVED5, v->reservedCmd5);
FieldCodec::putU8(payload, CO_RESERVED6, v->reservedCmd6);
FieldCodec::putU8(payload, CO_HEARTBEAT, v->heartbeat);
return Frame::build(id(), payload, checksum());
}
@@ -39,17 +60,19 @@ bool FcControlMessage::decode(const std::vector<uint8_t>& frame, void* obj) cons
FcControlValue* v = static_cast<FcControlValue*>(obj);
size_t o = FRAME_HEADER_LEN; // 数据域起点
v->mode = FieldCodec::getU8(frame, o + 0);
v->cmd = FieldCodec::getU8(frame, o + 1);
v->outputPower = FieldCodec::getU8(frame, o + 2);
v->pitch = static_cast<int16_t>(FieldCodec::getU16(frame, o + 3));
v->roll = static_cast<int16_t>(FieldCodec::getU16(frame, o + 5));
v->emergencyAllow = FieldCodec::getU8(frame, o + 7);
v->depth = FieldCodec::getU16(frame, o + 8);
v->supplyCmd = FieldCodec::getU8(frame, o + 10);
v->reservedCmd5 = FieldCodec::getU8(frame, o + 11);
v->reservedCmd6 = FieldCodec::getU8(frame, o + 12);
v->heartbeat = FieldCodec::getU8(frame, o + 13);
v->mode = FieldCodec::getU8(frame, o + CO_MODE);
v->cmd = FieldCodec::getU8(frame, o + CO_CMD);
v->outputPower = FieldCodec::getU8(frame, o + CO_OUT_POWER);
v->pitch1 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_PITCH1));
v->pitch2 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_PITCH2));
v->roll1 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_ROLL1));
v->roll2 = static_cast<int16_t>(FieldCodec::getU16(frame, o + CO_ROLL2));
v->emergencyAllow = FieldCodec::getU8(frame, o + CO_EMER_ALLOW);
v->depth = FieldCodec::getU16(frame, o + CO_DEPTH);
v->supplyCmd = FieldCodec::getU8(frame, o + CO_SUPPLY_CMD);
v->reservedCmd5 = FieldCodec::getU8(frame, o + CO_RESERVED5);
v->reservedCmd6 = FieldCodec::getU8(frame, o + CO_RESERVED6);
v->heartbeat = FieldCodec::getU8(frame, o + CO_HEARTBEAT);
return true;
}
+12 -9
View File
@@ -28,14 +28,16 @@ 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
int16_t pitch1 = 0; // 纵倾姿态数据1 int16,分辨率0.1°(字节10,11)
int16_t pitch2 = 0; // 纵倾姿态数据2 int16,分辨率0.1°(字节12,13)
int16_t roll1 = 0; // 横倾姿态数据1 int16,分辨率0.1°(字节14,15)
int16_t roll2 = 0; // 横倾姿态数据2 int16,分辨率0.1°(字节16,17)
uint8_t emergencyAllow = 0; // 应急允许:Bit0 允许降载 / Bit1 允许排气(字节18)
uint16_t depth = 0; // 潜深深度,单位 m(字节19,20)
uint8_t supplyCmd = 0; // 补给/排放指令 00~0B(字节21)
uint8_t reservedCmd5 = 0; // 预留指令5(字节22)
uint8_t reservedCmd6 = 0; // 预留指令6(字节23)
uint8_t heartbeat = 0; // 通信心跳,每次+1(字节24)
};
//--------------------------------------------------------------------------
@@ -132,7 +134,8 @@ 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; }
// 1.1.1 控制指令域:数据域 18 字节(字节7~24),整包 24 字节(域字节数 0x0018)
size_t payloadLength() const override { return 18; }
std::vector<uint8_t> encode(const void* obj) const override;
bool decode(const std::vector<uint8_t>& frame, void* obj) const override;
+3 -1
View File
@@ -12,7 +12,9 @@ bool Message::validateFrame(const std::vector<uint8_t>& frame,
FrameHeader h;
if (!Frame::parseHeader(frame, h)) return false;
if (h.id != expectId) return false;
if (h.length != expectTotalLen) return false;
// 协议规定域字节数=整个包长度;联调发现 FC 设备该字段填 120 而非 150,
// 与文档不符但不影响数据域布局,若实际包体足够容纳数据域则放行
if (h.length != expectTotalLen && frame.size() < expectTotalLen) return false;
// 校验和(长度不足由 verify 内部判断)
if (!policy.verify(frame)) return false;
+20 -8
View File
@@ -43,16 +43,14 @@ void WebServer::handleEvent(struct mg_connection* c, int ev, void* ev_data) {
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
} else if (ev == MG_EV_WS_OPEN) {
std::lock_guard<std::mutex> lock(m_wsMutex);
// Web 线程内:先构建欢迎快照(可能涉及 DB 查询,勿持锁),再登记连接
std::string welcome;
if (m_onOpen) welcome = m_onOpen();
m_wsConnections.push_back(c);
if (m_onOpen) {
std::string welcome = m_onOpen();
if (!welcome.empty()) {
mg_ws_send(c, welcome.c_str(), welcome.size(), WEBSOCKET_OP_TEXT);
}
}
} else if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) {
std::lock_guard<std::mutex> lock(m_wsMutex);
for (auto it = m_wsConnections.begin(); it != m_wsConnections.end(); ++it) {
if (*it == c) {
m_wsConnections.erase(it);
@@ -103,7 +101,7 @@ void WebServer::stop() {
m_running = false;
if (m_thread.joinable()) m_thread.join();
std::lock_guard<std::mutex> lock(m_wsMutex);
// Web 线程已退出,此处触碰连接列表安全
for (auto c : m_wsConnections) {
mg_ws_send(c, "", 0, WEBSOCKET_OP_CLOSE);
}
@@ -118,15 +116,29 @@ bool WebServer::serverThreadCB(void* pParam) {
bool WebServer::serverThreadFunc() {
while (m_running) {
mg_mgr_poll(&m_mgr, 200);
// 仅在本线程(poll 间隙)发送,避免跨线程操作 mongoose 连接
flushPending();
}
return true;
}
void WebServer::broadcast(const std::string& text) {
std::lock_guard<std::mutex> lock(m_wsMutex);
void WebServer::flushPending() {
std::string text;
{
std::lock_guard<std::mutex> lock(m_pendingMutex);
text = std::move(m_pending);
m_pending.clear();
}
if (text.empty()) return;
for (auto c : m_wsConnections) {
mg_ws_send(c, text.c_str(), text.size(), WEBSOCKET_OP_TEXT);
}
}
void WebServer::broadcast(const std::string& text) {
// 只投递到队列,实际发送在 Web 线程 flushPending() 中完成
std::lock_guard<std::mutex> lock(m_pendingMutex);
m_pending = text;
}
} // namespace ccu
+7 -2
View File
@@ -32,6 +32,8 @@ public:
bool isRunning() const { return m_running; }
// 向所有 WebSocket 客户端广播文本
// 线程安全:mongoose 非线程安全,本接口只把文本放入待发队列,
// 由 Web 线程在 mg_mgr_poll 返回后统一发送(禁止跨线程直接 mg_ws_send)
void broadcast(const std::string& text);
// 新客户端连接时调用,返回初始快照
@@ -44,14 +46,17 @@ private:
static bool serverThreadCB(void* pParam);
bool serverThreadFunc();
void handleEvent(struct mg_connection* c, int ev, void* ev_data);
// Web 线程内:把待发队列中的快照推给所有 WS 客户端(仅本线程触碰连接列表)
void flushPending();
struct mg_mgr m_mgr;
int m_port = 0;
std::atomic<bool> m_running{false};
std::thread m_thread;
std::mutex m_wsMutex;
std::vector<mg_connection*> m_wsConnections;
std::mutex m_pendingMutex; // 保护 m_pending
std::string m_pending; // 最新待广播快照(新快照覆盖旧快照)
std::vector<mg_connection*> m_wsConnections; // 仅 Web 线程访问
std::function<std::string()> m_onOpen;
std::function<std::string(const std::string&, const std::string&)> m_apiHandler;
};
+8 -3
View File
@@ -172,6 +172,9 @@ function hydroCard(d){
h+=row('电动阀开度', (d.electric_valve_open*0.1).toFixed(1)+'%');
h+=row('甲醇累计使用量', (d.methanol_total_use*0.1).toFixed(1)+' kg');
h+=row('甲醇溶液进料量', d.methanol_feed+' mL/min');
h+=row('氧侧生成水箱液位', (d.oxygen_side_water_level*0.1).toFixed(1)+' mm');
h+=row('氢侧生成水箱液位', (d.hydrogen_side_water_level*0.1).toFixed(1)+' mm');
h+=row('配重水箱液位', (d.ballast_water_level*0.1).toFixed(1)+' mm');
h+=row('I#FC最低单片电压', (d.fc1_min_cell_voltage*10)+' mV (位置'+d.fc1_min_cell_pos+')');
h+=row('I#FC平均单片电压', (d.fc1_avg_cell_voltage*10)+' mV');
h+=row('2#FC最低单片电压', (d.fc2_min_cell_voltage*10)+' mV (位置'+d.fc2_min_cell_pos+')');
@@ -211,7 +214,7 @@ function tankCard(d){
let h='';
h+=row('液氧罐压力', (d.tank_lo2_pressure*0.01).toFixed(2)+' MPa');
h+=row('二氧化碳压力', (d.tank_co2_pressure*0.01).toFixed(2)+' MPa');
h+=row('液氧罐液位', (d.tank_lo2_level*0.01).toFixed(2)+' mm');
h+=row('液氧罐液位', (d.tank_lo2_level*0.01).toFixed(2)+' m');
h+=row('合金供氢流量', (d.alloy_h2_flow*0.01).toFixed(2)+' L/min');
h+=row('FC供氢流量', (d.fc_h2_flow*0.01).toFixed(2)+' L/min');
h+=row('FC供氧流量', (d.fc_o2_flow*0.01).toFixed(2)+' L/min');
@@ -321,8 +324,10 @@ function fcCmdCard(d){
h+=row('模式设定', parsed(d.mode, modeText(d.mode)));
h+=row('操控指令', parsed(d.cmd, cmdText(d.cmd)));
h+=row('输出功率指令', d.outputPower+' (×0.1kW)');
h+=row('纵倾姿态', (d.pitch*0.1).toFixed(1)+'°');
h+=row('横倾姿态', (d.roll*0.1).toFixed(1)+'°');
h+=row('纵倾姿态数据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)));
+3 -2
View File
@@ -40,8 +40,9 @@
#include "faultCode.h"
#define IPORT 5001
#define CCUPORT 7000
#define CCUHOST "192.168.0.140"
// #define CCUHOST "127.0.0.0"
// #define CCUHOST "192.168.0.140"
// pCCU 与本程序同机部署时用回环地址;127.0.0.0 是网络号,内核不路由
#define CCUHOST "127.0.0.1"
#define MAX_UDP_PKT_SIZE 65535
#define SKEW_TOLERANCE 5
+14 -10
View File
@@ -40,7 +40,7 @@ static void testFrameLengths() {
PmParamSetFbMessage ppf;
PmStatusMessage psm;
CHECK(fcm.totalLength() == 20); // FC 控制指令 24B 文档表,实际 20B(域字节数0x0014)
CHECK(fcm.totalLength() == 24); // FC 控制指令 24B(1.1.1:域字节数0x0018)
CHECK(fsm.totalLength() == 150); // FC 状态反馈 150B
CHECK(pcm.totalLength() == 45); // PM 操控指令 45B
CHECK(pps.totalLength() == 28); // PM 参数设定 28B
@@ -52,29 +52,33 @@ static void testFrameLengths() {
// 2. FC 控制指令编解码往返
static void testFcControlRoundTrip() {
FcControlValue v;
v.mode = 2; v.cmd = 2; v.outputPower = 100; v.pitch = 35; v.roll = -12;
v.mode = 2; v.cmd = 2; v.outputPower = 100; v.pitch1 = 35; v.pitch2 = -21;
v.roll1 = -12; v.roll2 = 48;
v.emergencyAllow = 0x03; v.depth = 150;
v.supplyCmd = 1; v.reservedCmd5 = 5; v.reservedCmd6 = 6; v.heartbeat = 7;
FcControlMessage msg;
std::vector<uint8_t> frame = msg.encode(v);
CHECK(frame.size() == 20);
CHECK(frame.size() == 24);
CHECK(frame[0] == 0x40 && frame[1] == 0x40);
CHECK(frame[4] == 0x18 && frame[5] == 0x00); // 域字节数 0x0018 小端
// 字节位置校验:纵倾数据1=低位(字节10=0x23=35), 数据2=高位(字节11=0x00)
CHECK(frame[6 + 3] == 0x23); // 姿态数据1(低位)
CHECK(frame[6 + 4] == 0x00); // 姿态数据2(高位)
CHECK(frame[6 + 5] == 0xF4); // 横倾数据1(低位, -12 = 0xFFF4 小端)
CHECK(frame[6 + 6] == 0xFF); // 横倾数据2(高位)
// 字节位置校验(数据域起点=帧头6):姿态各 int16 小端
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 + 7] == 0xF4 && frame[6 + 8] == 0xFF); // 横倾数据1 = -12 (0xFFF4)
CHECK(frame[6 + 9] == 0x30 && frame[6 + 10] == 0x00); // 横倾数据2 = 48
FcControlValue out;
CHECK(msg.decode(frame, static_cast<void*>(&out)));
CHECK(out.mode == 2);
CHECK(out.cmd == 2);
CHECK(out.outputPower == 100);
CHECK(out.pitch == 35);
CHECK(out.roll == -12);
CHECK(out.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);