Files
H100PowerManger/test/pccu/pccu_sim.py
T
zjk 54c32a180a 新增复合管控器(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 补充编译产物/临时文件忽略
2026-08-26 22:43:10 +08:00

265 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()