Files
zjk 54c32a180a 新增复合管控器(pCCU) + 惯导数据解析 + 设备控制页增强 + 0824协议文档归档
- 新增 pCCU 复合管控器程序(src/pCCU):FC/PM 双链路(UDP)、协议编解码器
  (FcProtocol/PmProtocol/Frame/Message/MessageRegistry)、快照与落库(DbStore)、
  内置 WebServer;挂入 src/CMakeLists.txt 与板卡 mission(h100.moos)
- 惯导/深度数据:新增 uDevice_insData 消息解析,接入 UpperCommManager,
  pPowerMangerHost 注册监视,UpmsgTest 覆盖
- pPowerMangerHost 设备控制页增强(web/index.html)
- 部署/运维脚本改为 4 个 systemd 服务(moosdb/pPowerManger/pPowerMangerHost/pCCU):
  deploy/clean-data/fetch-data 同步适配
- 协议文档:归档 0824 控制主机与复合管控器通信协议 xlsx 及关联 docx,移除旧 md
- 测试:新增 pccu 单元/集成测试与 disSysTest Python 配电仿真
- 清理:移除未使用的 CCU 状态调试打印 coutMsg;.gitignore 补充编译产物/临时文件忽略
2026-08-26 22:43:10 +08:00

441 lines
18 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""电源管理/配电系统调试主程序。
主窗口组织:
- 菜单:通信选项 / 配电操作 / 配置 / 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()