新增复合管控器(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:
@@ -0,0 +1,96 @@
|
||||
# 配电系统调试助手(DisSysTest)
|
||||
|
||||
用于 H100 配电系统(高压母线 / 艏部高压 / 仪表母线 / 艏部低压)调试的
|
||||
桌面工具。可在真实配电板联调与本地模拟两种模式下工作,纯 Python
|
||||
标准库实现(Tkinter + SQLite),无第三方运行依赖。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 协议/配置/数据库自检(无界面,建议先跑)
|
||||
python3 run.py --selftest
|
||||
|
||||
# 启动界面
|
||||
python3 run.py
|
||||
```
|
||||
|
||||
> 旧目录中的 `PowerManagerApp.py`、`PowerManagerApp copy.py`、`udpMsg.py`、
|
||||
> `db.py`、`test.py`、`ccu.py` 等为历史调试脚本,新工程位于 `app/`,
|
||||
> 旧文件保留供参考,未参与运行。
|
||||
|
||||
## 界面与操作
|
||||
|
||||
- **四个状态页**:动力母线 / 艏部高压 / 仪表母线 / 艏部低压,实时显示
|
||||
电压、电流、断路器状态与报警。
|
||||
- **断路器指示灯**:绿=合(0x55) 黑=分(0xAA) 红=故障(0x5A) 灰=无数据。
|
||||
- **配电操作菜单**:打开各母线操作面板,点「合 / 分」下发指令。
|
||||
- **DCDC 状态**:查看 DC/DC 故障字1/2 的 16 个状态位。
|
||||
- **模拟模式**:勾选后无需设备即可联调,配电指令会同步回写模拟反馈,
|
||||
与真实设备共用同一套接收/落库/转发链路。
|
||||
- **转发反馈 / 透传指令**:把收到的反馈转发给上位机;把上位机指令透传
|
||||
给对应配电板。
|
||||
- **数据记录**:反馈与指令自动写入 SQLite(可取消勾选)。
|
||||
|
||||
## 通信配置
|
||||
|
||||
菜单「配置 → 通信配置」可修改并保存到 `config.json`(首次运行自动生成
|
||||
默认值):
|
||||
|
||||
| 端点 | 默认值 | 说明 |
|
||||
|----------|-----------------------|--------------------|
|
||||
| 本机 | 127.0.0.1:7000 | 监听反馈 |
|
||||
| 高压配电板(C1) | 192.168.1.154:6000 | 指令下发目标 |
|
||||
| 仪表配电板(C2) | 192.168.1.158:6000 | 指令下发目标 |
|
||||
| 上位机 | 127.0.0.1:5001 | 反馈转发目标 |
|
||||
|
||||
菜单「配置 → 启动连接」控制 UDP 接收线程(默认开启)。
|
||||
|
||||
## 消息协议
|
||||
|
||||
帧格式:`0x40 0x40 | id(LE u16) | length(LE u16) | body | 和校验(1B)`。
|
||||
|
||||
| 消息 ID | 方向 | 说明 | body 长度 |
|
||||
|---------|------|--------------------------|-----------|
|
||||
| 0x0001 | 指令 | 动力母线配电 | 14 |
|
||||
| 0x0002 | 指令 | 艏部高压配电 | 6 |
|
||||
| 0x0003 | 指令 | 仪表母线配电 | 7 |
|
||||
| 0x0004 | 指令 | 艏部低压配电 | 8 |
|
||||
| 0x0005 | 反馈 | 动力母线状态 | 53 |
|
||||
| 0x0006 | 反馈 | 艏部高压状态 | 25 |
|
||||
| 0x0007 | 反馈 | 仪表母线状态 | 34 |
|
||||
| 0x0008 | 反馈 | 艏部低压状态 | 29 |
|
||||
|
||||
字段定义与顺序对齐 `src/pPMtest/pmSysvariable.h`(注意历史版本 `udpMsg.py`
|
||||
中的 length 字段存在多处错误,`app/protocol.py` 已修正并由字段表自动推导)。
|
||||
|
||||
## 工程结构
|
||||
|
||||
```
|
||||
app/
|
||||
protocol.py 声明式协议定义(pack/unpack/长度自推导,含自检)
|
||||
config.py config.json 加载/保存
|
||||
udp_comm.py UDP 收发线程与按 ID 分发
|
||||
simulator.py 内置模拟器(指令回显)
|
||||
db.py SQLite 存储(表结构由协议自动生成)
|
||||
logger.py 日志(UI/控制台/文件,线程安全)
|
||||
panels.py 数据驱动的状态/操作面板
|
||||
app.py 主窗口
|
||||
ccuMsg.py / ccuWin.py 复合管控 CCU 模块(功能预留,暂未接入)
|
||||
run.py 入口
|
||||
tests/ 协议单元测试
|
||||
build.py PyInstaller 打包脚本
|
||||
```
|
||||
|
||||
## 测试与打包
|
||||
|
||||
```bash
|
||||
python3 run.py --selftest # 协议/配置/数据库自检
|
||||
python3 tests/test_protocol.py
|
||||
python3 build.py # 需先 pip install pyinstaller(在目标平台执行)
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- 现场联调前建议先用模拟模式 + 转发/透传验证链路;
|
||||
- 协议自检保证帧长与 C++ 结构一致,但最终以实际板卡抓包为准;
|
||||
- 数据库文件默认生成在 `config.json` 同目录(源码运行时为 `app/test.db`)。
|
||||
@@ -0,0 +1,3 @@
|
||||
"""配电系统调试助手应用包。"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
@@ -0,0 +1,85 @@
|
||||
"""程序入口:python -m app 或 python run.py。
|
||||
|
||||
支持 --selftest 无 GUI 自检(协议 / 配置 / 数据库)。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def selftest():
|
||||
ok = True
|
||||
print("== 协议自检 ==")
|
||||
proto_ok, report = _protocol_selftest()
|
||||
print(report)
|
||||
ok = ok and proto_ok
|
||||
|
||||
print("== 配置自检 ==")
|
||||
try:
|
||||
from .config import Config
|
||||
c = Config()
|
||||
for key in ("local", "c1", "c2", "host"):
|
||||
assert key in c.data, f"缺少配置节 {key}"
|
||||
print(" [OK] 配置加载/默认值")
|
||||
except Exception as e:
|
||||
print(f" [FAIL] 配置: {e}")
|
||||
ok = False
|
||||
|
||||
print("== 数据库自检 ==")
|
||||
try:
|
||||
from .db import DB, TABLE_NAMES
|
||||
from . import protocol
|
||||
assert len(TABLE_NAMES) == 8, "消息表数量应为 8"
|
||||
fd, path = tempfile.mkstemp(suffix=".db")
|
||||
os.close(fd)
|
||||
db = DB(path)
|
||||
db.connect()
|
||||
for cls in protocol.ALL_MESSAGES.values():
|
||||
assert db.insert_message(cls()), f"{cls.__name__} 插入失败"
|
||||
rows = {name: 0 for name in TABLE_NAMES.values()}
|
||||
for name in TABLE_NAMES.values():
|
||||
rows[name] = db.conn.execute(f'SELECT COUNT(*) FROM "{name}"').fetchone()[0]
|
||||
assert all(v == 1 for v in rows.values()), f"写入行数异常: {rows}"
|
||||
db.disconnect()
|
||||
os.unlink(path)
|
||||
print(" [OK] 建表 / 写入 / disconnect")
|
||||
except Exception as e:
|
||||
print(f" [FAIL] 数据库: {e}")
|
||||
ok = False
|
||||
|
||||
print("SELFTEST:", "PASS" if ok else "FAIL")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def _protocol_selftest():
|
||||
from . import protocol
|
||||
return protocol.self_test()
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="配电系统调试助手")
|
||||
parser.add_argument("--selftest", action="store_true",
|
||||
help="运行协议/配置/数据库自检后退出")
|
||||
parser.add_argument("--config", default=None, help="配置文件路径")
|
||||
parser.add_argument("--version", action="store_true", help="显示版本")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.version:
|
||||
from . import __version__
|
||||
print(f"配电系统调试助手 v{__version__}")
|
||||
return 0
|
||||
if args.selftest:
|
||||
return selftest()
|
||||
|
||||
import tkinter as tk
|
||||
from .app import PowerManagerApp
|
||||
root = tk.Tk()
|
||||
app = PowerManagerApp(root, config_path=args.config)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,440 @@
|
||||
"""电源管理/配电系统调试主程序。
|
||||
|
||||
主窗口组织:
|
||||
- 菜单:通信选项 / 配电操作 / 配置 / DCDC状态 / 复合管控面板(预留) / 软件说明
|
||||
- 四个母线状态页(Notebook),数据驱动刷新
|
||||
- 系统日志区
|
||||
|
||||
数据通路:
|
||||
真实设备/UDP 反馈 -> on_feedback -> 状态缓存 + DB + (可选)转发上位机
|
||||
配电指令(合/分) -> send_breaker_cmd -> UDP 下发 + DB + (可选)模拟回显
|
||||
模拟模式 -> Simulator.update -> 注入同一 on_feedback 通路
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from . import protocol
|
||||
from .config import Config
|
||||
from .db import DB
|
||||
from .logger import AppLogger
|
||||
from .panels import (BUS_SPECS, CMD_SPECS, build_bus_page, update_bus_page,
|
||||
build_control_panel, build_dcdc_display,
|
||||
update_dcdc_display)
|
||||
from .simulator import Simulator
|
||||
from .udp_comm import UdpComm
|
||||
from .protocol import BREAKER_ON
|
||||
|
||||
MSG_ID_BUS = {0x0005: "high_vol", 0x0006: "high_a_vol",
|
||||
0x0007: "low_main", 0x0008: "low_bus"}
|
||||
CMD_ID_DEST = {0x0001: "c1", 0x0002: "c1", 0x0003: "c2", 0x0004: "c2"}
|
||||
|
||||
|
||||
class PowerManagerApp:
|
||||
def __init__(self, root, config_path=None):
|
||||
self.root = root
|
||||
self.config = Config(config_path)
|
||||
|
||||
self.root.title("配电系统调试助手")
|
||||
self.root.tk.call("tk", "scaling", 1.5)
|
||||
self.root.minsize(860, 640)
|
||||
|
||||
# 菜单开关状态
|
||||
self.Forward_Feedback = tk.BooleanVar(value=False)
|
||||
self.Transparent_Command = tk.BooleanVar(value=False)
|
||||
self.Simulation_Mode = tk.BooleanVar(value=False)
|
||||
self.Start_Connection = tk.BooleanVar(value=True)
|
||||
self.Record_Data = tk.BooleanVar(value=True)
|
||||
self.Simulation_Mode.trace_add("write", self._on_sim_toggle)
|
||||
|
||||
self.db = DB(self.config.db_path)
|
||||
try:
|
||||
self.db.connect()
|
||||
except Exception as e:
|
||||
print(f"数据库打开失败: {e}")
|
||||
|
||||
self.sim = Simulator()
|
||||
self._sim_thread = None
|
||||
self._sim_running = False
|
||||
self._data_lock = threading.Lock()
|
||||
# 各母线最新反馈(原始 Message 对象),None 表示尚无数据
|
||||
self.raw_msgs = {key: None for key in BUS_SPECS}
|
||||
self.bus_refs = {}
|
||||
self.dcdc_refs = None # DCDC 弹窗 refs
|
||||
self.dcdc_popup = None
|
||||
|
||||
self._build_ui()
|
||||
|
||||
self.logger = AppLogger(widget=self.log_text)
|
||||
self.udp = UdpComm(logger=self.logger)
|
||||
self.udp.on_feedback = self._on_feedback
|
||||
self.udp.on_command = self._on_command
|
||||
self.udp.on_raw = self._on_raw
|
||||
try:
|
||||
self.udp.bind(self.config.local_ip, self.config.local_port)
|
||||
except OSError as e:
|
||||
self.logger.error(f"绑定本机 {self.config.local_ip}:{self.config.local_port} 失败: {e}")
|
||||
|
||||
if self.Start_Connection.get() and self.udp.bound:
|
||||
try:
|
||||
self.udp.start()
|
||||
self.logger.info("通信连接已启动")
|
||||
except RuntimeError as e:
|
||||
self.logger.error(str(e))
|
||||
|
||||
self.logger.info(f"配电系统调试助手启动(配置文件: {self.config.path})")
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
self.root.after(self.config.refresh_ms, self._refresh_ui)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 界面构建
|
||||
# ------------------------------------------------------------------
|
||||
def _build_ui(self):
|
||||
menubar = tk.Menu(self.root)
|
||||
|
||||
comm_menu = tk.Menu(menubar, tearoff=0)
|
||||
comm_menu.add_checkbutton(label="转发反馈", variable=self.Forward_Feedback)
|
||||
comm_menu.add_checkbutton(label="透传指令", variable=self.Transparent_Command)
|
||||
comm_menu.add_checkbutton(label="模拟模式", variable=self.Simulation_Mode)
|
||||
menubar.add_cascade(label="通信选项", menu=comm_menu)
|
||||
|
||||
op_menu = tk.Menu(menubar, tearoff=0)
|
||||
op_menu.add_command(label="动力母线操作",
|
||||
command=lambda: self.show_control_window("hvb"))
|
||||
op_menu.add_command(label="艏部高压操作",
|
||||
command=lambda: self.show_control_window("hvba"))
|
||||
op_menu.add_command(label="仪表母线操作",
|
||||
command=lambda: self.show_control_window("lvmb"))
|
||||
op_menu.add_command(label="艏部低压操作",
|
||||
command=lambda: self.show_control_window("lvb"))
|
||||
menubar.add_cascade(label="配电操作", menu=op_menu)
|
||||
|
||||
cfg_menu = tk.Menu(menubar, tearoff=0)
|
||||
cfg_menu.add_checkbutton(label="启动连接", variable=self.Start_Connection,
|
||||
command=self._on_connection_toggle)
|
||||
cfg_menu.add_checkbutton(label="记录数据", variable=self.Record_Data)
|
||||
cfg_menu.add_command(label="通信配置", command=self.show_config_window)
|
||||
cfg_menu.add_command(label="数据库配置", command=self.show_db_config_window)
|
||||
menubar.add_cascade(label="配置", menu=cfg_menu)
|
||||
|
||||
menubar.add_command(label="DCDC状态", command=self.show_dcdc_window)
|
||||
menubar.add_command(label="复合管控面板",
|
||||
command=lambda: self.show_placeholder_window("复合管控面板",
|
||||
"CCU 复合管控功能预留,待后续版本实现。"))
|
||||
menubar.add_command(label="软件说明", command=self.show_help_window)
|
||||
self.root.config(menu=menubar)
|
||||
|
||||
main_frame = ttk.Frame(self.root, padding=8)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
main_frame.grid_rowconfigure(0, weight=1)
|
||||
main_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self.notebook = ttk.Notebook(main_frame)
|
||||
self.notebook.grid(row=0, column=0, sticky="nsew")
|
||||
for key, spec in BUS_SPECS.items():
|
||||
page = build_bus_page(self.notebook, spec)
|
||||
self.bus_refs[key] = page
|
||||
self.notebook.add(page["frame"], text=spec.title)
|
||||
|
||||
log_frame = ttk.LabelFrame(main_frame, text="系统日志", padding=6)
|
||||
log_frame.grid(row=1, column=0, sticky="nsew", pady=(6, 0))
|
||||
log_frame.grid_rowconfigure(0, weight=1)
|
||||
log_frame.grid_columnconfigure(0, weight=1)
|
||||
self.log_text = tk.Text(log_frame, height=8, state=tk.DISABLED)
|
||||
self.log_text.grid(row=0, column=0, sticky="nsew")
|
||||
sb = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
|
||||
sb.grid(row=0, column=1, sticky="ns")
|
||||
self.log_text.config(yscrollcommand=sb.set)
|
||||
|
||||
self.status_bar = ttk.Label(main_frame, text="未连接", anchor="w",
|
||||
relief="sunken")
|
||||
self.status_bar.grid(row=2, column=0, sticky="ew", pady=(4, 0))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# UDP 回调(运行在接收线程)
|
||||
# ------------------------------------------------------------------
|
||||
def _on_feedback(self, msg, addr):
|
||||
bus_key = MSG_ID_BUS.get(msg.MSG_ID)
|
||||
if bus_key is None:
|
||||
return
|
||||
with self._data_lock:
|
||||
self.raw_msgs[bus_key] = msg
|
||||
if self.Record_Data.get():
|
||||
self.db.insert_message(msg)
|
||||
|
||||
def _on_command(self, msg, addr):
|
||||
if self.Simulation_Mode.get():
|
||||
self.sim.apply_command(msg)
|
||||
|
||||
def _on_raw(self, data, addr):
|
||||
msg_id = protocol.msg_id_of(data)
|
||||
if msg_id is None:
|
||||
return
|
||||
if self.Forward_Feedback.get() and msg_id in protocol.FB_MESSAGES:
|
||||
self.udp.sendto(data, self.config.host_ip, self.config.host_port)
|
||||
elif self.Transparent_Command.get() and msg_id in protocol.CMD_MESSAGES:
|
||||
self._forward_raw(data, msg_id)
|
||||
|
||||
def _forward_raw(self, data, msg_id):
|
||||
dest = CMD_ID_DEST.get(msg_id)
|
||||
if dest is None:
|
||||
return
|
||||
ip, port = self._dest(dest)
|
||||
self.udp.sendto(data, ip, port)
|
||||
|
||||
def _dest(self, key):
|
||||
if key == "c1":
|
||||
return self.config.c1_ip, self.config.c1_port
|
||||
return self.config.c2_ip, self.config.c2_port
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 指令下发
|
||||
# ------------------------------------------------------------------
|
||||
def send_breaker_cmd(self, spec, field, state):
|
||||
cmd = spec.msg_cls()
|
||||
setattr(cmd, field, state)
|
||||
if self.Simulation_Mode.get():
|
||||
self.sim.apply_command(cmd)
|
||||
ip, port = self._dest(spec.dest)
|
||||
ok = self.udp.send_message(cmd, ip, port)
|
||||
if self.Record_Data.get():
|
||||
self.db.insert_message(cmd)
|
||||
label = next((l for l, f in spec.actions if f == field), field)
|
||||
txt = "合" if state == BREAKER_ON else "分"
|
||||
self.logger.info(f"发送{spec.title}「{label}」{txt} → {ip}:{port}"
|
||||
+ ("" if ok else "(发送失败)"))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 开关回调
|
||||
# ------------------------------------------------------------------
|
||||
def _on_connection_toggle(self):
|
||||
if self.Start_Connection.get():
|
||||
if not self.udp.bound:
|
||||
self.logger.error("本机地址未绑定,无法启动")
|
||||
self.Start_Connection.set(False)
|
||||
return
|
||||
try:
|
||||
self.udp.start()
|
||||
except RuntimeError as e:
|
||||
self.logger.error(str(e))
|
||||
self.Start_Connection.set(False)
|
||||
return
|
||||
self.logger.info("通信连接已启动")
|
||||
else:
|
||||
self.udp.stop()
|
||||
self.logger.info("通信连接已停止")
|
||||
|
||||
def _on_sim_toggle(self, *args):
|
||||
if self.Simulation_Mode.get():
|
||||
self._start_simulation()
|
||||
else:
|
||||
self._stop_simulation()
|
||||
|
||||
def _start_simulation(self):
|
||||
if self._sim_running:
|
||||
return
|
||||
self.sim.reset()
|
||||
self._sim_running = True
|
||||
self._sim_thread = threading.Thread(target=self._sim_loop, daemon=True)
|
||||
self._sim_thread.start()
|
||||
self.logger.info("模拟模式已启动")
|
||||
|
||||
def _stop_simulation(self):
|
||||
was_running = self._sim_running
|
||||
self._sim_running = False
|
||||
if self._sim_thread:
|
||||
self._sim_thread.join(timeout=1.5)
|
||||
self._sim_thread = None
|
||||
if was_running:
|
||||
self.logger.info("模拟模式已停止")
|
||||
|
||||
def _sim_loop(self):
|
||||
while self._sim_running:
|
||||
msgs = self.sim.update(self.config.sim_interval)
|
||||
for msg in msgs:
|
||||
self._on_feedback(msg, ("sim", 0))
|
||||
if self.Forward_Feedback.get():
|
||||
self.udp.sendto(msg.pack(), self.config.host_ip,
|
||||
self.config.host_port)
|
||||
time.sleep(self.config.sim_interval)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 界面刷新(主线程定时器)
|
||||
# ------------------------------------------------------------------
|
||||
def _refresh_ui(self):
|
||||
self.logger.drain()
|
||||
for key, refs in self.bus_refs.items():
|
||||
with self._data_lock:
|
||||
msg = self.raw_msgs[key]
|
||||
update_bus_page(BUS_SPECS[key], refs, msg)
|
||||
# DCDC 弹窗刷新
|
||||
if self.dcdc_refs is not None:
|
||||
with self._data_lock:
|
||||
m = self.raw_msgs.get("high_vol")
|
||||
if m is not None:
|
||||
update_dcdc_display(self.dcdc_refs, m.dcDcFaultWord1,
|
||||
m.dcDcFaultWord2)
|
||||
# 状态栏
|
||||
state = "已连接" if self.udp.running else "未连接"
|
||||
self.status_bar.config(
|
||||
text=f"{state} 收包: {self.udp.rx_count} 发包: {self.udp.tx_count}"
|
||||
f" 模拟: {'开' if self.Simulation_Mode.get() else '关'}"
|
||||
f" 记录: {'开' if self.Record_Data.get() else '关'}")
|
||||
self.root.after(self.config.refresh_ms, self._refresh_ui)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 弹窗
|
||||
# ------------------------------------------------------------------
|
||||
def show_control_window(self, spec_key):
|
||||
spec = CMD_SPECS[spec_key]
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title(spec.title)
|
||||
frame = ttk.Frame(popup, padding=8)
|
||||
frame.pack(fill="both", expand=True)
|
||||
build_control_panel(frame, spec, self.send_breaker_cmd)
|
||||
ttk.Button(popup, text="关闭", command=popup.destroy).pack(pady=8)
|
||||
|
||||
def show_dcdc_window(self):
|
||||
if self.dcdc_popup is not None and self.dcdc_popup.winfo_exists():
|
||||
self.dcdc_popup.lift()
|
||||
return
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title("DC/DC 状态")
|
||||
frame = ttk.Frame(popup, padding=8)
|
||||
frame.pack(fill="both", expand=True, padx=5, pady=5)
|
||||
self.dcdc_refs = build_dcdc_display(frame, "DC/DC 故障字")
|
||||
self.dcdc_refs["widget"].pack(fill="both", expand=True)
|
||||
self.dcdc_popup = popup
|
||||
|
||||
def on_close():
|
||||
self.dcdc_refs = None
|
||||
self.dcdc_popup = None
|
||||
popup.destroy()
|
||||
|
||||
ttk.Button(popup, text="关闭", command=on_close).pack(pady=8)
|
||||
|
||||
def show_placeholder_window(self, title, text):
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title(title)
|
||||
popup.geometry("420x200")
|
||||
frame = ttk.Frame(popup, padding=20)
|
||||
frame.pack(fill="both", expand=True)
|
||||
ttk.Label(frame, text=text, wraplength=360, justify="left").pack()
|
||||
ttk.Button(popup, text="关闭", command=popup.destroy).pack(pady=10)
|
||||
|
||||
def show_help_window(self):
|
||||
popup = tk.Toplevel(self.root)
|
||||
popup.title("程序说明")
|
||||
popup.geometry("640x460")
|
||||
frame = ttk.Frame(popup, padding=10)
|
||||
frame.pack(fill="both", expand=True)
|
||||
text = tk.Text(frame, wrap="word", font=("Microsoft YaHei", 10))
|
||||
sb = ttk.Scrollbar(frame, orient="vertical", command=text.yview)
|
||||
text.configure(yscrollcommand=sb.set)
|
||||
text.pack(side="left", fill="both", expand=True)
|
||||
sb.pack(side="right", fill="y")
|
||||
help_text = f"""配电系统调试助手
|
||||
|
||||
功能概述:
|
||||
1. 高压母线 / 艏部高压 / 仪表母线 / 艏部低压 四路状态实时监控
|
||||
2. 断路器配电指令远程合/分操作
|
||||
3. 转发反馈:把收到的反馈转发给上位机(信息主机)
|
||||
4. 透传指令:把上位机下发的指令透传给对应配电板
|
||||
5. 模拟模式:无需真实设备即可本地联调
|
||||
6. 数据记录:反馈与指令自动存入 SQLite(可开关)
|
||||
|
||||
通信端点(可在 配置-通信配置 修改):
|
||||
本机监听: {self.config.local_ip}:{self.config.local_port}
|
||||
高压配电板: {self.config.c1_ip}:{self.config.c1_port}
|
||||
仪表配电板: {self.config.c2_ip}:{self.config.c2_port}
|
||||
上位机: {self.config.host_ip}:{self.config.host_port}
|
||||
数据库: {self.config.db_path}
|
||||
|
||||
指示灯说明:
|
||||
绿色=合闸(0x55) 黑色=分闸(0xAA) 红色=故障(0x5A) 灰色=无数据/未知
|
||||
|
||||
操作提示:
|
||||
1. 「启动连接」控制 UDP 接收线程(默认开启)
|
||||
2. 勾选「模拟模式」后,配电操作会同步回写模拟反馈
|
||||
3. 重要操作请以现场实际设备状态为准"""
|
||||
text.insert("1.0", help_text)
|
||||
text.config(state="disabled")
|
||||
ttk.Button(popup, text="关闭", command=popup.destroy).pack(pady=8)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 配置弹窗
|
||||
# ------------------------------------------------------------------
|
||||
def show_config_window(self):
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("通信配置")
|
||||
win.geometry("420x360")
|
||||
rows = [
|
||||
("本机 IP", "local_ip"), ("本机端口", "local_port"),
|
||||
("高压配电板 IP", "c1_ip"), ("高压配电板端口", "c1_port"),
|
||||
("仪表配电板 IP", "c2_ip"), ("仪表配电板端口", "c2_port"),
|
||||
("上位机 IP", "host_ip"), ("上位机端口", "host_port"),
|
||||
]
|
||||
entries = {}
|
||||
for i, (label, key) in enumerate(rows):
|
||||
ttk.Label(win, text=label).grid(row=i, column=0, padx=10, pady=4, sticky="w")
|
||||
e = ttk.Entry(win, width=20)
|
||||
e.grid(row=i, column=1, padx=10, pady=4)
|
||||
val = getattr(self.config, key)
|
||||
e.insert(0, str(val))
|
||||
entries[key] = e
|
||||
|
||||
def save():
|
||||
try:
|
||||
self.config.set_endpoints(
|
||||
entries["local_ip"].get(), int(entries["local_port"].get()),
|
||||
entries["c1_ip"].get(), int(entries["c1_port"].get()),
|
||||
entries["c2_ip"].get(), int(entries["c2_port"].get()),
|
||||
entries["host_ip"].get(), int(entries["host_port"].get()))
|
||||
self.config.save()
|
||||
# 重新绑定本机
|
||||
if self.udp.running:
|
||||
self.udp.stop()
|
||||
self.udp.bind(self.config.local_ip, self.config.local_port)
|
||||
if self.Start_Connection.get():
|
||||
self.udp.start()
|
||||
self.logger.info("通信配置已保存并生效")
|
||||
win.destroy()
|
||||
except Exception as e:
|
||||
self.logger.error(f"配置保存失败: {e}")
|
||||
|
||||
ttk.Button(win, text="保存", command=save).grid(row=8, column=0, columnspan=2, pady=10)
|
||||
ttk.Button(win, text="取消", command=win.destroy).grid(row=9, column=0, columnspan=2)
|
||||
|
||||
def show_db_config_window(self):
|
||||
win = tk.Toplevel(self.root)
|
||||
win.title("数据库配置")
|
||||
win.geometry("420x140")
|
||||
ttk.Label(win, text="数据库路径:").grid(row=0, column=0, padx=10, pady=8, sticky="w")
|
||||
e = ttk.Entry(win, width=32)
|
||||
e.grid(row=0, column=1, padx=10, pady=8)
|
||||
e.insert(0, self.config.db_path)
|
||||
|
||||
def save():
|
||||
path = e.get().strip()
|
||||
try:
|
||||
self.db.disconnect()
|
||||
self.db = DB(path)
|
||||
self.db.connect()
|
||||
self.config.db_path = path
|
||||
self.config.save()
|
||||
self.logger.info(f"数据库已切换: {path}")
|
||||
win.destroy()
|
||||
except Exception as ex:
|
||||
self.logger.error(f"数据库切换失败: {ex}")
|
||||
|
||||
ttk.Button(win, text="保存", command=save).grid(row=1, column=0, padx=10, pady=6)
|
||||
ttk.Button(win, text="取消", command=win.destroy).grid(row=1, column=1, pady=6)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def on_closing(self):
|
||||
self._stop_simulation()
|
||||
self.udp.stop()
|
||||
self.db.disconnect()
|
||||
self.root.destroy()
|
||||
@@ -0,0 +1,455 @@
|
||||
import struct
|
||||
|
||||
class msg_ccUFbMsg:
|
||||
def __init__(self):
|
||||
# 消息头 - 对应C++中的m_header
|
||||
self.start1 = 0x20
|
||||
self.start2 = 0x20
|
||||
self.id = 0x0051 # CCU_STATE_FB_ID
|
||||
self.length = 0
|
||||
self.checkCode = 0 # 使用4字节校验码,对应C++中的Checksum (unsigned int)
|
||||
|
||||
# CCU状态数据 - 完全对应C++中的ccuState结构体
|
||||
self.fc_mode = 0x00 # 燃料电池系统运行模式:00H-无效, 01H-调试, 02H-自动, 03H-补给
|
||||
self.fc_status = 0x00 # 燃料电池状态:00H-无效, 01H-初始化, ..., 0DH-维护完成
|
||||
self.fc_fault_level = 0x00 # 燃料电池故障等级:00H-无故障, 10H-一级, 20H-二级, 30H-三级, 40H-四级
|
||||
|
||||
self.output_power_limit = 0 # 系统输出功率限制值,单位:W,范围:0~30000W
|
||||
self.total_generation_time = 0 # 系统累计发电时间,单位:0.1小时,最大值6553.5小时
|
||||
self.total_generation_power = 0 # 系统累计发电总功率,单位:W
|
||||
self.fault_level_1 = 0 # 一级故障位,按位表示不同的故障项
|
||||
self.fault_level_2 = 0 # 二级故障位
|
||||
self.fault_level_3 = 0 # 三级故障位
|
||||
self.fault_level_4 = 0 # 四级故障位
|
||||
|
||||
self.hydrogen_capacity = 0 # 储氢罐剩余容量百分比,单位:%,范围:0~100
|
||||
self.liquid_oxygen_capacity = 0 # 液氧罐剩余容量百分比,单位:%,范围:0~100
|
||||
|
||||
self.palladium_temp = 0 # 钯膜最高温度,单位:℃
|
||||
self.main_pipe_pressure = 0 # 主水路压力,单位:0.01kPa
|
||||
self.aux_pipe_pressure = 0 # 辅水路压力,单位:0.01kPa
|
||||
|
||||
self.cabin_pressure1 = 0 # 舱室1压力,单位:0.01kPa
|
||||
self.cabin_pressure2 = 0 # 舱室2压力,单位:0.01kPa
|
||||
self.cabin_temp1 = 0 # 舱室1温度,单位:0.01℃
|
||||
self.cabin_temp2 = 0 # 舱室2温度,单位:0.01℃
|
||||
self.cabin_humidity1 = 0 # 舱室1湿度,单位:0.01%RH
|
||||
self.cabin_humidity2 = 0 # 舱室2湿度,单位:0.01%RH
|
||||
|
||||
self.flame_detector1 = 0x00 # 火焰探测器1状态:00H-无效, 01H-报警, 02H-故障
|
||||
self.flame_detector2 = 0x00 # 火焰探测器2状态:00H-无效, 01H-报警, 02H-故障
|
||||
|
||||
self.h2_concentration1 = 0 # 舱室H2浓度1,单位:0.01% LEL
|
||||
self.h2_concentration2 = 0 # 舱室H2浓度2,单位:0.01% LEL
|
||||
self.h2_concentration3 = 0 # 舱室H2浓度3,单位:0.01% LEL
|
||||
self.o2_concentration1 = 0 # 舱室O2浓度1,单位:0.01% LEL
|
||||
self.o2_concentration2 = 0 # 舱室O2浓度2,单位:0.01% LEL
|
||||
self.ch3oh_concentration1 = 0 # 舱室甲醇气体浓度1,单位:0.01% LEL
|
||||
self.ch3oh_concentration2 = 0 # 舱室甲醇气体浓度2,单位:0.01% LEL
|
||||
|
||||
self.reserved1 = [0, 0] # 保留字段前2个元素
|
||||
self.emergency_battery1_voltage = 0 # 应急电池1总电压 [V]
|
||||
self.emergency_battery1_current = 0 # 应急电池1总电流 [A]
|
||||
self.emergency_battery1_max_temp = 0 # 应急电池1最高温度 [°C]
|
||||
self.emergency_battery1_fault_word = 0 # 应急电池1故障字
|
||||
self.emergency_battery2_voltage = 0 # 应急电池2总电压 [V]
|
||||
self.emergency_battery2_current = 0 # 应急电池2总电流 [A]
|
||||
self.emergency_battery2_max_temp = 0 # 应急电池2最高温度 [°C]
|
||||
self.emergency_battery2_fault_word = 0 # 应急电池2故障字
|
||||
|
||||
self.cabin_ox_concentration = 0 # 仪表舱内氧气浓度 [%]
|
||||
self.cabin_temperature = 0 # 仪表舱内温度 [°C]
|
||||
self.cabin_humidity = 0 # 仪表舱内湿度 [%RH]
|
||||
self.cabin_pressure = 0 # 仪表舱内大气压 [kPa]
|
||||
|
||||
self.power_cabin_ox_concentration = 0 # 动力舱内氧气浓度 [%]
|
||||
self.power_cabin_temperature = 0 # 动力舱内温度 [°C]
|
||||
self.power_cabin_humidity = 0 # 动力舱内湿度 [%RH]
|
||||
self.power_cabin_pressure = 0 # 动力舱内大气压 [kPa]
|
||||
self.reserved2 = [0] # 保留字段,地址范围54~55,共2个uint8_t,协议预留
|
||||
|
||||
self.dyn_alarm_flag = [0] * 6 # 动力锂电池报警标志字数组,共6字节,按位表示各类报警
|
||||
self.ins_alarm_flag = [0] * 6 # 仪表锂电池报警标志字数组,共6字节,按位表示各类报警
|
||||
|
||||
self.ins_relay_status1 = 0x00 # 仪表电池继电器状态,高4位表示正极继电器,低4位表示充电继电器
|
||||
self.ins_relay_status2 = 0x00 # 仪表电池预充继电器和负极继电器状态
|
||||
self.dyn_relay_status1 = 0x00 # 动力电池正极与充电继电器状态
|
||||
self.dyn_relay_status2 = 0x00 # 动力电池预充与负极继电器状态
|
||||
|
||||
self.ins_max_discharge_power = 0 # 仪表电池最大允许放电功率,单位:kW,精度0.05kW
|
||||
self.dyn_max_discharge_power = 0 # 动力电池最大允许放电功率,单位:kW,精度0.05kW
|
||||
|
||||
self.ins_soc = 0 # 仪表锂电池荷电状态SOC,范围:0~100
|
||||
self.dyn_soc = 0 # 动力锂电池荷电状态SOC,范围:0~100
|
||||
|
||||
self.ins_total_energy = 0 # 仪表电池总能量,单位:kWh
|
||||
self.dyn_total_energy = 0 # 动力电池总能量,单位:kWh
|
||||
|
||||
self.ins_power_input = 0 # 仪表电池输入功率,单位:kW
|
||||
self.dyn_power_input = 0 # 动力电池输入功率,单位:kW
|
||||
|
||||
self.ins_charge_status = 0x00 # 仪表电池充放电状态:00H-无效,10H-充电,20H-放电
|
||||
self.dyn_charge_status = 0x00 # 动力电池充放电状态:00H-无效,10H-充电,20H-放电
|
||||
|
||||
self.ins_voltage_link = 0 # 仪表电池LINK端电压,单位:0.01V
|
||||
self.ins_voltage_pack = 0 # 仪表电池PACK端电压,单位:0.01V
|
||||
self.ins_current = 0 # 仪表电池电流,单位:0.01A(正值放电)
|
||||
self.ins_resistance_pos = 0 # 仪表电池正极绝缘电阻,单位:kΩ
|
||||
self.ins_resistance_neg = 0 # 仪表电池负极绝缘电阻,单位:kΩ
|
||||
|
||||
self.dyn_voltage_link = 0 # 动力电池LINK端电压,单位:0.01V
|
||||
self.dyn_voltage_pack = 0 # 动力电池PACK端电压,单位:0.01V
|
||||
self.dyn_current = 0 # 动力电池电流,单位:0.01A(正值放电)
|
||||
self.dyn_resistance_pos = 0 # 动力电池正极绝缘电阻,单位:kΩ
|
||||
self.dyn_resistance_neg = 0 # 动力电池负极绝缘电阻,单位:kΩ
|
||||
|
||||
self.ins_emergency_status = 0x00 # 仪表电池紧急状态字,按位表示异常或告警状态
|
||||
self.dyn_emergency_status = 0x00 # 动力电池紧急状态字,按位表示异常或告警状态
|
||||
|
||||
self.reserved3 = [0] * 2 # 保留字节,协议预留
|
||||
|
||||
self.ins_soc_threshold1 = 0 # 仪表电池1 SOC门限,范围:0~100
|
||||
self.ins_soc_threshold2 = 0 # 仪表电池2 SOC门限,范围:0~100
|
||||
self.ins_soc_threshold3 = 0 # 仪表电池3 SOC门限,范围:0~100
|
||||
self.dyn_soc_threshold1 = 0 # 动力电池1 SOC门限,范围:0~100
|
||||
self.dyn_soc_threshold2 = 0 # 动力电池2 SOC门限,范围:0~100
|
||||
self.dyn_soc_threshold3 = 0 # 动力电池3 SOC门限,范围:0~100
|
||||
|
||||
self.ins_power_limit1 = 0 # 仪表电池1功率限制值,单位:%
|
||||
self.ins_power_limit2 = 0 # 仪表电池2功率限制值,单位:%
|
||||
self.dyn_power_limit1 = 0 # 动力电池1功率限制值,单位:%
|
||||
self.dyn_power_limit2 = 0 # 动力电池2功率限制值,单位:%
|
||||
|
||||
self.device_online_flag1 = 0 # 设备在线状态标志1,byte0:低压配电板在线标志位 byte1:复合管控主机在线标志位 byte2:复合管控备机在线标志位
|
||||
self.device_online_flag2 = 0 # 设备在线状态标志2,byte0:仪表锂电在线标志位 byte1:动力锂电在线标志位 byte2:燃电锂电在线标志位 byte3:高压配电板在线标志位
|
||||
|
||||
self.dyn_charge_level = 0x00 # 动力电池充电状态:01H-1级(不限), 02H-2级(限功率), 03H-3级(关闭)
|
||||
self.dyn_water_leak_status = 0x00 # 动力漏水状态:0XH-正常, 1XH-异常漏水
|
||||
self.ins_water_leak_dcdc_status = 0x00 # 仪表漏水状态与DCDC状态:低4位-漏水状态(0XH正常,1XH异常),高4位-DCDC状态(X0H正常,X1H故障)
|
||||
self.reserved4 = 0x00
|
||||
self.heartbeat = 0 # 主机心跳计数器或标志值
|
||||
|
||||
def pack(self):
|
||||
"""打包消息为字节数据 - 完全对应C++中的msg_CcuStateFbMsg结构体"""
|
||||
# 重置校验码
|
||||
self.checkCode = 0
|
||||
|
||||
# 首先打包CCU状态数据(不包括消息头和校验码)
|
||||
body_data = b''
|
||||
|
||||
# 打包CCU状态数据 - 完全对应C++中的ccuState结构体
|
||||
# 第一部分:基本状态
|
||||
body_data += struct.pack('<BBB',
|
||||
self.fc_mode,
|
||||
self.fc_status,
|
||||
self.fc_fault_level)
|
||||
|
||||
# 第二部分:功率和时间
|
||||
body_data += struct.pack('<HHHHHHH',
|
||||
self.output_power_limit,
|
||||
self.total_generation_time,
|
||||
self.total_generation_power,
|
||||
self.fault_level_1,
|
||||
self.fault_level_2,
|
||||
self.fault_level_3,
|
||||
self.fault_level_4)
|
||||
|
||||
# 第三部分:容量
|
||||
body_data += struct.pack('<BB',
|
||||
self.hydrogen_capacity,
|
||||
self.liquid_oxygen_capacity)
|
||||
|
||||
# 第四部分:温度和压力
|
||||
body_data += struct.pack('<HHH',
|
||||
self.palladium_temp,
|
||||
self.main_pipe_pressure,
|
||||
self.aux_pipe_pressure)
|
||||
|
||||
# 第五部分:舱室环境
|
||||
body_data += struct.pack('<HHHHHH',
|
||||
self.cabin_pressure1,
|
||||
self.cabin_pressure2,
|
||||
self.cabin_temp1,
|
||||
self.cabin_temp2,
|
||||
self.cabin_humidity1,
|
||||
self.cabin_humidity2)
|
||||
|
||||
# 第六部分:火焰探测器
|
||||
body_data += struct.pack('<BB',
|
||||
self.flame_detector1,
|
||||
self.flame_detector2)
|
||||
|
||||
# 第七部分:气体浓度
|
||||
body_data += struct.pack('<HHHHHHH',
|
||||
self.h2_concentration1,
|
||||
self.h2_concentration2,
|
||||
self.h2_concentration3,
|
||||
self.o2_concentration1,
|
||||
self.o2_concentration2,
|
||||
self.ch3oh_concentration1,
|
||||
self.ch3oh_concentration2)
|
||||
|
||||
# 第八部分:保留字段和应急电池数据
|
||||
body_data += struct.pack('<HHHHHHHH',
|
||||
self.reserved1[0], self.reserved1[1],
|
||||
self.emergency_battery1_voltage,
|
||||
self.emergency_battery1_current,
|
||||
self.emergency_battery1_max_temp,
|
||||
self.emergency_battery1_fault_word,
|
||||
self.emergency_battery2_voltage,
|
||||
self.emergency_battery2_current)
|
||||
|
||||
body_data += struct.pack('<HHHH',
|
||||
self.emergency_battery2_max_temp,
|
||||
self.emergency_battery2_fault_word,
|
||||
self.cabin_ox_concentration,
|
||||
self.cabin_temperature)
|
||||
|
||||
# 第九部分:舱室环境2
|
||||
body_data += struct.pack('<HHHHHHHH',
|
||||
self.cabin_humidity,
|
||||
self.cabin_pressure,
|
||||
self.power_cabin_ox_concentration,
|
||||
self.power_cabin_temperature,
|
||||
self.power_cabin_humidity,
|
||||
self.power_cabin_pressure,
|
||||
self.reserved2[0], 0) # 填充第二个reserved2字节
|
||||
|
||||
# 第十部分:报警标志数组
|
||||
for flag in self.dyn_alarm_flag:
|
||||
body_data += struct.pack('<B', flag)
|
||||
for flag in self.ins_alarm_flag:
|
||||
body_data += struct.pack('<B', flag)
|
||||
|
||||
# 第十一部分:继电器状态
|
||||
body_data += struct.pack('<BBBB',
|
||||
self.ins_relay_status1,
|
||||
self.ins_relay_status2,
|
||||
self.dyn_relay_status1,
|
||||
self.dyn_relay_status2)
|
||||
|
||||
# 第十二部分:功率和SOC数据
|
||||
body_data += struct.pack('<HHBBHHHH',
|
||||
self.ins_max_discharge_power,
|
||||
self.dyn_max_discharge_power,
|
||||
self.ins_soc,
|
||||
self.dyn_soc,
|
||||
self.ins_total_energy,
|
||||
self.dyn_total_energy,
|
||||
self.ins_power_input,
|
||||
self.dyn_power_input)
|
||||
|
||||
# 第十三部分:充电状态
|
||||
body_data += struct.pack('<BB',
|
||||
self.ins_charge_status,
|
||||
self.dyn_charge_status)
|
||||
|
||||
# 第十四部分:电压电流和绝缘电阻
|
||||
body_data += struct.pack('<HHHHHHHHHH',
|
||||
self.ins_voltage_link,
|
||||
self.ins_voltage_pack,
|
||||
self.ins_current,
|
||||
self.ins_resistance_pos,
|
||||
self.ins_resistance_neg,
|
||||
self.dyn_voltage_link,
|
||||
self.dyn_voltage_pack,
|
||||
self.dyn_current,
|
||||
self.dyn_resistance_pos,
|
||||
self.dyn_resistance_neg)
|
||||
|
||||
# 第十五部分:紧急状态和保留字段
|
||||
body_data += struct.pack('<BB',
|
||||
self.ins_emergency_status,
|
||||
self.dyn_emergency_status)
|
||||
|
||||
body_data += struct.pack('<BB',
|
||||
self.reserved3[0], self.reserved3[1])
|
||||
|
||||
# 第十六部分:SOC门限值
|
||||
body_data += struct.pack('<BBBBBB',
|
||||
self.ins_soc_threshold1,
|
||||
self.ins_soc_threshold2,
|
||||
self.ins_soc_threshold3,
|
||||
self.dyn_soc_threshold1,
|
||||
self.dyn_soc_threshold2,
|
||||
self.dyn_soc_threshold3)
|
||||
|
||||
# 第十七部分:功率限制值
|
||||
body_data += struct.pack('<BBBB',
|
||||
self.ins_power_limit1,
|
||||
self.ins_power_limit2,
|
||||
self.dyn_power_limit1,
|
||||
self.dyn_power_limit2)
|
||||
|
||||
# 第十八部分:设备在线状态和心跳
|
||||
body_data += struct.pack('<II',
|
||||
self.device_online_flag1,
|
||||
self.device_online_flag2)
|
||||
|
||||
body_data += struct.pack('<BBBB',
|
||||
self.dyn_charge_level,
|
||||
self.dyn_water_leak_status,
|
||||
self.ins_water_leak_dcdc_status,
|
||||
self.reserved4)
|
||||
|
||||
body_data += struct.pack('<I', self.heartbeat)
|
||||
|
||||
# 计算长度字段:body_data的长度
|
||||
self.length = len(body_data)
|
||||
|
||||
# 打包消息头 - 对应C++中的m_header
|
||||
header_data = struct.pack('<BBHH',
|
||||
self.start1,
|
||||
self.start2,
|
||||
self.id,
|
||||
self.length)
|
||||
|
||||
# 组合头部和body数据
|
||||
data_without_checksum = header_data + body_data
|
||||
|
||||
# 计算校验码 - 根据C++代码,计算所有数据的和(不包括校验码本身)
|
||||
self.checkCode = 0
|
||||
for byte in data_without_checksum:
|
||||
self.checkCode += byte
|
||||
|
||||
# 添加校验码 - 使用4字节,对应C++中的Checksum (unsigned int)
|
||||
checksum_data = struct.pack('<I', self.checkCode)
|
||||
|
||||
# 完整数据:头部 + body + 校验码
|
||||
full_data = data_without_checksum + checksum_data
|
||||
|
||||
return full_data
|
||||
|
||||
def unpack(self, data):
|
||||
"""从字节数据解包消息 - 完全对应C++中的msg_CcuStateFbMsg结构体"""
|
||||
if len(data) < 10: # 最小消息长度检查
|
||||
return False
|
||||
|
||||
# 解析消息头
|
||||
self.start1 = data[0]
|
||||
self.start2 = data[1]
|
||||
self.id = struct.unpack_from('<H', data, 2)[0]
|
||||
self.length = struct.unpack_from('<H', data, 4)[0]
|
||||
|
||||
offset = 6 # 头部结束位置
|
||||
|
||||
# 检查数据长度是否足够(现在校验码是4字节)
|
||||
if len(data) < offset + self.length + 4: # +4 for 4-byte checksum
|
||||
return False
|
||||
|
||||
# 解析CCU状态数据 - 完全对应C++中的ccuState结构体
|
||||
# 第一部分:基本状态
|
||||
(self.fc_mode, self.fc_status, self.fc_fault_level) = struct.unpack_from('<BBB', data, offset); offset += 3
|
||||
|
||||
# 第二部分:功率和时间
|
||||
(self.output_power_limit, self.total_generation_time, self.total_generation_power,
|
||||
self.fault_level_1, self.fault_level_2, self.fault_level_3, self.fault_level_4) = struct.unpack_from('<HHHHHHH', data, offset); offset += 14
|
||||
|
||||
# 第三部分:容量
|
||||
(self.hydrogen_capacity, self.liquid_oxygen_capacity) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第四部分:温度和压力
|
||||
(self.palladium_temp, self.main_pipe_pressure, self.aux_pipe_pressure) = struct.unpack_from('<HHH', data, offset); offset += 6
|
||||
|
||||
# 第五部分:舱室环境
|
||||
(self.cabin_pressure1, self.cabin_pressure2, self.cabin_temp1, self.cabin_temp2,
|
||||
self.cabin_humidity1, self.cabin_humidity2) = struct.unpack_from('<HHHHHH', data, offset); offset += 12
|
||||
|
||||
# 第六部分:火焰探测器
|
||||
(self.flame_detector1, self.flame_detector2) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第七部分:气体浓度
|
||||
(self.h2_concentration1, self.h2_concentration2, self.h2_concentration3,
|
||||
self.o2_concentration1, self.o2_concentration2, self.ch3oh_concentration1,
|
||||
self.ch3oh_concentration2) = struct.unpack_from('<HHHHHHH', data, offset); offset += 14
|
||||
|
||||
# 第八部分:保留字段和应急电池数据
|
||||
(self.reserved1[0], self.reserved1[1], self.emergency_battery1_voltage,
|
||||
self.emergency_battery1_current, self.emergency_battery1_max_temp,
|
||||
self.emergency_battery1_fault_word, self.emergency_battery2_voltage,
|
||||
self.emergency_battery2_current) = struct.unpack_from('<HHHHHHHH', data, offset); offset += 16
|
||||
|
||||
(self.emergency_battery2_max_temp, self.emergency_battery2_fault_word,
|
||||
self.cabin_ox_concentration, self.cabin_temperature) = struct.unpack_from('<HHHH', data, offset); offset += 8
|
||||
|
||||
# 第九部分:舱室环境2
|
||||
(self.cabin_humidity, self.cabin_pressure, self.power_cabin_ox_concentration,
|
||||
self.power_cabin_temperature, self.power_cabin_humidity, self.power_cabin_pressure,
|
||||
reserved2_0, reserved2_1) = struct.unpack_from('<HHHHHHBB', data, offset); offset += 16
|
||||
self.reserved2 = [reserved2_0, reserved2_1]
|
||||
|
||||
# 第十部分:报警标志数组
|
||||
for i in range(6):
|
||||
self.dyn_alarm_flag[i] = data[offset]; offset += 1
|
||||
for i in range(6):
|
||||
self.ins_alarm_flag[i] = data[offset]; offset += 1
|
||||
|
||||
# 第十一部分:继电器状态
|
||||
(self.ins_relay_status1, self.ins_relay_status2,
|
||||
self.dyn_relay_status1, self.dyn_relay_status2) = struct.unpack_from('<BBBB', data, offset); offset += 4
|
||||
|
||||
# 第十二部分:功率和SOC数据
|
||||
(self.ins_max_discharge_power, self.dyn_max_discharge_power,
|
||||
self.ins_soc, self.dyn_soc, self.ins_total_energy, self.dyn_total_energy,
|
||||
self.ins_power_input, self.dyn_power_input) = struct.unpack_from('<HHBBHHHH', data, offset); offset += 14
|
||||
|
||||
# 第十三部分:充电状态
|
||||
(self.ins_charge_status, self.dyn_charge_status) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第十四部分:电压电流和绝缘电阻
|
||||
(self.ins_voltage_link, self.ins_voltage_pack, self.ins_current,
|
||||
self.ins_resistance_pos, self.ins_resistance_neg, self.dyn_voltage_link,
|
||||
self.dyn_voltage_pack, self.dyn_current, self.dyn_resistance_pos,
|
||||
self.dyn_resistance_neg) = struct.unpack_from('<HHHHHHHHHH', data, offset); offset += 20
|
||||
|
||||
# 第十五部分:紧急状态和保留字段
|
||||
(self.ins_emergency_status, self.dyn_emergency_status) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
(self.reserved3[0], self.reserved3[1]) = struct.unpack_from('<BB', data, offset); offset += 2
|
||||
|
||||
# 第十六部分:SOC门限值
|
||||
(self.ins_soc_threshold1, self.ins_soc_threshold2, self.ins_soc_threshold3,
|
||||
self.dyn_soc_threshold1, self.dyn_soc_threshold2, self.dyn_soc_threshold3) = struct.unpack_from('<BBBBBB', data, offset); offset += 6
|
||||
|
||||
# 第十七部分:功率限制值
|
||||
(self.ins_power_limit1, self.ins_power_limit2,
|
||||
self.dyn_power_limit1, self.dyn_power_limit2) = struct.unpack_from('<BBBB', data, offset); offset += 4
|
||||
|
||||
# 第十八部分:设备在线状态和心跳
|
||||
(self.device_online_flag1, self.device_online_flag2) = struct.unpack_from('<II', data, offset); offset += 8
|
||||
|
||||
(self.dyn_charge_level, self.dyn_water_leak_status,
|
||||
self.ins_water_leak_dcdc_status, self.reserved4) = struct.unpack_from('<BBBB', data, offset); offset += 4
|
||||
|
||||
(self.heartbeat,) = struct.unpack_from('<I', data, offset); offset += 4
|
||||
|
||||
# 解析校验码(4字节)
|
||||
(self.checkCode,) = struct.unpack_from('<I', data, offset); offset += 4
|
||||
|
||||
# 校验校验码 - 根据C++代码,计算所有数据的和(不包括校验码本身)
|
||||
checkCode = 0
|
||||
# 计算除校验码外的所有字节的和
|
||||
for i in range(len(data) - 4): # 不包括最后4字节的校验码
|
||||
checkCode += data[i]
|
||||
|
||||
return checkCode == self.checkCode
|
||||
|
||||
def print_info(self):
|
||||
"""打印消息信息"""
|
||||
print(f"消息头: start1=0x{self.start1:02X}, start2=0x{self.start2:02X}, id=0x{self.id:04X}, length={self.length}")
|
||||
print(f"燃料电池模式: 0x{self.fc_mode:02X}")
|
||||
print(f"燃料电池状态: 0x{self.fc_status:02X}")
|
||||
print(f"故障等级: 0x{self.fc_fault_level:02X}")
|
||||
print(f"输出功率限制: {self.output_power_limit}W")
|
||||
print(f"累计发电时间: {self.total_generation_time * 0.1}小时")
|
||||
print(f"发电总功率: {self.total_generation_power}W")
|
||||
print(f"储氢容量: {self.hydrogen_capacity}%")
|
||||
print(f"液氧容量: {self.liquid_oxygen_capacity}%")
|
||||
print(f"钯膜温度: {self.palladium_temp}℃")
|
||||
print(f"主水路压力: {self.main_pipe_pressure * 0.01}kPa")
|
||||
print(f"辅水路压力: {self.aux_pipe_pressure * 0.01}kPa")
|
||||
print(f"仪表SOC: {self.ins_soc}%")
|
||||
print(f"动力SOC: {self.dyn_soc}%")
|
||||
print(f"心跳计数器: {self.heartbeat}")
|
||||
print(f"校验码: 0x{self.checkCode:08X}")
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext
|
||||
|
||||
from . import ccuMsg
|
||||
from datetime import datetime
|
||||
|
||||
class CCUStatusSummaryDialog:
|
||||
"""CCU状态汇总弹窗"""
|
||||
def __init__(self, parent, ccu_status=None, title="CCU状态汇总"):
|
||||
self.parent = parent
|
||||
self.ccu_status = ccu_status or ccuMsg.msg_ccUFbMsg()
|
||||
|
||||
# 创建弹窗
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title(title)
|
||||
self.dialog.geometry("1000x700")
|
||||
self.dialog.transient(parent) # 设置为父窗口的临时窗口
|
||||
self.dialog.grab_set() # 模态对话框
|
||||
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(self.dialog, padding="10")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 标题
|
||||
title_label = ttk.Label(main_frame, text="CCU状态详细信息", font=('Arial', 16, 'bold'))
|
||||
title_label.pack(pady=10)
|
||||
|
||||
# 时间标签
|
||||
self.time_label = ttk.Label(main_frame, text="", font=('Arial', 10))
|
||||
self.time_label.pack(pady=5)
|
||||
|
||||
# 创建滚动文本框
|
||||
self.summary_text = scrolledtext.ScrolledText(main_frame, width=90, height=30, wrap=tk.WORD, font=('Courier', 10))
|
||||
self.summary_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(pady=10)
|
||||
|
||||
# 关闭按钮
|
||||
close_btn = ttk.Button(button_frame, text="关闭", command=self.dialog.destroy)
|
||||
close_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 刷新按钮
|
||||
refresh_btn = ttk.Button(button_frame, text="刷新", command=self.update_display)
|
||||
refresh_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 初始更新显示
|
||||
self.update_display()
|
||||
|
||||
def update_display(self):
|
||||
"""更新显示数据"""
|
||||
# 更新时间标签
|
||||
self.time_label.config(text=f"更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 生成汇总信息
|
||||
summary = self.generate_summary()
|
||||
|
||||
# 清空并插入新的汇总信息
|
||||
self.summary_text.delete(1.0, tk.END)
|
||||
self.summary_text.insert(1.0, summary)
|
||||
|
||||
def generate_summary(self):
|
||||
"""生成汇总信息"""
|
||||
return f"""CCU状态汇总 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
|
||||
燃料电池状态:
|
||||
运行模式: 0x{self.ccu_status.fc_mode:02X}
|
||||
运行状态: 0x{self.ccu_status.fc_status:02X}
|
||||
故障等级: 0x{self.ccu_status.fc_fault_level:02X}
|
||||
输出功率限制: {self.ccu_status.output_power_limit}W
|
||||
累计发电时间: {self.ccu_status.total_generation_time * 0.1:.1f}小时
|
||||
发电总功率: {self.ccu_status.total_generation_power}W
|
||||
故障等级1-4: {self.ccu_status.fault_level_1}, {self.ccu_status.fault_level_2}, {self.ccu_status.fault_level_3}, {self.ccu_status.fault_level_4}
|
||||
储氢容量: {self.ccu_status.hydrogen_capacity}%
|
||||
液氧容量: {self.ccu_status.liquid_oxygen_capacity}%
|
||||
|
||||
传感器数据:
|
||||
钯膜温度: {self.ccu_status.palladium_temp}℃
|
||||
主水路压力: {self.ccu_status.main_pipe_pressure * 0.01:.2f}kPa
|
||||
辅水路压力: {self.ccu_status.aux_pipe_pressure * 0.01:.2f}kPa
|
||||
舱室压力1/2: {self.ccu_status.cabin_pressure1}/{self.ccu_status.cabin_pressure2}Pa
|
||||
舱室温度1/2: {self.ccu_status.cabin_temp1}/{self.ccu_status.cabin_temp2}℃
|
||||
舱室湿度1/2: {self.ccu_status.cabin_humidity1}/{self.ccu_status.cabin_humidity2}%
|
||||
火焰探测器1/2: 0x{self.ccu_status.flame_detector1:02X}/0x{self.ccu_status.flame_detector2:02X}
|
||||
|
||||
浓度检测:
|
||||
H2浓度1/2/3: {self.ccu_status.h2_concentration1}/{self.ccu_status.h2_concentration2}/{self.ccu_status.h2_concentration3}
|
||||
O2浓度1/2: {self.ccu_status.o2_concentration1}/{self.ccu_status.o2_concentration2}
|
||||
CH3OH浓度1/2: {self.ccu_status.ch3oh_concentration1}/{self.ccu_status.ch3oh_concentration2}
|
||||
|
||||
应急电池:
|
||||
电池1电压/电流: {self.ccu_status.emergency_battery1_voltage}V/{self.ccu_status.emergency_battery1_current}A
|
||||
电池1最高温度: {self.ccu_status.emergency_battery1_max_temp}℃
|
||||
电池1故障字: 0x{self.ccu_status.emergency_battery1_fault_word:04X}
|
||||
电池2电压/电流: {self.ccu_status.emergency_battery2_voltage}V/{self.ccu_status.emergency_battery2_current}A
|
||||
电池2最高温度: {self.ccu_status.emergency_battery2_max_temp}℃
|
||||
电池2故障字: 0x{self.ccu_status.emergency_battery2_fault_word:04X}
|
||||
|
||||
舱室环境:
|
||||
舱室氧浓度: {self.ccu_status.cabin_ox_concentration}
|
||||
舱室温度: {self.ccu_status.cabin_temperature}℃
|
||||
舱室湿度: {self.ccu_status.cabin_humidity}%
|
||||
舱室压力: {self.ccu_status.cabin_pressure}Pa
|
||||
动力舱氧浓度: {self.ccu_status.power_cabin_ox_concentration}
|
||||
动力舱温度: {self.ccu_status.power_cabin_temperature}℃
|
||||
动力舱湿度: {self.ccu_status.power_cabin_humidity}%
|
||||
动力舱压力: {self.ccu_status.power_cabin_pressure}Pa
|
||||
|
||||
报警状态:
|
||||
动态报警标志: {[flag for flag in self.ccu_status.dyn_alarm_flag]}
|
||||
绝缘报警标志: {[flag for flag in self.ccu_status.ins_alarm_flag]}
|
||||
动态报警总数: {sum(self.ccu_status.dyn_alarm_flag)}
|
||||
绝缘报警总数: {sum(self.ccu_status.ins_alarm_flag)}
|
||||
|
||||
继电器状态:
|
||||
仪表继电器1/2: 0x{self.ccu_status.ins_relay_status1:02X}/0x{self.ccu_status.ins_relay_status2:02X}
|
||||
动力继电器1/2: 0x{self.ccu_status.dyn_relay_status1:02X}/0x{self.ccu_status.dyn_relay_status2:02X}
|
||||
|
||||
功率信息:
|
||||
仪表最大放电功率: {self.ccu_status.ins_max_discharge_power}W
|
||||
动力最大放电功率: {self.ccu_status.dyn_max_discharge_power}W
|
||||
仪表SOC: {self.ccu_status.ins_soc}%
|
||||
动力SOC: {self.ccu_status.dyn_soc}%
|
||||
仪表总能量: {self.ccu_status.ins_total_energy}Wh
|
||||
动力总能量: {self.ccu_status.dyn_total_energy}Wh
|
||||
仪表功率输入: {self.ccu_status.ins_power_input}W
|
||||
动力功率输入: {self.ccu_status.dyn_power_input}W
|
||||
|
||||
充电状态:
|
||||
仪表充电状态: 0x{self.ccu_status.ins_charge_status:02X}
|
||||
动力充电状态: 0x{self.ccu_status.dyn_charge_status:02X}
|
||||
|
||||
电气参数:
|
||||
仪表链路电压: {self.ccu_status.ins_voltage_link * 0.1:.1f}V
|
||||
仪表电池电压: {self.ccu_status.ins_voltage_pack * 0.1:.1f}V
|
||||
仪表电流: {self.ccu_status.ins_current * 0.1:.1f}A
|
||||
仪表正极绝缘电阻: {self.ccu_status.ins_resistance_pos}Ω
|
||||
仪表负极绝缘电阻: {self.ccu_status.ins_resistance_neg}Ω
|
||||
动力链路电压: {self.ccu_status.dyn_voltage_link * 0.1:.1f}V
|
||||
动力电池电压: {self.ccu_status.dyn_voltage_pack * 0.1:.1f}V
|
||||
动力电流: {self.ccu_status.dyn_current * 0.1:.1f}A
|
||||
动力正极绝缘电阻: {self.ccu_status.dyn_resistance_pos}Ω
|
||||
动力负极绝缘电阻: {self.ccu_status.dyn_resistance_neg}Ω
|
||||
|
||||
紧急状态:
|
||||
仪表紧急状态: 0x{self.ccu_status.ins_emergency_status:02X}
|
||||
动力紧急状态: 0x{self.ccu_status.dyn_emergency_status:02X}
|
||||
|
||||
SOC门限值:
|
||||
仪表SOC门限1/2/3: {self.ccu_status.ins_soc_threshold1}/{self.ccu_status.ins_soc_threshold2}/{self.ccu_status.ins_soc_threshold3}%
|
||||
动力SOC门限1/2/3: {self.ccu_status.dyn_soc_threshold1}/{self.ccu_status.dyn_soc_threshold2}/{self.ccu_status.dyn_soc_threshold3}%
|
||||
|
||||
功率限制值:
|
||||
仪表功率限制1/2: {self.ccu_status.ins_power_limit1}/{self.ccu_status.ins_power_limit2}W
|
||||
动力功率限制1/2: {self.ccu_status.dyn_power_limit1}/{self.ccu_status.dyn_power_limit2}W
|
||||
|
||||
设备在线状态:
|
||||
设备在线标志1: 0x{self.ccu_status.device_online_flag1:08X}
|
||||
设备在线标志2: 0x{self.ccu_status.device_online_flag2:08X}
|
||||
|
||||
其他状态:
|
||||
动力充电等级: 0x{self.ccu_status.dyn_charge_level:02X}
|
||||
动力漏水状态: 0x{self.ccu_status.dyn_water_leak_status:02X}
|
||||
仪表漏水DCDC状态: 0x{self.ccu_status.ins_water_leak_dcdc_status:02X}
|
||||
心跳计数器: {self.ccu_status.heartbeat}
|
||||
"""
|
||||
|
||||
def set_ccu_status(self, ccu_status):
|
||||
"""设置CCU状态数据"""
|
||||
self.ccu_status = ccu_status
|
||||
self.update_display()
|
||||
|
||||
|
||||
# 测试函数
|
||||
def test_ccu_summary_dialog():
|
||||
"""测试CCU状态汇总弹窗"""
|
||||
root = tk.Tk()
|
||||
root.withdraw() # 隐藏主窗口
|
||||
|
||||
# 设置一些测试数据
|
||||
test_status = ccuMsg.msg_ccUFbMsg()
|
||||
test_status.fc_mode = 0x02 # 自动模式
|
||||
test_status.fc_status = 0x06 # 运行状态
|
||||
test_status.output_power_limit = 15000
|
||||
test_status.total_generation_time = 100 # 10小时
|
||||
test_status.hydrogen_capacity = 80
|
||||
test_status.liquid_oxygen_capacity = 75
|
||||
test_status.ins_soc = 85
|
||||
test_status.dyn_soc = 90
|
||||
test_status.heartbeat = 12345
|
||||
test_status.palladium_temp = 65
|
||||
test_status.main_pipe_pressure = 1500 # 15.00 kPa
|
||||
test_status.aux_pipe_pressure = 800 # 8.00 kPa
|
||||
|
||||
# 设置一些报警标志
|
||||
test_status.dyn_alarm_flag[0] = 1
|
||||
test_status.ins_alarm_flag[2] = 1
|
||||
|
||||
# 创建弹窗
|
||||
dialog = CCUStatusSummaryDialog(root, test_status)
|
||||
|
||||
# 等待弹窗关闭
|
||||
root.wait_window(dialog.dialog)
|
||||
root.destroy()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_ccu_summary_dialog()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""运行配置管理:config.json 的加载 / 保存 / 默认值。
|
||||
|
||||
配置文件位置:
|
||||
- 打包后(exe):与可执行文件同目录
|
||||
- 源码运行:app/ 目录下
|
||||
首次运行不存在时自动生成默认配置。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
DEFAULTS = {
|
||||
"local": {"ip": "127.0.0.1", "port": 7000}, # 本机监听地址
|
||||
"c1": {"ip": "192.168.1.154", "port": 6000}, # 高压配电板(C1)
|
||||
"c2": {"ip": "192.168.1.158", "port": 6000}, # 仪表配电板(C2)
|
||||
"host": {"ip": "127.0.0.1", "port": 5001}, # 上位机(信息主机)
|
||||
"db_path": "test.db",
|
||||
"refresh_ms": 1000,
|
||||
"sim": {"interval": 1.0},
|
||||
}
|
||||
|
||||
|
||||
def default_config_dir():
|
||||
"""返回配置文件所在目录(打包后为 exe 目录,源码为 app/ 目录)。"""
|
||||
if getattr(sys, "frozen", False):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, path=None):
|
||||
self.path = path or os.path.join(default_config_dir(), "config.json")
|
||||
self.data = self._load()
|
||||
|
||||
def _load(self):
|
||||
if os.path.exists(self.path):
|
||||
try:
|
||||
with open(self.path, "r", encoding="utf-8") as f:
|
||||
loaded = json.load(f)
|
||||
merged = {}
|
||||
for key, value in DEFAULTS.items():
|
||||
if isinstance(value, dict):
|
||||
merged[key] = {**value, **loaded.get(key, {})}
|
||||
else:
|
||||
merged[key] = loaded.get(key, value)
|
||||
return merged
|
||||
except (json.JSONDecodeError, OSError):
|
||||
pass
|
||||
return json.loads(json.dumps(DEFAULTS))
|
||||
|
||||
def save(self):
|
||||
with open(self.path, "w", encoding="utf-8") as f:
|
||||
json.dump(self.data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# ---- 便捷访问 -------------------------------------------------
|
||||
@property
|
||||
def local_ip(self):
|
||||
return self.data["local"]["ip"]
|
||||
|
||||
@local_ip.setter
|
||||
def local_ip(self, v):
|
||||
self.data["local"]["ip"] = v
|
||||
|
||||
@property
|
||||
def local_port(self):
|
||||
return int(self.data["local"]["port"])
|
||||
|
||||
@local_port.setter
|
||||
def local_port(self, v):
|
||||
self.data["local"]["port"] = int(v)
|
||||
|
||||
@property
|
||||
def c1_ip(self):
|
||||
return self.data["c1"]["ip"]
|
||||
|
||||
@property
|
||||
def c1_port(self):
|
||||
return int(self.data["c1"]["port"])
|
||||
|
||||
@property
|
||||
def c2_ip(self):
|
||||
return self.data["c2"]["ip"]
|
||||
|
||||
@property
|
||||
def c2_port(self):
|
||||
return int(self.data["c2"]["port"])
|
||||
|
||||
@property
|
||||
def host_ip(self):
|
||||
return self.data["host"]["ip"]
|
||||
|
||||
@property
|
||||
def host_port(self):
|
||||
return int(self.data["host"]["port"])
|
||||
|
||||
@property
|
||||
def refresh_ms(self):
|
||||
return int(self.data["refresh_ms"])
|
||||
|
||||
@property
|
||||
def db_path(self):
|
||||
p = self.data["db_path"]
|
||||
if os.path.isabs(p):
|
||||
return p
|
||||
return os.path.join(default_config_dir(), p)
|
||||
|
||||
@db_path.setter
|
||||
def db_path(self, v):
|
||||
self.data["db_path"] = v
|
||||
|
||||
@property
|
||||
def sim_interval(self):
|
||||
return float(self.data["sim"]["interval"])
|
||||
|
||||
@sim_interval.setter
|
||||
def sim_interval(self, v):
|
||||
self.data["sim"]["interval"] = float(v)
|
||||
|
||||
# ---- 批量设置(供配置弹窗使用) --------------------------------
|
||||
def set_endpoints(self, local_ip, local_port, c1_ip, c1_port,
|
||||
c2_ip, c2_port, host_ip, host_port):
|
||||
self.data["local"] = {"ip": local_ip, "port": int(local_port)}
|
||||
self.data["c1"] = {"ip": c1_ip, "port": int(c1_port)}
|
||||
self.data["c2"] = {"ip": c2_ip, "port": int(c2_port)}
|
||||
self.data["host"] = {"ip": host_ip, "port": int(host_port)}
|
||||
@@ -0,0 +1,78 @@
|
||||
"""SQLite 存储:表结构与插入字段由协议 schema 自动生成。
|
||||
|
||||
消息类 -> 表名 映射见 TABLE_NAMES;插入时从 msg.to_dict() 取字段,
|
||||
与 protocol.py 保持单一数据源,避免手写列定义漂移。
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import threading
|
||||
|
||||
from . import protocol
|
||||
|
||||
|
||||
def _table_name(msg_cls):
|
||||
name = msg_cls.__name__
|
||||
if name.startswith("MsgDis"):
|
||||
name = name[len("MsgDis"):]
|
||||
# MsgDisHighVolBusFb -> msg_disHighVolBusFbMsg
|
||||
return "msg_dis" + name + "Msg"
|
||||
|
||||
|
||||
TABLE_NAMES = {cls: _table_name(cls) for cls in protocol.ALL_MESSAGES.values()}
|
||||
|
||||
|
||||
class DB:
|
||||
def __init__(self, db_path):
|
||||
self.db_path = db_path
|
||||
self.conn = None
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def connect(self):
|
||||
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self.create_tables()
|
||||
|
||||
def disconnect(self):
|
||||
"""与 close() 等价,兼容历史调用。"""
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
if self.conn is not None:
|
||||
self.conn.close()
|
||||
self.conn = None
|
||||
|
||||
def create_tables(self):
|
||||
with self.lock:
|
||||
cursor = self.conn.cursor()
|
||||
for msg_cls, table in TABLE_NAMES.items():
|
||||
cols = ", ".join(f'"{name}" INTEGER' for name in msg_cls.field_names())
|
||||
cursor.execute(
|
||||
f'CREATE TABLE IF NOT EXISTS "{table}" ('
|
||||
f'id INTEGER PRIMARY KEY AUTOINCREMENT, '
|
||||
f'timestamp DATETIME DEFAULT (datetime(\'now\', \'localtime\')), '
|
||||
f'{cols})'
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def insert_message(self, msg):
|
||||
"""按消息类型路由到对应数据表。返回 True/False。"""
|
||||
table = TABLE_NAMES.get(msg.__class__)
|
||||
if table is None:
|
||||
return False
|
||||
return self.insert_dict(table, msg.to_dict())
|
||||
|
||||
def insert_dict(self, table, data_dict):
|
||||
with self.lock:
|
||||
try:
|
||||
if self.conn is None:
|
||||
self.connect()
|
||||
if self.conn is None:
|
||||
return False
|
||||
columns = ", ".join(f'"{c}"' for c in data_dict.keys())
|
||||
placeholders = ", ".join(["?"] * len(data_dict))
|
||||
query = f'INSERT INTO "{table}" ({columns}) VALUES ({placeholders})'
|
||||
self.conn.cursor().execute(query, list(data_dict.values()))
|
||||
self.conn.commit()
|
||||
return True
|
||||
except sqlite3.Error:
|
||||
return False
|
||||
@@ -0,0 +1,65 @@
|
||||
"""日志模块:UI 文本控件 + 控制台/文件输出,线程安全。
|
||||
|
||||
UI 更新不直接在任意线程操作 Tk 控件,而是写入 queue;
|
||||
主线程通过 drain() 周期性取走并写入文本框(见 app._refresh_ui)。
|
||||
"""
|
||||
|
||||
import os
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class AppLogger:
|
||||
def __init__(self, widget=None, file_path=None, max_lines=200,
|
||||
console=True):
|
||||
self.widget = widget
|
||||
self.file_path = file_path
|
||||
self.max_lines = max_lines
|
||||
self.console = console
|
||||
self._lock = threading.Lock()
|
||||
self._queue = queue.Queue()
|
||||
if file_path:
|
||||
os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)
|
||||
|
||||
def _format(self, level, msg):
|
||||
return f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] [{level}] {msg}"
|
||||
|
||||
def info(self, msg):
|
||||
self._write("INFO", msg)
|
||||
|
||||
def warning(self, msg):
|
||||
self._write("WARN", msg)
|
||||
|
||||
def error(self, msg):
|
||||
self._write("ERROR", msg)
|
||||
|
||||
def _write(self, level, msg):
|
||||
line = self._format(level, msg)
|
||||
self._queue.put(line)
|
||||
with self._lock:
|
||||
if self.console:
|
||||
print(line)
|
||||
if self.file_path:
|
||||
try:
|
||||
with open(self.file_path, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def drain(self):
|
||||
"""主线程调用:把队列中的日志写入 UI 文本框。"""
|
||||
if self.widget is None or self._queue.empty():
|
||||
return
|
||||
try:
|
||||
self.widget.config(state="normal")
|
||||
while not self._queue.empty():
|
||||
self.widget.insert("end", self._queue.get_nowait() + "\n")
|
||||
count = int(self.widget.index("end-1c").split(".")[0])
|
||||
if count > self.max_lines:
|
||||
self.widget.delete("1.0", f"{count - self.max_lines}.0")
|
||||
self.widget.see("end")
|
||||
self.widget.config(state="disabled")
|
||||
except Exception:
|
||||
# 窗口销毁等异常静默
|
||||
pass
|
||||
@@ -0,0 +1,440 @@
|
||||
"""数据驱动的状态/操作面板构建器。
|
||||
|
||||
由 BusSpec / CmdSpec 声明式描述每个母线的字段、显示方式与控制项,
|
||||
build_bus_page / update_bus_page 负责创建与刷新界面,
|
||||
避免每个母线重复手写布局与 if/elif 更新逻辑。
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from dataclasses import dataclass
|
||||
from tkinter import ttk
|
||||
|
||||
from . import protocol
|
||||
from .protocol import BREAKER_ON, BREAKER_OFF, BREAKER_FAULT
|
||||
|
||||
# 配色
|
||||
GREEN = "#2e7d32"
|
||||
RED = "#d32f2f"
|
||||
BLACK = "#212121"
|
||||
GRAY = "#9e9e9e"
|
||||
|
||||
|
||||
def breaker_color(raw):
|
||||
if raw == BREAKER_ON:
|
||||
return GREEN
|
||||
if raw == BREAKER_OFF:
|
||||
return BLACK
|
||||
if raw == BREAKER_FAULT:
|
||||
return RED
|
||||
return GRAY
|
||||
|
||||
|
||||
def bool_ok(raw, ok=BREAKER_ON):
|
||||
return raw == ok
|
||||
|
||||
|
||||
def make_light(parent, size=18):
|
||||
canvas = tk.Canvas(parent, width=size, height=size, bg="white",
|
||||
highlightthickness=0)
|
||||
canvas.create_oval(2, 2, size - 2, size - 2, fill=GRAY, outline="black")
|
||||
return canvas
|
||||
|
||||
|
||||
def set_light(canvas, color):
|
||||
canvas.delete("all")
|
||||
canvas.create_oval(2, 2, int(canvas["width"]) - 2,
|
||||
int(canvas["height"]) - 2, fill=color, outline="black")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@dataclass
|
||||
class BusSpec:
|
||||
key: str # 总线标识
|
||||
title: str # 页标题
|
||||
msg_cls: type
|
||||
breakers: list # [(label, field)]
|
||||
voltages: list # [(label, field, scale, unit)]
|
||||
currents: list # [(label, field, scale, unit)]
|
||||
booleans: list # [(label, field)] ok=BREAKER_ON 为正常
|
||||
derived: list # [(label, getter)] getter(msg)->(text, color)
|
||||
|
||||
|
||||
HIGH_VOL_BUS = BusSpec(
|
||||
key="high_vol",
|
||||
title="动力母线状态",
|
||||
msg_cls=protocol.MsgDisHighVolBusFb,
|
||||
breakers=[
|
||||
("燃料电池断路器", "fuelCellCircuitBreaker"),
|
||||
("动力锂电池断路器", "powerLithiumBatteryCircuitBreaker"),
|
||||
("推进电机断路器", "propulsionMotorCircuitBreaker"),
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("DC/DC模块断路器", "dcDc5ModuleCircuitBreaker"),
|
||||
("艏高压配电盒断路器", "bowHighVoltageDistributionBoxCircuitBreaker"),
|
||||
("艉FT设备45断路器", "sternFTDevice45CircuitBreaker"),
|
||||
("艉舵机开关1断路器", "sternRudderSwitch1CircuitBreaker"),
|
||||
("艉舵机开关2断路器", "sternRudderSwitch2CircuitBreaker"),
|
||||
("FB备用断路器", "fbReservedCircuitBreaker"),
|
||||
("TYZ备用断路器", "tyzReservedCircuitBreaker"),
|
||||
("备用断路器", "reservedCircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("动力母线电压", "powerBusVoltage", 0.1, "V"),
|
||||
("A母线电压", "busbarAVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("动力母线电流", "powerBusCurrent", 0.1, "A"),
|
||||
("A汇流排电流", "busbarACurrent", 0.1, "A"),
|
||||
("DC/DC5模块电流", "dcDc5ModuleCurrent", 0.1, "A"),
|
||||
("推进电机控制箱电流", "propulsionMotorControlBoxCurrent", 0.1, "A"),
|
||||
("锂电池组(仪表)电流", "lithiumBatteryGroupMeterCurrent", 0.1, "A"),
|
||||
("艏部高压配电箱电流", "bowHighVoltageDistributionBoxCurrent", 0.1, "A"),
|
||||
("艉部FT装置4/5电流", "sternFTDevice45Current", 0.1, "A"),
|
||||
("TYZ预留开关电流", "tyzReservedSwitchCurrent", 0.1, "A"),
|
||||
("艉舵开关1电流", "sternRudderSwitch1Current", 0.1, "A"),
|
||||
("艉舵开关2电流", "sternRudderSwitch2Current", 0.1, "A"),
|
||||
("FB预留开关电流", "fbReservedSwitchCurrent", 0.1, "A"),
|
||||
("备用电流1", "reservedCurrent1", 0.1, "A"),
|
||||
("备用电流2", "reservedCurrent2", 0.1, "A"),
|
||||
("备用电流3", "reservedCurrent3", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表失电信号", "meterPowerLossSignal"),
|
||||
("应急失电信号", "emergencyPowerLossSignal"),
|
||||
("动力母线绝缘", "powerBusInsulationStatus"),
|
||||
("艏高压绝缘", "aBusInsulationStatus"),
|
||||
],
|
||||
derived=[
|
||||
("冷却水压力", lambda m: (f"{m.coolWaterPressure * 0.1:.1f} kPa", BLACK)),
|
||||
("DC/DC温度", lambda m: (f"{m.dcdcTemperature - 40} ℃", BLACK)),
|
||||
],
|
||||
)
|
||||
|
||||
HIGH_A_VOL_BUS = BusSpec(
|
||||
key="high_a_vol",
|
||||
title="艏部高压状态",
|
||||
msg_cls=protocol.MsgDisHighAVolBusFb,
|
||||
breakers=[
|
||||
("艏FT设备123断路器", "bowFTDevice123CircuitBreaker"),
|
||||
("执行机构断路器", "actuatorCircuitBreaker"),
|
||||
("桅舵机控制箱断路器", "mastSteeringGearControlBoxCircuitBreaker"),
|
||||
("XCZ断路器", "xczCircuitBreaker"),
|
||||
("艏舵控制箱断路器", "bowRudderControlBoxCircuitBreaker"),
|
||||
("开盖启动气缸断路器", "openWaterCoverStartCylinderCircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("汇流排B电压", "busbarBVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("汇流排B电流", "busbarBCurrent", 0.1, "A"),
|
||||
("艏部FT设备123电流", "bowFTDevice123Current", 0.1, "A"),
|
||||
("启闭机构电流", "actuatorCurrent", 0.1, "A"),
|
||||
("桅杆舵机控制箱电流", "mastSteeringGearControlBoxCurrent", 0.1, "A"),
|
||||
("XCZ电流", "xczCurrent", 0.1, "A"),
|
||||
("艏舵控制箱电流", "bowRudderControlBoxCurrent", 0.1, "A"),
|
||||
("敞水盖启动电缸电流", "openWaterCoverStartCylinderCurrent", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表电源失电信号", "instrumentPowerFailureSignal"),
|
||||
("应急电源失电信号", "emergencyPowerFailureSignal"),
|
||||
("进水报警", "waterIngressionAlarm"),
|
||||
],
|
||||
derived=[],
|
||||
)
|
||||
|
||||
LOW_MAIN_BUS = BusSpec(
|
||||
key="low_main",
|
||||
title="仪表母线状态",
|
||||
msg_cls=protocol.MsgDisLowMainBusFb,
|
||||
breakers=[
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("艏低压配电盒断路器", "bowLowVoltageDistributionBoxCircuitBreaker"),
|
||||
("单元4仪表48V断路器", "unit4InstrumentDC48VCircuitBreaker"),
|
||||
("DC/C1直流配电板断路器", "dcC1DCDistributionPanelCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组2断路器", "emergencyLithiumBatteryGroup2CircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("仪表母线电压", "instrumentBusbarVoltage", 0.1, "V"),
|
||||
("应急2汇流排电压", "emergency2BusbarVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("艏部低压配电箱电流", "bowLowVoltageDistributionBoxCurrent", 0.1, "A"),
|
||||
("锂电池组(仪表)电流", "lithiumBatteryGroupInstrumentCurrent", 0.1, "A"),
|
||||
("DC-C1仪表DC48V电流", "dcC1InstrumentDC48VCurrent", 0.1, "A"),
|
||||
("应急锂电池组2电流", "emergencyLithiumBatteryGroup2Current", 0.1, "A"),
|
||||
("单元4仪表48V电流", "unit4InstrumentDC48VCurrent", 0.1, "A"),
|
||||
("复合能源管控应急DC48V电流", "compositeEnergyManagementSystemEmergencyDC48VCurrent", 0.1, "A"),
|
||||
("燃料电池安保应急DC48V电流", "fuelCellSecuritySystemEmergencyDC48VCurrent", 0.1, "A"),
|
||||
("PLC控制电源电流", "plcControlPowerCurrent", 0.1, "A"),
|
||||
("备用电流1", "reservedCurrent1", 0.1, "A"),
|
||||
("备用电流2", "reservedCurrent2", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表电源失电信号", "instrumentPowerFailureSignal"),
|
||||
("应急电源失电信号", "emergencyPowerFailureSignal"),
|
||||
("仪表汇流排绝缘低", "instrumentBusbarInsulationLow"),
|
||||
],
|
||||
derived=[],
|
||||
)
|
||||
|
||||
LOW_BUS = BusSpec(
|
||||
key="low_bus",
|
||||
title="艏部低压状态",
|
||||
msg_cls=protocol.MsgDisLowBusFb,
|
||||
breakers=[
|
||||
("单元1断路器", "unit1CircuitBreaker"),
|
||||
("单元2断路器", "unit2CircuitBreaker"),
|
||||
("单元3断路器", "unit3CircuitBreaker"),
|
||||
("单元5断路器", "unit5CircuitBreaker"),
|
||||
("艏PZ设备断路器", "bowPZDeviceCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组1断路器", "emergencyLithiumBatteryGroup1CircuitBreaker"),
|
||||
],
|
||||
voltages=[
|
||||
("仪表母排电压", "instrumentBusbarVoltage", 0.1, "V"),
|
||||
("应急1母线电压", "emergency1BusbarVoltage", 0.1, "V"),
|
||||
],
|
||||
currents=[
|
||||
("仪表母排电流", "instrumentBusbarCurrent", 0.1, "A"),
|
||||
("单元1电流", "unit1Current", 0.1, "A"),
|
||||
("单元2电流", "unit2Current", 0.1, "A"),
|
||||
("单元3电流", "unit3Current", 0.1, "A"),
|
||||
("单元5电流", "unit5Current", 0.1, "A"),
|
||||
("艏PZ设备电流", "bowPZDeviceCurrent", 0.1, "A"),
|
||||
("应急锂电池组1电流", "emergencyLithiumBatteryGroup1Current", 0.1, "A"),
|
||||
],
|
||||
booleans=[
|
||||
("仪表电失电信号", "instrumentPowerFailureSignal"),
|
||||
("应急电失电信号", "emergencyPowerFailureSignal"),
|
||||
("浸水报警", "waterIngressionAlarm"),
|
||||
],
|
||||
derived=[],
|
||||
)
|
||||
|
||||
BUS_SPECS = {spec.key: spec for spec in (HIGH_VOL_BUS, HIGH_A_VOL_BUS,
|
||||
LOW_MAIN_BUS, LOW_BUS)}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 状态页构建 / 刷新
|
||||
# ------------------------------------------------------------------
|
||||
def build_bus_page(parent, spec):
|
||||
"""构建一个母线状态页,返回用于刷新的 refs 字典。"""
|
||||
page = ttk.Frame(parent, padding=6)
|
||||
page.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# ---- 顶部:电压 + 断路器 ----
|
||||
top = ttk.LabelFrame(page, text="电压与断路器状态", padding=8)
|
||||
top.grid(row=0, column=0, sticky="nsew", pady=(0, 6))
|
||||
for c in range(3):
|
||||
top.grid_columnconfigure(c, weight=1)
|
||||
|
||||
voltage_widgets = {}
|
||||
for i, (label, field, scale, unit) in enumerate(spec.voltages):
|
||||
sub = ttk.Frame(top)
|
||||
sub.grid(row=0, column=i, padx=10, pady=4, sticky="w")
|
||||
ttk.Label(sub, text=f"{label}({unit}):", width=20, anchor="w").pack(side="left")
|
||||
value_lbl = ttk.Label(sub, text="--", width=10, anchor="e", relief="groove")
|
||||
value_lbl.pack(side="right")
|
||||
voltage_widgets[field] = {"widget": value_lbl, "scale": scale, "unit": unit}
|
||||
|
||||
ttk.Label(top, text="断路器状态:",
|
||||
font=("Arial", 10, "bold")).grid(row=1, column=0,
|
||||
columnspan=6, sticky="w", pady=(10, 4))
|
||||
|
||||
breaker_lights = {}
|
||||
field_for_label = {label: f for label, f in spec.breakers}
|
||||
for i, (label, _f) in enumerate(spec.breakers):
|
||||
bf = tk.Frame(top, relief="solid", borderwidth=1, padx=6, pady=2)
|
||||
bf.grid(row=2 + i // 3, column=i % 3, padx=6, pady=4, sticky="w")
|
||||
ttk.Label(bf, text=label, width=20, anchor="w").pack(side="left")
|
||||
light = make_light(bf)
|
||||
light.pack(side="right")
|
||||
breaker_lights[label] = light
|
||||
|
||||
# ---- 下部:电流 / 布尔量 / 派生量 ----
|
||||
group = ttk.LabelFrame(page, text="系统状态", padding=8)
|
||||
group.grid(row=1, column=0, sticky="nsew")
|
||||
page.grid_rowconfigure(1, weight=1)
|
||||
for c in range(3):
|
||||
group.grid_columnconfigure(c, weight=1)
|
||||
|
||||
values = {}
|
||||
booleans = {}
|
||||
derived = {}
|
||||
|
||||
items = []
|
||||
for it in spec.currents:
|
||||
items.append(("value", it))
|
||||
for it in spec.booleans:
|
||||
items.append(("bool", it))
|
||||
for it in spec.derived:
|
||||
items.append(("derived", it))
|
||||
|
||||
for i, (kind, it) in enumerate(items):
|
||||
f = ttk.Frame(group)
|
||||
f.grid(row=i // 3, column=i % 3, sticky="w", padx=8, pady=3)
|
||||
if kind == "value":
|
||||
label, field, scale, unit = it
|
||||
ttk.Label(f, text=f"{label}({unit}):", width=26, anchor="w").pack(side="left")
|
||||
v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove")
|
||||
v.pack(side="right")
|
||||
values[field] = {"widget": v, "scale": scale, "unit": unit}
|
||||
elif kind == "bool":
|
||||
label, field = it
|
||||
ttk.Label(f, text=f"{label}:", width=26, anchor="w").pack(side="left")
|
||||
v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove")
|
||||
v.pack(side="right")
|
||||
booleans[field] = {"widget": v}
|
||||
else:
|
||||
label, getter = it
|
||||
ttk.Label(f, text=f"{label}:", width=26, anchor="w").pack(side="left")
|
||||
v = ttk.Label(f, text="--", width=10, anchor="e", relief="groove")
|
||||
v.pack(side="right")
|
||||
derived[label] = {"widget": v, "getter": getter}
|
||||
|
||||
return {
|
||||
"frame": page,
|
||||
"field_for_label": field_for_label,
|
||||
"breaker_lights": breaker_lights,
|
||||
"voltage_widgets": voltage_widgets,
|
||||
"values": values,
|
||||
"booleans": booleans,
|
||||
"derived": derived,
|
||||
}
|
||||
|
||||
|
||||
def update_bus_page(spec, refs, msg):
|
||||
"""用最新反馈消息刷新状态页。msg 为 None 时跳过。"""
|
||||
if msg is None:
|
||||
return
|
||||
for label, light in refs["breaker_lights"].items():
|
||||
field = refs["field_for_label"][label]
|
||||
set_light(light, breaker_color(getattr(msg, field)))
|
||||
for field, info in refs["voltage_widgets"].items():
|
||||
v = getattr(msg, field) * info["scale"]
|
||||
info["widget"].config(text=f"{v:.1f} {info['unit']}")
|
||||
for field, info in refs["values"].items():
|
||||
v = getattr(msg, field) * info["scale"]
|
||||
info["widget"].config(text=f"{v:.1f} {info['unit']}")
|
||||
for field, info in refs["booleans"].items():
|
||||
ok = bool_ok(getattr(msg, field))
|
||||
info["widget"].config(text="正常" if ok else "故障",
|
||||
foreground=GREEN if ok else RED)
|
||||
for label, info in refs["derived"].items():
|
||||
text, color = info["getter"](msg)
|
||||
info["widget"].config(text=text, foreground=color)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 控制面板
|
||||
# ------------------------------------------------------------------
|
||||
@dataclass
|
||||
class CmdSpec:
|
||||
key: str
|
||||
title: str
|
||||
msg_cls: type
|
||||
dest: str # 'c1' / 'c2'
|
||||
actions: list # [(label, field)]
|
||||
|
||||
|
||||
HVB_CMD = CmdSpec("hvb", "动力母线配电指令", protocol.MsgDisHVBCmd, "c1", [
|
||||
("燃料电池断路器", "fuelCellCircuitBreaker"),
|
||||
("动力锂电池断路器", "powerLithiumBatteryCircuitBreaker"),
|
||||
("推进电机断路器", "propulsionMotorCircuitBreaker"),
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("DC/DC模块断路器", "dcDc5ModuleCircuitBreaker"),
|
||||
("艏高压配电盒断路器", "bowHighVoltageDistributionBoxCircuitBreaker"),
|
||||
("艉FT设备45断路器", "sternFTDevice45CircuitBreaker"),
|
||||
("艉舵机开关1断路器", "sternRudderSwitch1CircuitBreaker"),
|
||||
("艉舵机开关2断路器", "sternRudderSwitch2CircuitBreaker"),
|
||||
("FB备用断路器", "fbReservedCircuitBreaker"),
|
||||
("TYZ备用断路器", "tyzReservedCircuitBreaker"),
|
||||
("备用断路器", "reservedCircuitBreaker"),
|
||||
("DC/DC模块启动", "dcdc5Module"),
|
||||
("冷却系统启动", "coolingSystem"),
|
||||
])
|
||||
|
||||
HVBA_CMD = CmdSpec("hvba", "艏部高压配电指令", protocol.MsgDisHVBACmd, "c1", [
|
||||
("艏FT设备123断路器", "bowFTDevice123CircuitBreaker"),
|
||||
("执行机构断路器", "actuatorCircuitBreaker"),
|
||||
("桅舵机控制箱断路器", "mastSteeringGearControlBoxCircuitBreaker"),
|
||||
("XCZ断路器", "xczCircuitBreaker"),
|
||||
("艏舵控制箱断路器", "bowRudderControlBoxCircuitBreaker"),
|
||||
("开盖启动气缸断路器", "openWaterCoverStartCylinderCircuitBreaker"),
|
||||
])
|
||||
|
||||
LVMB_CMD = CmdSpec("lvmb", "仪表母线配电指令", protocol.MsgDisLVMBCmd, "c2", [
|
||||
("锂电池组仪表断路器", "lithiumBatteryGroupInstrumentCircuitBreaker"),
|
||||
("艏低压配电盒断路器", "bowLowVoltageDistributionBoxCircuitBreaker"),
|
||||
("单元4仪表48V断路器", "unit4InstrumentDC48VCircuitBreaker"),
|
||||
("DC/C1直流配电板断路器", "dcC1DCDistributionPanelCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组2断路器", "emergencyLithiumBatteryGroup2CircuitBreaker"),
|
||||
])
|
||||
|
||||
LVB_CMD = CmdSpec("lvb", "艏部低压配电指令", protocol.MsgDisLVBCmd, "c2", [
|
||||
("单元1断路器", "unit1CircuitBreaker"),
|
||||
("单元2断路器", "unit2CircuitBreaker"),
|
||||
("单元3断路器", "unit3CircuitBreaker"),
|
||||
("单元5断路器", "unit5CircuitBreaker"),
|
||||
("艏PZ设备断路器", "bowPZDeviceCircuitBreaker"),
|
||||
("备用断路器1", "reservedCircuitBreaker1"),
|
||||
("备用断路器2", "reservedCircuitBreaker2"),
|
||||
("应急锂电池组1断路器", "emergencyLithiumBatteryGroup1CircuitBreaker"),
|
||||
])
|
||||
|
||||
CMD_SPECS = {spec.key: spec for spec in (HVB_CMD, HVBA_CMD, LVMB_CMD, LVB_CMD)}
|
||||
|
||||
|
||||
def build_control_panel(parent, spec, on_cmd):
|
||||
"""构建控制面板。on_cmd(spec, field, state)。"""
|
||||
frame = ttk.LabelFrame(parent, text=spec.title, padding=10)
|
||||
frame.pack(fill="x", pady=5)
|
||||
for i, (label, field) in enumerate(spec.actions):
|
||||
row, col = i // 3, i % 3
|
||||
bf = ttk.Frame(frame)
|
||||
bf.grid(row=row, column=col, padx=10, pady=2, sticky="w")
|
||||
ttk.Label(bf, text=label, width=20).pack(side="left")
|
||||
ttk.Button(bf, text="合", width=3,
|
||||
command=lambda f=field: on_cmd(spec, f, BREAKER_ON)).pack(side="left", padx=2)
|
||||
ttk.Button(bf, text="分", width=3,
|
||||
command=lambda f=field: on_cmd(spec, f, BREAKER_OFF)).pack(side="left", padx=2)
|
||||
return frame
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DC/DC 故障字显示(供状态页与独立弹窗复用)
|
||||
# ------------------------------------------------------------------
|
||||
FAULT1_LABELS = ["过温关机", "输出过流", "输出过压", "输出欠压",
|
||||
"输入过压", "输入欠压", "输出短路保护", "内部故障"]
|
||||
FAULT2_LABELS = ["通信故障", "工作状态", "冷却泵1故障", "冷却泵2故障",
|
||||
"冷却水泄漏", "冷却泵1运行状态", "冷却泵2运行状态", "保留"]
|
||||
|
||||
|
||||
def build_dcdc_display(parent, title="DC/DC 故障字"):
|
||||
"""构建 DC/DC 故障字灯组,返回 refs={"words": {1: [canvas...], 2: [...]}}。"""
|
||||
frame = ttk.LabelFrame(parent, text=title, padding=8)
|
||||
frame.grid_columnconfigure(0, weight=1)
|
||||
words = {1: [], 2: []}
|
||||
for wi, labels in ((1, FAULT1_LABELS), (2, FAULT2_LABELS)):
|
||||
ttk.Label(frame, text=f"DC/DC故障字{wi}:",
|
||||
font=("Microsoft YaHei", 9, "bold")).grid(
|
||||
row=(wi - 1) * 2, column=0, sticky="w", pady=(6, 2))
|
||||
for i, label in enumerate(labels):
|
||||
pf = tk.Frame(frame, borderwidth=1, relief="solid", padx=4, pady=2)
|
||||
pf.grid(row=(wi - 1) * 2 + 1 + i // 4, column=i % 4, padx=5, pady=3, sticky="w")
|
||||
ttk.Label(pf, text=label).pack(side="left", padx=(0, 6))
|
||||
light = make_light(pf, 18)
|
||||
light.pack(side="right")
|
||||
words[wi].append(light)
|
||||
return {"widget": frame, "words": words}
|
||||
|
||||
|
||||
def update_dcdc_display(refs, word1, word2):
|
||||
for i in range(8):
|
||||
set_light(refs["words"][1][i], RED if (word1 & (1 << i)) else GREEN)
|
||||
set_light(refs["words"][2][i], RED if (word2 & (1 << i)) else GREEN)
|
||||
@@ -0,0 +1,374 @@
|
||||
"""配电系统 UDP 协议封装(声明式消息定义)。
|
||||
|
||||
字段顺序、大小与 C++ 侧 pmSysvariable.h / ccuUdpMsg.h 严格对齐。
|
||||
|
||||
帧格式(配电消息,单字节校验):
|
||||
start1(0x40) start2(0x40) | id(uint16 LE) | length(uint16 LE) | body | checkCode(和校验 &0xFF)
|
||||
|
||||
说明:
|
||||
- 字段表(FIELDS)是唯一数据源,length、pack/unpack 均由此推导,
|
||||
避免手工维护长度字段与 C++ 结构体大小不一致。
|
||||
- unpack 采用固定偏移解析 + 起始字节/最小长度/校验和校验。
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
DIS_START1 = 0x40
|
||||
DIS_START2 = 0x40
|
||||
|
||||
# 断路器状态定义(与 C++ 一致)
|
||||
BREAKER_ON = 0x55 # 合
|
||||
BREAKER_OFF = 0xAA # 分
|
||||
BREAKER_FAULT = 0x5A # 故障
|
||||
|
||||
|
||||
class Field:
|
||||
"""协议字段定义。fmt 支持 'B'(uchar) / 'H'(ushort LE)。"""
|
||||
|
||||
__slots__ = ("name", "fmt", "size")
|
||||
|
||||
def __init__(self, name, fmt):
|
||||
self.name = name
|
||||
self.fmt = fmt
|
||||
self.size = struct.calcsize(fmt)
|
||||
|
||||
|
||||
class Message:
|
||||
"""声明式消息基类:由 FIELDS 自动生成 pack/unpack 与长度。"""
|
||||
|
||||
MSG_ID = 0x0000
|
||||
CN = "未知消息"
|
||||
FIELDS = () # tuple[Field]
|
||||
|
||||
def __init__(self):
|
||||
self.start1 = DIS_START1
|
||||
self.start2 = DIS_START2
|
||||
self.msg_id = self.MSG_ID
|
||||
self.length = self.body_size()
|
||||
self.checkCode = 0
|
||||
for f in self.FIELDS:
|
||||
setattr(self, f.name, 0)
|
||||
|
||||
# ---- 派生属性 -------------------------------------------------
|
||||
@classmethod
|
||||
def body_size(cls):
|
||||
return sum(f.size for f in cls.FIELDS)
|
||||
|
||||
@classmethod
|
||||
def field_names(cls):
|
||||
return [f.name for f in cls.FIELDS]
|
||||
|
||||
def to_dict(self):
|
||||
return {f.name: getattr(self, f.name) for f in self.FIELDS}
|
||||
|
||||
def __repr__(self):
|
||||
parts = [f"{self.__class__.__name__}(id=0x{self.MSG_ID:04X}, len={self.length}"]
|
||||
for f in self.FIELDS:
|
||||
parts.append(f"{f.name}={getattr(self, f.name)}")
|
||||
return ", ".join(parts) + ")"
|
||||
|
||||
# ---- 打包/解包 ------------------------------------------------
|
||||
def pack(self):
|
||||
body = b""
|
||||
for f in self.FIELDS:
|
||||
body += struct.pack("<" + f.fmt, getattr(self, f.name))
|
||||
self.length = self.body_size()
|
||||
header = struct.pack("<BBHH", DIS_START1, DIS_START2, self.MSG_ID, self.length)
|
||||
data = header + body
|
||||
self.checkCode = sum(data) & 0xFF
|
||||
return data + struct.pack("<B", self.checkCode)
|
||||
|
||||
def unpack(self, data):
|
||||
if len(data) < 7:
|
||||
return False
|
||||
if data[0] != DIS_START1 or data[1] != DIS_START2:
|
||||
return False
|
||||
if struct.unpack_from("<H", data, 2)[0] != self.MSG_ID:
|
||||
return False
|
||||
expected = 6 + self.body_size() + 1
|
||||
if len(data) < expected:
|
||||
return False
|
||||
|
||||
self.start1, self.start2 = data[0], data[1]
|
||||
self.msg_id = struct.unpack_from("<H", data, 2)[0]
|
||||
self.length = struct.unpack_from("<H", data, 4)[0]
|
||||
|
||||
offset = 6
|
||||
for f in self.FIELDS:
|
||||
if f.size == 1:
|
||||
setattr(self, f.name, data[offset])
|
||||
offset += 1
|
||||
else:
|
||||
setattr(self, f.name, struct.unpack_from("<H", data, offset)[0])
|
||||
offset += 2
|
||||
self.checkCode = data[-1]
|
||||
|
||||
checksum = sum(data[:-1]) & 0xFF
|
||||
return checksum == data[-1]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 高压母线反馈 0x0005 body=53
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHighVolBusFb(Message):
|
||||
MSG_ID = 0x0005
|
||||
CN = "动力母线反馈"
|
||||
FIELDS = (
|
||||
Field("fuelCellCircuitBreaker", "B"),
|
||||
Field("powerLithiumBatteryCircuitBreaker", "B"),
|
||||
Field("propulsionMotorCircuitBreaker", "B"),
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("dcDc5ModuleCircuitBreaker", "B"),
|
||||
Field("bowHighVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("sternFTDevice45CircuitBreaker", "B"),
|
||||
Field("fbReservedCircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch1CircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch2CircuitBreaker", "B"),
|
||||
Field("tyzReservedCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker", "B"),
|
||||
Field("dcDcFaultWord1", "B"),
|
||||
Field("dcDcFaultWord2", "B"),
|
||||
Field("powerBusInsulationStatus", "B"),
|
||||
Field("aBusInsulationStatus", "B"),
|
||||
Field("meterPowerLossSignal", "B"),
|
||||
Field("emergencyPowerLossSignal", "B"),
|
||||
Field("dcdcTemperature", "B"),
|
||||
Field("powerBusVoltage", "H"),
|
||||
Field("dcDc5ModuleCurrent", "H"),
|
||||
Field("powerBusCurrent", "H"),
|
||||
Field("busbarAVoltage", "H"),
|
||||
Field("propulsionMotorControlBoxCurrent", "H"),
|
||||
Field("busbarACurrent", "H"),
|
||||
Field("lithiumBatteryGroupMeterCurrent", "H"),
|
||||
Field("bowHighVoltageDistributionBoxCurrent", "H"),
|
||||
Field("sternFTDevice45Current", "H"),
|
||||
Field("tyzReservedSwitchCurrent", "H"),
|
||||
Field("sternRudderSwitch1Current", "H"),
|
||||
Field("reservedCurrent1", "H"),
|
||||
Field("sternRudderSwitch2Current", "H"),
|
||||
Field("reservedCurrent2", "H"),
|
||||
Field("fbReservedSwitchCurrent", "H"),
|
||||
Field("reservedCurrent3", "H"),
|
||||
Field("coolWaterPressure", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 高压A母线反馈 0x0006 body=25
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHighAVolBusFb(Message):
|
||||
MSG_ID = 0x0006
|
||||
CN = "艏部高压反馈"
|
||||
FIELDS = (
|
||||
Field("bowFTDevice123CircuitBreaker", "B"),
|
||||
Field("actuatorCircuitBreaker", "B"),
|
||||
Field("mastSteeringGearControlBoxCircuitBreaker", "B"),
|
||||
Field("xczCircuitBreaker", "B"),
|
||||
Field("bowRudderControlBoxCircuitBreaker", "B"),
|
||||
Field("openWaterCoverStartCylinderCircuitBreaker", "B"),
|
||||
Field("instrumentPowerFailureSignal", "B"),
|
||||
Field("emergencyPowerFailureSignal", "B"),
|
||||
Field("waterIngressionAlarm", "B"),
|
||||
Field("busbarBVoltage", "H"),
|
||||
Field("busbarBCurrent", "H"),
|
||||
Field("bowFTDevice123Current", "H"),
|
||||
Field("actuatorCurrent", "H"),
|
||||
Field("mastSteeringGearControlBoxCurrent", "H"),
|
||||
Field("xczCurrent", "H"),
|
||||
Field("bowRudderControlBoxCurrent", "H"),
|
||||
Field("openWaterCoverStartCylinderCurrent", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表主母线反馈 0x0007 body=34
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLowMainBusFb(Message):
|
||||
MSG_ID = 0x0007
|
||||
CN = "仪表主母线反馈"
|
||||
FIELDS = (
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("bowLowVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("unit4InstrumentDC48VCircuitBreaker", "B"),
|
||||
Field("dcC1DCDistributionPanelCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup2CircuitBreaker", "B"),
|
||||
Field("instrumentPowerFailureSignal", "B"),
|
||||
Field("emergencyPowerFailureSignal", "B"),
|
||||
Field("instrumentBusbarInsulationLow", "B"),
|
||||
Field("instrumentBusbarVoltage", "H"),
|
||||
Field("bowLowVoltageDistributionBoxCurrent", "H"),
|
||||
Field("dcC1InstrumentDC48VCurrent", "H"),
|
||||
Field("reservedCurrent1", "H"),
|
||||
Field("emergencyLithiumBatteryGroup2Current", "H"),
|
||||
Field("lithiumBatteryGroupInstrumentCurrent", "H"),
|
||||
Field("unit4InstrumentDC48VCurrent", "H"),
|
||||
Field("reservedCurrent2", "H"),
|
||||
Field("emergency2BusbarVoltage", "H"),
|
||||
Field("compositeEnergyManagementSystemEmergencyDC48VCurrent", "H"),
|
||||
Field("fuelCellSecuritySystemEmergencyDC48VCurrent", "H"),
|
||||
Field("plcControlPowerCurrent", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表汇流排反馈 0x0008 body=29
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLowBusFb(Message):
|
||||
MSG_ID = 0x0008
|
||||
CN = "艏部低压反馈"
|
||||
FIELDS = (
|
||||
Field("unit1CircuitBreaker", "B"),
|
||||
Field("unit2CircuitBreaker", "B"),
|
||||
Field("unit3CircuitBreaker", "B"),
|
||||
Field("unit5CircuitBreaker", "B"),
|
||||
Field("bowPZDeviceCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup1CircuitBreaker", "B"),
|
||||
Field("instrumentPowerFailureSignal", "B"),
|
||||
Field("emergencyPowerFailureSignal", "B"),
|
||||
Field("waterIngressionAlarm", "B"),
|
||||
Field("instrumentBusbarVoltage", "H"),
|
||||
Field("instrumentBusbarCurrent", "H"),
|
||||
Field("unit1Current", "H"),
|
||||
Field("unit2Current", "H"),
|
||||
Field("unit3Current", "H"),
|
||||
Field("unit5Current", "H"),
|
||||
Field("bowPZDeviceCurrent", "H"),
|
||||
Field("emergencyLithiumBatteryGroup1Current", "H"),
|
||||
Field("emergency1BusbarVoltage", "H"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 动力母线配电指令 0x0001 body=14
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHVBCmd(Message):
|
||||
MSG_ID = 0x0001
|
||||
CN = "动力母线配电指令"
|
||||
FIELDS = (
|
||||
Field("fuelCellCircuitBreaker", "B"),
|
||||
Field("powerLithiumBatteryCircuitBreaker", "B"),
|
||||
Field("propulsionMotorCircuitBreaker", "B"),
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("dcDc5ModuleCircuitBreaker", "B"),
|
||||
Field("bowHighVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("sternFTDevice45CircuitBreaker", "B"),
|
||||
Field("fbReservedCircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch1CircuitBreaker", "B"),
|
||||
Field("sternRudderSwitch2CircuitBreaker", "B"),
|
||||
Field("tyzReservedCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker", "B"),
|
||||
Field("dcdc5Module", "B"),
|
||||
Field("coolingSystem", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 动力汇流排A配电指令 0x0002 body=6
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisHVBACmd(Message):
|
||||
MSG_ID = 0x0002
|
||||
CN = "艏部高压配电指令"
|
||||
FIELDS = (
|
||||
Field("bowFTDevice123CircuitBreaker", "B"),
|
||||
Field("actuatorCircuitBreaker", "B"),
|
||||
Field("mastSteeringGearControlBoxCircuitBreaker", "B"),
|
||||
Field("xczCircuitBreaker", "B"),
|
||||
Field("bowRudderControlBoxCircuitBreaker", "B"),
|
||||
Field("openWaterCoverStartCylinderCircuitBreaker", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表主母线配电指令 0x0003 body=7
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLVMBCmd(Message):
|
||||
MSG_ID = 0x0003
|
||||
CN = "仪表主母线配电指令"
|
||||
FIELDS = (
|
||||
Field("lithiumBatteryGroupInstrumentCircuitBreaker", "B"),
|
||||
Field("bowLowVoltageDistributionBoxCircuitBreaker", "B"),
|
||||
Field("unit4InstrumentDC48VCircuitBreaker", "B"),
|
||||
Field("dcC1DCDistributionPanelCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup2CircuitBreaker", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 仪表汇流排配电指令 0x0004 body=8
|
||||
# ---------------------------------------------------------------
|
||||
class MsgDisLVBCmd(Message):
|
||||
MSG_ID = 0x0004
|
||||
CN = "艏部低压配电指令"
|
||||
FIELDS = (
|
||||
Field("unit1CircuitBreaker", "B"),
|
||||
Field("unit2CircuitBreaker", "B"),
|
||||
Field("unit3CircuitBreaker", "B"),
|
||||
Field("unit5CircuitBreaker", "B"),
|
||||
Field("bowPZDeviceCircuitBreaker", "B"),
|
||||
Field("reservedCircuitBreaker1", "B"),
|
||||
Field("reservedCircuitBreaker2", "B"),
|
||||
Field("emergencyLithiumBatteryGroup1CircuitBreaker", "B"),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 消息注册表
|
||||
# ---------------------------------------------------------------
|
||||
FB_MESSAGES = {c.MSG_ID: c for c in (MsgDisHighVolBusFb, MsgDisHighAVolBusFb,
|
||||
MsgDisLowMainBusFb, MsgDisLowBusFb)}
|
||||
CMD_MESSAGES = {c.MSG_ID: c for c in (MsgDisHVBCmd, MsgDisHVBACmd,
|
||||
MsgDisLVMBCmd, MsgDisLVBCmd)}
|
||||
ALL_MESSAGES = {**FB_MESSAGES, **CMD_MESSAGES}
|
||||
|
||||
|
||||
def msg_id_of(data):
|
||||
"""从原始帧提取消息 ID。数据不合法时返回 None。"""
|
||||
if len(data) < 6:
|
||||
return None
|
||||
if data[0] != DIS_START1 or data[1] != DIS_START2:
|
||||
return None
|
||||
return struct.unpack_from("<H", data, 2)[0]
|
||||
|
||||
|
||||
def make_message(msg_id):
|
||||
cls = ALL_MESSAGES.get(msg_id)
|
||||
return cls() if cls else None
|
||||
|
||||
|
||||
def self_test():
|
||||
"""pack/unpack 往返 + 长度断言。返回 (ok, 报告)。"""
|
||||
lines = []
|
||||
ok = True
|
||||
for cls in (MsgDisHighVolBusFb, MsgDisHighAVolBusFb, MsgDisLowMainBusFb,
|
||||
MsgDisLowBusFb, MsgDisHVBCmd, MsgDisHVBACmd, MsgDisLVMBCmd,
|
||||
MsgDisLVBCmd):
|
||||
m = cls()
|
||||
data = m.pack()
|
||||
expected = 6 + cls.body_size() + 1
|
||||
frame_ok = len(data) == expected
|
||||
len_ok = m.length == cls.body_size()
|
||||
m2 = cls()
|
||||
round_ok = m2.unpack(data)
|
||||
# 翻转一个字节应导致校验失败
|
||||
bad = bytearray(data)
|
||||
bad[6] ^= 0xFF
|
||||
neg_ok = not m2.unpack(bytes(bad))
|
||||
status = "OK" if (frame_ok and len_ok and round_ok and neg_ok) else "FAIL"
|
||||
if status == "FAIL":
|
||||
ok = False
|
||||
lines.append(f" [{status}] {cls.__name__}: id=0x{cls.MSG_ID:04X} "
|
||||
f"body={cls.body_size()}B frame={len(data)}B "
|
||||
f"length_field={m.length} roundtrip={round_ok} neg={neg_ok}")
|
||||
return ok, "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ok, report = self_test()
|
||||
print(report)
|
||||
print("PROTOCOL SELF-TEST:", "PASS" if ok else "FAIL")
|
||||
@@ -0,0 +1,189 @@
|
||||
"""内置模拟器:产生四路母线反馈,支持配电指令回显。
|
||||
|
||||
设计:模拟器状态(反馈消息 + 最近指令)独立维护;周期性调用 update()
|
||||
返回需要注入解析管线的反馈消息列表。配电指令通过 apply_command()
|
||||
回写对应断路器状态,从而与真实设备的收发链路完全一致。
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
from . import protocol
|
||||
from .protocol import (MsgDisHighVolBusFb, MsgDisHighAVolBusFb,
|
||||
MsgDisLowMainBusFb, MsgDisLowBusFb,
|
||||
BREAKER_ON)
|
||||
|
||||
|
||||
class Simulator:
|
||||
# 指令消息 ID -> 对应反馈消息 ID
|
||||
CMD_TO_FB = {0x0001: 0x0005, 0x0002: 0x0006,
|
||||
0x0003: 0x0007, 0x0004: 0x0008}
|
||||
|
||||
def __init__(self):
|
||||
self.fb = {
|
||||
0x0005: MsgDisHighVolBusFb(),
|
||||
0x0006: MsgDisHighAVolBusFb(),
|
||||
0x0007: MsgDisLowMainBusFb(),
|
||||
0x0008: MsgDisLowBusFb(),
|
||||
}
|
||||
# 最近一次配电指令(用于回显)
|
||||
self.last_cmd = {
|
||||
0x0001: protocol.MsgDisHVBCmd(),
|
||||
0x0002: protocol.MsgDisHVBACmd(),
|
||||
0x0003: protocol.MsgDisLVMBCmd(),
|
||||
0x0004: protocol.MsgDisLVBCmd(),
|
||||
}
|
||||
self.fault_animation = True
|
||||
self.noise = True
|
||||
self._fault_counter = 0
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self._init_high_vol()
|
||||
self._init_high_a_vol()
|
||||
self._init_low_main()
|
||||
self._init_low_bus()
|
||||
# 复位指令状态
|
||||
for cmd in self.last_cmd.values():
|
||||
for name in cmd.field_names():
|
||||
setattr(cmd, name, 0)
|
||||
|
||||
# ---- 初始值(与 test.py 一致) ---------------------------------
|
||||
def _init_high_vol(self):
|
||||
m = self.fb[0x0005]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
for name in ("fuelCellCircuitBreaker", "powerLithiumBatteryCircuitBreaker",
|
||||
"propulsionMotorCircuitBreaker", "lithiumBatteryGroupInstrumentCircuitBreaker",
|
||||
"dcDc5ModuleCircuitBreaker", "bowHighVoltageDistributionBoxCircuitBreaker",
|
||||
"sternFTDevice45CircuitBreaker", "sternRudderSwitch1CircuitBreaker",
|
||||
"sternRudderSwitch2CircuitBreaker", "fbReservedCircuitBreaker",
|
||||
"tyzReservedCircuitBreaker", "reservedCircuitBreaker"):
|
||||
setattr(m, name, BREAKER_ON)
|
||||
m.dcDcFaultWord1 = 0x01
|
||||
m.dcDcFaultWord2 = 0x01
|
||||
m.powerBusInsulationStatus = BREAKER_ON
|
||||
m.aBusInsulationStatus = BREAKER_ON
|
||||
m.meterPowerLossSignal = BREAKER_ON
|
||||
m.emergencyPowerLossSignal = BREAKER_ON
|
||||
m.dcdcTemperature = 0x08
|
||||
m.powerBusVoltage = 6000
|
||||
m.dcDc5ModuleCurrent = 500
|
||||
m.powerBusCurrent = 1000
|
||||
m.busbarAVoltage = 6000
|
||||
m.propulsionMotorControlBoxCurrent = 800
|
||||
m.busbarACurrent = 500
|
||||
m.lithiumBatteryGroupMeterCurrent = 300
|
||||
m.bowHighVoltageDistributionBoxCurrent = 200
|
||||
m.sternFTDevice45Current = 150
|
||||
m.tyzReservedSwitchCurrent = 0
|
||||
m.sternRudderSwitch1Current = 100
|
||||
m.reservedCurrent1 = 1000
|
||||
m.sternRudderSwitch2Current = 100
|
||||
m.reservedCurrent2 = 1000
|
||||
m.fbReservedSwitchCurrent = 10000
|
||||
m.reservedCurrent3 = 1000
|
||||
m.coolWaterPressure = 1000
|
||||
|
||||
def _init_high_a_vol(self):
|
||||
m = self.fb[0x0006]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
for name in ("bowFTDevice123CircuitBreaker", "actuatorCircuitBreaker",
|
||||
"mastSteeringGearControlBoxCircuitBreaker", "xczCircuitBreaker",
|
||||
"bowRudderControlBoxCircuitBreaker", "openWaterCoverStartCylinderCircuitBreaker"):
|
||||
setattr(m, name, BREAKER_ON)
|
||||
m.instrumentPowerFailureSignal = BREAKER_ON
|
||||
m.emergencyPowerFailureSignal = BREAKER_ON
|
||||
m.waterIngressionAlarm = BREAKER_ON
|
||||
m.busbarBVoltage = 6000
|
||||
m.busbarBCurrent = 300
|
||||
m.bowFTDevice123Current = 200
|
||||
m.actuatorCurrent = 150
|
||||
m.mastSteeringGearControlBoxCurrent = 100
|
||||
m.xczCurrent = 50
|
||||
m.bowRudderControlBoxCurrent = 80
|
||||
m.openWaterCoverStartCylinderCurrent = 0
|
||||
|
||||
def _init_low_main(self):
|
||||
m = self.fb[0x0007]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
m.lithiumBatteryGroupInstrumentCircuitBreaker = BREAKER_ON
|
||||
m.bowLowVoltageDistributionBoxCircuitBreaker = BREAKER_ON
|
||||
m.unit4InstrumentDC48VCircuitBreaker = BREAKER_ON
|
||||
m.dcC1DCDistributionPanelCircuitBreaker = BREAKER_ON
|
||||
m.reservedCircuitBreaker1 = BREAKER_ON
|
||||
m.reservedCircuitBreaker2 = BREAKER_ON
|
||||
m.emergencyLithiumBatteryGroup2CircuitBreaker = BREAKER_ON
|
||||
m.instrumentPowerFailureSignal = BREAKER_ON
|
||||
m.emergencyPowerFailureSignal = BREAKER_ON
|
||||
m.instrumentBusbarInsulationLow = BREAKER_ON
|
||||
m.instrumentBusbarVoltage = 480
|
||||
m.bowLowVoltageDistributionBoxCurrent = 100
|
||||
m.dcC1InstrumentDC48VCurrent = 50
|
||||
m.reservedCurrent1 = 1100
|
||||
m.emergencyLithiumBatteryGroup2Current = 1000
|
||||
m.lithiumBatteryGroupInstrumentCurrent = 200
|
||||
m.unit4InstrumentDC48VCurrent = 50
|
||||
m.reservedCurrent2 = 1000
|
||||
m.emergency2BusbarVoltage = 480
|
||||
m.compositeEnergyManagementSystemEmergencyDC48VCurrent = 20
|
||||
m.fuelCellSecuritySystemEmergencyDC48VCurrent = 20
|
||||
m.plcControlPowerCurrent = 10
|
||||
|
||||
def _init_low_bus(self):
|
||||
m = self.fb[0x0008]
|
||||
for name in m.field_names():
|
||||
setattr(m, name, 0)
|
||||
for name in ("unit1CircuitBreaker", "unit2CircuitBreaker",
|
||||
"unit3CircuitBreaker", "unit5CircuitBreaker",
|
||||
"bowPZDeviceCircuitBreaker", "reservedCircuitBreaker1",
|
||||
"reservedCircuitBreaker2", "emergencyLithiumBatteryGroup1CircuitBreaker"):
|
||||
setattr(m, name, BREAKER_ON)
|
||||
m.instrumentPowerFailureSignal = BREAKER_ON
|
||||
m.emergencyPowerFailureSignal = BREAKER_ON
|
||||
m.waterIngressionAlarm = BREAKER_ON
|
||||
m.instrumentBusbarVoltage = 480
|
||||
m.instrumentBusbarCurrent = 5000
|
||||
m.unit1Current = 1000
|
||||
m.unit2Current = 1000
|
||||
m.unit3Current = 1000
|
||||
m.unit5Current = 1000
|
||||
m.bowPZDeviceCurrent = 500
|
||||
m.emergencyLithiumBatteryGroup1Current = 1000
|
||||
m.emergency1BusbarVoltage = 480
|
||||
|
||||
# ---- 对外接口 ------------------------------------------------
|
||||
def apply_command(self, cmd):
|
||||
"""收到配电指令后回写对应母线反馈的断路器状态。"""
|
||||
fb = self.fb.get(self.CMD_TO_FB.get(cmd.MSG_ID))
|
||||
if fb is None:
|
||||
return
|
||||
self.last_cmd[cmd.MSG_ID] = cmd
|
||||
for name in cmd.field_names():
|
||||
if hasattr(fb, name):
|
||||
setattr(fb, name, getattr(cmd, name))
|
||||
|
||||
def _jitter(self, fb):
|
||||
"""给电流/电压叠加轻微随机波动,模拟真实读数。"""
|
||||
for name in fb.field_names():
|
||||
if name.endswith("Current") or name.endswith("Voltage") or name.endswith("Pressure"):
|
||||
v = getattr(fb, name)
|
||||
if v > 0:
|
||||
setattr(fb, name, max(0, v + random.randint(-int(v * 0.05), int(v * 0.05))))
|
||||
|
||||
def _step_fault(self):
|
||||
m = self.fb[0x0005]
|
||||
w1 = ((m.dcDcFaultWord1 << 1) | (m.dcDcFaultWord1 >> 7)) & 0xFF
|
||||
w2 = ((m.dcDcFaultWord2 >> 1) | (m.dcDcFaultWord2 << 7)) & 0xFF
|
||||
m.dcDcFaultWord1 = w1
|
||||
m.dcDcFaultWord2 = w2
|
||||
|
||||
def update(self, interval=1.0):
|
||||
"""推进一帧模拟。返回需要注入解析管线的反馈消息列表。"""
|
||||
if self.fault_animation:
|
||||
self._step_fault()
|
||||
if self.noise:
|
||||
for fb in self.fb.values():
|
||||
self._jitter(fb)
|
||||
return list(self.fb.values())
|
||||
@@ -0,0 +1,117 @@
|
||||
"""UDP 通信层:socket 管理、收发线程、按消息 ID 分发。
|
||||
|
||||
线程安全:start/stop/reconfigure 可随时调用;
|
||||
收包在独立线程解析,解析成功后回调 on_feedback / on_command,
|
||||
原始帧经 on_raw 回调(用于转发/透传)。
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from . import protocol
|
||||
|
||||
|
||||
class UdpComm:
|
||||
def __init__(self, logger=None):
|
||||
self.logger = logger
|
||||
self.sock = None
|
||||
self.running = False
|
||||
self._thread = None
|
||||
self._lock = threading.Lock()
|
||||
# 回调
|
||||
self.on_feedback = None # callable(msg, addr)
|
||||
self.on_command = None # callable(msg, addr)
|
||||
self.on_raw = None # callable(data, addr)
|
||||
self.on_unknown = None # callable(data, addr) 解析失败/未知ID
|
||||
# 统计
|
||||
self.rx_count = 0
|
||||
self.tx_count = 0
|
||||
self.last_rx_time = None
|
||||
|
||||
def bind(self, ip, port):
|
||||
"""创建并绑定监听 socket(带超时 + 地址复用)。失败抛 OSError。"""
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.settimeout(0.1)
|
||||
s.bind((ip, int(port)))
|
||||
with self._lock:
|
||||
old = self.sock
|
||||
self.sock = s
|
||||
if old:
|
||||
old.close()
|
||||
return self
|
||||
|
||||
@property
|
||||
def bound(self):
|
||||
return self.sock is not None
|
||||
|
||||
def start(self):
|
||||
if self.running:
|
||||
return
|
||||
if self.sock is None:
|
||||
raise RuntimeError("socket 未绑定")
|
||||
self.running = True
|
||||
self._thread = threading.Thread(target=self._recv_loop, daemon=True,
|
||||
name="udp-recv")
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
t = self._thread
|
||||
self._thread = None
|
||||
if t:
|
||||
t.join(timeout=1.5)
|
||||
|
||||
def _recv_loop(self):
|
||||
while self.running:
|
||||
try:
|
||||
data, addr = self.sock.recvfrom(2048)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except OSError:
|
||||
break
|
||||
self.rx_count += 1
|
||||
self.last_rx_time = time.time()
|
||||
try:
|
||||
self._dispatch(data, addr)
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"分发消息异常: {e}")
|
||||
|
||||
def _dispatch(self, data, addr):
|
||||
if self.on_raw:
|
||||
self.on_raw(data, addr)
|
||||
msg_id = protocol.msg_id_of(data)
|
||||
cls = protocol.ALL_MESSAGES.get(msg_id)
|
||||
msg = None
|
||||
if cls:
|
||||
msg = cls()
|
||||
if not msg.unpack(data):
|
||||
msg = None
|
||||
if msg is None:
|
||||
if self.on_unknown:
|
||||
self.on_unknown(data, addr)
|
||||
return
|
||||
if msg_id in protocol.CMD_MESSAGES:
|
||||
if self.on_command:
|
||||
self.on_command(msg, addr)
|
||||
else:
|
||||
if self.on_feedback:
|
||||
self.on_feedback(msg, addr)
|
||||
|
||||
def sendto(self, data, ip, port):
|
||||
"""发送原始字节。返回 True/False。"""
|
||||
if self.sock is None:
|
||||
return False
|
||||
try:
|
||||
self.sock.sendto(data, (ip, int(port)))
|
||||
self.tx_count += 1
|
||||
return True
|
||||
except OSError as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"发送到 {ip}:{port} 失败: {e}")
|
||||
return False
|
||||
|
||||
def send_message(self, msg, ip, port):
|
||||
return self.sendto(msg.pack(), ip, port)
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PyInstaller 打包脚本(可选)。
|
||||
|
||||
用法:
|
||||
pip install pyinstaller
|
||||
python3 build.py
|
||||
产物:dist/DisSysTest(.exe) —— 单文件免安装,可在目标平台直接运行。
|
||||
|
||||
注意:需在目标平台(如 Windows)上执行,PyInstaller 不支持跨平台交叉打包。
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def main():
|
||||
if shutil.which("pyinstaller") is None and not os.path.exists(
|
||||
os.path.join(sys.prefix, "Scripts", "pyinstaller.exe")):
|
||||
print("未检测到 PyInstaller,请先安装: pip install pyinstaller")
|
||||
return 1
|
||||
|
||||
cmd = [
|
||||
sys.executable, "-m", "PyInstaller",
|
||||
"--onefile", # 单文件
|
||||
"--windowed", # 无控制台窗口
|
||||
"--name", "DisSysTest",
|
||||
"--paths", HERE,
|
||||
"--clean", "--noconfirm",
|
||||
os.path.join(HERE, "run.py"),
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
print("打包完成:dist/DisSysTest(.exe)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,4 @@
|
||||
# 运行时无第三方依赖(Tkinter/SQLite 均为 Python 标准库)。
|
||||
|
||||
# 仅打包时需要:
|
||||
pyinstaller>=5.0
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""配电系统调试助手入口。
|
||||
|
||||
用法:
|
||||
python3 run.py 启动界面
|
||||
python3 run.py --selftest 协议/配置/数据库自检
|
||||
python3 run.py --version 显示版本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from app.__main__ import main # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,92 @@
|
||||
"""协议层单元测试(无第三方依赖,直接运行)。
|
||||
|
||||
用法:python3 tests/test_protocol.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app import protocol # noqa: E402
|
||||
from app.protocol import (MsgDisHighVolBusFb, MsgDisHighAVolBusFb, # noqa: E402
|
||||
MsgDisLowMainBusFb, MsgDisLowBusFb,
|
||||
MsgDisHVBCmd, MsgDisHVBACmd, MsgDisLVMBCmd,
|
||||
MsgDisLVBCmd)
|
||||
|
||||
|
||||
def test_expected_sizes():
|
||||
# 与 C++ pmSysvariable.h 结构体大小对齐
|
||||
expected = {
|
||||
MsgDisHighVolBusFb: 53,
|
||||
MsgDisHighAVolBusFb: 25,
|
||||
MsgDisLowMainBusFb: 34,
|
||||
MsgDisLowBusFb: 29,
|
||||
MsgDisHVBCmd: 14,
|
||||
MsgDisHVBACmd: 6,
|
||||
MsgDisLVMBCmd: 7,
|
||||
MsgDisLVBCmd: 8,
|
||||
}
|
||||
for cls, size in expected.items():
|
||||
assert cls.body_size() == size, f"{cls.__name__} 应为 {size},实际 {cls.body_size()}"
|
||||
|
||||
|
||||
def test_roundtrip():
|
||||
for cls in (MsgDisHighVolBusFb, MsgDisHighAVolBusFb, MsgDisLowMainBusFb,
|
||||
MsgDisLowBusFb, MsgDisHVBCmd, MsgDisHVBACmd, MsgDisLVMBCmd,
|
||||
MsgDisLVBCmd):
|
||||
m = cls()
|
||||
for i, name in enumerate(m.field_names()):
|
||||
setattr(m, name, (i + 1) % 256)
|
||||
data = m.pack()
|
||||
assert len(data) == 6 + cls.body_size() + 1
|
||||
assert protocol.msg_id_of(data) == cls.MSG_ID
|
||||
m2 = cls()
|
||||
assert m2.unpack(data)
|
||||
for name in m.field_names():
|
||||
assert getattr(m2, name) == getattr(m, name), f"{cls.__name__}.{name}"
|
||||
|
||||
|
||||
def test_checksum_reject():
|
||||
for cls in (MsgDisHighVolBusFb, MsgDisHVBACmd):
|
||||
data = bytearray(cls().pack())
|
||||
data[6] ^= 0xFF
|
||||
assert not cls().unpack(bytes(data))
|
||||
|
||||
|
||||
def test_bad_start_byte_reject():
|
||||
data = bytearray(MsgDisHVBCmd().pack())
|
||||
data[0] = 0x00
|
||||
assert protocol.msg_id_of(bytes(data)) is None
|
||||
assert not MsgDisHVBCmd().unpack(bytes(data))
|
||||
|
||||
|
||||
def test_unknown_id():
|
||||
data = protocol.MsgDisHVBCmd().pack()
|
||||
data = data[0:2] + b'\x00\x00' + data[4:] # 把 id 改为 0x0000
|
||||
assert protocol.msg_id_of(data) == 0x0000
|
||||
assert protocol.make_message(0x0000) is None
|
||||
assert protocol.make_message(MsgDisHVBCmd.MSG_ID).MSG_ID == MsgDisHVBCmd.MSG_ID
|
||||
|
||||
|
||||
def test_short_frame_reject():
|
||||
data = MsgDisHVBCmd().pack()[:10]
|
||||
assert not MsgDisHVBCmd().unpack(data)
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||
failed = 0
|
||||
for t in tests:
|
||||
try:
|
||||
t()
|
||||
print(f" [OK] {t.__name__}")
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
print(f" [FAIL] {t.__name__}: {e}")
|
||||
print("TEST:", "PASS" if failed == 0 else f"FAIL ({failed})")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_all())
|
||||
Reference in New Issue
Block a user