0708更新

main
zcwBit 4 months ago
parent 3b075df941
commit 1eb5e3f2d3

@ -4,7 +4,9 @@ import pandas as pd
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.Qt import * 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 import POINTER, cast
from ctypes.wintypes import MSG from ctypes.wintypes import MSG
from win32 import win32api, win32gui from win32 import win32api, win32gui
@ -15,6 +17,7 @@ from UI.Main.MainLeft import MainLeft
from UI.Main.MainTop import MainTop from UI.Main.MainTop import MainTop
from ..ProjectManages.ProjectWidget import ProjectWidgets from ..ProjectManages.ProjectWidget import ProjectWidgets
from ..VarManages.VarWidget import VarWidgets, HartWidgets, TcRtdWidgets, AnalogWidgets, HartSimulateWidgets from ..VarManages.VarWidget import VarWidgets, HartWidgets, TcRtdWidgets, AnalogWidgets, HartSimulateWidgets
from UI.VarManages.VarTable import RpcVarTableView
from ..UserManage.UserWidget import UserWidgets from ..UserManage.UserWidget import UserWidgets
from ..TrendManage.TrendWidget import TrendWidgets from ..TrendManage.TrendWidget import TrendWidgets
from ..Setting.Setting import SettingWidget from ..Setting.Setting import SettingWidget
@ -97,6 +100,7 @@ class MainWindow(QMainWindow):
self.tcrtdWidget = TcRtdWidgets() self.tcrtdWidget = TcRtdWidgets()
self.analogWidget = AnalogWidgets() self.analogWidget = AnalogWidgets()
self.hartsimulateWidget = HartSimulateWidgets() self.hartsimulateWidget = HartSimulateWidgets()
self.rpcVarTableWidget = RpcVarTableView()
self.userWidget.setObjectName('userWidget') self.userWidget.setObjectName('userWidget')
self.projectWidget.setObjectName('projectWidget') self.projectWidget.setObjectName('projectWidget')
@ -122,6 +126,7 @@ class MainWindow(QMainWindow):
self.varManageTabWidget.addTab(self.hartWidget,'HART读取') self.varManageTabWidget.addTab(self.hartWidget,'HART读取')
self.varManageTabWidget.addTab(self.hartsimulateWidget,'HART模拟') self.varManageTabWidget.addTab(self.hartsimulateWidget,'HART模拟')
self.varManageTabWidget.addTab(self.profibusWidget,'PROFIBUS') self.varManageTabWidget.addTab(self.profibusWidget,'PROFIBUS')
self.varManageTabWidget.addTab(self.rpcVarTableWidget, '远程通讯')
#添加导入变量按钮 #添加导入变量按钮
self.importVarButton = QPushButton(QIcon(':/static/import.png'), '导入变量') self.importVarButton = QPushButton(QIcon(':/static/import.png'), '导入变量')

@ -58,17 +58,29 @@ class RemoteConnectSettingWidget(QWidget):
self.mainLayout.addStretch() # 底部弹簧(将内容向上推) self.mainLayout.addStretch() # 底部弹簧(将内容向上推)
def switchMode(self, mode): 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 # 移除当前Widget
self.contentLayout.removeWidget(self.currentWidget) self.contentLayout.removeWidget(self.currentWidget)
self.currentWidget.hide() self.currentWidget.hide()
# 根据选择显示对应的Widget # 根据选择显示对应的Widget
if mode == "主站模式": if mode == "主站模式":
self.currentWidget = self.deviceMasterWidget self.currentWidget = self.deviceMasterWidget
else: else:
self.currentWidget = self.deviceSlaveWidget self.currentWidget = self.deviceSlaveWidget
# 重新添加Widget保持居中 # 重新添加Widget保持居中
self.contentLayout.insertWidget(1, self.currentWidget) # 插入到两个弹簧中间 self.contentLayout.insertWidget(1, self.currentWidget) # 插入到两个弹簧中间
self.currentWidget.show() self.currentWidget.show()

@ -3,7 +3,7 @@ import socket
import threading import threading
from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, \ from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, \
QListWidget, QLabel, QWidget, QMessageBox, QTextEdit 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 PyQt5.QtNetwork import QNetworkInterface,QAbstractSocket
from datetime import datetime from datetime import datetime
@ -24,19 +24,26 @@ class TcpClient(object):
udpSock.sendto(b"DISCOVERY_REQUEST", (self.broadcastAddr, self.udpPort)) udpSock.sendto(b"DISCOVERY_REQUEST", (self.broadcastAddr, self.udpPort))
servers = [] servers = []
seen = set()
local_ips = self.getLocalIps()
try: try:
while True: while True:
data, addr = udpSock.recvfrom(1024) data, addr = udpSock.recvfrom(1024)
if data.startswith(b"DISCOVERY_RESPONSE"): if data.startswith(b"DISCOVERY_RESPONSE"):
# 修正点正确解析服务端IP和TCP端口
_, hostname, tcpPort = data.decode('utf-8').split(':') _, 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({ servers.append({
'ip': serverIp, # 存储为字符串 'ip': (serverIp,),
'port': int(tcpPort), # 存储为整数 'port': int(tcpPort),
'hostname': hostname 'hostname': hostname
}) })
# self.connectToServer(serverIp, tcpPort)
print(data.decode('utf-8')) print(data.decode('utf-8'))
except socket.timeout: except socket.timeout:
pass pass
@ -49,14 +56,15 @@ class TcpClient(object):
try: try:
# 显式转换类型确保安全 # 显式转换类型确保安全
client.connect((str(ip), int(tcpPort))) client.connect((str(ip), int(tcpPort)))
print(f"[+] 已连接到 {ip}:{tcpPort}") # print(f"[+] 已连接到 {ip}:{tcpPort}")
Globals.getValue('protocolManage').setServerMode() Globals.getValue('protocolManage').setServerMode()
clientName = Globals.getValue('protocolManage').RpcServer.getNextClientName() clientName = Globals.getValue('protocolManage').RpcServer.getNextClientName()
client.send(clientName.encode('utf-8')) client.send(clientName.encode('utf-8'))
response = client.recv(1024) response = client.recv(1024)
print(f"服务端响应: {response.decode('utf-8')}") # print(f"服务端响应: {response.decode('utf-8')}")
resClientName = response.decode('utf-8') resClientName = response.decode('utf-8')
Globals.getValue('protocolManage').addClient(clientName = resClientName) Globals.getValue('protocolManage').addClient(clientName = resClientName)
QMessageBox.information(None, '连接成功', f'已成功连接到从站 {ip}:{tcpPort}')
return True return True
except Exception as e: except Exception as e:
print(f"连接失败: {e}") print(f"连接失败: {e}")
@ -64,6 +72,17 @@ class TcpClient(object):
finally: finally:
client.close() 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): class DiscoveryThread(QThread):
"""用于在后台执行设备发现的线程""" """用于在后台执行设备发现的线程"""
@ -83,7 +102,15 @@ class DeviceMasterWidget(QMainWindow):
super().__init__() super().__init__()
self.tcpClient = TcpClient() self.tcpClient = TcpClient()
self.servers = [] self.servers = []
self.discoveryThread = None
self.statusLabel = None
self.infoLabel = None
self.disconnectList = None
self.initUI() self.initUI()
# 定时器启动放到控件初始化后
self.statusTimer = QTimer(self)
self.statusTimer.timeout.connect(self.updateStatusInfo)
self.statusTimer.start(1000)
def initUI(self): def initUI(self):
self.setWindowTitle('局域网设备发现') self.setWindowTitle('局域网设备发现')
@ -94,6 +121,17 @@ class DeviceMasterWidget(QMainWindow):
self.setCentralWidget(centralWidget) self.setCentralWidget(centralWidget)
layout = QVBoxLayout() 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 = QLabel(self)
self.ipAddresslabel.setObjectName("setlabel") self.ipAddresslabel.setObjectName("setlabel")
self.ipAddresslabel.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignVCenter) self.ipAddresslabel.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignVCenter)
@ -159,10 +197,22 @@ class DeviceMasterWidget(QMainWindow):
if selectedIndex == -1: if selectedIndex == -1:
QMessageBox.warning(self, '警告', '请先选择一个设备') QMessageBox.warning(self, '警告', '请先选择一个设备')
return return
selectedServer = self.servers[selectedIndex] selectedServer = self.servers[selectedIndex]
# print(selectedServer['ip'][0], selectedServer['port']) from utils import Globals
self.tcpClient.connectToServer(selectedServer['ip'][0], selectedServer['port']) 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): def getLocalIp(self):
for interface in QNetworkInterface.allInterfaces(): for interface in QNetworkInterface.allInterfaces():
@ -174,6 +224,62 @@ class DeviceMasterWidget(QMainWindow):
return ip.toString() return ip.toString()
return "192.168.1.1" # 默认回退值 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): class TcpServer(QObject):
@ -229,11 +335,12 @@ class TcpServer(QObject):
data = clientSock.recv(1024) data = clientSock.recv(1024)
if not data: if not data:
break break
# print(addr, data) Globals.getValue('protocolManage').closeServer()
Globals.getValue('protocolManage').setClentMode(data.decode('utf-8'), rabbitmqHost = addr[0]) Globals.getValue('protocolManage').setClentMode(data.decode('utf-8'), rabbitmqHost = addr[0])
# print(data.decode('utf-8'))
self.log(f"收到消息: {data.decode('utf-8')}") self.log(f"收到消息: {data.decode('utf-8')}")
clientSock.send(data) # 返回确认 clientSock.send(data) # 返回确认
# 新增:弹窗提示本机已被连接
# QMessageBox.information(None, '连接提示', f'本机已被 {addr[0]} 连接')
except Exception as e: except Exception as e:
self.log(f"客户端异常断开: {e}") self.log(f"客户端异常断开: {e}")
finally: finally:
@ -273,13 +380,18 @@ class TcpServer(QObject):
def stopTcpServer(self): def stopTcpServer(self):
if self.tcpRunning: if self.tcpRunning:
self.tcpRunning = False self.tcpRunning = False
# 创建一个临时连接来解除accept阻塞 # 唤醒accept阻塞安全关闭线程
try: if self.serverSocket:
tempSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try:
tempSocket.connect(('127.0.0.1', self.tcpPort)) self.serverSocket.shutdown(socket.SHUT_RDWR)
tempSocket.close() except:
except: pass
pass try:
self.serverSocket.close()
except:
pass
if self.tcpThread and self.tcpThread.is_alive():
self.tcpThread.join(timeout=1)
def startUdpServer(self): def startUdpServer(self):
if not self.udpRunning: if not self.udpRunning:
@ -290,13 +402,18 @@ class TcpServer(QObject):
def stopUdpServer(self): def stopUdpServer(self):
if self.udpRunning: if self.udpRunning:
self.udpRunning = False self.udpRunning = False
# 发送一个空数据包来解除recvfrom阻塞 # 唤醒recvfrom阻塞安全关闭线程
try: if self.udpSocket:
tempSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try:
tempSocket.sendto(b'', ('127.0.0.1', self.udpPort)) self.udpSocket.shutdown(socket.SHUT_RDWR)
tempSocket.close() except:
except: pass
pass try:
self.udpSocket.close()
except:
pass
if self.udpThread and self.udpThread.is_alive():
self.udpThread.join(timeout=1)
class DeviceSlaveWidget(QMainWindow): class DeviceSlaveWidget(QMainWindow):
def __init__(self): def __init__(self):
@ -306,12 +423,22 @@ class DeviceSlaveWidget(QMainWindow):
self.server = TcpServer() self.server = TcpServer()
self.server.updateSignal.connect(self.updateLog) self.server.updateSignal.connect(self.updateLog)
self.statusLabel = None
self.infoLabel = None
self.initUI() self.initUI()
# 定时器启动放到控件初始化后
self.statusTimer = QTimer(self)
self.statusTimer.timeout.connect(self.updateStatusInfo)
self.statusTimer.start(1000)
def initUI(self): def initUI(self):
# 主布局 # 主布局
mainLayout = QVBoxLayout() mainLayout = QVBoxLayout()
self.statusLabel = QLabel('服务器模式: 无')
mainLayout.addWidget(self.statusLabel)
self.infoLabel = QLabel('连接信息: 无')
mainLayout.addWidget(self.infoLabel)
# 控制按钮 # 控制按钮
# self.tcpButton = QPushButton("启动 TCP 服务器") # self.tcpButton = QPushButton("启动 TCP 服务器")
@ -338,10 +465,10 @@ class DeviceSlaveWidget(QMainWindow):
def toggleTcpServer(self): def toggleTcpServer(self):
if self.server.tcpRunning: if self.server.tcpRunning:
self.server.stopTcpServer() self.server.stopTcpServer()
print("关闭 TCP 服务器") # print("关闭 TCP 服务器")
else: else:
self.server.startTcpServer() self.server.startTcpServer()
print("启动 TCP 服务器") # print("启动 TCP 服务器")
def toggleUdpServer(self): def toggleUdpServer(self):
if self.server.udpRunning: if self.server.udpRunning:
@ -355,10 +482,39 @@ class DeviceSlaveWidget(QMainWindow):
def updateLog(self, message): def updateLog(self, message):
self.logDisplay.append(message) self.logDisplay.append(message)
def closeEvent(self, event): def cleanup(self):
# 关闭窗口时停止所有服务
if self.server.tcpRunning: if self.server.tcpRunning:
self.server.stopTcpServer() self.server.stopTcpServer()
if self.server.udpRunning: if self.server.udpRunning:
self.server.stopUdpServer() self.server.stopUdpServer()
def closeEvent(self, event):
self.cleanup()
event.accept() 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}')

@ -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}')

@ -11,6 +11,7 @@ from .AnalogModel import *
from .HartModel import * from .HartModel import *
from .HartSimulateModel import * from .HartSimulateModel import *
from .TCRTDModel import * from .TCRTDModel import *
from .RpcVarModel import RpcVarModel, RpcVarButtonDelegate
from model.ProjectModel.VarManage import * from model.ProjectModel.VarManage import *
@ -180,3 +181,21 @@ class HartSimulateTableView(VarTableView):
self.setItemDelegateForColumn(10, HartSimulateButtonDelegate(self)) self.setItemDelegateForColumn(10, HartSimulateButtonDelegate(self))
self.setItemDelegateForColumn(9, HartSimulateVarModelBox(self)) self.setItemDelegateForColumn(9, HartSimulateVarModelBox(self))
self.model = HartSimulateModel([' ID', '仪器名', '描述', '主变量', '过程变量1', '过程变量2', '过程变量3', '工程量下限', '工程量上限', '值类型','操作'], [], table=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)

@ -1,10 +1,10 @@
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QSize, Qt, QTimer from PyQt5.QtCore import QSize, Qt, QTimer
from PyQt5.QtGui import QIcon 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, \ from UI.VarManages.VarTable import VarTableView, HartTableView, TcRtdTableView, AnalogTableView, \
HartSimulateTableView HartSimulateTableView, RpcVarTableView
from UI.VarManages.MessageWidget import MessageWidget from UI.VarManages.MessageWidget import MessageWidget
from model.ProjectModel.ProjectManage import ProjectManage from model.ProjectModel.ProjectManage import ProjectManage
@ -391,51 +391,38 @@ class TcRtdWidgets(VarWidgets):
self.varView.model.append_data(['', '', '', '', '', '', '', '', '','']) self.varView.model.append_data(['', '', '', '', '', '', '', '', '',''])
def forceVar(self): 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: for i in check:
value = self.varView.model.datas[i][1] value = self.varView.model.datas[i][1]
varType = self.varView.model.datas[i][5] varName = self.varView.model.datas[i][3]
min = self.varView.model.datas[i][6] min_ = self.varView.model.datas[i][7]
max = self.varView.model.datas[i][7] max_ = self.varView.model.datas[i][8]
pattern = re.compile(r'[^0-9\.-]+') pattern = re.compile(r'[^0-9\.-]+')
if not value or re.findall(pattern, str(value)): if not value or re.findall(pattern, str(value)):
reply = QMessageBox.question(self.parent(), QMessageBox.warning(self, '警告', "请输入强制值或数字", QMessageBox.Yes)
'警告',
"请输入强制值或数字",
QMessageBox.Yes)
return return
if min and max: if min_ and max_:
if float(value) < float(min) or float(value) > float(max): try:
reply = QMessageBox.question(self.parent(), if float(value) < float(min_) or float(value) > float(max_):
'警告', QMessageBox.warning(self, '警告', "超出量程范围", QMessageBox.Yes)
"超出量程范围", return
QMessageBox.Yes) except Exception:
return QMessageBox.warning(self, '警告', "量程范围格式错误", QMessageBox.Yes)
elif min and not max: return
if float(value) < float(min): res = Globals.getValue('protocolManage').writeVariableValue(varName, float(value))
reply = QMessageBox.question(self.parent(), if res:
'警告', forceVars.add(varName)
"超出量程范围", else:
QMessageBox.Yes) QMessageBox.information(self, '提示', f'变量 {varName} 写入失败', QMessageBox.Yes)
return Globals.setValue('forceVars', forceVars)
elif max and not min: # self.varView.model.refreshValueCache()
if float(value) > float(max): # self.varView.model.layoutChanged.emit()
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]
@ -494,58 +481,38 @@ class AnalogWidgets(VarWidgets):
self.varView.model.append_data(['', '', '', '', '', '', '', '', '']) self.varView.model.append_data(['', '', '', '', '', '', '', '', ''])
def forceVar(self): 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') forceVars = Globals.getValue('forceVars')
if forceVars is None:
forceVars = set()
for i in check: for i in check:
value = self.varView.model.datas[i][1] value = self.varView.model.datas[i][1]
min = self.varView.model.datas[i][6] varName = self.varView.model.datas[i][3]
max = self.varView.model.datas[i][7] min_ = self.varView.model.datas[i][7]
max_ = self.varView.model.datas[i][8]
pattern = re.compile(r'[^0-9\.-]+') pattern = re.compile(r'[^0-9\.-]+')
if not value or re.findall(pattern, str(value)): if not value or re.findall(pattern, str(value)):
reply = QMessageBox.question(self.parent(), QMessageBox.warning(self, '警告', "请输入强制值或数字", QMessageBox.Yes)
'警告',
"请输入强制值或数字",
QMessageBox.Yes)
return return
if i > 16: if min_ and max_:
if not value.isdigit: try:
reply = QMessageBox.question(self.parent(), if float(value) < float(min_) or float(value) > float(max_):
'警告', QMessageBox.warning(self, '警告', "超出量程范围", QMessageBox.Yes)
"请输入0或1", return
QMessageBox.Yes) except Exception:
QMessageBox.warning(self, '警告', "量程范围格式错误", QMessageBox.Yes)
return return
if min and max and i < 16: res = Globals.getValue('protocolManage').writeVariableValue(varName, float(value))
if float(value) < float(min) or float(value) > float(max): if res:
reply = QMessageBox.question(self.parent(), forceVars.add(varName)
'警告',
"超出量程范围",
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)
else: else:
self.varView.model.table.realList[i] = float(value) QMessageBox.information(self, '提示', f'变量 {varName} 写入失败', QMessageBox.Yes)
self.varView.model.table.valueList[i] = float(value) Globals.setValue('forceVars', forceVars)
self.varView.valueList[i] = float(value) # self.varView.model.refreshValueCache()
forceVars.add(self.varView.model.datas[i][3]) # self.varView.model.layoutChanged.emit()
Globals.setValue('forceVars', forceVars)

@ -55,7 +55,7 @@ class ProtocolManage(object):
def setClentMode(self, clentName, rabbitmqHost='localhost'): def setClentMode(self, clentName, rabbitmqHost='localhost'):
if self.RpcClient: if self.RpcClient:
self.RpcClient.close() self.RpcClient.close()
self.RpcClient = RpcClient(clentName, rabbitmqHost) self.RpcClient = RpcClient(clentName, rabbitmqHost, self)
# 使用非阻塞方式启动RPC客户端 # 使用非阻塞方式启动RPC客户端
self.RpcClient.startNonBlocking() self.RpcClient.startNonBlocking()
@ -66,7 +66,7 @@ class ProtocolManage(object):
def setServerMode(self, rabbitmqHost='localhost'): def setServerMode(self, rabbitmqHost='localhost'):
if self.RpcServer: if self.RpcServer:
self.RpcServer.close() return
self.RpcServer = RpcServer(rabbitmqHost) self.RpcServer = RpcServer(rabbitmqHost)
@ -109,15 +109,15 @@ class ProtocolManage(object):
# 仅设置值,不保存到数据库 # 仅设置值,不保存到数据库
pass pass
if modelType == 'ModbusTcpSlaveVar': elif modelType == 'ModbusTcpSlaveVar':
# 仅设置值,不保存到数据库 # 仅设置值,不保存到数据库
pass pass
if modelType == 'ModbusRtuMasterVar': elif modelType == 'ModbusRtuMasterVar':
# 仅设置值,不保存到数据库 # 仅设置值,不保存到数据库
pass pass
if modelType == 'ModbusRtuSlaveVar': elif modelType == 'ModbusRtuSlaveVar':
# 仅设置值,不保存到数据库 # 仅设置值,不保存到数据库
pass pass
@ -165,6 +165,14 @@ class ProtocolManage(object):
elif modelType == 'HartSimulateVar': elif modelType == 'HartSimulateVar':
# 仅设置值,不保存到数据库 # 仅设置值,不保存到数据库
pass 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: if self.RpcClient:
self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType'])
@ -249,15 +257,21 @@ class ProtocolManage(object):
# HART模拟变量处理 # HART模拟变量处理
elif modelType == 'HartSimulateVar': elif modelType == 'HartSimulateVar':
pass 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 return None # 暂时返回None
except Exception as e: except Exception as e:
print(f"读取变量值失败: {str(e)}") print(f"读取变量值失败: {str(e)}")
return str(e) return str(e)
def readRpcVariable(self):
if self.RpcServer:
print(self.RpcServer.broadcastRead())
# def sendTrigger(self, variableName, value, timeoutMS): # def sendTrigger(self, variableName, value, timeoutMS):
# self.writeVariableValue(variableName, value, trigger=True, timeoutMS = timeoutMS) # self.writeVariableValue(variableName, value, trigger=True, timeoutMS = timeoutMS)
@ -306,4 +320,8 @@ class ProtocolManage(object):
elif varModel == '模拟值': elif varModel == '模拟值':
return SimModel return SimModel
def disconnectClient(self, clientName):
if self.RpcServer:
self.RpcServer.removeClient(clientName)

@ -2,6 +2,7 @@ import pika
import uuid import uuid
import json import json
import threading import threading
import socket
class RpcClient: class RpcClient:
def __init__(self, clientName, rabbitHost='localhost', protocolManager=None): def __init__(self, clientName, rabbitHost='localhost', protocolManager=None):
@ -13,6 +14,7 @@ class RpcClient:
""" """
self.clientName = clientName self.clientName = clientName
self.protocolManager = protocolManager self.protocolManager = protocolManager
self.rabbitHost = rabbitHost
self.variables = {} self.variables = {}
# self.variables = { # self.variables = {
# "temp": {"type": "AI", "min": 0, "max": 100, "value": 25.5}, # "temp": {"type": "AI", "min": 0, "max": 100, "value": 25.5},
@ -25,7 +27,7 @@ class RpcClient:
self.channel = self.connection.channel() self.channel = self.connection.channel()
self.queueName = f"rpc_queue_{self.clientName}" self.queueName = f"rpc_queue_{self.clientName}"
self.channel.queue_declare(queue=self.queueName) self.channel.queue_declare(queue=self.queueName)
print(f"[{self.clientName}] 等待服务端指令...") # print(f"[{self.clientName}] 等待服务端指令...")
def onRequest(self, ch, method, props, body): def onRequest(self, ch, method, props, body):
request = json.loads(body) request = json.loads(body)
@ -33,8 +35,9 @@ class RpcClient:
response = {} response = {}
if cmd == "ping": if cmd == "ping":
# 响应ping命令 # 响应ping命令带本机IP
response = {"client": self.clientName, "result": "pong"} ip = self.getLocalIp()
response = {"client": self.clientName, "result": "pong", "ip": ip}
elif cmd == "read": elif cmd == "read":
response = {"client": self.clientName, "variables": self.variables} response = {"client": self.clientName, "variables": self.variables}
elif cmd == "write": elif cmd == "write":
@ -91,8 +94,8 @@ class RpcClient:
if type in ['DI', 'DO']: if type in ['DI', 'DO']:
min = 0 min = 0
max = 1 max = 1
self.variables[variableName] = {} variableName = variableName + '.' + self.clientName
self.variables[variableName]["value"] = value self.variables[variableName]["value"] = value + '.' + self.clientName
self.variables[variableName]["min"] = min self.variables[variableName]["min"] = min
self.variables[variableName]["max"] = max self.variables[variableName]["max"] = max
self.variables[variableName]["type"] = type self.variables[variableName]["type"] = type
@ -126,6 +129,17 @@ class RpcClient:
except Exception as e: except Exception as e:
print(f"[{self.clientName}] 关闭连接时出错: {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__": if __name__ == "__main__":
import sys import sys
clientName = sys.argv[1] if len(sys.argv) > 1 else "Client1" clientName = sys.argv[1] if len(sys.argv) > 1 else "Client1"

@ -18,10 +18,13 @@ class RpcServer:
self.callbackQueue = None self.callbackQueue = None
self.responses = {} self.responses = {}
self.lock = threading.Lock() self.lock = threading.Lock()
self.connected = False self.connected = False
self.clientIpMap = {} # 客户端名到IP的映射
# 初始化连接 # 初始化连接
self.connectToRabbitMQ() self.connectToRabbitMQ()
self.autoCheckThread = threading.Thread(target=self._autoCheckClients, daemon=True)
self.autoCheckThread.start()
def connectToRabbitMQ(self): def connectToRabbitMQ(self):
"""连接到RabbitMQ服务器""" """连接到RabbitMQ服务器"""
@ -58,18 +61,22 @@ class RpcServer:
if not self.connected: if not self.connected:
print("服务端未连接到RabbitMQ") print("服务端未连接到RabbitMQ")
return False return False
# 修复:防止重复添加同名客户端
if clientName in self.clientNames: if clientName in self.clientNames:
print(f"客户端 {clientName} 已存在") print(f"客户端 {clientName} 已存在")
return False return False
# 检查客户端是否在线 # 检查客户端是否在线
try: try:
# 发送ping消息测试客户端是否响应 # 发送ping消息测试客户端是否响应
response = self.call(clientName, {"cmd": "ping"}) response = self.call(clientName, {"cmd": "ping"})
if response and response.get("result") == "pong": if response and response.get("result") == "pong":
self.clientNames.append(clientName) 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 return True
else: else:
print(f"客户端 {clientName} 无响应") print(f"客户端 {clientName} 无响应")
@ -86,7 +93,8 @@ class RpcServer:
""" """
if clientName in self.clientNames: if clientName in self.clientNames:
self.clientNames.remove(clientName) self.clientNames.remove(clientName)
print(f"客户端 {clientName} 已移除") if clientName in self.clientIpMap:
self.clientIpMap.pop(clientName)
return True return True
return False return False
@ -134,6 +142,10 @@ class RpcServer:
""" """
return self.clientNames.copy() return self.clientNames.copy()
def getClientIpMap(self):
"""获取客户端名到IP的映射"""
return self.clientIpMap.copy()
def pingClient(self, clientName): def pingClient(self, clientName):
""" """
测试客户端是否在线 测试客户端是否在线
@ -193,7 +205,7 @@ class RpcServer:
:return: 所有客户端的变量数据 :return: 所有客户端的变量数据
""" """
if not self.clientNames: if not self.clientNames:
print("没有可用的客户端") # print("没有可用的客户端")
return [] return []
results = [] results = []
@ -215,18 +227,20 @@ class RpcServer:
""" """
if not self.clientNames: if not self.clientNames:
return {"result": "fail", "reason": "no clients available"} return {"result": "fail", "reason": "no clients available"}
# 先查找变量在哪个客户端 exists, clients = self.existsVar(varName)
for client in self.clientNames: if not exists or not clients:
return {"result": "fail", "reason": "var not found"}
# 支持多个客户端有同名变量,优先写第一个成功的
for client in clients:
try: try:
resp = self.call(client, {"cmd": "read"}) print(f"写入客户端 {client} 变量 {varName}{value}")
if resp and "variables" in resp and varName in resp["variables"]: writeResp = self.call(client, {"cmd": "write", "varName": varName, "value": value})
writeResp = self.call(client, {"cmd": "write", "varName": varName, "value": value}) if writeResp and writeResp.get("result") == "success":
return writeResp return writeResp
except Exception as e: except Exception as e:
print(f"检查客户端 {client} 失败: {e}")
continue continue
return {"result": "fail", "reason": "var not found"} return {"result": "fail", "reason": "write failed on all clients"}
def scanAndAddClients(self, clientNameList): def scanAndAddClients(self, clientNameList):
""" """
@ -269,6 +283,51 @@ class RpcServer:
self.connected = False self.connected = False
print("服务端连接已关闭") 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__": if __name__ == "__main__":
# 创建服务端 # 创建服务端
server = RpcServer() server = RpcServer()

Loading…
Cancel
Save