diff --git a/docs/控制主机与复合管控器通信协议- 0824.xlsx b/docs/控制主机与复合管控器通信协议- 0824.xlsx index 5ff1dc4..1b8801c 100644 Binary files a/docs/控制主机与复合管控器通信协议- 0824.xlsx and b/docs/控制主机与复合管控器通信协议- 0824.xlsx differ diff --git a/docs/超滑FC和复合管控器通讯协议 - 0821.docx b/docs/超滑FC和复合管控器通讯协议 - 0821.docx index 7356d48..e1dda24 100644 Binary files a/docs/超滑FC和复合管控器通讯协议 - 0821.docx and b/docs/超滑FC和复合管控器通讯协议 - 0821.docx differ diff --git a/scripts/build-board.sh b/scripts/build-board.sh index 436b733..5974d2a 100755 --- a/scripts/build-board.sh +++ b/scripts/build-board.sh @@ -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 "产物已部署" diff --git a/src/pCCU/CCU.cpp b/src/pCCU/CCU.cpp index 441c1fd..092adfb 100644 --- a/src/pCCU/CCU.cpp +++ b/src/pCCU/CCU.cpp @@ -218,13 +218,15 @@ void CCU::handlePmMessage(Message* msg, const std::vector& 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; diff --git a/src/pCCU/core/SnapshotBuilder.cpp b/src/pCCU/core/SnapshotBuilder.cpp index da26839..a4b7f71 100644 --- a/src/pCCU/core/SnapshotBuilder.cpp +++ b/src/pCCU/core/SnapshotBuilder.cpp @@ -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(v.pitch)); // 有符号 int16,×0.1° - j["roll"] = JsonVal(static_cast(v.roll)); + j["pitch1"] = JsonVal(static_cast(v.pitch1)); // 有符号 int16,×0.1° + j["pitch2"] = JsonVal(static_cast(v.pitch2)); + j["roll1"] = JsonVal(static_cast(v.roll1)); + j["roll2"] = JsonVal(static_cast(v.roll2)); put(j, "emergencyAllow", v.emergencyAllow); put(j, "depth", v.depth); put(j, "supplyCmd", v.supplyCmd); diff --git a/src/pCCU/protocol/FcProtocol.cpp b/src/pCCU/protocol/FcProtocol.cpp index b0f3184..3a7d95f 100644 --- a/src/pCCU/protocol/FcProtocol.cpp +++ b/src/pCCU/protocol/FcProtocol.cpp @@ -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 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); + 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(v->pitch1)); + FieldCodec::putU16(payload, CO_PITCH2, static_cast(v->pitch2)); + FieldCodec::putU16(payload, CO_ROLL1, static_cast(v->roll1)); + FieldCodec::putU16(payload, CO_ROLL2, static_cast(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& frame, void* obj) cons 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); + 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(FieldCodec::getU16(frame, o + CO_PITCH1)); + v->pitch2 = static_cast(FieldCodec::getU16(frame, o + CO_PITCH2)); + v->roll1 = static_cast(FieldCodec::getU16(frame, o + CO_ROLL1)); + v->roll2 = static_cast(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; } diff --git a/src/pCCU/protocol/FcProtocol.h b/src/pCCU/protocol/FcProtocol.h index d42c111..1a755b1 100644 --- a/src/pCCU/protocol/FcProtocol.h +++ b/src/pCCU/protocol/FcProtocol.h @@ -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 encode(const void* obj) const override; bool decode(const std::vector& frame, void* obj) const override; diff --git a/src/pCCU/protocol/Message.cpp b/src/pCCU/protocol/Message.cpp index 2af0ec5..d8fe855 100644 --- a/src/pCCU/protocol/Message.cpp +++ b/src/pCCU/protocol/Message.cpp @@ -12,7 +12,9 @@ bool Message::validateFrame(const std::vector& 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; diff --git a/src/pCCU/web/WebServer.cpp b/src/pCCU/web/WebServer.cpp index 4720b53..c756c59 100644 --- a/src/pCCU/web/WebServer.cpp +++ b/src/pCCU/web/WebServer.cpp @@ -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 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); - } + 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); @@ -103,7 +101,7 @@ void WebServer::stop() { m_running = false; if (m_thread.joinable()) m_thread.join(); - std::lock_guard 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 lock(m_wsMutex); +void WebServer::flushPending() { + std::string text; + { + std::lock_guard 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 lock(m_pendingMutex); + m_pending = text; +} + } // namespace ccu \ No newline at end of file diff --git a/src/pCCU/web/WebServer.h b/src/pCCU/web/WebServer.h index 4a4dc7a..fe05865 100644 --- a/src/pCCU/web/WebServer.h +++ b/src/pCCU/web/WebServer.h @@ -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 m_running{false}; std::thread m_thread; - std::mutex m_wsMutex; - std::vector m_wsConnections; + std::mutex m_pendingMutex; // 保护 m_pending + std::string m_pending; // 最新待广播快照(新快照覆盖旧快照) + std::vector m_wsConnections; // 仅 Web 线程访问 std::function m_onOpen; std::function m_apiHandler; }; diff --git a/src/pCCU/web/pages/index.h b/src/pCCU/web/pages/index.h index 965cf4c..4c205ef 100644 --- a/src/pCCU/web/pages/index.h +++ b/src/pCCU/web/pages/index.h @@ -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))); diff --git a/src/pPowerManger/PowerManger.h b/src/pPowerManger/PowerManger.h index aee5f25..626b761 100644 --- a/src/pPowerManger/PowerManger.h +++ b/src/pPowerManger/PowerManger.h @@ -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 diff --git a/test/pccu/pccuTest.cpp b/test/pccu/pccuTest.cpp index 3de3891..83fa833 100644 --- a/test/pccu/pccuTest.cpp +++ b/test/pccu/pccuTest.cpp @@ -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 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(&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);