新增复合管控器(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
+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())