新增复合管控器(pCCU) + 惯导数据解析 + 设备控制页增强 + 0824协议文档归档
- 新增 pCCU 复合管控器程序(src/pCCU):FC/PM 双链路(UDP)、协议编解码器 (FcProtocol/PmProtocol/Frame/Message/MessageRegistry)、快照与落库(DbStore)、 内置 WebServer;挂入 src/CMakeLists.txt 与板卡 mission(h100.moos) - 惯导/深度数据:新增 uDevice_insData 消息解析,接入 UpperCommManager, pPowerMangerHost 注册监视,UpmsgTest 覆盖 - pPowerMangerHost 设备控制页增强(web/index.html) - 部署/运维脚本改为 4 个 systemd 服务(moosdb/pPowerManger/pPowerMangerHost/pCCU): deploy/clean-data/fetch-data 同步适配 - 协议文档:归档 0824 控制主机与复合管控器通信协议 xlsx 及关联 docx,移除旧 md - 测试:新增 pccu 单元/集成测试与 disSysTest Python 配电仿真 - 清理:移除未使用的 CCU 状态调试打印 coutMsg;.gitignore 补充编译产物/临时文件忽略
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
//============================================================================
|
||||
// pccuTest:pCCU 协议编解码 / 校验和 / 帧长度 / 链路分发 / 数据库 单元测试
|
||||
// 无第三方测试库,独立可执行。参照 test/until/testSqlitdb.cpp 的轻量风格。
|
||||
//============================================================================
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
#include "../../src/pCCU/protocol/FieldCodec.h"
|
||||
#include "../../src/pCCU/protocol/Frame.h"
|
||||
#include "../../src/pCCU/protocol/ChecksumPolicy.h"
|
||||
#include "../../src/pCCU/protocol/MessageRegistry.h"
|
||||
#include "../../src/pCCU/protocol/FcProtocol.h"
|
||||
#include "../../src/pCCU/protocol/PmProtocol.h"
|
||||
#include "../../src/pCCU/comm/LinkManager.h"
|
||||
#include "../../src/pCCU/comm/FcLinkManager.h"
|
||||
#include "../../src/pCCU/comm/PmLinkManager.h"
|
||||
#include "../../src/pCCU/store/DbStore.h"
|
||||
|
||||
using namespace ccu;
|
||||
|
||||
static int g_fail = 0;
|
||||
static int g_pass = 0;
|
||||
|
||||
#define CHECK(cond) do { \
|
||||
if (cond) { ++g_pass; } \
|
||||
else { ++g_fail; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } \
|
||||
} while (0)
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 1. 帧长度必须与协议文档一致
|
||||
static void testFrameLengths() {
|
||||
FcControlMessage fcm;
|
||||
FcStatusMessage fsm;
|
||||
PmControlMessage pcm;
|
||||
PmParamSetMessage pps;
|
||||
PmParamSetFbMessage ppf;
|
||||
PmStatusMessage psm;
|
||||
|
||||
CHECK(fcm.totalLength() == 20); // FC 控制指令 24B 文档表,实际 20B(域字节数0x0014)
|
||||
CHECK(fsm.totalLength() == 150); // FC 状态反馈 150B
|
||||
CHECK(pcm.totalLength() == 45); // PM 操控指令 45B
|
||||
CHECK(pps.totalLength() == 28); // PM 参数设定 28B
|
||||
CHECK(ppf.totalLength() == 12); // PM 参数反馈 12B
|
||||
CHECK(psm.totalLength() == 248); // PM 状态报文 248B
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 2. FC 控制指令编解码往返
|
||||
static void testFcControlRoundTrip() {
|
||||
FcControlValue v;
|
||||
v.mode = 2; v.cmd = 2; v.outputPower = 100; v.pitch = 35; v.roll = -12;
|
||||
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[0] == 0x40 && frame[1] == 0x40);
|
||||
|
||||
// 字节位置校验:纵倾数据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(高位)
|
||||
|
||||
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.emergencyAllow == 0x03);
|
||||
CHECK(out.depth == 150);
|
||||
CHECK(out.supplyCmd == 1);
|
||||
CHECK(out.reservedCmd5 == 5 && out.reservedCmd6 == 6);
|
||||
CHECK(out.heartbeat == 7);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 3. FC 状态编解码往返(关键字段抽查)
|
||||
static void testFcStatusRoundTrip() {
|
||||
FcStatusValue v;
|
||||
v.fc_mode = 2; v.fc_status = 6; v.fc_fault_level = 0;
|
||||
v.fault_level_1 = 0x0102; v.fault_level_2 = 0x0304;
|
||||
v.fault_level_3 = 0x0506; v.fault_level_4 = 0x0708;
|
||||
v.total_generation_time = 12345;
|
||||
v.generation_power = 23456;
|
||||
v.hydrogen_capacity = 80; v.liquid_oxygen_capacity = 70;
|
||||
v.palladium_temp = 350; v.buffer_tank_pressure = 200;
|
||||
v.dcdc1_in_voltage = 12; v.dcdc_out_current = 33;
|
||||
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;
|
||||
|
||||
FcStatusMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
CHECK(frame.size() == 150);
|
||||
|
||||
// 字节位置校验(对照 0821 docx):发电功率在字节21-22(offset 14),心跳在字节149(offset 142)
|
||||
CHECK(frame[6 + 12] == 0); // 故障等级 offset12
|
||||
CHECK(frame[6 + 14] == 0xA0 && frame[6 + 15] == 0x5B); // 发电功率 23456 -> 0x5BA0 小端
|
||||
CHECK(frame[6 + 142] == 9); // 心跳 offset142
|
||||
CHECK(frame[6 + 143] == 0x01);// 应急指令 offset143
|
||||
|
||||
FcStatusValue out;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&out)));
|
||||
CHECK(out.fc_mode == 2 && out.fc_status == 6);
|
||||
CHECK(out.fault_level_1 == 0x0102 && out.fault_level_4 == 0x0708);
|
||||
CHECK(out.total_generation_time == 12345);
|
||||
CHECK(out.generation_power == 23456);
|
||||
CHECK(out.hydrogen_capacity == 80 && out.liquid_oxygen_capacity == 70);
|
||||
CHECK(out.palladium_temp == 350);
|
||||
CHECK(out.dcdc1_in_voltage == 12 && out.dcdc_out_current == 33);
|
||||
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);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 4. PM 操控指令编解码往返
|
||||
static void testPmControlRoundTrip() {
|
||||
PmControlValue v;
|
||||
v.year = 2026; v.month = 8; v.day = 26; v.hour = 17; v.minute = 30; v.second = 5;
|
||||
v.millisecond10 = 3;
|
||||
v.mode = 2; v.cmd = 2; v.outputPower = 120;
|
||||
v.pitch = 35; v.roll = -12;
|
||||
v.emergencyAllow = 0x03; v.depth = 200;
|
||||
v.supplyCmd = 1; v.reservedCmd5 = 5; v.reservedCmd6 = 6;
|
||||
v.insBatCmd = 0x10; v.dynBatCmd = 0x20; v.dynBatPower = 300;
|
||||
v.heartbeat = 11; v.hostState = 0xAA;
|
||||
v.reserved1 = 0x11223344; v.reserved2 = 0x55667788;
|
||||
|
||||
PmControlMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
CHECK(frame.size() == 45);
|
||||
|
||||
PmControlValue out;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&out)));
|
||||
CHECK(out.year == 2026 && out.month == 8 && out.day == 26);
|
||||
CHECK(out.hour == 17 && out.minute == 30 && out.second == 5);
|
||||
CHECK(out.millisecond10 == 3);
|
||||
CHECK(out.mode == 2 && out.cmd == 2 && out.outputPower == 120);
|
||||
CHECK(out.pitch == 35 && out.roll == -12);
|
||||
CHECK(out.depth == 200);
|
||||
CHECK(out.insBatCmd == 0x10 && out.dynBatCmd == 0x20 && out.dynBatPower == 300);
|
||||
CHECK(out.heartbeat == 11 && out.hostState == 0xAA);
|
||||
CHECK(out.reserved1 == 0x11223344 && out.reserved2 == 0x55667788);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 5. PM 状态报文编解码往返(抽查关键字段,覆盖各分组)
|
||||
static void testPmStatusRoundTrip() {
|
||||
PmStatusValue v;
|
||||
v.fc_mode = 2; v.fc_status = 6;
|
||||
v.fault_level_1 = 1; v.fault_level_2 = 2; v.fault_level_3 = 3; v.fault_level_4 = 4;
|
||||
v.total_generation_time = 500; v.fc_fault_level = 0;
|
||||
v.output_power_limit = 30000; v.generation_power = 100;
|
||||
v.hydrogen_capacity = 80; v.liquid_oxygen_capacity = 70;
|
||||
v.main_pipe_pressure = 500; v.aux_pipe_pressure = 400;
|
||||
v.dcdc_out_voltage = 240; v.dcdc_out_current = 100;
|
||||
v.cabin_temp1 = 2500; v.cabin_temp2 = 2400;
|
||||
v.h2_concentration1 = 300; v.o2_concentration1 = 200;
|
||||
v.emergency_battery1_voltage = 24; v.emergency_battery2_voltage = 26;
|
||||
v.ins_soc = 60; v.dyn_soc = 55;
|
||||
v.ins_voltage_link = 4800; v.dyn_voltage_link = 4900;
|
||||
v.ins_current = 120; v.dyn_current = 130;
|
||||
v.device_online_flag1 = 0x0F; v.device_online_flag2 = 0x07;
|
||||
v.heartbeat = 42; v.emergency_cmd = 0x01;
|
||||
v.dyn_alarm_flag[0] = 0xAA; v.ins_alarm_flag[5] = 0x55;
|
||||
|
||||
PmStatusMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
CHECK(frame.size() == 248);
|
||||
|
||||
PmStatusValue out;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&out)));
|
||||
CHECK(out.fc_mode == 2 && out.fc_status == 6);
|
||||
CHECK(out.fault_level_1 == 1 && out.fault_level_4 == 4);
|
||||
CHECK(out.total_generation_time == 500 && out.fc_fault_level == 0);
|
||||
CHECK(out.output_power_limit == 30000 && out.generation_power == 100);
|
||||
CHECK(out.hydrogen_capacity == 80 && out.liquid_oxygen_capacity == 70);
|
||||
CHECK(out.main_pipe_pressure == 500 && out.aux_pipe_pressure == 400);
|
||||
CHECK(out.dcdc_out_voltage == 240 && out.dcdc_out_current == 100);
|
||||
CHECK(out.cabin_temp1 == 2500 && out.h2_concentration1 == 300);
|
||||
CHECK(out.emergency_battery1_voltage == 24 && out.emergency_battery2_voltage == 26);
|
||||
CHECK(out.ins_soc == 60 && out.dyn_soc == 55);
|
||||
CHECK(out.ins_voltage_link == 4800 && out.dyn_voltage_link == 4900);
|
||||
CHECK(out.device_online_flag1 == 0x0F && out.device_online_flag2 == 0x07);
|
||||
CHECK(out.heartbeat == 42 && out.emergency_cmd == 0x01);
|
||||
CHECK(out.dyn_alarm_flag[0] == 0xAA && out.ins_alarm_flag[5] == 0x55);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 6. PM 校验和:正确帧通过,篡改字节校验失败
|
||||
static void testChecksum() {
|
||||
PmControlValue v;
|
||||
v.mode = 2; v.cmd = 3; v.heartbeat = 1;
|
||||
|
||||
PmControlMessage msg;
|
||||
std::vector<uint8_t> frame = msg.encode(v);
|
||||
PmControlValue tmp;
|
||||
CHECK(msg.decode(frame, static_cast<void*>(&tmp))); // 校验通过
|
||||
|
||||
// 篡改数据域中间字节
|
||||
std::vector<uint8_t> bad = frame;
|
||||
bad[10] ^= 0xFF; // 数据域某字节
|
||||
CHECK(!msg.decode(bad, static_cast<void*>(&tmp)));
|
||||
|
||||
// 篡改帧头长度
|
||||
std::vector<uint8_t> badLen = frame;
|
||||
badLen[4] = 0x01; // length 低字节改小 -> 0x012D != 45
|
||||
CHECK(!msg.decode(badLen, static_cast<void*>(&tmp)));
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 7. 链路分发:FC 链路收到状态帧 -> 回调解码
|
||||
static void testLinkDispatch() {
|
||||
// 注册表按链路隔离:FC 与 PM 共用 0x0001/0x0002 不应冲突
|
||||
MessageRegistry fcReg;
|
||||
registerFcMessages(fcReg);
|
||||
MessageRegistry pmReg;
|
||||
registerPmMessages(pmReg);
|
||||
|
||||
CHECK(fcReg.size() == 2);
|
||||
CHECK(pmReg.size() == 4);
|
||||
CHECK(fcReg.find(0x0001)->id() == 0x0001);
|
||||
CHECK(pmReg.find(0x0001)->id() == 0x0001); // id 重叠但隔离
|
||||
|
||||
// 用 LinkManager 的 handleIncoming 模拟收帧
|
||||
FcStatusValue fv;
|
||||
fv.fc_status = 6; fv.heartbeat = 8;
|
||||
FcStatusMessage fsMsg;
|
||||
std::vector<uint8_t> frame = fsMsg.encode(fv);
|
||||
|
||||
LinkManager lm("test_fc", 0);
|
||||
registerFcMessages(lm.registry());
|
||||
|
||||
bool got = false;
|
||||
lm.setOnMessage([&](Message* m, const std::vector<uint8_t>& f) {
|
||||
if (m && m->id() == 0x0002) {
|
||||
FcStatusValue out;
|
||||
if (m->decode(f, static_cast<void*>(&out))) {
|
||||
got = (out.fc_status == 6 && out.heartbeat == 8);
|
||||
}
|
||||
}
|
||||
});
|
||||
CHECK(lm.handleIncoming(frame));
|
||||
CHECK(got);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 8. 数据库落库与查询
|
||||
static void testDbStore() {
|
||||
std::string path = "test_pccu.db";
|
||||
std::remove(path.c_str());
|
||||
{
|
||||
DbStore db(path);
|
||||
CHECK(db.open());
|
||||
|
||||
std::vector<uint8_t> frame = {0x40,0x40,0x01,0x00,0x14,0x00, 1,2,3,4,5,6,7,8,9,10,11,12,13,14};
|
||||
db.onRawFrame(0, 0x0001, "fc", frame, true);
|
||||
db.onRawFrame(1, 0x0004, "pm", frame, false);
|
||||
|
||||
CHECK(db.count() == 2);
|
||||
|
||||
auto rows = db.queryRecent("", -1, 0, 10);
|
||||
CHECK(rows.size() == 2);
|
||||
CHECK(rows[0].msgId == 0x0004 || rows[0].msgId == 0x0001);
|
||||
CHECK(rows[0].checksumOk == 0); // 最近一条是 pm 的 false
|
||||
|
||||
auto fcRows = db.queryRecent("fc", 0, 0, 10);
|
||||
CHECK(fcRows.size() == 1);
|
||||
CHECK(fcRows[0].msgId == 0x0001 && fcRows[0].direction == 0);
|
||||
|
||||
db.close();
|
||||
}
|
||||
std::remove(path.c_str());
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
int main() {
|
||||
testFrameLengths();
|
||||
testFcControlRoundTrip();
|
||||
testFcStatusRoundTrip();
|
||||
testPmControlRoundTrip();
|
||||
testPmStatusRoundTrip();
|
||||
testChecksum();
|
||||
testLinkDispatch();
|
||||
testDbStore();
|
||||
|
||||
std::printf("\n==== pccuTest: %d passed, %d failed ====\n", g_pass, g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pCCU 集成测试:验证真实 UDP 链路上的完整数据流。
|
||||
|
||||
拓扑:
|
||||
测试脚本(FC模拟) --16000--> pCCU(fc_local) [FC状态 0x0002]
|
||||
测试脚本(PM模拟) --16002--> pCCU(pm_local) [PM操控 0x0001, PM参数设定 0x0002]
|
||||
pCCU --16001--> 测试脚本 [FC控制 0x0001 转发]
|
||||
pCCU --16003--> 测试脚本 [PM状态 0x0004, PM参数反馈 0x0003]
|
||||
|
||||
校验:
|
||||
1. FC 状态收到并整合,pCCU 周期发送 PM 状态(0x0004, 248B)到 16003
|
||||
2. PM 操控(0x0001)转发为 FC 控制(0x0001, 20B)到 16001
|
||||
3. PM 参数设定(0x0002)触发参数反馈(0x0003, 12B)到 16003
|
||||
4. SQLite 记录了收/发原始帧
|
||||
5. Web 页面可访问
|
||||
"""
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
# ---- 配置 ----
|
||||
FC_STATUS_PORT = 16000
|
||||
FC_CTRL_RECV_PORT = 16001
|
||||
PM_CTRL_PORT = 16002
|
||||
PM_STATUS_RECV_PORT = 16003
|
||||
WEB_PORT = 18081
|
||||
DB_PATH = "/tmp/pccu_it_test.db"
|
||||
MOOSDB_EXE = "/usr/local/bin/MOOSDB"
|
||||
PCCU_EXE = "/home/zjk/project/H100PowerManger/bin/pCCU"
|
||||
MISSION = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pccu_it.moos")
|
||||
|
||||
# 帧头
|
||||
START = bytes([0x40, 0x40])
|
||||
|
||||
|
||||
def frame(msg_id, payload, checksum=True):
|
||||
total = 6 + len(payload) + (4 if checksum else 0)
|
||||
hdr = START + struct.pack("<HH", msg_id, total)
|
||||
body = hdr + payload
|
||||
if checksum:
|
||||
cks = sum(body) & 0xFFFFFFFF
|
||||
return body + struct.pack("<I", cks)
|
||||
return body
|
||||
|
||||
|
||||
def fc_status_frame():
|
||||
# payload 144B,仅填关键字段,其余为0
|
||||
p = bytearray(144)
|
||||
p[0] = 2 # 运行模式 自动
|
||||
p[1] = 6 # 运行状态 运行
|
||||
struct.pack_into("<H", p, 2, 0x0102) # 一级故障码
|
||||
struct.pack_into("<H", p, 10, 1234) # 累积发电时间
|
||||
p[12] = 0 # 故障等级
|
||||
struct.pack_into("<H", p, 14, 2000) # 系统发电功率 0.01kW
|
||||
p[16] = 80 # 储氢
|
||||
p[17] = 70 # 液氧
|
||||
struct.pack_into("<H", p, 24, 350) # 钯膜温度
|
||||
struct.pack_into("<H", p, 68, 2500) # 舱室温度1
|
||||
p[142] = 5 # 心跳
|
||||
p[143] = 0x01 # 应急指令
|
||||
return frame(0x0002, bytes(p), checksum=False)
|
||||
|
||||
|
||||
def pm_control_frame():
|
||||
p = bytearray(35)
|
||||
struct.pack_into("<H", p, 0, 2026)
|
||||
p[2], p[3], p[4], p[5], p[6], p[7] = 8, 26, 10, 30, 0, 0
|
||||
p[8] = 2 # 模式 自动
|
||||
p[9] = 2 # 操控 启动
|
||||
p[10] = 120 # 输出功率 0.1kW
|
||||
p[15] = 0x03 # 应急允许
|
||||
struct.pack_into("<H", p, 16, 150) # 潜深
|
||||
p[21] = 0x10 # 仪表锂电启动
|
||||
p[22] = 0x20 # 动力锂电关闭
|
||||
struct.pack_into("<H", p, 23, 300) # 动力电池功率
|
||||
p[25] = 7 # 心跳
|
||||
p[26] = 0xAA # 主机状态
|
||||
return frame(0x0001, bytes(p))
|
||||
|
||||
|
||||
def pm_paramset_frame():
|
||||
p = bytearray(18)
|
||||
p[0], p[1], p[2] = 75, 85, 93
|
||||
p[5], p[6], p[7] = 75, 85, 93
|
||||
return frame(0x0002, bytes(p))
|
||||
|
||||
|
||||
def recv_loop(sock, label, results, msg_ids, timeout):
|
||||
"""持续接收指定 msg_id 的帧,记录到 results"""
|
||||
sock.settimeout(timeout)
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
try:
|
||||
data, addr = sock.recvfrom(2048)
|
||||
except socket.timeout:
|
||||
break
|
||||
except OSError:
|
||||
break
|
||||
if len(data) < 6:
|
||||
continue
|
||||
mid = struct.unpack_from("<H", data, 2)[0]
|
||||
if mid in msg_ids:
|
||||
results.append(data)
|
||||
|
||||
|
||||
def main():
|
||||
# 清理
|
||||
for f in (DB_PATH, DB_PATH + "-wal", DB_PATH + "-shm", "/tmp/pccu_it_test.log"):
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
|
||||
procs = []
|
||||
try:
|
||||
# 1. 启动 MOOSDB
|
||||
moosdb = subprocess.Popen([MOOSDB_EXE, "--moos", "--port", "9000"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
procs.append(moosdb)
|
||||
time.sleep(1.0)
|
||||
|
||||
# 2. 启动 pCCU
|
||||
pccu = subprocess.Popen([PCCU_EXE, MISSION],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
procs.append(pccu)
|
||||
time.sleep(2.0)
|
||||
if pccu.poll() is not None:
|
||||
print("FAIL: pCCU exited early, code", pccu.returncode)
|
||||
return 1
|
||||
|
||||
# 3. 启动接收监听(FC 控制转发 16001,PM 状态 16003)
|
||||
fc_ctl_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
fc_ctl_recv.bind(("127.0.0.1", FC_CTRL_RECV_PORT))
|
||||
pm_status_recv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
pm_status_recv.bind(("127.0.0.1", PM_STATUS_RECV_PORT))
|
||||
|
||||
fc_ctl_results, pm_status_results = [], []
|
||||
t1 = threading.Thread(target=recv_loop,
|
||||
args=(fc_ctl_recv, "fc-ctl", fc_ctl_results, {0x0001}, 12))
|
||||
t2 = threading.Thread(target=recv_loop,
|
||||
args=(pm_status_recv, "pm-status", pm_status_results, {0x0004, 0x0003}, 12))
|
||||
t1.start(); t2.start()
|
||||
|
||||
# 4. 发送 FC 状态(周期模拟)
|
||||
fc_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
for _ in range(3):
|
||||
fc_send.sendto(fc_status_frame(), ("127.0.0.1", FC_STATUS_PORT))
|
||||
time.sleep(0.5)
|
||||
|
||||
# 5. 发送 PM 操控指令 + 参数设定
|
||||
pm_send = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
pm_send.sendto(pm_control_frame(), ("127.0.0.1", PM_CTRL_PORT))
|
||||
time.sleep(0.5)
|
||||
pm_send.sendto(pm_paramset_frame(), ("127.0.0.1", PM_CTRL_PORT))
|
||||
|
||||
t1.join(); t2.join()
|
||||
|
||||
ok = True
|
||||
|
||||
# 校验 1:PM 状态报文(248B)周期发送
|
||||
pm_status = [d for d in pm_status_results if len(d) == 248]
|
||||
print(f"[PM 状态报文] 收到 {len(pm_status)} 帧 (期望>=1, 每帧248B)")
|
||||
if len(pm_status) >= 1:
|
||||
d = pm_status[0]
|
||||
total = struct.unpack_from("<H", d, 4)[0]
|
||||
mode = d[6]; status = d[7]
|
||||
print(f" -> length={total}, mode={mode}, status={status}, size={len(d)}")
|
||||
ok = ok and (total == 248) and (mode == 2) and (status == 6)
|
||||
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)")
|
||||
if fc_ctl:
|
||||
d = fc_ctl[0]
|
||||
cmd = d[7]; power = d[8]; hb = d[19]
|
||||
print(f" -> cmd={cmd}, outputPower={power}, heartbeat={hb}")
|
||||
ok = ok and (cmd == 2) and (power == 120) and (hb == 7)
|
||||
else:
|
||||
ok = False
|
||||
|
||||
# 校验 3:PM 参数反馈(12B)
|
||||
fb = [d for d in pm_status_results if len(d) == 12]
|
||||
print(f"[PM 参数反馈] 收到 {len(fb)} 帧 (期望>=1, 每帧12B)")
|
||||
if fb:
|
||||
flag = fb[0][6]
|
||||
print(f" -> flag=0x{flag:02X}")
|
||||
ok = ok and (flag == 0x10)
|
||||
else:
|
||||
ok = False
|
||||
|
||||
# 校验 4:数据库有记录
|
||||
time.sleep(1.0)
|
||||
db_count = -1
|
||||
if os.path.exists(DB_PATH):
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
db_count = conn.execute("SELECT COUNT(*) FROM comm_log").fetchone()[0]
|
||||
conn.close()
|
||||
print(f"[数据库] comm_log 记录数 = {db_count}")
|
||||
ok = ok and (db_count >= 6) # 3条FC状态 + 1条PM操控 + 1条PM参数 + 周期PM状态(至少1)
|
||||
|
||||
# 校验 5:Web 页面
|
||||
web_ok = False
|
||||
try:
|
||||
resp = urllib.request.urlopen(f"http://127.0.0.1:{WEB_PORT}/", timeout=3)
|
||||
html = resp.read().decode()
|
||||
web_ok = ("pCCU" in html)
|
||||
print(f"[Web] 页面可访问, 长度={len(html)}")
|
||||
except Exception as e:
|
||||
print(f"[Web] 访问失败: {e}")
|
||||
ok = ok and web_ok
|
||||
|
||||
print("\n==== 集成测试 " + ("PASSED" if ok else "FAILED") + " ====")
|
||||
return 0 if ok else 1
|
||||
|
||||
finally:
|
||||
for p in reversed(procs):
|
||||
try:
|
||||
p.terminate()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
for p in procs:
|
||||
try:
|
||||
p.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,24 @@
|
||||
//============================================================================
|
||||
// 测试用 pCCU 配置(集成测试专用,端口避开生产默认值)
|
||||
//============================================================================
|
||||
|
||||
ProcessConfig = pCCU
|
||||
{
|
||||
AppTick = 4
|
||||
CommsTick = 4
|
||||
|
||||
// 测试端口
|
||||
fc_local_port = 16000 // 监听 FC 状态
|
||||
fc_remote_ip = 127.0.0.1
|
||||
fc_remote_port = 16001 // FC 控制器(测试监听此端口收转发指令)
|
||||
|
||||
pm_local_port = 16002 // 监听 PM 指令
|
||||
pm_remote_ip = 127.0.0.1
|
||||
pm_remote_port = 16003 // 控制主机(测试监听此端口收状态报文)
|
||||
|
||||
dbpath = /tmp/pccu_it_test.db
|
||||
logpath = /tmp/pccu_it_test.log
|
||||
|
||||
web_port = 18081 // 避开 pPowerMangerHost(18080)
|
||||
web_enable = true
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pCCU 演示模拟器:模拟燃料电池(FC) 与 控制主机(PM) 两端。
|
||||
|
||||
- 每 1s 向 pCCU 的 fc_local 端口发送 FC 状态帧(0x0002, 150B),数据带波动
|
||||
- 每 5s 向 pCCU 的 pm_local 端口发送 PM 操控指令(0x0001) 与参数设定(0x0002)
|
||||
- 监听 pCCU 的 fc_remote / pm_remote 端口,打印 pCCU 转发的 FC 控制与 PM 状态反馈
|
||||
|
||||
用法: python3 pccu_sim.py [fc_status_port] [pm_cmd_port] [fc_ctl_listen] [pm_status_listen]
|
||||
默认端口与 test/pccu/pccu_it.moos 一致。
|
||||
"""
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import math
|
||||
import sys
|
||||
|
||||
# 默认端口(与 pccu_it.moos 一致)
|
||||
FC_STATUS_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 16000
|
||||
PM_CMD_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 16002
|
||||
FC_CTL_LISTEN = int(sys.argv[3]) if len(sys.argv) > 3 else 16001
|
||||
PM_STAT_LISTEN = int(sys.argv[4]) if len(sys.argv) > 4 else 16003
|
||||
|
||||
START = bytes([0x40, 0x40])
|
||||
|
||||
|
||||
def frame(msg_id, payload, checksum=True):
|
||||
total = 6 + len(payload) + (4 if checksum else 0)
|
||||
body = START + struct.pack("<HH", msg_id, total) + payload
|
||||
if checksum:
|
||||
return body + struct.pack("<I", sum(body) & 0xFFFFFFFF)
|
||||
return body
|
||||
|
||||
|
||||
class FcSim:
|
||||
"""燃料电池控制器:周期发送带波动的状态帧"""
|
||||
|
||||
def __init__(self, port):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.port = port
|
||||
self.t = 0.0
|
||||
self.hydrogen = 85.0
|
||||
self.lo2 = 70.0
|
||||
self.gen_time = 0.0
|
||||
self.heartbeat = 0
|
||||
self.fc_status = 4 # 从就绪开始
|
||||
self.status_timer = 0.0
|
||||
|
||||
def build(self):
|
||||
p = bytearray(144)
|
||||
p[0] = 2 # 运行模式: 自动
|
||||
p[1] = self.fc_status # 运行状态
|
||||
struct.pack_into("<H", p, 10, int(self.gen_time)) # 累积发电时间 0.1h
|
||||
p[12] = 0 # 故障等级 无 (字节19)
|
||||
# 字节20 隐式保留
|
||||
# 发电功率 0.01kW:500kW + 正弦波动 (字节21-22)
|
||||
struct.pack_into("<H", p, 14, int(50000 + 3000 * math.sin(self.t / 5)))
|
||||
p[16] = int(self.hydrogen) # 储氢 % (字节23)
|
||||
p[17] = int(self.lo2) # 液氧 % (字节24)
|
||||
# FC 单片电压 (10mV) (字节25-30)
|
||||
p[18] = 75; p[20] = 74; p[21] = 73; p[22] = 72; p[24] = 72
|
||||
p[19] = 12; p[20] = 13; p[23] = 11 # 电压位置
|
||||
# 钯膜温度 0.1℃:350℃ + 波动 (字节31-32)
|
||||
struct.pack_into("<H", p, 24, int(3500 + 20 * math.sin(self.t / 3)))
|
||||
# 缓冲罐压力 0.1kPa (字节33-34)
|
||||
struct.pack_into("<H", p, 26, int(1800 + 100 * math.sin(self.t / 7)))
|
||||
# 反应器压力 0.001MPa (字节39-40)
|
||||
struct.pack_into("<H", p, 32, int(1200 + 50 * math.sin(self.t / 4)))
|
||||
# 电动阀开度 0.1% (字节41-42)
|
||||
struct.pack_into("<H", p, 34, int(500 + 100 * math.sin(self.t / 2)))
|
||||
# DC/DC 输出电压 0.1V: 240V (字节51-52)
|
||||
struct.pack_into("<H", p, 44, int(2400 + 20 * math.sin(self.t / 6)))
|
||||
# DC/DC 输出电流 0.1A (字节53-54)
|
||||
struct.pack_into("<H", p, 46, int(1200 + 400 * math.sin(self.t / 5)))
|
||||
p[48] = 55 # 控制电源电压 0.25V (字节55)
|
||||
p[49] = 30 # 辅电输出 0.125V (字节56)
|
||||
# 甲醇累计使用量 0.1kg:缓慢增加 (字节57-58)
|
||||
struct.pack_into("<H", p, 50, int(self.gen_time * 3))
|
||||
struct.pack_into("<H", p, 52, int(120 + 20 * math.sin(self.t / 4))) # 进料量 mL/min (字节59-60)
|
||||
# 氧/氢/配重水箱液位 0.01mm (字节61-66)
|
||||
struct.pack_into("<H", p, 54, int(5000 + 200 * math.sin(self.t / 5)))
|
||||
struct.pack_into("<H", p, 56, int(4200 + 150 * math.sin(self.t / 6)))
|
||||
struct.pack_into("<H", p, 58, int(3000 + 100 * math.sin(self.t / 7)))
|
||||
# 尾气装置进气/排气压力 0.01MPa (字节67-70)
|
||||
struct.pack_into("<H", p, 60, int(200 + 10 * math.sin(self.t / 3)))
|
||||
struct.pack_into("<H", p, 62, int(150 + 8 * math.sin(self.t / 4)))
|
||||
# 舱室压力 0.01kPa:101.3kPa + 波动 (字节71-74)
|
||||
struct.pack_into("<H", p, 64, int(10130 + 20 * math.sin(self.t / 8)))
|
||||
struct.pack_into("<H", p, 66, int(10120 + 20 * math.sin(self.t / 9)))
|
||||
# 舱室温度 0.01℃:25℃ + 波动 (字节75-78)
|
||||
struct.pack_into("<H", p, 68, int(2500 + 30 * math.sin(self.t / 10)))
|
||||
struct.pack_into("<H", p, 70, int(2480 + 25 * math.sin(self.t / 11)))
|
||||
# 舱室湿度 0.01%RH (字节79-82)
|
||||
struct.pack_into("<H", p, 72, int(4500 + 300 * math.sin(self.t / 12)))
|
||||
struct.pack_into("<H", p, 74, int(4400 + 300 * math.sin(self.t / 13)))
|
||||
# H2 浓度 0.01%LEL:0.5% 波动 (字节83-88)
|
||||
struct.pack_into("<H", p, 76, int(50 + 20 * math.sin(self.t / 3)))
|
||||
struct.pack_into("<H", p, 78, int(45 + 15 * math.sin(self.t / 4)))
|
||||
struct.pack_into("<H", p, 80, int(40 + 10 * math.sin(self.t / 5)))
|
||||
# O2 浓度 0.01%Vol:21% (字节89-92)
|
||||
struct.pack_into("<H", p, 82, int(2100 + 50 * math.sin(self.t / 6)))
|
||||
struct.pack_into("<H", p, 84, int(2050 + 40 * math.sin(self.t / 7)))
|
||||
# 甲醇浓度 0.01%LEL (字节93-96)
|
||||
struct.pack_into("<H", p, 86, int(30 + 10 * math.sin(self.t / 8)))
|
||||
struct.pack_into("<H", p, 88, int(25 + 8 * math.sin(self.t / 9)))
|
||||
# 火焰探测器 (字节97-98)
|
||||
p[90] = 0; p[91] = 0
|
||||
# 应急上浮深度/时间 (字节99-102)
|
||||
struct.pack_into("<H", p, 92, 100)
|
||||
struct.pack_into("<H", p, 94, 60)
|
||||
# 尾气运行频率 0.01Hz (字节103-104)
|
||||
struct.pack_into("<H", p, 96, int(5000 + 300 * math.sin(self.t / 4)))
|
||||
# 一体化罐液氧罐压力 0.01MPa (字节113-114)
|
||||
struct.pack_into("<H", p, 106, int(300 + 20 * math.sin(self.t / 5)))
|
||||
struct.pack_into("<H", p, 108, int(200 + 15 * math.sin(self.t / 6)))
|
||||
struct.pack_into("<H", p, 110, int(8000 + 200 * math.sin(self.t / 7)))
|
||||
# 供氢/供氧流量 0.01 L/min (字节119-124)
|
||||
struct.pack_into("<H", p, 112, int(1500 + 200 * math.sin(self.t / 4)))
|
||||
struct.pack_into("<H", p, 114, int(1800 + 250 * math.sin(self.t / 4)))
|
||||
struct.pack_into("<H", p, 116, int(900 + 100 * math.sin(self.t / 4)))
|
||||
p[142] = self.heartbeat & 0xFF # 通信心跳 (字节149)
|
||||
p[143] = 0 # 应急指令 (字节150)
|
||||
return frame(0x0002, bytes(p), checksum=False)
|
||||
|
||||
def tick(self, dt):
|
||||
self.t += dt
|
||||
self.gen_time += dt / 360.0 # 每秒累积 0.1h/360 ≈ 1s 的发电时间
|
||||
self.hydrogen -= 0.0005 # 缓慢消耗
|
||||
self.lo2 -= 0.0004
|
||||
self.heartbeat += 1
|
||||
# 状态推进: 就绪(4) -> 启动(5) -> 运行(6)
|
||||
self.status_timer += dt
|
||||
if self.fc_status == 4 and self.status_timer > 3:
|
||||
self.fc_status = 5; self.status_timer = 0
|
||||
elif self.fc_status == 5 and self.status_timer > 3:
|
||||
self.fc_status = 6; self.status_timer = 0
|
||||
return self.build()
|
||||
|
||||
|
||||
class PmSim:
|
||||
"""控制主机:周期发送操控指令,监听 pCCU 状态反馈"""
|
||||
|
||||
def __init__(self, cmd_port, stat_listen):
|
||||
self.cmd_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.cmd_port = cmd_port
|
||||
self.stat_listen = stat_listen
|
||||
self.power = 100
|
||||
self.heartbeat = 0
|
||||
|
||||
def control_frame(self):
|
||||
p = bytearray(35)
|
||||
now = time.localtime()
|
||||
struct.pack_into("<H", p, 0, now.tm_year)
|
||||
p[2] = now.tm_mon; p[3] = now.tm_mday; p[4] = now.tm_hour
|
||||
p[5] = now.tm_min; p[6] = now.tm_sec; p[7] = (int(time.time() * 100) % 100)
|
||||
p[8] = 2 # 模式: 自动
|
||||
p[9] = 2 # 操控: 启动
|
||||
p[10] = self.power # 输出功率 (0.1kW)
|
||||
# 纵倾姿态 (数据1=低8位/数据2=高8位, int16, 0.1°) / 横倾姿态
|
||||
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)
|
||||
struct.pack_into("<h", p, 13, roll)
|
||||
p[15] = 0x03 # 应急允许: 降载+排气
|
||||
struct.pack_into("<H", p, 16, 150) # 潜深 m
|
||||
p[18] = 0 # 补给/排放
|
||||
p[21] = 0x10 # 仪表锂电启动
|
||||
p[22] = 0x10 # 动力锂电启动
|
||||
struct.pack_into("<H", p, 23, 300) # 动力电池功率配置 kW
|
||||
p[25] = self.heartbeat & 0xFF # 通信心跳
|
||||
p[26] = 0xAA # 主机状态: 运行
|
||||
return frame(0x0001, bytes(p))
|
||||
|
||||
def param_frame(self):
|
||||
p = bytearray(18)
|
||||
p[0], p[1], p[2] = 75, 85, 93 # 仪表 SOC 门限
|
||||
p[3] = 80 # 仪表输出功率
|
||||
p[5], p[6], p[7] = 75, 85, 93 # 动力 SOC 门限
|
||||
p[8] = 80 # 动力输出功率
|
||||
return frame(0x0002, bytes(p))
|
||||
|
||||
def send_cmd(self):
|
||||
self.cmd_sock.sendto(self.control_frame(), ("127.0.0.1", self.cmd_port))
|
||||
self.heartbeat += 1
|
||||
self.power = 100 + ((self.power - 100 + 20) % 80) # 100->120->140 循环
|
||||
|
||||
|
||||
def listen_loop(port, label, msg_ids, name_of):
|
||||
"""监听 pCCU 输出端口,打印收到的帧关键字段"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
except OSError as e:
|
||||
print(f"[{label}] 监听端口 {port} 失败: {e}")
|
||||
return
|
||||
sock.settimeout(1.0)
|
||||
print(f"[{label}] 监听 {label} 输出端口 {port}")
|
||||
while True:
|
||||
try:
|
||||
data, addr = sock.recvfrom(2048)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
if len(data) < 6:
|
||||
continue
|
||||
mid = struct.unpack_from("<H", data, 2)[0]
|
||||
if mid not in msg_ids:
|
||||
continue
|
||||
total = struct.unpack_from("<H", data, 4)[0]
|
||||
if mid == 0x0001 and len(data) >= 20: # FC 控制转发
|
||||
cmd = data[7]; power = data[8]; hb = data[19]
|
||||
print(f"[{label}] ← FC控制转发 0x0001: cmd={cmd} power={power} 心跳={hb} ({len(data)}B)")
|
||||
elif mid == 0x0004 and len(data) >= 248: # PM 状态报文
|
||||
mode = data[6]; status = data[7]; hb = data[242]
|
||||
genp = struct.unpack_from("<H", data, 6 + 15)[0] # 发电功率 payload偏移15
|
||||
print(f"[{label}] ← PM状态 0x0004: mode={mode} status={status} 心跳={hb} 功率={genp*0.01:.2f}kW ({len(data)}B)")
|
||||
elif mid == 0x0003 and len(data) >= 12: # 参数反馈
|
||||
flag = data[6]
|
||||
print(f"[{label}] ← 参数反馈 0x0003: flag=0x{flag:02X} ({len(data)}B)")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 64)
|
||||
print(f"pCCU 演示模拟器启动")
|
||||
print(f" FC 状态 -> pCCU:{FC_STATUS_PORT} (0x0002 150B)")
|
||||
print(f" PM 指令 -> pCCU:{PM_CMD_PORT} (0x0001 45B / 0x0002 28B)")
|
||||
print(f" 监听 FC 控制转发 于 {FC_CTL_LISTEN}")
|
||||
print(f" 监听 PM 状态报文 于 {PM_STAT_LISTEN}")
|
||||
print("=" * 64)
|
||||
|
||||
fc = FcSim(FC_STATUS_PORT)
|
||||
pm = PmSim(PM_CMD_PORT, PM_STAT_LISTEN)
|
||||
|
||||
threading.Thread(target=listen_loop,
|
||||
args=(FC_CTL_LISTEN, "FC", {0x0001}, None), daemon=True).start()
|
||||
threading.Thread(target=listen_loop,
|
||||
args=(PM_STAT_LISTEN, "PM", {0x0004, 0x0003}, None), daemon=True).start()
|
||||
|
||||
last_cmd_t = 0
|
||||
last_print_t = 0
|
||||
try:
|
||||
while True:
|
||||
# FC 状态 1Hz
|
||||
f = fc.tick(1.0)
|
||||
fc.sock.sendto(f, ("127.0.0.1", FC_STATUS_PORT))
|
||||
# PM 指令每 5s
|
||||
if time.time() - last_cmd_t >= 5.0:
|
||||
pm.send_cmd()
|
||||
pm.cmd_sock.sendto(pm.param_frame(), ("127.0.0.1", PM_CMD_PORT))
|
||||
print(f"[PM] → 发送操控指令 power={pm.power}, 参数设定")
|
||||
last_cmd_t = time.time()
|
||||
# 每秒打印一行 FC 发送摘要
|
||||
if time.time() - last_print_t >= 5.0:
|
||||
last_print_t = time.time()
|
||||
print(f"[FC] → 状态: status={fc.fc_status} 心跳={fc.heartbeat} "
|
||||
f"储氢={fc.hydrogen:.1f}% 液氧={fc.lo2:.1f}% 发电时间={fc.gen_time:.1f}h")
|
||||
time.sleep(1.0)
|
||||
except KeyboardInterrupt:
|
||||
print("\n模拟器退出")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""验证 pCCU WebSocket 推送的 JSON 快照格式与分组完整性。"""
|
||||
import socket, base64, os, struct, time, json, sys
|
||||
|
||||
HOST, PORT = "127.0.0.1", 18081
|
||||
|
||||
class WsClient:
|
||||
def __init__(self):
|
||||
self.s = socket.create_connection((HOST, PORT), timeout=5)
|
||||
key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (f"GET /ws HTTP/1.1\r\nHost: {HOST}:{PORT}\r\n"
|
||||
f"Upgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n")
|
||||
self.s.sendall(req.encode())
|
||||
self.buf = b""
|
||||
# 读取 HTTP 101 响应头
|
||||
while b"\r\n\r\n" not in self.buf:
|
||||
chunk = self.s.recv(4096)
|
||||
if not chunk:
|
||||
raise RuntimeError("连接关闭")
|
||||
self.buf += chunk
|
||||
head, self.buf = self.buf.split(b"\r\n\r\n", 1)
|
||||
if b"101" not in head.split(b"\r\n")[0]:
|
||||
raise RuntimeError("WebSocket 升级失败: " + head.decode(errors="replace"))
|
||||
|
||||
def _read(self, n):
|
||||
while len(self.buf) < n:
|
||||
chunk = self.s.recv(4096)
|
||||
if not chunk:
|
||||
raise RuntimeError("连接关闭")
|
||||
self.buf += chunk
|
||||
out, self.buf = self.buf[:n], self.buf[n:]
|
||||
return out
|
||||
|
||||
def recv_text(self, timeout=6):
|
||||
self.s.settimeout(timeout)
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
if len(self.buf) < 2:
|
||||
try:
|
||||
chunk = self.s.recv(4096)
|
||||
except socket.timeout:
|
||||
return None
|
||||
if not chunk:
|
||||
return None
|
||||
self.buf += chunk
|
||||
continue
|
||||
b1, b2 = self.buf[0], self.buf[1]
|
||||
opcode = b1 & 0x0F
|
||||
length = b2 & 0x7F
|
||||
hdr_len = 2
|
||||
self.buf = self.buf[2:] # 消费帧头前2字节
|
||||
if length == 126:
|
||||
length = struct.unpack(">H", self._read(2))[0]
|
||||
hdr_len += 2
|
||||
elif length == 127:
|
||||
length = struct.unpack(">Q", self._read(8))[0]
|
||||
hdr_len += 8
|
||||
masked = (b2 & 0x80) != 0
|
||||
mask = self._read(4) if masked else b""
|
||||
payload = self._read(length)
|
||||
if opcode == 1: # text
|
||||
if masked:
|
||||
payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
|
||||
return payload.decode("utf-8", "replace")
|
||||
# ping/pong/close 跳过,继续
|
||||
return None
|
||||
|
||||
def main():
|
||||
try:
|
||||
ws = WsClient()
|
||||
except Exception as e:
|
||||
print("FAIL: WS 连接失败:", e)
|
||||
return 1
|
||||
text = ws.recv_text()
|
||||
if not text:
|
||||
print("FAIL: 未收到快照")
|
||||
return 1
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except Exception as e:
|
||||
print("FAIL: 快照非合法JSON:", text[:200])
|
||||
return 1
|
||||
ok = True
|
||||
for key in ("fc", "pmCmd", "links", "fcStatusCount", "pmControlCount", "logs"):
|
||||
ok = ok and key in data
|
||||
print(f"PASS: 快照 JSON 有效, 分组keys={sorted(data.keys())}")
|
||||
print(f" fc.fc_status={data['fc']['fc_status']}, fc.fc_mode={data['fc']['fc_mode']}, "
|
||||
f"heartbeat={data['fc']['heartbeat']}")
|
||||
print(f" links={json.dumps(data['links'], ensure_ascii=False)}")
|
||||
print(f" logs条数={len(data['logs'])}, fcStatusCount={data['fcStatusCount']}")
|
||||
ws.s.close()
|
||||
return 0 if ok else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user