- 新增 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 补充编译产物/临时文件忽略
265 lines
12 KiB
Python
265 lines
12 KiB
Python
#!/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() |