From 1eb5e3f2d3b5376210623fb1774d38aba0038c2c Mon Sep 17 00:00:00 2001 From: zcwBit Date: Tue, 8 Jul 2025 22:35:32 +0800 Subject: [PATCH] =?UTF-8?q?0708=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UI/Main/Main.py | 7 +- UI/Setting/RemoteConnectSetting.py | 18 ++- UI/Setting/SearchDeviceWidget.py | 220 ++++++++++++++++++++++++----- UI/VarManages/RpcVarModel.py | 135 ++++++++++++++++++ UI/VarManages/VarTable.py | 19 +++ UI/VarManages/VarWidget.py | 139 +++++++----------- protocol/ProtocolManage.py | 36 +++-- protocol/RPC/RpcClient.py | 24 +++- protocol/RPC/RpcServer.py | 87 ++++++++++-- 9 files changed, 535 insertions(+), 150 deletions(-) create mode 100644 UI/VarManages/RpcVarModel.py diff --git a/UI/Main/Main.py b/UI/Main/Main.py index d0de344..e092cf5 100644 --- a/UI/Main/Main.py +++ b/UI/Main/Main.py @@ -4,7 +4,9 @@ import pandas as pd from PyQt5 import QtWidgets from PyQt5.QtCore import Qt from PyQt5.Qt import * -from PyQt5.QtWidgets import QApplication, QMainWindow, QStackedWidget, QMessageBox, QStackedWidget, QWidget, QTabWidget, QFileDialog +from PyQt5.QtWidgets import QApplication, QMainWindow, QStackedWidget, QMessageBox, QStackedWidget, QWidget, QTabWidget, QFileDialog, QPushButton +from PyQt5.QtGui import QIcon +from PyQt5.QtCore import QSize from ctypes import POINTER, cast from ctypes.wintypes import MSG from win32 import win32api, win32gui @@ -15,6 +17,7 @@ from UI.Main.MainLeft import MainLeft from UI.Main.MainTop import MainTop from ..ProjectManages.ProjectWidget import ProjectWidgets from ..VarManages.VarWidget import VarWidgets, HartWidgets, TcRtdWidgets, AnalogWidgets, HartSimulateWidgets +from UI.VarManages.VarTable import RpcVarTableView from ..UserManage.UserWidget import UserWidgets from ..TrendManage.TrendWidget import TrendWidgets from ..Setting.Setting import SettingWidget @@ -97,6 +100,7 @@ class MainWindow(QMainWindow): self.tcrtdWidget = TcRtdWidgets() self.analogWidget = AnalogWidgets() self.hartsimulateWidget = HartSimulateWidgets() + self.rpcVarTableWidget = RpcVarTableView() self.userWidget.setObjectName('userWidget') self.projectWidget.setObjectName('projectWidget') @@ -122,6 +126,7 @@ class MainWindow(QMainWindow): self.varManageTabWidget.addTab(self.hartWidget,'HART读取') self.varManageTabWidget.addTab(self.hartsimulateWidget,'HART模拟') self.varManageTabWidget.addTab(self.profibusWidget,'PROFIBUS') + self.varManageTabWidget.addTab(self.rpcVarTableWidget, '远程通讯') #添加导入变量按钮 self.importVarButton = QPushButton(QIcon(':/static/import.png'), '导入变量') diff --git a/UI/Setting/RemoteConnectSetting.py b/UI/Setting/RemoteConnectSetting.py index ef4f750..2118a51 100644 --- a/UI/Setting/RemoteConnectSetting.py +++ b/UI/Setting/RemoteConnectSetting.py @@ -58,17 +58,29 @@ class RemoteConnectSettingWidget(QWidget): self.mainLayout.addStretch() # 底部弹簧(将内容向上推) def switchMode(self, mode): - """切换主站/从站模式""" + """切换主站/从站模式,自动关闭另一端""" + # 关闭当前Widget对应的服务/客户端 + if self.currentWidget == self.deviceMasterWidget: + # 主站切换到从站,关闭服务端 + from utils import Globals + protocolManage = Globals.getValue('protocolManage') + if protocolManage and hasattr(protocolManage, 'closeServer'): + protocolManage.closeServer() + else: + # 从站切换到主站,关闭客户端 + from utils import Globals + protocolManage = Globals.getValue('protocolManage') + if protocolManage and hasattr(protocolManage, 'closeClent'): + protocolManage.closeClent() # 移除当前Widget self.contentLayout.removeWidget(self.currentWidget) self.currentWidget.hide() - # 根据选择显示对应的Widget if mode == "主站模式": self.currentWidget = self.deviceMasterWidget else: self.currentWidget = self.deviceSlaveWidget - # 重新添加Widget(保持居中) self.contentLayout.insertWidget(1, self.currentWidget) # 插入到两个弹簧中间 self.currentWidget.show() + diff --git a/UI/Setting/SearchDeviceWidget.py b/UI/Setting/SearchDeviceWidget.py index 0f6423c..e3b021f 100644 --- a/UI/Setting/SearchDeviceWidget.py +++ b/UI/Setting/SearchDeviceWidget.py @@ -3,7 +3,7 @@ import socket import threading from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, \ QListWidget, QLabel, QWidget, QMessageBox, QTextEdit -from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer from PyQt5.QtNetwork import QNetworkInterface,QAbstractSocket from datetime import datetime @@ -24,19 +24,26 @@ class TcpClient(object): udpSock.sendto(b"DISCOVERY_REQUEST", (self.broadcastAddr, self.udpPort)) servers = [] + seen = set() + local_ips = self.getLocalIps() try: while True: data, addr = udpSock.recvfrom(1024) if data.startswith(b"DISCOVERY_RESPONSE"): - # 修正点:正确解析服务端IP和TCP端口 _, hostname, tcpPort = data.decode('utf-8').split(':') - serverIp = addr # 提取IP字符串 + serverIp = addr[0] + # 跳过本机IP + if serverIp in local_ips: + continue + key = (serverIp, int(tcpPort)) + if key in seen: + continue + seen.add(key) servers.append({ - 'ip': serverIp, # 存储为字符串 - 'port': int(tcpPort), # 存储为整数 + 'ip': (serverIp,), + 'port': int(tcpPort), 'hostname': hostname }) - # self.connectToServer(serverIp, tcpPort) print(data.decode('utf-8')) except socket.timeout: pass @@ -49,14 +56,15 @@ class TcpClient(object): try: # 显式转换类型确保安全 client.connect((str(ip), int(tcpPort))) - print(f"[+] 已连接到 {ip}:{tcpPort}") + # print(f"[+] 已连接到 {ip}:{tcpPort}") Globals.getValue('protocolManage').setServerMode() clientName = Globals.getValue('protocolManage').RpcServer.getNextClientName() client.send(clientName.encode('utf-8')) response = client.recv(1024) - print(f"服务端响应: {response.decode('utf-8')}") + # print(f"服务端响应: {response.decode('utf-8')}") resClientName = response.decode('utf-8') Globals.getValue('protocolManage').addClient(clientName = resClientName) + QMessageBox.information(None, '连接成功', f'已成功连接到从站 {ip}:{tcpPort}') return True except Exception as e: print(f"连接失败: {e}") @@ -64,6 +72,17 @@ class TcpClient(object): finally: client.close() + def getLocalIps(self): + """获取本机所有IPv4地址""" + ips = set() + for interface in QNetworkInterface.allInterfaces(): + if interface.flags() & QNetworkInterface.IsUp: + for entry in interface.addressEntries(): + ip = entry.ip() + if ip.protocol() == QAbstractSocket.IPv4Protocol and not ip.isLoopback(): + ips.add(ip.toString()) + return ips + class DiscoveryThread(QThread): """用于在后台执行设备发现的线程""" @@ -83,7 +102,15 @@ class DeviceMasterWidget(QMainWindow): super().__init__() self.tcpClient = TcpClient() self.servers = [] + self.discoveryThread = None + self.statusLabel = None + self.infoLabel = None + self.disconnectList = None self.initUI() + # 定时器启动放到控件初始化后 + self.statusTimer = QTimer(self) + self.statusTimer.timeout.connect(self.updateStatusInfo) + self.statusTimer.start(1000) def initUI(self): self.setWindowTitle('局域网设备发现') @@ -94,6 +121,17 @@ class DeviceMasterWidget(QMainWindow): self.setCentralWidget(centralWidget) layout = QVBoxLayout() + self.statusLabel = QLabel('服务器模式: 无') + layout.addWidget(self.statusLabel) + self.infoLabel = QLabel('连接信息: 无') + layout.addWidget(self.infoLabel) + # 新增:断开客户端区 + self.disconnectList = QListWidget() + self.disconnectList.setFixedHeight(60) + layout.addWidget(QLabel('已连接客户端(点击可断开):')) + layout.addWidget(self.disconnectList) + self.disconnectList.itemClicked.connect(self.disconnectClient) + self.ipAddresslabel = QLabel(self) self.ipAddresslabel.setObjectName("setlabel") self.ipAddresslabel.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignVCenter) @@ -159,10 +197,22 @@ class DeviceMasterWidget(QMainWindow): if selectedIndex == -1: QMessageBox.warning(self, '警告', '请先选择一个设备') return - selectedServer = self.servers[selectedIndex] - # print(selectedServer['ip'][0], selectedServer['port']) - self.tcpClient.connectToServer(selectedServer['ip'][0], selectedServer['port']) + from utils import Globals + protocolManage = Globals.getValue('protocolManage') + already_connected = False + ip = selectedServer['ip'][0] if isinstance(selectedServer['ip'], (list, tuple)) else selectedServer['ip'] + if protocolManage and hasattr(protocolManage, 'RpcServer') and protocolManage.RpcServer: + clients = protocolManage.RpcServer.getClientNames() + ipMap = protocolManage.RpcServer.getClientIpMap() if hasattr(protocolManage.RpcServer, 'getClientIpMap') else {} + for c in clients: + if ipMap.get(c, '') == ip: + already_connected = True + break + if already_connected: + QMessageBox.information(self, '提示', f'客户端 {ip} 已连接,无需重复连接') + return + self.tcpClient.connectToServer(ip, selectedServer['port']) def getLocalIp(self): for interface in QNetworkInterface.allInterfaces(): @@ -174,6 +224,62 @@ class DeviceMasterWidget(QMainWindow): return ip.toString() return "192.168.1.1" # 默认回退值 + def cleanup(self): + # 释放后台线程 + if self.discoveryThread and self.discoveryThread.isRunning(): + self.discoveryThread.terminate() + self.discoveryThread.wait() + + def closeEvent(self, event): + self.cleanup() + event.accept() + + def __del__(self): + self.cleanup() + + def updateStatusInfo(self): + protocolManage = Globals.getValue('protocolManage') + mode = '无' + info = '无' + if protocolManage: + if hasattr(protocolManage, 'RpcServer') and protocolManage.RpcServer: + mode = '服务端模式' + try: + clients = protocolManage.RpcServer.getClientNames() + clientIpMap = protocolManage.RpcServer.getClientIpMap() if hasattr(protocolManage.RpcServer, 'getClientIpMap') else {} + info = '已连接客户端: ' + (', '.join([f"{c} ({clientIpMap.get(c, '')})" for c in clients]) if clients else '无') + self.disconnectList.clear() + for c in clients: + ip = clientIpMap.get(c, '') + display = f"{c} ({ip})" if ip else c + self.disconnectList.addItem(display) + except: + info = '已连接客户端: 无' + self.disconnectList.clear() + elif hasattr(protocolManage, 'RpcClient') and protocolManage.RpcClient: + mode = '客户端模式' + try: + clientName = protocolManage.RpcClient.clientName + serverInfo = protocolManage.RpcClient.rabbitHost + info = f'客户端名: {clientName} 服务器: {serverInfo}' + self.disconnectList.clear() + except: + info = '客户端信息获取失败' + self.disconnectList.clear() + self.statusLabel.setText(f'服务器模式: {mode}') + self.infoLabel.setText(f'连接信息: {info}') + + def disconnectClient(self, item): + # 支持“客户端名 (IP)”格式 + text = item.text() + clientName = text.split(' ')[0] + from utils import Globals + protocolManage = Globals.getValue('protocolManage') + if protocolManage and hasattr(protocolManage, 'disconnectClient'): + protocolManage.disconnectClient(clientName) + QMessageBox.information(self, '断开连接', f'已断开客户端 {clientName}') + # 立即刷新列表 + self.updateStatusInfo() class TcpServer(QObject): @@ -229,11 +335,12 @@ class TcpServer(QObject): data = clientSock.recv(1024) if not data: break - # print(addr, data) + Globals.getValue('protocolManage').closeServer() Globals.getValue('protocolManage').setClentMode(data.decode('utf-8'), rabbitmqHost = addr[0]) - # print(data.decode('utf-8')) self.log(f"收到消息: {data.decode('utf-8')}") clientSock.send(data) # 返回确认 + # 新增:弹窗提示本机已被连接 + # QMessageBox.information(None, '连接提示', f'本机已被 {addr[0]} 连接') except Exception as e: self.log(f"客户端异常断开: {e}") finally: @@ -273,13 +380,18 @@ class TcpServer(QObject): def stopTcpServer(self): if self.tcpRunning: self.tcpRunning = False - # 创建一个临时连接来解除accept阻塞 - try: - tempSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - tempSocket.connect(('127.0.0.1', self.tcpPort)) - tempSocket.close() - except: - pass + # 唤醒accept阻塞,安全关闭线程 + if self.serverSocket: + try: + self.serverSocket.shutdown(socket.SHUT_RDWR) + except: + pass + try: + self.serverSocket.close() + except: + pass + if self.tcpThread and self.tcpThread.is_alive(): + self.tcpThread.join(timeout=1) def startUdpServer(self): if not self.udpRunning: @@ -290,13 +402,18 @@ class TcpServer(QObject): def stopUdpServer(self): if self.udpRunning: self.udpRunning = False - # 发送一个空数据包来解除recvfrom阻塞 - try: - tempSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - tempSocket.sendto(b'', ('127.0.0.1', self.udpPort)) - tempSocket.close() - except: - pass + # 唤醒recvfrom阻塞,安全关闭线程 + if self.udpSocket: + try: + self.udpSocket.shutdown(socket.SHUT_RDWR) + except: + pass + try: + self.udpSocket.close() + except: + pass + if self.udpThread and self.udpThread.is_alive(): + self.udpThread.join(timeout=1) class DeviceSlaveWidget(QMainWindow): def __init__(self): @@ -306,12 +423,22 @@ class DeviceSlaveWidget(QMainWindow): self.server = TcpServer() self.server.updateSignal.connect(self.updateLog) - + self.statusLabel = None + self.infoLabel = None self.initUI() + # 定时器启动放到控件初始化后 + self.statusTimer = QTimer(self) + self.statusTimer.timeout.connect(self.updateStatusInfo) + self.statusTimer.start(1000) def initUI(self): # 主布局 mainLayout = QVBoxLayout() + + self.statusLabel = QLabel('服务器模式: 无') + mainLayout.addWidget(self.statusLabel) + self.infoLabel = QLabel('连接信息: 无') + mainLayout.addWidget(self.infoLabel) # 控制按钮 # self.tcpButton = QPushButton("启动 TCP 服务器") @@ -338,10 +465,10 @@ class DeviceSlaveWidget(QMainWindow): def toggleTcpServer(self): if self.server.tcpRunning: self.server.stopTcpServer() - print("关闭 TCP 服务器") + # print("关闭 TCP 服务器") else: self.server.startTcpServer() - print("启动 TCP 服务器") + # print("启动 TCP 服务器") def toggleUdpServer(self): if self.server.udpRunning: @@ -355,10 +482,39 @@ class DeviceSlaveWidget(QMainWindow): def updateLog(self, message): self.logDisplay.append(message) - def closeEvent(self, event): - # 关闭窗口时停止所有服务 + def cleanup(self): if self.server.tcpRunning: self.server.stopTcpServer() if self.server.udpRunning: self.server.stopUdpServer() + + def closeEvent(self, event): + self.cleanup() event.accept() + + def __del__(self): + # print(111111111111111111) + self.cleanup() + + def updateStatusInfo(self): + protocolManage = Globals.getValue('protocolManage') + mode = '无' + info = '无' + if protocolManage: + if hasattr(protocolManage, 'RpcClient') and protocolManage.RpcClient: + mode = '客户端模式' + try: + clientName = protocolManage.RpcClient.clientName + serverInfo = protocolManage.RpcClient.rabbitHost + info = f'客户端名: {clientName} 服务器: {serverInfo}' + except: + info = '客户端信息获取失败' + else: + # 没有RpcClient,显示无 + mode = '无' + info = '无' + else: + mode = '无' + info = '无' + self.statusLabel.setText(f'服务器模式: {mode}') + self.infoLabel.setText(f'连接信息: {info}') diff --git a/UI/VarManages/RpcVarModel.py b/UI/VarManages/RpcVarModel.py new file mode 100644 index 0000000..6aa1794 --- /dev/null +++ b/UI/VarManages/RpcVarModel.py @@ -0,0 +1,135 @@ +from PyQt5.QtCore import Qt, QAbstractTableModel, QVariant, QTimer, QModelIndex, QEvent, QSize +from PyQt5.QtGui import QColor +from PyQt5.QtWidgets import QMessageBox, QPushButton, QItemDelegate, QStyleOptionButton, QStyle, QWidget, QInputDialog, QHBoxLayout +from utils import Globals +import qtawesome + +class RpcVarModel(QAbstractTableModel): + def __init__(self, header, parent=None): + # 新顺序:当前值、强制值、变量名、类型、下限、上限、客户端、强制按钮 + self.header = ['当前值', '强制值', '变量名', '类型', '下限', '上限', '客户端', '操作'] + super().__init__(parent) + self.datas = [] + self.valueCache = {} + self.inputValues = {} # 行号->输入值 + self.refreshTimer = QTimer() + self.refreshTimer.timeout.connect(self.refreshData) + self.refreshTimer.start(500) # 每秒刷新一次 + + def refreshData(self): + """刷新所有远程变量数据,并按类型排序""" + protocolManage = Globals.getValue('protocolManage') + if protocolManage and protocolManage.RpcServer: + allVars = protocolManage.RpcServer.broadcastRead() + temp = [] + for client in allVars: + clientName = client.get('client', '') + variables = client.get('variables', {}) + for varName, varInfo in variables.items(): + row = [ + varInfo.get('value', ''), # 当前值 + self.inputValues.get(len(temp), ''),# 强制值(输入缓存) + varName, # 变量名 + varInfo.get('type', ''), # 类型 + varInfo.get('min', ''), # 下限 + varInfo.get('max', ''), # 上限 + clientName, # 客户端(倒数第二列) + '', # 强制按钮(最后一列) + ] + temp.append(row) + # 按类型排序 + temp.sort(key=lambda x: x[3]) + self.datas = temp + self.layoutChanged.emit() + + def rowCount(self, parent=None): + return len(self.datas) + + def columnCount(self, parent=None): + return len(self.header) + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return QVariant() + row = index.row() + col = index.column() + if role == Qt.DisplayRole: + # 强制值输入列显示缓存值 + if col == 1: + return self.inputValues.get(row, '') + return self.datas[row][col] + if role == Qt.TextAlignmentRole: + return Qt.AlignCenter + if role == Qt.BackgroundRole: + if row % 2 == 0: + return QColor('#EFEFEF') + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if index.isValid() and index.column() == 1: + self.inputValues[index.row()] = value + self.dataChanged.emit(index, index) + return True + return False + + def headerData(self, section, orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self.header[section] + return QVariant() + + def flags(self, index): + if not index.isValid(): + return Qt.ItemIsEnabled + if index.column() == 1: + return Qt.ItemIsEnabled | Qt.ItemIsEditable + if index.column() == 7: + return Qt.ItemIsEnabled # 按钮列不可编辑 + return Qt.ItemIsEnabled + +from PyQt5.QtWidgets import QStyledItemDelegate, QPushButton, QStyleOptionButton, QStyle, QApplication + +class RpcVarButtonDelegate(QStyledItemDelegate): + def __init__(self, parent=None): + super(RpcVarButtonDelegate, self).__init__(parent) + + def paint(self, painter, option, index): + if not self.parent().indexWidget(index) and index.column() == 7: + button = QPushButton( + qtawesome.icon('fa.play', color='#1fbb6f'), + '', + self.parent() + ) + button.setIconSize(QSize(15, 15)) + button.setStyleSheet('border:none;') + button.index = [index.row(), index.column()] + button.clicked.connect(lambda: self.force_action(button)) + h_box_layout = QHBoxLayout() + h_box_layout.addWidget(button) + h_box_layout.setContentsMargins(2, 0, 0, 2) + h_box_layout.setAlignment(Qt.AlignCenter) + widget = QWidget() + widget.setLayout(h_box_layout) + self.parent().setIndexWidget(index, widget) + # 不再用QStyleOptionButton绘制 + + def force_action(self, sender): + model = self.parent().model + row = sender.index[0] + varName = model.datas[row][2] + minValue = model.datas[row][4] + maxValue = model.datas[row][5] + forceValue = model.inputValues.get(row, '') + if forceValue == '': + QMessageBox.warning(self.parent(), '提示', '请先输入强制值') + return + try: + protocolManage = Globals.getValue('protocolManage') + if protocolManage and protocolManage.RpcServer: + resp = protocolManage.RpcServer.writeVar(varName, float(forceValue)) + if resp.get('result') == 'success': + QMessageBox.information(self.parent(), '提示', f'{varName} 强制成功!') + else: + reason = resp.get('reason', '') + QMessageBox.warning(self.parent(), '失败', f'{varName} 强制失败: {reason}') + except Exception as e: + QMessageBox.warning(self.parent(), '错误', f'强制失败: {e}') \ No newline at end of file diff --git a/UI/VarManages/VarTable.py b/UI/VarManages/VarTable.py index d229171..2ca68a4 100644 --- a/UI/VarManages/VarTable.py +++ b/UI/VarManages/VarTable.py @@ -11,6 +11,7 @@ from .AnalogModel import * from .HartModel import * from .HartSimulateModel import * from .TCRTDModel import * +from .RpcVarModel import RpcVarModel, RpcVarButtonDelegate from model.ProjectModel.VarManage import * @@ -180,3 +181,21 @@ class HartSimulateTableView(VarTableView): self.setItemDelegateForColumn(10, HartSimulateButtonDelegate(self)) self.setItemDelegateForColumn(9, HartSimulateVarModelBox(self)) self.model = HartSimulateModel([' ID', '仪器名', '描述', '主变量', '过程变量1', '过程变量2', '过程变量3', '工程量下限', '工程量上限', '值类型','操作'], [], table=self) + +class RpcVarTableView(QTableView): + def __init__(self, parent=None): + super(RpcVarTableView, self).__init__(parent) + self.setShowGrid(True) + self.setAlternatingRowColors(True) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setItemDelegateForColumn(7, RpcVarButtonDelegate(self)) + self.model = RpcVarModel(['客户端', '变量名', '类型', '下限', '上限', '当前值', '操作'], self) + self.setModel(self.model) + self.header = QHeaderView(Qt.Horizontal, self) + self.header.setStretchLastSection(True) + self.setHorizontalHeader(self.header) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setupColumnWidths() + + def setupColumnWidths(self): + self.header.setSectionResizeMode(QHeaderView.Stretch) diff --git a/UI/VarManages/VarWidget.py b/UI/VarManages/VarWidget.py index de8486d..3ee0592 100644 --- a/UI/VarManages/VarWidget.py +++ b/UI/VarManages/VarWidget.py @@ -1,10 +1,10 @@ from PyQt5 import QtCore, QtWidgets from PyQt5.QtCore import QSize, Qt, QTimer from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QSplitter, QPushButton, QFileDialog, QMessageBox +from PyQt5.QtWidgets import QSplitter, QPushButton, QFileDialog, QMessageBox, QVBoxLayout from UI.VarManages.VarTable import VarTableView, HartTableView, TcRtdTableView, AnalogTableView, \ - HartSimulateTableView + HartSimulateTableView, RpcVarTableView from UI.VarManages.MessageWidget import MessageWidget from model.ProjectModel.ProjectManage import ProjectManage @@ -391,51 +391,38 @@ class TcRtdWidgets(VarWidgets): self.varView.model.append_data(['', '', '', '', '', '', '', '', '','']) def forceVar(self): - # print(self.varView.model.datas) - check = [i for i,x in enumerate(self.varView.model.checkList) if x == 'Checked'] + check = [i for i, x in enumerate(self.varView.model.checkList) if x == 'Checked'] + if not check: + QMessageBox.information(self, '提示', '请先勾选要强制的变量', QMessageBox.Yes) + return + forceVars = Globals.getValue('forceVars') + if forceVars is None: + forceVars = set() for i in check: value = self.varView.model.datas[i][1] - varType = self.varView.model.datas[i][5] - min = self.varView.model.datas[i][6] - max = self.varView.model.datas[i][7] + varName = self.varView.model.datas[i][3] + min_ = self.varView.model.datas[i][7] + max_ = self.varView.model.datas[i][8] pattern = re.compile(r'[^0-9\.-]+') if not value or re.findall(pattern, str(value)): - reply = QMessageBox.question(self.parent(), - '警告', - "请输入强制值或数字", - QMessageBox.Yes) + QMessageBox.warning(self, '警告', "请输入强制值或数字", QMessageBox.Yes) return - if min and max: - if float(value) < float(min) or float(value) > float(max): - reply = QMessageBox.question(self.parent(), - '警告', - "超出量程范围", - QMessageBox.Yes) - return - elif min and not max: - if float(value) < float(min): - reply = QMessageBox.question(self.parent(), - '警告', - "超出量程范围", - QMessageBox.Yes) - return - elif max and not min: - if float(value) > float(max): - reply = QMessageBox.question(self.parent(), - '警告', - "超出量程范围", - QMessageBox.Yes) - return - mv = temToMv(varType, float(value)) - if not mv and mv != 0: - continue - self.varView.mvList[i] = mv - self.varView.valueList[i] = float(value) - forceVars = Globals.getValue('forceVars') - # forceVars.add(model.datas[sender.index[0]][3]) - forceVars.add(self.varView.model.datas[i][3]) - Globals.setValue('forceVars', forceVars) - # self.varView.valueList = [float(x[1]) for x in self.varView.model.datas] + if min_ and max_: + try: + if float(value) < float(min_) or float(value) > float(max_): + QMessageBox.warning(self, '警告', "超出量程范围", QMessageBox.Yes) + return + except Exception: + QMessageBox.warning(self, '警告', "量程范围格式错误", QMessageBox.Yes) + return + res = Globals.getValue('protocolManage').writeVariableValue(varName, float(value)) + if res: + forceVars.add(varName) + else: + QMessageBox.information(self, '提示', f'变量 {varName} 写入失败', QMessageBox.Yes) + Globals.setValue('forceVars', forceVars) + # self.varView.model.refreshValueCache() + # self.varView.model.layoutChanged.emit() @@ -494,58 +481,38 @@ class AnalogWidgets(VarWidgets): self.varView.model.append_data(['', '', '', '', '', '', '', '', '']) def forceVar(self): - check = [i for i,x in enumerate(self.varView.model.checkList) if x == 'Checked'] + check = [i for i, x in enumerate(self.varView.model.checkList) if x == 'Checked'] + if not check: + QMessageBox.information(self, '提示', '请先勾选要强制的变量', QMessageBox.Yes) + return forceVars = Globals.getValue('forceVars') + if forceVars is None: + forceVars = set() for i in check: value = self.varView.model.datas[i][1] - min = self.varView.model.datas[i][6] - max = self.varView.model.datas[i][7] + varName = self.varView.model.datas[i][3] + min_ = self.varView.model.datas[i][7] + max_ = self.varView.model.datas[i][8] pattern = re.compile(r'[^0-9\.-]+') if not value or re.findall(pattern, str(value)): - reply = QMessageBox.question(self.parent(), - '警告', - "请输入强制值或数字", - QMessageBox.Yes) + QMessageBox.warning(self, '警告', "请输入强制值或数字", QMessageBox.Yes) return - if i > 16: - if not value.isdigit: - reply = QMessageBox.question(self.parent(), - '警告', - "请输入0或1", - QMessageBox.Yes) + if min_ and max_: + try: + if float(value) < float(min_) or float(value) > float(max_): + QMessageBox.warning(self, '警告', "超出量程范围", QMessageBox.Yes) + return + except Exception: + QMessageBox.warning(self, '警告', "量程范围格式错误", QMessageBox.Yes) return - if min and max and i < 16: - if float(value) < float(min) or float(value) > float(max): - reply = QMessageBox.question(self.parent(), - '警告', - "超出量程范围", - QMessageBox.Yes) - return - else: - min = None - max = None - - if (float(value) > 20 or float(value)) < 4 and i < 8: - reply = QMessageBox.question(self.parent(), - '警告', - "超出量程范围", - QMessageBox.Yes) - return - if (float(value) > 10000 or float(value) < 0.1) and 7 < i < 16: - reply = QMessageBox.question(self.parent(), - '警告', - "超出量程范围", - QMessageBox.Yes) - return - if i < 8: - self.varView.model.table.realList[i] = getRealAO(float(value), float(max), float(min)) - self.varView.model.table.valueList[i] = float(value) + res = Globals.getValue('protocolManage').writeVariableValue(varName, float(value)) + if res: + forceVars.add(varName) else: - self.varView.model.table.realList[i] = float(value) - self.varView.model.table.valueList[i] = float(value) - self.varView.valueList[i] = float(value) - forceVars.add(self.varView.model.datas[i][3]) - Globals.setValue('forceVars', forceVars) + QMessageBox.information(self, '提示', f'变量 {varName} 写入失败', QMessageBox.Yes) + Globals.setValue('forceVars', forceVars) + # self.varView.model.refreshValueCache() + # self.varView.model.layoutChanged.emit() diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py index 168b445..b650ab6 100644 --- a/protocol/ProtocolManage.py +++ b/protocol/ProtocolManage.py @@ -55,7 +55,7 @@ class ProtocolManage(object): def setClentMode(self, clentName, rabbitmqHost='localhost'): if self.RpcClient: self.RpcClient.close() - self.RpcClient = RpcClient(clentName, rabbitmqHost) + self.RpcClient = RpcClient(clentName, rabbitmqHost, self) # 使用非阻塞方式启动RPC客户端 self.RpcClient.startNonBlocking() @@ -66,7 +66,7 @@ class ProtocolManage(object): def setServerMode(self, rabbitmqHost='localhost'): if self.RpcServer: - self.RpcServer.close() + return self.RpcServer = RpcServer(rabbitmqHost) @@ -109,15 +109,15 @@ class ProtocolManage(object): # 仅设置值,不保存到数据库 pass - if modelType == 'ModbusTcpSlaveVar': + elif modelType == 'ModbusTcpSlaveVar': # 仅设置值,不保存到数据库 pass - if modelType == 'ModbusRtuMasterVar': + elif modelType == 'ModbusRtuMasterVar': # 仅设置值,不保存到数据库 pass - if modelType == 'ModbusRtuSlaveVar': + elif modelType == 'ModbusRtuSlaveVar': # 仅设置值,不保存到数据库 pass @@ -165,6 +165,14 @@ class ProtocolManage(object): elif modelType == 'HartSimulateVar': # 仅设置值,不保存到数据库 pass + else: + if self.RpcServer: + existsVar, clientNames = self.RpcServer.existsVar(variableName) + if existsVar: + value = self.RpcServer.writeVar(variableName, value) + return True + else: + return True if self.RpcClient: self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) @@ -249,15 +257,21 @@ class ProtocolManage(object): # HART模拟变量处理 elif modelType == 'HartSimulateVar': pass + # print(1111111111111111) + if self.RpcServer: + existsVar, clientNames = self.RpcServer.existsVar(variableName) + if existsVar: + # print(clientNames, 1111111111111111) + value = self.RpcServer.getVarValue(clientNames[0], variableName) + return value + else: + return None return None # 暂时返回None except Exception as e: print(f"读取变量值失败: {str(e)}") return str(e) - - def readRpcVariable(self): - if self.RpcServer: - print(self.RpcServer.broadcastRead()) + # def sendTrigger(self, variableName, value, timeoutMS): # self.writeVariableValue(variableName, value, trigger=True, timeoutMS = timeoutMS) @@ -306,4 +320,8 @@ class ProtocolManage(object): elif varModel == '模拟值': return SimModel + def disconnectClient(self, clientName): + if self.RpcServer: + self.RpcServer.removeClient(clientName) + diff --git a/protocol/RPC/RpcClient.py b/protocol/RPC/RpcClient.py index 244a8f2..918aa20 100644 --- a/protocol/RPC/RpcClient.py +++ b/protocol/RPC/RpcClient.py @@ -2,6 +2,7 @@ import pika import uuid import json import threading +import socket class RpcClient: def __init__(self, clientName, rabbitHost='localhost', protocolManager=None): @@ -13,6 +14,7 @@ class RpcClient: """ self.clientName = clientName self.protocolManager = protocolManager + self.rabbitHost = rabbitHost self.variables = {} # self.variables = { # "temp": {"type": "AI", "min": 0, "max": 100, "value": 25.5}, @@ -25,7 +27,7 @@ class RpcClient: self.channel = self.connection.channel() self.queueName = f"rpc_queue_{self.clientName}" self.channel.queue_declare(queue=self.queueName) - print(f"[{self.clientName}] 等待服务端指令...") + # print(f"[{self.clientName}] 等待服务端指令...") def onRequest(self, ch, method, props, body): request = json.loads(body) @@ -33,8 +35,9 @@ class RpcClient: response = {} if cmd == "ping": - # 响应ping命令 - response = {"client": self.clientName, "result": "pong"} + # 响应ping命令,带本机IP + ip = self.getLocalIp() + response = {"client": self.clientName, "result": "pong", "ip": ip} elif cmd == "read": response = {"client": self.clientName, "variables": self.variables} elif cmd == "write": @@ -91,8 +94,8 @@ class RpcClient: if type in ['DI', 'DO']: min = 0 max = 1 - self.variables[variableName] = {} - self.variables[variableName]["value"] = value + variableName = variableName + '.' + self.clientName + self.variables[variableName]["value"] = value + '.' + self.clientName self.variables[variableName]["min"] = min self.variables[variableName]["max"] = max self.variables[variableName]["type"] = type @@ -126,6 +129,17 @@ class RpcClient: except Exception as e: print(f"[{self.clientName}] 关闭连接时出错: {e}") + def getLocalIp(self): + # 获取本机第一个非回环IPv4地址 + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 80)) + ip = s.getsockname()[0] + s.close() + return ip + except: + return '127.0.0.1' + if __name__ == "__main__": import sys clientName = sys.argv[1] if len(sys.argv) > 1 else "Client1" diff --git a/protocol/RPC/RpcServer.py b/protocol/RPC/RpcServer.py index d9730dc..546441b 100644 --- a/protocol/RPC/RpcServer.py +++ b/protocol/RPC/RpcServer.py @@ -18,10 +18,13 @@ class RpcServer: self.callbackQueue = None self.responses = {} self.lock = threading.Lock() - self.connected = False + self.connected = False + self.clientIpMap = {} # 客户端名到IP的映射 # 初始化连接 self.connectToRabbitMQ() + self.autoCheckThread = threading.Thread(target=self._autoCheckClients, daemon=True) + self.autoCheckThread.start() def connectToRabbitMQ(self): """连接到RabbitMQ服务器""" @@ -58,18 +61,22 @@ class RpcServer: if not self.connected: print("服务端未连接到RabbitMQ") return False - + # 修复:防止重复添加同名客户端 if clientName in self.clientNames: print(f"客户端 {clientName} 已存在") return False - # 检查客户端是否在线 try: # 发送ping消息测试客户端是否响应 response = self.call(clientName, {"cmd": "ping"}) if response and response.get("result") == "pong": self.clientNames.append(clientName) - print(f"客户端 {clientName} 添加成功") + # 记录客户端IP + if 'ip' in response: + self.clientIpMap[clientName] = response['ip'] + # 去重,防止多线程下重复 + self.clientNames = list(dict.fromkeys(self.clientNames)) + # print(f"客户端 {clientName} 添加成功") return True else: print(f"客户端 {clientName} 无响应") @@ -86,7 +93,8 @@ class RpcServer: """ if clientName in self.clientNames: self.clientNames.remove(clientName) - print(f"客户端 {clientName} 已移除") + if clientName in self.clientIpMap: + self.clientIpMap.pop(clientName) return True return False @@ -134,6 +142,10 @@ class RpcServer: """ return self.clientNames.copy() + def getClientIpMap(self): + """获取客户端名到IP的映射""" + return self.clientIpMap.copy() + def pingClient(self, clientName): """ 测试客户端是否在线 @@ -193,7 +205,7 @@ class RpcServer: :return: 所有客户端的变量数据 """ if not self.clientNames: - print("没有可用的客户端") + # print("没有可用的客户端") return [] results = [] @@ -215,18 +227,20 @@ class RpcServer: """ if not self.clientNames: return {"result": "fail", "reason": "no clients available"} - - # 先查找变量在哪个客户端 - for client in self.clientNames: + + exists, clients = self.existsVar(varName) + if not exists or not clients: + return {"result": "fail", "reason": "var not found"} + # 支持多个客户端有同名变量,优先写第一个成功的 + for client in clients: try: - resp = self.call(client, {"cmd": "read"}) - if resp and "variables" in resp and varName in resp["variables"]: - writeResp = self.call(client, {"cmd": "write", "varName": varName, "value": value}) + print(f"写入客户端 {client} 变量 {varName} 值 {value}") + writeResp = self.call(client, {"cmd": "write", "varName": varName, "value": value}) + if writeResp and writeResp.get("result") == "success": return writeResp except Exception as e: - print(f"检查客户端 {client} 失败: {e}") continue - return {"result": "fail", "reason": "var not found"} + return {"result": "fail", "reason": "write failed on all clients"} def scanAndAddClients(self, clientNameList): """ @@ -269,6 +283,51 @@ class RpcServer: self.connected = False print("服务端连接已关闭") + def _autoCheckClients(self): + """后台线程:每2秒自动检测客户端是否在线,不在线则移除""" + while True: + try: + self.checkAllClients() + except Exception as e: + print(f"自动检测客户端异常: {e}") + time.sleep(1) + + def existsVar(self, varName): + """ + 判断变量名是否存在于所有已连接客户端的变量中 + :param varName: 变量名 + :return: (True, clientName) 或 (False, None) + """ + foundClients = [] + for client in self.clientNames: + try: + resp = self.call(client, {"cmd": "read"}) + if resp and "variables" in resp and varName in resp["variables"]: + foundClients.append(client) + except Exception as e: + continue + if foundClients: + return True, foundClients + else: + return False, None + + def getVarValue(self, clientName, varName): + """ + 获取指定客户端上指定变量的值 + :param clientName: 客户端名称 + :param varName: 变量名 + :return: 变量值或None + """ + if clientName not in self.clientNames: + return None + try: + resp = self.call(clientName, {"cmd": "read"}) + if resp and "variables" in resp and varName in resp["variables"]: + return resp["variables"][varName] + except Exception as e: + pass + return None + if __name__ == "__main__": # 创建服务端 server = RpcServer()