2026-08-27 调试完成版本
- FC 控制指令域修正:payload 14→18 字节,纵倾/横倾姿态拆分为数据1/数据2 - WebServer 广播改为待发队列模式,修复 mongoose 跨线程调用问题 - build-board.sh 增加板卡组网说明(/23 掩码方案)、pCCU 服务部署与 mission 同步 - 同步 0824 协议文档更新
This commit is contained in:
+5
-3
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+22
-10
@@ -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);
|
||||
}
|
||||
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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user