下面的代码 开关设备和关闭linux 按钮失效 无法发出控制信号
import sys
import time
import socket
import paramiko
from threading import Thread
from configparser import ConfigParser
from pathlib import Path
from PySide6.QtWidgets import (QApplication, QWidget, QLineEdit, QPushButton,
QHBoxLayout, QVBoxLayout, QMessageBox, QLabel, QComboBox)
from PySide6.QtCore import QObject, Signal, Qt
from PySide6.QtGui import QIcon
import os
# ---------- 配置文件路径 ----------
CONFIG_FILE = "data.ini"
# ---------- 工具函数 ----------
def tcp_port_alive(ip: str, port: int, timeout: int = 2) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(timeout)
return s.connect_ex((ip, port)) == 0
def host_reachable(ip: str, port: int = 502) -> tuple[bool, str]:
if tcp_port_alive(ip, port):
return True, f'TCP {port} 端口开放'
return False, '502 端口无法连接'
def shutdown_remote_linux(remote_host: str, ssh_user: str, ssh_pass: str, port: int = 22) -> None:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
ssh.connect(remote_host, port=port, username=ssh_user, password=ssh_pass)
stdin, stdout, stderr = ssh.exec_command('sudo shutdown -h now')
stdout.channel.recv_exit_status()
except Exception as e:
raise RuntimeError(f"SSH 关机失败: {e}") from e
finally:
ssh.close()
def verify_shutdown(remote_host: str, port: int = 22, timeout: int = 3) -> bool:
try:
with socket.create_connection((remote_host, port), timeout=timeout):
return False
except (socket.timeout, socket.error):
return True
def wait_shutdown_complete(remote_host: str, port: int = 22, max_wait: int = 60) -> None:
for _ in range(max_wait):
if verify_shutdown(remote_host, port):
return
time.sleep(1)
raise RuntimeError("等待关机超时")
def openlinux(remote_host: str, port: int = 22) -> bool:
while True:
try:
with socket.create_connection((remote_host, port), timeout=5):
return True
except (socket.timeout, socket.error):
continue
# ---------- 周期性SSH检测线程 ----------
class PeriodicSSHCheckWorker(QObject):
status_changed = Signal(bool, str) # True/"下位机已开机" or False/"下位机未连接"
def __init__(self, ssh_ip: str, port: int = 22):
super().__init__()
self.ssh_ip = ssh_ip
self.port = port
self._running = True
def stop(self):
self._running = False
def run(self):
last_status = None
while self._running:
try:
current_ok = tcp_port_alive(self.ssh_ip, self.port, timeout=3)
current_status = "下位机已开机" if current_ok else "下位机未连接"
if current_ok != last_status:
self.status_changed.emit(current_ok, current_status)
last_status = current_ok
except Exception:
if last_status is not None:
self.status_changed.emit(False, "下位机未连接")
last_status = None
# 每隔30秒检测一次
for _ in range(30):
if not self._running:
break
time.sleep(1)
# ---------- 探测线程 ----------
class DetectWorker(QObject):
done = Signal(bool, str)
ssh_status_first = Signal(bool, str)
def __init__(self, ip: str, port: int, ssh_ip: str, ssh_port: int = 22):
super().__init__()
self.ip, self.port = ip, port
self.ssh_ip, self._ssh_port = ssh_ip, ssh_port
def run(self):
ok, msg = host_reachable(self.ip, self.port)
ssh_ok = tcp_port_alive(self.ssh_ip, self._ssh_port, timeout=2)
if ssh_ok:
self.ssh_status_first.emit(True, "下位机已开机")
else:
self.ssh_status_first.emit(False, "下位机未连接")
self.done.emit(ok, msg)
# ---------- 继电器 ----------
class RelayControl:
def __init__(self):
self.client_socket = None
self._ip = None
self._port = None
def tcp_connect(self, ip_addr: str, ip_port: int):
self._ip, self._port = ip_addr, ip_port
self._reconnect()
def _reconnect(self):
try:
if self.client_socket:
self.client_socket.close()
except Exception:
pass
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.client_socket.settimeout(3)
self.client_socket.connect((self._ip, self._port))
def _ensure_socket(self):
try:
self.client_socket.getpeername()
except (OSError, AttributeError):
self._reconnect()
def tcp_disconnect(self):
try:
self.client_socket.close()
except Exception:
pass
self.client_socket = None
def power_up_1(self):
self._ensure_socket()
self.client_socket.sendall(bytes.fromhex('00000000000601052040FF00'))
def power_up_2(self):
self._ensure_socket()
self.client_socket.sendall(bytes.fromhex('000100000006010520400000'))
def power_down_1(self):
self._ensure_socket()
self.client_socket.sendall(bytes.fromhex('00020000000601052041FF00'))
def power_down_2(self):
self._ensure_socket()
self.client_socket.sendall(bytes.fromhex('000300000006010520410000'))
# ---------- 启动/停止线程(仅 PLC) ----------
class Worker(QObject):
finished = Signal()
error = Signal(str)
def __init__(self, relay: RelayControl, up: bool):
super().__init__()
self.relay = relay
self.up = up
def run(self):
try:
if self.up:
self.relay.power_up_1()
time.sleep(1)
self.relay.power_up_2()
else:
self.relay.power_down_1()
time.sleep(1)
self.relay.power_down_2()
self.finished.emit()
except Exception as e:
self.error.emit(str(e))
# ---------- 独立关机线程(仅 SSH) ----------
class ShutdownWorker(QObject):
finished = Signal()
error = Signal(str)
def __init__(self, ssh_ip: str, ssh_user: str, ssh_pwd: str):
super().__init__()
self.ssh_ip = ssh_ip
self.ssh_user = ssh_user
self.ssh_pwd = ssh_pwd
def run(self):
try:
shutdown_remote_linux(self.ssh_ip, self.ssh_user, self.ssh_pwd)
wait_shutdown_complete(self.ssh_ip)
self.finished.emit()
except Exception as e:
self.error.emit(str(e))
# ---------- 指示灯线程 ----------
class IndicatorWorker(QObject):
status_changed = Signal(bool, str)
def __init__(self, target_ip: str, port: int = 22):
super().__init__()
self.target_ip = target_ip
self.port = port
self._running = True
self._wait_for_on = False
self._wait_for_off = False
def wait_for_on(self):
self._wait_for_on = True
self._wait_for_off = False
def wait_for_off(self):
self._wait_for_off = True
self._wait_for_on = False
def stop(self):
self._running = False
def run(self):
while self._running:
if self._wait_for_on:
if openlinux(self.target_ip, self.port):
self.status_changed.emit(True, "下位机已开机")
self._wait_for_on = False
elif self._wait_for_off:
if verify_shutdown(self.target_ip, self.port):
self.status_changed.emit(False, "下位机已关机")
self._wait_for_off = False
else:
time.sleep(0.2)
# ---------- GUI 主窗口 ----------
class MainWindow(QWidget):
def __init__(self):
super().__init__()
base_path = os.path.dirname(os.path.abspath(__file__))
icon_path = os.path.join(base_path, "logo.ico") # 动态路径
# 设置窗口图标
self.setWindowIcon(QIcon(icon_path))
# ════════════ 可调常量 ════════════
WIN_WIDTH = 480
WIN_HEIGHT = 150
IP_WIDTH = 110
PORT_WIDTH = 80
USER_WIDTH = 80
BTN_WIDTH = 80
FONT_SIZE = 13
BG_COLOR = "#f5f5f5"
BTN_COLOR = "#e5e5e5"
BTN_HOVER = "#d0d0d0"
BTN_PRESSED = "#c0c0c0"
# ═════════════════════════════════
self.setFixedSize(WIN_WIDTH, WIN_HEIGHT)
self.setWindowTitle('HIL 开关机控制')
self.setStyleSheet(f"""
QWidget{{
background-color:{BG_COLOR};
font:{FONT_SIZE}px "Microsoft YaHei";
}}
QLineEdit{{
height:24px;
border:1px solid #ccc;
border-radius:4px;
padding:0 4px;
background:white;
}}
QPushButton{{
height:26px;
border:1px solid #bbb;
border-radius:4px;
background:{BTN_COLOR};
min-width:{BTN_WIDTH}px;
}}
QPushButton:hover{{
background:{BTN_HOVER};
}}
QPushButton:pressed{{
background:{BTN_PRESSED};
}}
QLabel{{
color:#333;
}}
QComboBox{{
height:24px;
border:1px solid #ccc;
border-radius:4px;
padding:0 4px;
}}
""")
# ---------------- 初始化配置 ----------------
self.config = ConfigParser()
self.load_config()
# ---------------- 创建控件 ----------------
self.profile_combo = QComboBox()
self.profile_combo.setFixedWidth(120)
self.update_profile_list()
self.ip_edit = QLineEdit('')
self.port_edit = QLineEdit('')
self.port_edit.setInputMask('00000')
self.ssh_ip_edit = QLineEdit('')
self.ssh_user_edit = QLineEdit('')
self.ssh_pwd_edit = QLineEdit('')
self.ssh_pwd_edit.setEchoMode(QLineEdit.Password)
self.ssh_pwd_edit.setMaxLength(32)
self.ssh_pwd_edit.setFixedWidth(110)
# 统一宽度
self.ip_edit.setFixedWidth(IP_WIDTH)
self.ssh_ip_edit.setFixedWidth(IP_WIDTH)
self.port_edit.setFixedWidth(PORT_WIDTH)
self.ssh_user_edit.setFixedWidth(USER_WIDTH)
self.profile_combo.setFixedWidth(IP_WIDTH)
self.conn_btn = QPushButton('连接')
self.conn_btn.setCheckable(True)
self.conn_btn.setFixedWidth(BTN_WIDTH)
self.conn_btn.clicked.connect(self.on_toggle_connect)
self._set_conn_style(False)
self.start_btn = QPushButton('启动')
self.stop_btn = QPushButton('停止')
self.shutdown_btn = QPushButton('关闭Linux')
self.start_btn.setFixedWidth(BTN_WIDTH)
self.indicator = QLabel('█') # 使用方块符号替代圆形
self.indicator.setFixedSize(18, 18) # 设置为正方形
self.indicator.setAlignment(Qt.AlignCenter) # 文字居中
self.indicator.setStyleSheet("""
QLabel {
font-size: 24px; /* 调整字体大小 */
color: gray;
border: 1px solid #ccc; /* 添加边框,使其更像方块 */
border-radius: 4px; /* 轻微圆角,可选 */
}
""")
self.indicator_text = QLabel("下位机未启动")
# ---------------- 布局 ----------------
row0 = QHBoxLayout()
row0.addWidget(QLabel('设备选择:'))
row0.addWidget(self.profile_combo)
row0.addWidget(self.conn_btn)
row0.addStretch()
row1 = QHBoxLayout()
row1.addWidget(QLabel('继电器IP:'))
row1.addWidget(self.ip_edit)
row1.addWidget(QLabel('端 口:'))
row1.addWidget(self.port_edit)
row1.addStretch()
row2 = QHBoxLayout()
row2.addWidget(QLabel('下位机IP:'))
row2.addWidget(self.ssh_ip_edit)
row2.addWidget(QLabel('用户名:'))
row2.addWidget(self.ssh_user_edit)
row2.addWidget(QLabel('密码:'))
row2.addWidget(self.ssh_pwd_edit)
row2.addStretch()
row3 = QHBoxLayout()
row3.addWidget(self.start_btn)
row3.addWidget(self.stop_btn)
row3.addWidget(self.shutdown_btn)
# 创建一个水平布局用于居中显示指示灯和文字
indicator_layout = QHBoxLayout()
indicator_layout.addWidget(QLabel('下位机状态:'))
indicator_layout.addWidget(self.indicator)
indicator_layout.addWidget(self.indicator_text)
indicator_layout.addStretch() # 添加弹性空间以居中
row3.addLayout(indicator_layout) # 将指示灯布局添加到主布局
row3.addStretch()
main = QVBoxLayout(self)
main.addLayout(row0)
main.addLayout(row1)
main.addLayout(row2)
main.addLayout(row3)
main.addStretch()
# 业务对象
self.relay = RelayControl()
self._thread = None
self.indicator_thread = None
self.indicator_worker = None
self.periodic_thread = None # 周期检测线程
self.periodic_worker = None # 周期检测工作对象
self._ssh_ever_connected = False
self._set_ctrl_enabled(False)
# 信号绑定
self.profile_combo.currentTextChanged.connect(self.on_profile_selected)
# 默认加载第一个配置
if self.profile_combo.count() > 0:
self.on_profile_selected(self.profile_combo.currentText())
# 注意:这里不再启动周期检测!只等“连接”后再启动
def load_config(self):
if not Path(CONFIG_FILE).exists():
# 创建默认配置
self.config['设备1'] = {
'relay_ip': '192.168.1.3',
'relay_port': '502',
'ssh_ip': '192.168.1.119',
'ssh_user': 'root',
'ssh_password': 'aertp2020'
}
self.config['设备2'] = {
'relay_ip': '192.168.0.3',
'relay_port': '502',
'ssh_ip': '192.168.2.119',
'ssh_user': 'root',
'ssh_password': '111111'
}
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
self.config.write(f)
else:
self.config.read(CONFIG_FILE, encoding='utf-8')
def update_profile_list(self):
self.profile_combo.clear()
self.profile_combo.addItems(self.config.sections())
def on_profile_selected(self, name):
if not name or not self.config.has_section(name):
return
sec = self.config[name]
self.ip_edit.setText(sec.get('relay_ip', ''))
self.port_edit.setText(sec.get('relay_port', ''))
self.ssh_ip_edit.setText(sec.get('ssh_ip', ''))
self.ssh_user_edit.setText(sec.get('ssh_user', ''))
self.ssh_pwd_edit.setText(sec.get('ssh_password', ''))
# ---------------- 连接/断开 ----------------
def on_toggle_connect(self):
if self.conn_btn.isChecked():
ip = self.ip_edit.text().strip()
if not ip:
QMessageBox.warning(self, '提示', 'IP 地址不能为空')
self.conn_btn.setChecked(False)
return
try:
port = int(self.port_edit.text())
except ValueError:
QMessageBox.warning(self, '提示', '端口必须是数字')
self.conn_btn.setChecked(False)
return
# === 禁止切换配置 ===
self.profile_combo.setEnabled(False)
self.conn_btn.setEnabled(False)
self.conn_btn.setText('连接中…')
self.detector = DetectWorker(ip, port, self.ssh_ip_edit.text().strip())
self.detector.ssh_status_first.connect(self._set_indicator)
self.detector.done.connect(self._on_detect_done)
Thread(target=self.detector.run, daemon=True).start()
else:
# 断开时关闭周期检测
self._stop_periodic_ssh_check()
try:
self.relay.tcp_disconnect()
except Exception:
pass
self._set_conn_style(False)
self._set_ctrl_enabled(False)
self._stop_indicator()
self._set_indicator(False, "下位机未连接")
# === 断开后恢复选择 ===
self.profile_combo.setEnabled(True)
def _on_detect_done(self, ok: bool, msg: str):
self.conn_btn.setEnabled(True)
if not ok:
QMessageBox.warning(self, '不可达', f'{self.ip_edit.text().strip()} 不可达({msg})')
self.conn_btn.setChecked(False)
self._set_conn_style(False)
# === 连接失败,恢复选择 ===
self.profile_combo.setEnabled(True)
return
try:
self.relay.tcp_connect(self.ip_edit.text().strip(), int(self.port_edit.text()))
self._set_conn_style(True)
self._set_ctrl_enabled(True)
self._start_indicator()
# === 成功连接后启动周期性SSH检测 ===
self._start_periodic_ssh_check()
except Exception as e:
QMessageBox.critical(self, '错误', f'连接失败:\n{e}')
self.conn_btn.setChecked(False)
self._set_conn_style(False)
# === 连接失败,恢复选择 ===
self.profile_combo.setEnabled(True)
# === 启动周期性检测(仅在连接成功后调用)===
def _start_periodic_ssh_check(self):
ssh_ip = self.ssh_ip_edit.text().strip()
if not ssh_ip or self.periodic_worker is not None:
return
self.periodic_worker = PeriodicSSHCheckWorker(ssh_ip)
self.periodic_worker.status_changed.connect(self._set_indicator)
self.periodic_thread = Thread(target=self.periodic_worker.run, daemon=True)
self.periodic_thread.start()
def _stop_periodic_ssh_check(self):
if self.periodic_worker:
self.periodic_worker.stop()
self.periodic_worker = None
# ---------------- 启动/停止(仅PLC) ----------------
def on_ctrl(self, up: bool):
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(False)
self.shutdown_btn.setEnabled(False)
if up:
self.indicator_worker.wait_for_on()
self.worker = Worker(self.relay, up=up)
self.worker.finished.connect(self._done)
self.worker.error.connect(self._error)
self._thread = Thread(target=self.worker.run, daemon=True)
self._thread.start()
# ---------------- 独立关闭Linux(仅SSH) ----------------
def on_shutdown(self):
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(False)
self.shutdown_btn.setEnabled(False)
self.indicator_worker.wait_for_off()
self.shutdown_worker = ShutdownWorker(
self.ssh_ip_edit.text().strip(),
self.ssh_user_edit.text().strip(),
self.ssh_pwd_edit.text().strip())
self.shutdown_worker.finished.connect(self._shutdown_done)
self.shutdown_worker.error.connect(self._shutdown_error)
Thread(target=self.shutdown_worker.run, daemon=True).start()
# ---------------- 指示灯 ----------------
def _start_indicator(self):
self._stop_indicator()
self.indicator_worker = IndicatorWorker(self.ssh_ip_edit.text().strip())
self.indicator_worker.status_changed.connect(self._set_indicator)
self.indicator_thread = Thread(target=self.indicator_worker.run, daemon=True)
self.indicator_thread.start()
def _stop_indicator(self):
if self.indicator_worker:
self.indicator_worker.stop()
self.indicator_worker = None
def _set_indicator(self, on: bool, text: str = ""):
color = "green" if on else "gray"
self.indicator.setStyleSheet(f"""
QLabel {{
font-size: 24px;
color: {color};
border: 1px solid #ccc;
border-radius: 4px;
}}
""")
if text:
self.indicator_text.setText(text)
if on and not self._ssh_ever_connected:
self._ssh_ever_connected = True
self.shutdown_btn.setEnabled(True)
# ---------------- 回调 ----------------
def _done(self):
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(True)
self.shutdown_btn.setEnabled(True)
QMessageBox.information(self, '成功', '指令执行完成')
def _error(self, msg):
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(True)
self.shutdown_btn.setEnabled(True)
QMessageBox.critical(self, '错误', f'指令执行失败:\n{msg}')
def _shutdown_done(self):
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(True)
self.shutdown_btn.setEnabled(True)
QMessageBox.information(self, '成功', '下位机已关闭')
def _shutdown_error(self, msg):
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(True)
self.shutdown_btn.setEnabled(True)
QMessageBox.critical(self, '错误', f'关闭下位机失败:\n{msg}')
# ---------------- UI 辅助 ----------------
def _set_conn_style(self, connected: bool):
if connected:
self.conn_btn.setText('已连接')
self.conn_btn.setStyleSheet('background-color:#4CAF50;color:white;')
else:
self.conn_btn.setText('连接')
self.conn_btn.setStyleSheet('background-color:#e5e5e5;color:black;')
def _set_ctrl_enabled(self, on: bool):
self.start_btn.setEnabled(on)
self.stop_btn.setEnabled(on)
self.shutdown_btn.setEnabled(on and self._ssh_ever_connected)
#pyinstaller -F --clean --onefile --windowed --name UI2 --icon=logo.ico --add-data="logo.ico;." --exclude-module=matplotlib --exclude-module=tkinter --exclude-module=PyQt5 --exclude-module=PyQt6 --exclude-module=PySide2 --exclude-module=PySide6.QtWebEngine --exclude-module=PySide6.QtWebEngineCore --exclude-module=PySide6.QtWebEngineWidgets --exclude-module=PySide6.QtSql --exclude-module=PySide6.QtXml --exclude-module=PySide6.QtMultimedia --exclude-module=PySide6.QtMultimediaWidgets --exclude-module=PySide6.QtOpenGL --exclude-module=PySide6.QtOpenGLWidgets --exclude-module=PySide6.QtPrintSupport --exclude-module=PySide6.QtTest --exclude-module=PySide6.QtHelp --exclude-module=PySide6.QtDesigner --exclude-module=PySide6.QtUiTools --exclude-module=PySide6.QtQml --exclude-module=PySide6.QtQuick --exclude-module=PySide6.QtQuickWidgets --exclude-module=PySide6.QtRemoteObjects --exclude-module=PySide6.QtScxml --exclude-module=PySide6.QtSensors --exclude-module=PySide6.QtSpatialAudio --exclude-module=PySide6.QtStateMachine --exclude-module=PySide6.QtSvg --exclude-module=PySide6.QtSvgWidgets --exclude-module=PySide6.QtTextToSpeech --exclude-module=PySide6.QtVirtualKeyboard --exclude-module=PySide6.Qt3DCore --exclude-module=PySide6.Qt3DRender --exclude-module=PySide6.Qt3DInput --exclude-module=PySide6.Qt3DLogic --exclude-module=PySide6.Qt3DExtras --exclude-module=PySide6.Qt3DAnimation --exclude-module=PySide6.Qt3DPhysics UI2.py
# # -------------------- main --------------------
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())