Files
H100PowerManger/test/pccu/pccu_sim.py
T
zjk 2590282aed
build-test-deploy / ci (push) Successful in 1h13m13s
0827协议修订:姿态数据2改预留 + 状态帧新增剩余发电量 + CCU地址可配置
FC协议(0827 docx):
- 控制指令域:字节12,13/16,17由纵倾/横倾姿态数据2改为预留(编码恒0、解码忽略)
- 状态反馈域:字节147,148由预留12改为剩余发电量(MWh),贯通JSON/网页显示
- PM→CCU链路:姿态字段4xuchar改为2xshort(数据1),结构体大小与线上布局不变
- 同步更新:CCU转发映射、SnapshotBuilder、pCCU网页、fuelcell网页、SQLite列描述、单测/模拟器
- 修正集成测试遗留断言(20B→24B、心跳d[19]→d[23])并增强预留字节校验

CCU地址可配置:
- pPowerManger 支持 moos 配置项 ccuhost/ccuport(默认127.0.0.1:7000)
- build-board.sh 增加 --ccu-host/--ccu-port 参数

文档:FC协议文档更新为0827版,清理过期协议docx
2026-08-28 00:09:17 +08:00

267 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)
# 剩余发电量 MWh (字节147-148, 0827协议由预留12改为):随发电缓慢消耗
struct.pack_into("<H", p, 140, int(500 - self.gen_time / 360.0))
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 / 横倾姿态数据1 (int16, 0.1°, 0827协议数据2字节预留)
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()