- 新增 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 补充编译产物/临时文件忽略
441 lines
18 KiB
Python
441 lines
18 KiB
Python
"""电源管理/配电系统调试主程序。
|
||
|
||
主窗口组织:
|
||
- 菜单:通信选项 / 配电操作 / 配置 / 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()
|