展示按钮 推送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 pathlib import Path
|
||||||
from typing import Dict, Callable, Optional, List, Tuple
|
from typing import Dict, Callable, Optional, List, Tuple
|
||||||
|
import subprocess
|
||||||
from PySide6.QtCore import QTimer, QThread, QObject, Signal, QUrl, QUrlQuery, QDateTime, Qt
|
from PySide6.QtCore import QTimer, QThread, QObject, Signal, QUrl, QUrlQuery, QDateTime, Qt
|
||||||
from PySide6.QtGui import QDesktopServices, QFont
|
from PySide6.QtGui import QDesktopServices, QFont
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
|
@ -1219,7 +1219,17 @@ class MainWindow(QMainWindow):
|
||||||
self.power_off_btn.setFixedHeight(36)
|
self.power_off_btn.setFixedHeight(36)
|
||||||
self.power_off_btn.clicked.connect(self._power_off_experiment_table)
|
self.power_off_btn.clicked.connect(self._power_off_experiment_table)
|
||||||
toolbar_layout.addWidget(self.power_off_btn)
|
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模式显示)
|
# 开始记录按钮(仅Debug模式显示)
|
||||||
self.start_exp_btn = QPushButton("开始记录")
|
self.start_exp_btn = QPushButton("开始记录")
|
||||||
self.start_exp_btn.setStyleSheet(
|
self.start_exp_btn.setStyleSheet(
|
||||||
|
|
@ -5483,6 +5493,122 @@ class MainWindow(QMainWindow):
|
||||||
self.statusBar().showMessage(f"❌ 实验台断电异常: {e}", 5000)
|
self.statusBar().showMessage(f"❌ 实验台断电异常: {e}", 5000)
|
||||||
QMessageBox.critical(self, "错误", f"实验台断电时发生异常: {str(e)}")
|
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:
|
def _write_modbus_control_register(self, value: int) -> bool:
|
||||||
"""通过原始Socket直接发送Modbus TCP报文写入控制寄存器1200
|
"""通过原始Socket直接发送Modbus TCP报文写入控制寄存器1200
|
||||||
完全抛弃pymodbus,使用最底层的socket通信,模拟Modbus Poll的行为
|
完全抛弃pymodbus,使用最底层的socket通信,模拟Modbus Poll的行为
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue