新增复合管控器(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:
zjk
2026-08-26 22:43:10 +08:00
parent e0488d513b
commit 54c32a180a
79 changed files with 8731 additions and 1398 deletions
+301
View File
@@ -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;
}
+237
View File
@@ -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())
+24
View File
@@ -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
}
+265
View File
@@ -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()
+96
View File
@@ -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())