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, QPainter, QBrush, QPen, QColor
import os
# ---------- 配置文件路径 ----------
CONFIG_FILE = "data.ini"
# ---------- Indicator 类 ----------
class Indicator(QWidget):
def __init__(self, color="gray"):
super().__init__()
self.color = color
self.init_ui()
def init_ui(self):
self.setFixedSize(20, 20) # 设置指示灯大小
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(QBrush(QColor(self.color)))
painter.setPen(QPen(Qt.black, 1))
painter.drawEllipse(2, 2, 16, 16) # 绘制圆形指示灯
def set_color(self, color):
self.color = color
self.update() # 重新绘制
# ---------- RelayControl 类 ----------
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(self):
self._ensure_socket()
self.client_socket.sendall(bytes.fromhex('00000000000601052040FF00'))
time.sleep(0.5)
self.client_socket.sendall(bytes.fromhex('000100000006010520400000'))
time.sleep(0.5)
def power_down(self):
self._ensure_socket()
self.client_socket.sendall(bytes.fromhex('00020000000601052041FF00'))
time.sleep(0.5)
self.client_socket.sendall(bytes.fromhex('000300000006010520410000'))
time.sleep(0.5)
# ---------- 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.start_btn.clicked.connect(lambda: self.on_ctrl(up=True))
self.stop_btn.clicked.connect(lambda: self.on_ctrl(up=False))
self.shutdown_btn.clicked.connect(self.on_shutdown)
# 替换为 Indicator 类
self.indicator = Indicator() # 使用 Indicator 类创建指示灯
self.indicator.setFixedSize(20, 20) # 设置指示灯大小
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()
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
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()
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.set_color(color) # 使用 Indicator 类更新颜色
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}')
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)
# -------------------- main --------------------
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
优化代码 删除没有存在的定义应用
最新发布