diff --git a/UI/Setting/RemoteConnectSetting.py b/UI/Setting/RemoteConnectSetting.py index 2118a51..5d9fa3f 100644 --- a/UI/Setting/RemoteConnectSetting.py +++ b/UI/Setting/RemoteConnectSetting.py @@ -70,8 +70,8 @@ class RemoteConnectSettingWidget(QWidget): # 从站切换到主站,关闭客户端 from utils import Globals protocolManage = Globals.getValue('protocolManage') - if protocolManage and hasattr(protocolManage, 'closeClent'): - protocolManage.closeClent() + if protocolManage and hasattr(protocolManage, 'closeClient'): + protocolManage.closeClient() # 移除当前Widget self.contentLayout.removeWidget(self.currentWidget) self.currentWidget.hide() diff --git a/UI/Setting/SearchDeviceWidget.py b/UI/Setting/SearchDeviceWidget.py index e3b021f..43c847c 100644 --- a/UI/Setting/SearchDeviceWidget.py +++ b/UI/Setting/SearchDeviceWidget.py @@ -1,8 +1,13 @@ import sys import socket import threading +import requests +import json +import time +import uuid from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, \ - QListWidget, QLabel, QWidget, QMessageBox, QTextEdit + QListWidget, QLabel, QWidget, QMessageBox, QTextEdit, \ + QLineEdit, QHBoxLayout, QRadioButton, QButtonGroup, QGroupBox from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer from PyQt5.QtNetwork import QNetworkInterface,QAbstractSocket from datetime import datetime @@ -14,6 +19,7 @@ class TcpClient(object): # 配置信息 self.udpPort = 54321 self.broadcastAddr = '' + def discoverServers(self): # print("[*] 正在扫描局域网设备...") @@ -106,14 +112,23 @@ class DeviceMasterWidget(QMainWindow): self.statusLabel = None self.infoLabel = None self.disconnectList = None + self.publicServerInput = None + self.publicClientList = None + self.publicClients = [] + self.autoRefreshEnabled = False + self.initUI() # 定时器启动放到控件初始化后 self.statusTimer = QTimer(self) self.statusTimer.timeout.connect(self.updateStatusInfo) self.statusTimer.start(1000) + + # 公网客户端自动刷新定时器 + self.publicRefreshTimer = QTimer(self) + self.publicRefreshTimer.timeout.connect(self.autoRefreshPublicClients) def initUI(self): - self.setWindowTitle('局域网设备发现') + self.setWindowTitle('设备连接管理') # self.setGeometry(1000, 500, 400, 300) # 主窗口部件 @@ -125,6 +140,7 @@ class DeviceMasterWidget(QMainWindow): layout.addWidget(self.statusLabel) self.infoLabel = QLabel('连接信息: 无') layout.addWidget(self.infoLabel) + # 新增:断开客户端区 self.disconnectList = QListWidget() self.disconnectList.setFixedHeight(60) @@ -132,29 +148,87 @@ class DeviceMasterWidget(QMainWindow): layout.addWidget(self.disconnectList) self.disconnectList.itemClicked.connect(self.disconnectClient) + # 公网服务端配置区域 + publicGroup = QGroupBox('公网服务端配置') + publicLayout = QVBoxLayout() + + # 服务器地址输入 + serverLayout = QHBoxLayout() + serverLayout.addWidget(QLabel('服务器地址:')) + self.publicServerInput = QLineEdit('43.138.48.181') + self.publicServerInput.setPlaceholderText('输入公网服务器IP地址') + serverLayout.addWidget(self.publicServerInput) + publicLayout.addLayout(serverLayout) + + # 启动公网服务端按钮 + self.publicConnectButton = QPushButton('启动公网服务端模式') + self.publicConnectButton.clicked.connect(self.startPublicServer) + self.publicConnectButton.setObjectName('setButton') + publicLayout.addWidget(self.publicConnectButton) + + # 公网客户端扫描区域 + publicLayout.addWidget(QLabel('公网服务器客户端列表:')) + self.publicClientList = QListWidget() + self.publicClientList.setFixedHeight(100) + self.publicClientList.itemDoubleClicked.connect(self.connectToPublicClient) + publicLayout.addWidget(self.publicClientList) + + # 扫描和连接按钮 + publicButtonLayout = QHBoxLayout() + self.scanPublicButton = QPushButton('扫描公网客户端') + self.scanPublicButton.clicked.connect(self.scanPublicClients) + self.scanPublicButton.setObjectName('setButton') + publicButtonLayout.addWidget(self.scanPublicButton) + + self.connectPublicButton = QPushButton('连接选中客户端') + self.connectPublicButton.clicked.connect(self.connectToPublicClient) + self.connectPublicButton.setEnabled(False) + self.connectPublicButton.setObjectName('setButton') + publicButtonLayout.addWidget(self.connectPublicButton) + + publicLayout.addLayout(publicButtonLayout) + + # 自动刷新选项 + from PyQt5.QtWidgets import QCheckBox + self.autoRefreshCheckBox = QCheckBox('自动刷新客户端列表 (5秒)') + self.autoRefreshCheckBox.stateChanged.connect(self.toggleAutoRefresh) + publicLayout.addWidget(self.autoRefreshCheckBox) + + publicGroup.setLayout(publicLayout) + layout.addWidget(publicGroup) + self.ipAddresslabel = QLabel(self) self.ipAddresslabel.setObjectName("setlabel") self.ipAddresslabel.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignVCenter) layout.addWidget(self.ipAddresslabel) + + # 局域网设备发现区域 + lanGroup = QGroupBox('局域网设备发现') + lanLayout = QVBoxLayout() + # 标题标签 self.titleLabel = QLabel('点击"扫描设备"按钮发现局域网中的设备') - layout.addWidget(self.titleLabel) + lanLayout.addWidget(self.titleLabel) # 设备列表 self.deviceList = QListWidget() - layout.addWidget(self.deviceList) + lanLayout.addWidget(self.deviceList) - + lanLayout2 = QHBoxLayout() self.scanButton = QPushButton('扫描设备') self.scanButton.clicked.connect(self.startDiscovery) - layout.addWidget(self.scanButton) self.scanButton.setObjectName('setButton') + lanLayout2.addWidget(self.scanButton) self.connectButton = QPushButton('连接设备') self.connectButton.clicked.connect(self.connectToDevice) self.connectButton.setEnabled(False) self.connectButton.setObjectName('setButton') - layout.addWidget(self.connectButton) + lanLayout2.addWidget(self.connectButton) + lanLayout.addLayout(lanLayout2) + + lanGroup.setLayout(lanLayout) + layout.addWidget(lanGroup) self.ipAddresslabel.setText('当前设备IP地址: ' + self.getLocalIp()) @@ -191,6 +265,176 @@ class DeviceMasterWidget(QMainWindow): self.connectButton.setEnabled(True) + def startPublicServer(self): + """启动公网服务端模式""" + serverIp = self.publicServerInput.text().strip() + if not serverIp: + QMessageBox.warning(self, '警告', '请输入服务器地址') + return + + try: + # 设置为服务端模式 + Globals.getValue('protocolManage').closeClient() + Globals.getValue('protocolManage').setServerMode(rabbitmqHost=serverIp) + QMessageBox.information(self, '成功', f'已启动公网服务端模式\n服务器地址: {serverIp}') + except Exception as e: + QMessageBox.critical(self, '失败', f'启动公网服务端模式失败: {str(e)}') + + def scanPublicClients(self): + """扫描公网服务器上的客户端""" + serverIp = self.publicServerInput.text().strip() + if not serverIp: + QMessageBox.warning(self, '警告', '请输入服务器地址') + return + + try: + # 通过RabbitMQ管理API获取所有RPC队列 + url = f"http://{serverIp}:15672/api/queues" + response = requests.get(url, auth=('dcs', '123456'), timeout=10) + queues = response.json() + + # 筛选RPC客户端队列 + rpc_queues = [] + for queue in queues: + queue_name = queue['name'] + if queue_name.startswith('rpc_queue_client'): + client_name = queue_name.replace('rpc_queue_', '') + rpc_queues.append({ + 'name': client_name, + 'queue': queue_name, + 'messages': queue.get('messages', 0), + 'consumers': queue.get('consumers', 0), + 'status': 'online' if queue.get('consumers', 0) > 0 else 'offline' + }) + + # 更新客户端列表 + self.publicClients = rpc_queues + self.publicClientList.clear() + + if not rpc_queues: + self.publicClientList.addItem('未发现任何客户端') + self.connectPublicButton.setEnabled(False) + else: + for client in rpc_queues: + status_text = "在线" if client['status'] == 'online' else "离线" + item_text = f"{client['name']} ({status_text}) - 消息:{client['messages']}" + self.publicClientList.addItem(item_text) + self.connectPublicButton.setEnabled(True) + + QMessageBox.information(self, '扫描完成', f'发现 {len(rpc_queues)} 个客户端') + + except Exception as e: + QMessageBox.critical(self, '扫描失败', f'扫描公网客户端失败: {str(e)}') + self.publicClientList.clear() + self.publicClientList.addItem('扫描失败') + self.connectPublicButton.setEnabled(False) + + def connectToPublicClient(self): + """连接到选中的公网客户端""" + selectedIndex = self.publicClientList.currentRow() + if selectedIndex == -1: + QMessageBox.warning(self, '警告', '请先选择一个客户端') + return + + if selectedIndex >= len(self.publicClients): + QMessageBox.warning(self, '警告', '选择的客户端无效') + return + + selectedClient = self.publicClients[selectedIndex] + clientName = selectedClient['name'] + + # 检查客户端状态 + # if selectedClient['status'] == 'offline': + # reply = QMessageBox.question(self, '客户端离线', + # f'客户端 {clientName} 当前离线,是否仍要尝试连接?', + # QMessageBox.Yes | QMessageBox.No) + # if reply == QMessageBox.No: + # return + + try: + # 检查是否已经连接 + protocolManage = Globals.getValue('protocolManage') + if protocolManage and hasattr(protocolManage, 'RpcServer') and protocolManage.RpcServer: + existing_clients = protocolManage.RpcServer.getClientNames() + if clientName in existing_clients: + QMessageBox.information(self, '提示', f'客户端 {clientName} 已连接,无需重复连接') + return + + # 尝试添加客户端 + if protocolManage.RpcServer.addClient(clientName): + QMessageBox.information(self, '连接成功', f'已成功连接到客户端 {clientName}') + # 刷新状态信息 + self.updateStatusInfo() + else: + QMessageBox.warning(self, '连接失败', f'无法连接到客户端 {clientName},客户端可能不在线') + else: + QMessageBox.warning(self, '错误', '请先启动公网服务端模式') + + except Exception as e: + QMessageBox.critical(self, '连接失败', f'连接客户端失败: {str(e)}') + + def autoRefreshPublicClients(self): + """自动刷新公网客户端列表(静默模式)""" + if not self.autoRefreshEnabled: + return + + serverIp = self.publicServerInput.text().strip() + if not serverIp: + return + + try: + # 静默扫描,不显示消息框 + url = f"http://{serverIp}:15672/api/queues" + response = requests.get(url, auth=('dcs', '123456'), timeout=5) + queues = response.json() + + # 筛选RPC客户端队列 + rpc_queues = [] + for queue in queues: + queue_name = queue['name'] + if queue_name.startswith('rpc_queue_client'): + client_name = queue_name.replace('rpc_queue_', '') + rpc_queues.append({ + 'name': client_name, + 'queue': queue_name, + 'messages': queue.get('messages', 0), + 'consumers': queue.get('consumers', 0), + 'status': 'online' if queue.get('consumers', 0) > 0 else 'offline' + }) + + # 更新客户端列表(保持选中状态) + current_selection = self.publicClientList.currentRow() + self.publicClients = rpc_queues + self.publicClientList.clear() + + if not rpc_queues: + self.publicClientList.addItem('未发现任何客户端') + self.connectPublicButton.setEnabled(False) + else: + for client in rpc_queues: + status_text = "在线" if client['status'] == 'online' else "离线" + item_text = f"{client['name']} ({status_text}) - 消息:{client['messages']}" + self.publicClientList.addItem(item_text) + self.connectPublicButton.setEnabled(True) + + # 恢复选中状态 + if 0 <= current_selection < len(rpc_queues): + self.publicClientList.setCurrentRow(current_selection) + + except Exception as e: + # 静默处理错误,不显示消息框 + print(f"自动刷新公网客户端失败: {e}") + + def toggleAutoRefresh(self, state): + """切换自动刷新状态""" + self.autoRefreshEnabled = state == 2 # Qt.Checked = 2 + if self.autoRefreshEnabled: + self.publicRefreshTimer.start(5000) # 每5秒刷新一次 + print("已启用公网客户端自动刷新") + else: + self.publicRefreshTimer.stop() + print("已禁用公网客户端自动刷新") + def connectToDevice(self): """连接选中的设备""" selectedIndex = self.deviceList.currentRow() @@ -418,13 +662,14 @@ class TcpServer(QObject): class DeviceSlaveWidget(QMainWindow): def __init__(self): super().__init__() - self.setWindowTitle("TCP/UDP 服务器控制") + self.setWindowTitle("设备从站控制") # self.setGeometry(100, 100, 600, 400) self.server = TcpServer() self.server.updateSignal.connect(self.updateLog) self.statusLabel = None self.infoLabel = None + self.publicServerInput = None self.initUI() # 定时器启动放到控件初始化后 self.statusTimer = QTimer(self) @@ -440,20 +685,42 @@ class DeviceSlaveWidget(QMainWindow): self.infoLabel = QLabel('连接信息: 无') mainLayout.addWidget(self.infoLabel) - # 控制按钮 - # self.tcpButton = QPushButton("启动 TCP 服务器") - # self.tcpButton.clicked.connect(self.toggleTcpServer) - # self.tcpButton.setObjectName('setButton') + # 公网客户端连接配置 + publicGroup = QGroupBox('公网客户端连接') + publicLayout = QVBoxLayout() + + # 服务器地址输入 + serverLayout = QHBoxLayout() + serverLayout.addWidget(QLabel('服务器地址:')) + self.publicServerInput = QLineEdit('43.138.48.181') + self.publicServerInput.setPlaceholderText('输入公网服务器IP地址') + serverLayout.addWidget(self.publicServerInput) + publicLayout.addLayout(serverLayout) + + # 公网客户端连接按钮 + self.publicConnectButton = QPushButton('连接公网服务器') + self.publicConnectButton.clicked.connect(self.connectToPublicServer) + self.publicConnectButton.setObjectName('setButton') + publicLayout.addWidget(self.publicConnectButton) + + publicGroup.setLayout(publicLayout) + mainLayout.addWidget(publicGroup) + + # 局域网模式控制 + lanGroup = QGroupBox('局域网通讯模式') + lanLayout = QVBoxLayout() + self.udpButton = QPushButton("开启远程通讯模式") self.udpButton.clicked.connect(self.toggleUdpServer) - self.udpButton.setObjectName('setButton') + self.udpButton.setObjectName('setButton') + lanLayout.addWidget(self.udpButton) + + lanGroup.setLayout(lanLayout) + mainLayout.addWidget(lanGroup) + # 日志显示 self.logDisplay = QTextEdit() self.logDisplay.setReadOnly(True) - - # 添加到布局 - # mainLayout.addWidget(self.tcpButton) - mainLayout.addWidget(self.udpButton) mainLayout.addWidget(QLabel("服务器日志:")) mainLayout.addWidget(self.logDisplay) @@ -470,6 +737,27 @@ class DeviceSlaveWidget(QMainWindow): self.server.startTcpServer() # print("启动 TCP 服务器") + def connectToPublicServer(self): + """连接到公网服务器作为客户端""" + serverIp = self.publicServerInput.text().strip() + if not serverIp: + QMessageBox.warning(self, '警告', '请输入服务器地址') + return + + # try: + # 获取客户端名称 + from protocol.RPC.RpcClient import RpcClient + clientName = RpcClient.getNextClientNameFromRabbitMQ(serverIp) + print(f"获取到客户端名称: {clientName}") + + # 设置为客户端模式 + Globals.getValue('protocolManage').closeServer() + Globals.getValue('protocolManage').setClentMode(clientName, rabbitmqHost=serverIp) + + QMessageBox.information(self, '连接成功', f'已成功连接到公网服务器 {serverIp}\n客户端名称: {clientName}') + # except Exception as e: + # QMessageBox.critical(self, '连接失败', f'连接公网服务器失败: {str(e)}') + def toggleUdpServer(self): if self.server.udpRunning: self.server.stopUdpServer() diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py index 2055306..330266a 100644 --- a/protocol/ProtocolManage.py +++ b/protocol/ProtocolManage.py @@ -99,7 +99,7 @@ class ProtocolManage(object): # 使用非阻塞方式启动RPC客户端 self.RpcClient.startNonBlocking() - def closeClent(self): + def closeClient(self): if self.RpcClient: self.RpcClient.close() self.RpcClient = None @@ -297,6 +297,8 @@ class ProtocolManage(object): self.tcpVarManager.shutdown() # 关闭所有Modbus通讯 self.modbusManager.stopAllModbus() + self.closeClient() + self.closeServer() # 关闭后台读取线程 if hasattr(self, 'readThreadStop') and hasattr(self, 'readThread'): self.readThreadStop.set() diff --git a/protocol/RPC/RpcClient.py b/protocol/RPC/RpcClient.py index 918aa20..42b095a 100644 --- a/protocol/RPC/RpcClient.py +++ b/protocol/RPC/RpcClient.py @@ -3,6 +3,7 @@ import uuid import json import threading import socket +import requests class RpcClient: def __init__(self, clientName, rabbitHost='localhost', protocolManager=None): @@ -63,7 +64,7 @@ class RpcClient: :param request: 请求数据 :return: 响应数据 """ - varName = request.get("varName") + varName = '.'.join(request.get("varName").split('.')[:-1]) value = request.get("value") # 如果有协议管理器,使用它来处理写入 @@ -95,7 +96,12 @@ class RpcClient: min = 0 max = 1 variableName = variableName + '.' + self.clientName - self.variables[variableName]["value"] = value + '.' + self.clientName + + # 确保变量条目存在,如果不存在则创建 + if variableName not in self.variables: + self.variables[variableName] = {} + + self.variables[variableName]["value"] = str(value) self.variables[variableName]["min"] = min self.variables[variableName]["max"] = max self.variables[variableName]["type"] = type @@ -122,6 +128,7 @@ class RpcClient: """关闭RPC连接""" try: if self.channel and not self.channel.is_closed: + self.channel.queue_delete(queue=self.queueName) self.channel.close() if self.connection and not self.connection.is_closed: self.connection.close() @@ -138,7 +145,46 @@ class RpcClient: s.close() return ip except: - return '127.0.0.1' + return '127.0.0.1'\ + + @classmethod + def getNextClientNameFromRabbitMQ(cls, rabbitHost, username='dcs', password='123456'): + # try: + # 获取所有队列 + url = f"http://{rabbitHost}:15672/api/queues" + response = requests.get(url, auth=(username, password), timeout=5) + queues = response.json() + print(queues) + # 筛选RPC队列 + if not queues: + return "client1" + rpcQueues = [q['name'] for q in queues if q['name'].startswith('rpc_queue_client')] + + # 提取客户端编号 + clientNumbers = [] + for queueName in rpcQueues: + # rpc_queue_client1 -> client1 -> 1 + if queueName.startswith('rpc_queue_client'): + try: + numberStr = queueName.replace('rpc_queue_client', '') + if numberStr.isdigit(): + clientNumbers.append(int(numberStr)) + except: + continue + + # 返回下一个可用编号 + if not clientNumbers: + return "client1" + else: + nextNumber = max(clientNumbers) + 1 + return f"client{nextNumber}" + + # except Exception as e: + # print(f"从RabbitMQ获取客户端名称失败: {e}") + # # 降级方案:使用时间戳 + # import time + # timestamp = int(time.time() % 10000) + # return f"client{timestamp}" if __name__ == "__main__": import sys