展示按钮 推送udp
parent
da0e41f475
commit
ee351733fe
|
|
@ -0,0 +1,181 @@
|
|||
"""
|
||||
PCM_Report 看板查看器按钮模块
|
||||
使用方法:在 ui_main.py 中导入并调用 add_dashboard_button()
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import (
|
||||
QPushButton, QFileDialog, QMessageBox, QHBoxLayout, QWidget
|
||||
)
|
||||
|
||||
|
||||
def get_viewer_executable():
|
||||
"""
|
||||
获取看板查看器的可执行文件路径
|
||||
优先查找打包后的 exe,否则使用 Python 源文件
|
||||
"""
|
||||
# 获取当前程序所在目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的环境
|
||||
current_dir = Path(sys.executable).parent
|
||||
else:
|
||||
# 开发环境
|
||||
current_dir = Path(__file__).parent
|
||||
|
||||
# 1. 首先查找打包后的 exe(与当前程序同级目录)
|
||||
exe_path = current_dir / "PCM_Viewer.exe"
|
||||
if exe_path.exists():
|
||||
return str(exe_path), True # True 表示是 exe
|
||||
|
||||
# 2. 查找开发环境的 Python 源文件
|
||||
dev_path = Path(r"F:\PyPro\PCM_Viewer\main.py")
|
||||
if dev_path.exists():
|
||||
return str(dev_path), False # False 表示是 py 文件
|
||||
|
||||
# 3. 尝试相对路径(假设两个项目在同一目录下)
|
||||
relative_path = current_dir.parent / "PCM_Viewer" / "main.py"
|
||||
if relative_path.exists():
|
||||
return str(relative_path), False
|
||||
|
||||
return None, False
|
||||
|
||||
|
||||
def add_dashboard_button(window, toolbar_layout: QHBoxLayout):
|
||||
"""
|
||||
在工具栏添加"打开看板"按钮
|
||||
|
||||
Args:
|
||||
window: MainWindow 实例 (用于日志记录和消息框)
|
||||
toolbar_layout: 工具栏布局
|
||||
"""
|
||||
# 创建按钮
|
||||
open_viewer_btn = QPushButton("打开看板")
|
||||
open_viewer_btn.setToolTip("打开看板查看器 (全屏模式)")
|
||||
|
||||
# 绑定点击事件
|
||||
def on_open_viewer():
|
||||
"""打开 PCM_Viewer 全屏展示"""
|
||||
# 选择布局文件
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
window,
|
||||
"选择看板布局文件",
|
||||
"",
|
||||
"JSON文件 (*.json)"
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
# 检查文件是否存在
|
||||
if not Path(file_path).exists():
|
||||
QMessageBox.warning(window, "警告", "选择的文件不存在!")
|
||||
return
|
||||
|
||||
# 获取查看器可执行文件
|
||||
viewer_path, is_exe = get_viewer_executable()
|
||||
|
||||
if not viewer_path:
|
||||
QMessageBox.critical(
|
||||
window,
|
||||
"错误",
|
||||
"未找到看板查看器程序!\n"
|
||||
"请确保 PCM_Viewer.exe 或 main.py 存在。"
|
||||
)
|
||||
return
|
||||
|
||||
# 启动 PCM_Viewer 子进程
|
||||
try:
|
||||
if is_exe:
|
||||
# 打包后的 exe,直接运行
|
||||
cmd = [viewer_path, file_path, "--fullscreen"]
|
||||
else:
|
||||
# Python 源文件,使用 python 解释器运行
|
||||
cmd = ["python", viewer_path, file_path, "--fullscreen"]
|
||||
|
||||
# 使用 subprocess.Popen 启动独立进程
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
shell=False, # 不使用 shell,更安全
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
# 设置工作目录为查看器所在目录
|
||||
cwd=os.path.dirname(viewer_path) if is_exe else None
|
||||
)
|
||||
|
||||
# 记录日志
|
||||
if hasattr(window, 'logger'):
|
||||
window.logger.info(f"已启动看板查看器: {viewer_path} {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(window, "错误", f"启动看板失败: {str(e)}")
|
||||
if hasattr(window, 'logger'):
|
||||
window.logger.error(f"启动看板失败: {e}")
|
||||
|
||||
open_viewer_btn.clicked.connect(on_open_viewer)
|
||||
|
||||
# 添加到工具栏
|
||||
toolbar_layout.addWidget(open_viewer_btn)
|
||||
|
||||
return open_viewer_btn
|
||||
|
||||
|
||||
# 兼容直接复制到 ui_main.py 的函数形式
|
||||
def open_dashboard_viewer(window):
|
||||
"""
|
||||
打开 PCM_Viewer 全屏展示(可直接复制到 ui_main.py 使用)
|
||||
"""
|
||||
# 选择布局文件
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
window,
|
||||
"选择看板布局文件",
|
||||
"",
|
||||
"JSON文件 (*.json)"
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
# 检查文件是否存在
|
||||
if not Path(file_path).exists():
|
||||
QMessageBox.warning(window, "警告", "选择的文件不存在!")
|
||||
return
|
||||
|
||||
# 获取查看器可执行文件
|
||||
viewer_path, is_exe = get_viewer_executable()
|
||||
|
||||
if not viewer_path:
|
||||
QMessageBox.critical(
|
||||
window,
|
||||
"错误",
|
||||
"未找到看板查看器程序!\n"
|
||||
"请确保 PCM_Viewer.exe 或 main.py 存在。"
|
||||
)
|
||||
return
|
||||
|
||||
# 启动 PCM_Viewer 子进程
|
||||
try:
|
||||
if is_exe:
|
||||
# 打包后的 exe,直接运行
|
||||
cmd = [viewer_path, file_path, "--fullscreen"]
|
||||
else:
|
||||
# Python 源文件,使用 python 解释器运行
|
||||
cmd = ["python", viewer_path, file_path, "--fullscreen"]
|
||||
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
shell=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
cwd=os.path.dirname(viewer_path) if is_exe else None
|
||||
)
|
||||
|
||||
if hasattr(window, 'logger'):
|
||||
window.logger.info(f"已启动看板查看器: {viewer_path} {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(window, "错误", f"启动看板失败: {str(e)}")
|
||||
if hasattr(window, 'logger'):
|
||||
window.logger.error(f"启动看板失败: {e}")
|
||||
130
ui_main.py
130
ui_main.py
|
|
@ -1,6 +1,6 @@
|
|||
from pathlib import Path
|
||||
from typing import Dict, Callable, Optional, List, Tuple
|
||||
|
||||
import subprocess
|
||||
from PySide6.QtCore import QTimer, QThread, QObject, Signal, QUrl, QUrlQuery, QDateTime, Qt
|
||||
from PySide6.QtGui import QDesktopServices, QFont
|
||||
from PySide6.QtWidgets import (
|
||||
|
|
@ -1219,7 +1219,17 @@ class MainWindow(QMainWindow):
|
|||
self.power_off_btn.setFixedHeight(36)
|
||||
self.power_off_btn.clicked.connect(self._power_off_experiment_table)
|
||||
toolbar_layout.addWidget(self.power_off_btn)
|
||||
|
||||
# 数据展示按钮(放在上电和断电之间)
|
||||
self.data_display_btn = QPushButton("数据展示")
|
||||
self.data_display_btn.setStyleSheet(
|
||||
"QPushButton { font-weight:700; font-size:13px; color:white;"
|
||||
"padding:8px 20px; border-radius:6px; background-color:#2196f3; }"
|
||||
"QPushButton:hover { background-color:#1976d2; }"
|
||||
)
|
||||
self.data_display_btn.setCursor(Qt.PointingHandCursor)
|
||||
self.data_display_btn.setFixedHeight(36)
|
||||
self.data_display_btn.clicked.connect(self._open_dashboard_viewer)
|
||||
toolbar_layout.addWidget(self.data_display_btn)
|
||||
# 开始记录按钮(仅Debug模式显示)
|
||||
self.start_exp_btn = QPushButton("开始记录")
|
||||
self.start_exp_btn.setStyleSheet(
|
||||
|
|
@ -5483,6 +5493,122 @@ class MainWindow(QMainWindow):
|
|||
self.statusBar().showMessage(f"❌ 实验台断电异常: {e}", 5000)
|
||||
QMessageBox.critical(self, "错误", f"实验台断电时发生异常: {str(e)}")
|
||||
|
||||
def _open_dashboard_viewer(self):
|
||||
"""打开 PCM_Viewer 全屏展示"""
|
||||
# 选择布局文件
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"选择看板布局文件",
|
||||
"",
|
||||
"JSON文件 (*.json)"
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
# 检查文件是否存在
|
||||
from pathlib import Path
|
||||
if not Path(file_path).exists():
|
||||
QMessageBox.warning(self, "警告", "选择的文件不存在!")
|
||||
return
|
||||
|
||||
# 启动 PCM_Viewer 子进程(隐藏模式,通过UDP通信)
|
||||
try:
|
||||
self._start_pcm_viewer(file_path)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"启动看板失败: {str(e)}")
|
||||
self.logger.error(f"启动看板失败: {e}")
|
||||
|
||||
def _start_pcm_viewer(self, layout_path: str, udp_port: int = 9876):
|
||||
"""启动 PCM_Viewer 并通过 UDP 发送显示命令
|
||||
|
||||
Args:
|
||||
layout_path: 布局文件路径
|
||||
udp_port: UDP 通信端口
|
||||
"""
|
||||
import socket
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
|
||||
viewer_path = r"F:\PyPro\PCM_Viewer\dist\PCM_Viewer.exe"
|
||||
|
||||
# 检查 PCM_Viewer 是否已在运行(通过UDP探测)
|
||||
viewer_running = self._check_viewer_running(udp_port)
|
||||
|
||||
if not viewer_running:
|
||||
# 启动 PCM_Viewer(隐藏模式)
|
||||
self.logger.info("启动 PCM_Viewer...")
|
||||
subprocess.Popen(
|
||||
[viewer_path, "--hidden"],
|
||||
shell=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW # 不显示控制台窗口
|
||||
)
|
||||
# 等待 PCM_Viewer 启动
|
||||
time.sleep(1.5)
|
||||
|
||||
# 通过 UDP 发送显示命令
|
||||
self._send_udp_command({
|
||||
'action': 'show_and_fullscreen',
|
||||
'path': layout_path
|
||||
}, udp_port)
|
||||
|
||||
self.logger.info(f"已发送显示命令: {layout_path}")
|
||||
|
||||
def _check_viewer_running(self, port: int = 9876) -> bool:
|
||||
"""检查 PCM_Viewer 是否已在运行
|
||||
|
||||
Args:
|
||||
port: UDP 端口
|
||||
|
||||
Returns:
|
||||
bool: 是否运行中
|
||||
"""
|
||||
import socket
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(0.5)
|
||||
# 发送探测命令
|
||||
sock.sendto(b'ping', ('127.0.0.1', port))
|
||||
sock.close()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def _send_udp_command(self, command: dict, port: int = 9876):
|
||||
"""发送 UDP 命令到 PCM_Viewer
|
||||
|
||||
Args:
|
||||
command: 命令字典
|
||||
port: UDP 端口
|
||||
"""
|
||||
import socket
|
||||
import json
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
message = json.dumps(command, ensure_ascii=False)
|
||||
sock.sendto(message.encode('utf-8'), ('127.0.0.1', port))
|
||||
sock.close()
|
||||
except Exception as e:
|
||||
self.logger.error(f"发送UDP命令失败: {e}")
|
||||
raise
|
||||
|
||||
def _show_pcm_viewer(self):
|
||||
"""显示 PCM_Viewer 窗口(用于菜单或按钮调用)"""
|
||||
self._send_udp_command({'action': 'show'})
|
||||
|
||||
def _hide_pcm_viewer(self):
|
||||
"""隐藏 PCM_Viewer 窗口"""
|
||||
self._send_udp_command({'action': 'hide'})
|
||||
|
||||
def _exit_pcm_viewer(self):
|
||||
"""退出 PCM_Viewer"""
|
||||
self._send_udp_command({'action': 'exit'})
|
||||
|
||||
def _write_modbus_control_register(self, value: int) -> bool:
|
||||
"""通过原始Socket直接发送Modbus TCP报文写入控制寄存器1200
|
||||
完全抛弃pymodbus,使用最底层的socket通信,模拟Modbus Poll的行为
|
||||
|
|
|
|||
Loading…
Reference in New Issue