From be4f422da74aa339094ef0293e2c78296b459fbe Mon Sep 17 00:00:00 2001 From: "DESKTOP-3D7M4SA\\Hicent" <452669850@qq.com> Date: Sat, 13 Dec 2025 00:01:17 +0800 Subject: [PATCH] =?UTF-8?q?1212=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Static/Area.qss | 370 +++++++++++------------- Static/Main.qss | 2 +- UI/ProfibusWidgets/AreaTabWidget.py | 7 +- UI/ProfibusWidgets/ProfibusWindow.py | 38 ++- model/ProjectModel/DeviceManage.py | 2 +- protocol/ProtocolManage.py | 416 ++++++++++++++++++++++++++- 6 files changed, 621 insertions(+), 214 deletions(-) diff --git a/Static/Area.qss b/Static/Area.qss index 06aa39b..829f31e 100644 --- a/Static/Area.qss +++ b/Static/Area.qss @@ -1,281 +1,243 @@ +/* ==================== Area 样式表 ==================== */ +/* 统一项目样式风格 */ - - -QPushButton#okBtn, QPushButton#delAreaBtn { - - font-size: 22px; - +/* ==================== 按钮样式 ==================== */ +QPushButton#okBtn { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 18px; border: none; - + border-radius: 4px; + padding: 6px 12px; + min-height: 28px; + background-color: #DDEEFF; + color: #2277EF; } -QPushButton#okBtn:hover, QPushButton#delAreaBtn:hover { - - font-size: 22px; - - font: bold; - +QPushButton#delAreaBtn { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 18px; border: none; - - margin-bottom: -2px - + border-radius: 4px; + padding: 6px 12px; + min-height: 28px; + background-color: #FFEEEE; + color: #EE1169; } -QPushButton#wirteDIDOforceBtn{ - - font-size: 16px; - - padding-left: 5px; - - padding-right: 5px; - - padding-top: 5px; - - padding-bottom: 5px; +QPushButton#okBtn:hover { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 18px; + font-weight: bold; + border: none; + background-color: #EEFFFF; + color: #2277EF; +} - border: 1px solid black; +QPushButton#delAreaBtn:hover { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 18px; + font-weight: bold; + border: none; + background-color: #FFDDDD; + color: #EE1169; +} +QPushButton#wirteDIDOforceBtn { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 18px; + padding: 5px; + border: 1px solid #CCCCCC; border-radius: 5px; - + background-color: #FFFFFF; + color: #333333; } -QPushButton#wirteDIDOforceBtn:hover{ - - font: bold; - +QPushButton#wirteDIDOforceBtn:hover { + font-weight: bold; + background-color: #F5F5F5; } - QPushButton#profibusForceBtn { - - font-size: 22px; - - padding-left: 5px; - - padding-right: 5px; - - padding-top: 6px; - - padding-bottom: 5px; - - border: 1px solid black; - + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 18px; + padding: 5px; + border: 1px solid #CCCCCC; border-radius: 5px; - -/* background-color: #328ffc;*/ - - + background-color: #FFFFFF; + color: #333333; } -QPushButton#profibusForceBtn:hover{ - - - font: bold; - - +QPushButton#profibusForceBtn:hover { + font-weight: bold; + background-color: #F5F5F5; } - -QPushButton#deviceAddButton{ - - color: #328ffc; - - font-size: 20px; - +QPushButton#deviceAddButton { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + color: #2277EF; + font-size: 14px; padding-bottom: 5px; - border-top: none; - border-right: none; - border-left: none; + background-color: transparent; +} - } - - -QPushButton#addareabutton{ - - color: #328ffc; - - font-size: 20px; - +QPushButton#addareabutton { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + color: #2277EF; + font-size: 14px; padding-top: 7px; + background-color: transparent; + border: none; +} - - - } - -QPushButton#initAddDeviceButton, QPushButton#initAreaAddButton{ - - font-size: 45px; - - color: #328ffc; - +QPushButton#initAddDeviceButton, +QPushButton#initAreaAddButton { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 24px; + color: #2277EF; background-color: rgba(255, 255, 255, 0); - border-radius: 5px; - } -QPushButton#initAddDeviceButton:hover, QPushButton#initAreaAddButton:hover, QPushButton#deviceAddButton:hover, QPushButton#addareabutton:hover{ - - color: #0f8ef0; - - font: bold; - +QPushButton#initAddDeviceButton:hover, +QPushButton#initAreaAddButton:hover, +QPushButton#deviceAddButton:hover, +QPushButton#addareabutton:hover { + color: #3787F7; + font-weight: bold; border-radius: 3px; - - margin-bottom: -4px - - + background-color: transparent; } - - -QLabel#wirteDIDOareaMessLabel, QLabel#wirteDIDOareaValueLabel{ - - font-size: 16px; - -} - -QLabel#wirteDIDOareaValueLabel, QLabel#readDIDOareaValueLabel{ - - color: #0160aa; - +/* ==================== 标签样式 ==================== */ +QLabel#wirteDIDOareaMessLabel, +QLabel#wirteDIDOareaValueLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 14px; } -QLabel#areaMessLabel, QLabel#areaValueLabel{ - - font-size: 28px; - +QLabel#wirteDIDOareaValueLabel, +QLabel#readDIDOareaValueLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + color: #2277EF; } -QLabel#massesageLabel{ - - font-size: 23px; +QLabel#areaMessLabel, +QLabel#areaValueLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 18px; } -QLabel#areaValueLabel{ - - color: #0160aa; - +QLabel#massesageLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 16px; } -QLabel#readDIDOareaMessLabel, QLabel#readDIDOareaValueLabel{ - - font-size: 23px; - - +QLabel#areaValueLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + color: #2277EF; } -QLabel#dataTypeLabel, QLabel#dataOrderLabel, QLabel#byteLineLabel{ - - font-size: 24px; - +QLabel#readDIDOareaMessLabel, +QLabel#readDIDOareaValueLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 16px; } -QLabel#pvUpperLimit, QLabel#pvLowerLimit, QLabel#pvUnit{ - - font-size: 24px; - - +QLabel#dataTypeLabel, +QLabel#dataOrderLabel, +QLabel#byteLineLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 16px; } - -QLabel#areaValueLabel{ - - color: #0160aa; - +QLabel#pvUpperLimit, +QLabel#pvLowerLimit, +QLabel#pvUnit { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 16px; } -QLabel#deviceNameLabel, QLabel#pvUpperLimitLabel, QLabel#pvLowerLimitLabel, QLabel#pvUnitLabel{ - - font:28px; - - +QLabel#deviceNameLabel, +QLabel#pvUpperLimitLabel, +QLabel#pvLowerLimitLabel, +QLabel#pvUnitLabel { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 16px; } -QLineEdit#byteLineEdit{ - - font-size: 20px; - +/* ==================== 输入框样式 ==================== */ +QLineEdit#byteLineEdit { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 16px; border-top: none; - border-left: none; - border-right: none; - - border-bottom: 2px solid gary; - - + border-bottom: 2px solid #CCCCCC; + background-color: transparent; } - - QLineEdit#areaLineEdit { - - font-size: 28px; - - - -} - - - -QLineEdit#wirteDIDOareaLineEdit { - + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; font-size: 16px; - border-top: none; - border-left: none; - border-right: none; - - border-bottom: 2px solid gary; - - + border-bottom: 2px solid #CCCCCC; + background-color: transparent; } -QLineEdit#areaLineEdit { - - font-size: 28px; - +QLineEdit#wirteDIDOareaLineEdit { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 14px; border-top: none; - border-left: none; - border-right: none; - - border-bottom: 2px solid gary; - + border-bottom: 2px solid #CCCCCC; + background-color: transparent; } -QLineEdit#deviceName, QLineEdit#pvUpperLimit, QLineEdit#pvLowerLimit, QLineEdit#pvUnit{ - - font: 28px; - +QLineEdit#deviceName, +QLineEdit#pvUpperLimit, +QLineEdit#pvLowerLimit, +QLineEdit#pvUnit { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 16px; border-top: none; - border-left: none; - border-right: none; - - border-bottom: 2px solid gary; - + border-bottom: 2px solid #CCCCCC; + background-color: transparent; } - -QComboBox#dataTypeCombox, QComboBox#orderCombox{ - - font-size: 20px; - -/* border: 1px solid black;*/ - -/* border-radius: 5px;*/ - +/* ==================== 下拉框样式 ==================== */ +QComboBox#dataTypeCombox, +QComboBox#orderCombox { + font-family: "PingFangSC-Regular", "Microsoft YaHei", sans-serif; + font-size: 14px; + border: none; + background-color: #FFFFFF; + height: 35px; + padding: 5px 30px 5px 5px; + border-radius: 5px; + min-height: 35px; + max-height: 35px; } +QComboBox#dataTypeCombox::drop-down, +QComboBox#orderCombox::drop-down { + image: url(Static/down.png); + subcontrol-origin: padding; + subcontrol-position: top right; + background-color: #FFFFFF; + width: 20px; + border: none; + border-radius: 5px; +} - - +QComboBox#dataTypeCombox::item, +QComboBox#orderCombox::item { + padding: 8px 5px; +} \ No newline at end of file diff --git a/Static/Main.qss b/Static/Main.qss index 9587b6f..5c8e744 100644 --- a/Static/Main.qss +++ b/Static/Main.qss @@ -67,7 +67,7 @@ QPushButton#procedureMag, QPushButton#varMag, QPushButton#trendMag { border: none; - font-size: 16px; + font-size: 20px; text-align: left; font-family: "PingFangSC-Medium", "Microsoft YaHei", sans-serif; color: #6B7280; diff --git a/UI/ProfibusWidgets/AreaTabWidget.py b/UI/ProfibusWidgets/AreaTabWidget.py index d0aa1af..3c8c6b6 100644 --- a/UI/ProfibusWidgets/AreaTabWidget.py +++ b/UI/ProfibusWidgets/AreaTabWidget.py @@ -11,6 +11,7 @@ from PyQt5.QtGui import QIcon from PyQt5.QtCore import QSize + from model.ProjectModel.DeviceManage import Device, DevicesManange from UI.ProfibusWidgets.RightAreaWidget import RightAreaWidgets @@ -141,7 +142,7 @@ class AreaWidget(QWidget): self.state = True self.devicesManange = self.areaTabWidget.devicesManange self.rightAreaWidgetState = False - self.initUI() + self.initUI() def initUI(self): self.mainLayout = QHBoxLayout() @@ -198,9 +199,11 @@ class AreaWidget(QWidget): self.okBtnValue = True self.delAreaBtn = QPushButton('删除') - self.delAreaBtn.setIcon(QIcon('./Static/delete.png')) + # self.delAreaBtn.setIcon(QIcon('./Static/delete.png')) + self.delAreaBtn.setIcon(qtawesome.icon('fa.trash-o', color='#EE1169')) self.delAreaBtn.setObjectName('delAreaBtn') self.delAreaBtn.clicked.connect(self.removeAreaTab) + hLayout = QHBoxLayout() hLayout.addWidget(QSplitter()) diff --git a/UI/ProfibusWidgets/ProfibusWindow.py b/UI/ProfibusWidgets/ProfibusWindow.py index fdcf26a..a171e1f 100644 --- a/UI/ProfibusWidgets/ProfibusWindow.py +++ b/UI/ProfibusWidgets/ProfibusWindow.py @@ -74,8 +74,19 @@ class ProfibusWidgets(QWidget): super().__init__() InitParameterDB() self.setObjectName("MainWindow") - self.devicesManange = DevicesManange() + self.protocolManage = Globals.getValue('protocolManage') + self.usingSharedProfibus = False + self.devicesManange = None + if self.protocolManage and self.protocolManage.hasProfibusSupport(): + sharedManager = self.protocolManage.getSharedProfibusManager() + if sharedManager: + self.devicesManange = sharedManager + self.usingSharedProfibus = True + if self.devicesManange is None: + print('没有找到共享的Profibus') + self.devicesManange = DevicesManange() # self.batteryManange = BatteryManange() + self.dpv1Master = DPV1Master('192.168.4.38', 502) self.blockParameterManageWidget = BlockParameterManageWidget() self.process = None @@ -228,9 +239,10 @@ class ProfibusWidgets(QWidget): self.refreshProgressBar() - self.devicesManange.connect() + self._connectProfibusBackend() self.setWindowFlags(Qt.FramelessWindowHint) + # self.resize(800, 600) # self.showMaximized() @@ -241,16 +253,20 @@ class ProfibusWidgets(QWidget): self.startProtocolBtn.setText('停止通讯') self.startProtocolBtn.setIcon(QIcon('./Static/pause.png')) self.startProtocolBtn.setIconSize(QSize(22, 22)) + self._connectProfibusBackend() self.protocolTimer.start(500) else: + self.startProtocolBtn.setText('开始通讯') self.startProtocolBtn.setIcon(QIcon('./Static/start.png')) self.protocolTimer.stop() def readValues(self): - self.devicesManange.readAreas() + if not self._updateProfibusAreasForUI(): + return dockWidgets = self.findChildren(QDockWidget) #找到四个dockWidget窗口 + for dockWidget in dockWidgets: if dockWidget.widget().currentWidget().objectName() == 'initWidget': # print(dockWidget.widget().currentWidget().objectName()) @@ -280,8 +296,24 @@ class ProfibusWidgets(QWidget): # except Exception as e: # print(e) + def _connectProfibusBackend(self): + if self.usingSharedProfibus and self.protocolManage: + sharedManager = self.protocolManage.getSharedProfibusManager() + if sharedManager: + self.devicesManange = sharedManager + self.protocolManage.connectProfibus() + else: + self.devicesManange.connect() + + def _updateProfibusAreasForUI(self): + if self.usingSharedProfibus and self.protocolManage: + self.protocolManage.updateProfibusAreas(force=True) + return True + self.devicesManange.readAreas() + return True def refreshProgressBar(self): + # self.temp = temp / 10 if temp > -3276.8 else float('nan') # 假设温度值需要除以10得到实际℃数 # self.current = current # mA # self.volt = volt # mV diff --git a/model/ProjectModel/DeviceManage.py b/model/ProjectModel/DeviceManage.py index 86078ff..e8aed2a 100644 --- a/model/ProjectModel/DeviceManage.py +++ b/model/ProjectModel/DeviceManage.py @@ -338,7 +338,7 @@ class DevicesManange(): if bytesNums == 0: continue intValues = modbusM.readHoldingRegisters(slaveId = 1, startAddress = 0, varNums = intNums, order = 'Profibus') - print(intValues, index) + # print(intValues, index) if intValues == 'error': self.connect() return diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py index 4445cd9..d96e7b8 100644 --- a/protocol/ProtocolManage.py +++ b/protocol/ProtocolManage.py @@ -3,6 +3,7 @@ from utils.DBModels.ProtocolModel import ( HartVar, TcRtdVar, AnalogVar, HartSimulateVar ) from model.ProjectModel.GlobalConfigManager import GlobalConfigManager +from model.ProjectModel.DeviceManage import DevicesManange from protocol.HartRtuSlaveManager import HartRtuSlaveManager from protocol.TCP.TCPVarManage import * @@ -13,6 +14,10 @@ from protocol.ModBus.ModbusManager import ModbusManager from utils import Globals import threading import time +import json +from typing import Dict, Any, Optional + + class ProtocolManage(object): """通讯变量查找类,用于根据变量名在数据库模型中检索变量信息""" @@ -37,9 +42,20 @@ class ProtocolManage(object): self.varInfoCache = {} # 保持驼峰命名 self.historyDBManage = Globals.getValue('historyDBManage') self.variableValueCache = {} # {varName: value} + self.profibusManager = None + self.profibusVarMap = {} + self.profibusDeviceMeta = {} + self.profibusEnabled = GlobalConfigManager.isModuleEnabled('profibusModule') + self._profibusConnected = False + self._profibusLastUpdate = 0 + self.profibusLock = threading.Lock() + if self.profibusEnabled: + # print('yeyeye') + self._initializeProfibusSupport() # Modbus 管理器 self.modbusManager = ModbusManager() + # self.modbusManager.setVariableCache(self.variableValueCache, None, self.varInfoCache) # HART模拟RTU从站管理器 @@ -85,6 +101,9 @@ class ProtocolManage(object): } except Exception as e: print(f"刷新缓存时出错: {modelClass.__name__}: {e}") + if self.profibusEnabled and self.profibusManager: + self._refreshProfibusVarCache() + def lookupVariable(self, variableName): """ @@ -229,8 +248,16 @@ class ProtocolManage(object): trigger = FPGATrigger if trigger else trigger self.tcpVarManager.writeValue(varType, channel, value, trigger=trigger, model=model, timeoutMS=timeoutMS) + # PROFIBUS变量处理 + elif modelType == 'ProfibusVar': + success, normalizedValue = self._writeProfibusVariable(info, value) + if not success: + return False + value = normalizedValue + # HART模拟变量处理 elif modelType == 'HartSimulateVar': + if not self.hartRtuSlaveManager: print("HART RTU从站管理器未初始化,无法处理HartSimulateVar变量") return False @@ -253,9 +280,17 @@ class ProtocolManage(object): print(f"HART模拟变量 {variableName} 写入失败") return False + rpcValue = self._prepareExternalValue(value) if self.RpcClient: - self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) + self.RpcClient.setVarContent( + variableName, + rpcValue, + info.get('min'), + info.get('max'), + info.get('varType') + ) return True + except Exception as e: print(f"写入变量值失败: {str(e)}") return False @@ -265,7 +300,10 @@ class ProtocolManage(object): while not self.readThreadStop.is_set(): try: allVarNames = list(self.getAllVariableNames()) + if self.profibusEnabled and self.profibusManager: + self._updateProfibusAreas(force=True) for varName in allVarNames: + if self.readThreadStop.is_set(): break try: @@ -283,9 +321,360 @@ class ProtocolManage(object): print(f"后台读取线程异常: {e}") time.sleep(5) # 异常时等待更长时间 + # ==================== PROFIBUS 变量管理 ==================== + def _initializeProfibusSupport(self): + try: + self.profibusManager = DevicesManange() + self._loadProfibusDevicesFromDB() + self.profibusManager.recalculateAddress() + except Exception as e: + print(f"初始化PROFIBUS管理器失败: {e}") + self.profibusManager = None + self.profibusEnabled = False + + def _loadProfibusDevicesFromDB(self): + self.profibusDeviceMeta = {} + allDevices = DevicesManange.getAllDevice() + if not allDevices or isinstance(allDevices, str): + return + + for deviceRow in allDevices: + if not deviceRow: + continue + try: + deviceName = deviceRow[0] + except (IndexError, TypeError): + continue + proType = deviceRow[1] if len(deviceRow) > 1 else None + masterSlaveModel = deviceRow[2] if len(deviceRow) > 2 else None + areaJson = deviceRow[3] if len(deviceRow) > 3 else None + pvUpper = deviceRow[4] if len(deviceRow) > 4 else None + pvLower = deviceRow[5] if len(deviceRow) > 5 else None + pvUnit = deviceRow[6] if len(deviceRow) > 6 else None + self.profibusDeviceMeta[deviceName] = { + 'pvUpperLimit': pvUpper, + 'pvLowerLimit': pvLower, + 'pvUnit': pvUnit + } + try: + self.profibusManager.addDevice( + proType=proType, + masterSlaveModel=masterSlaveModel, + deviceName=deviceName + ) + except Exception as e: + print(f"初始化设备 {deviceName} 失败: {e}") + continue + self._loadAreasForDevice(deviceName, areaJson) + + def _loadAreasForDevice(self, deviceName, areaJson): + if not areaJson: + return + try: + areas = json.loads(areaJson) if isinstance(areaJson, str) else areaJson + except Exception as e: + print(f"解析设备 {deviceName} 的区域配置失败: {e}") + return + device = self.profibusManager.getDevice(deviceName) + if not device or not areas: + return + for areaConfig in areas: + if not isinstance(areaConfig, dict): + continue + dataType = areaConfig.get('type') + valueName = areaConfig.get('valueName') + if not dataType or not valueName: + continue + order = areaConfig.get('order', 'ABCD') + bytesCount = areaConfig.get('bytes', 0) + try: + bytesCount = int(bytesCount) + except (TypeError, ValueError): + bytesCount = 0 + try: + device.addArea( + type=dataType, + nums=1, + bytes=bytesCount, + order=order, + valueName=valueName + ) + except Exception as e: + print(f"添加设备 {deviceName} 的区域 {valueName} 失败: {e}") + continue + + def _refreshProfibusVarCache(self): + self.profibusVarMap.clear() + if not self.profibusEnabled: + return + with self.profibusLock: + try: + self.profibusManager = DevicesManange() + self._loadProfibusDevicesFromDB() + self.profibusManager.recalculateAddress() + self._profibusConnected = False + self._profibusLastUpdate = 0 + except Exception as e: + print(f"刷新PROFIBUS设备失败: {e}") + return + deviceGroups = [ + self.profibusManager.dpMasterDevices, + self.profibusManager.dpSlaveDevices, + self.profibusManager.paMasterDevices, + self.profibusManager.paSlaveDevices + ] + snapshot = [] + for devicesDict in deviceGroups: + for deviceName, device in devicesDict.items(): + deviceMeta = self.profibusDeviceMeta.get(deviceName, {}) + for areaIndex, area in enumerate(device.areas): + valueName = getattr(area, 'valueName', None) + if not valueName: + continue + areaInfo = { + 'deviceName': deviceName, + 'areaIndex': areaIndex, + 'areaType': area.type, + 'bytes': area.bytes, + 'order': area.order, + 'valueName': valueName, + 'proType': device.type, + 'masterSlaveModel': device.masterOrSlave, + 'min': deviceMeta.get('pvLowerLimit'), + 'max': deviceMeta.get('pvUpperLimit'), + 'unit': deviceMeta.get('pvUnit'), + 'varType': area.type + } + snapshot.append((valueName, areaInfo)) + for valueName, areaInfo in snapshot: + self.profibusVarMap[valueName] = areaInfo + self.varInfoCache[valueName] = { + 'modelType': 'ProfibusVar', + 'variableData': areaInfo + } + + + def _ensureProfibusConnected(self): + if not self.profibusManager: + return False + if self._profibusConnected: + return True + try: + self.profibusManager.connect() + self._profibusConnected = True + except Exception as e: + print(f"连接PROFIBUS失败: {e}") + self._profibusConnected = False + return False + return True + + def _updateProfibusAreas(self, force=False): + if not self.profibusManager: + return + now = time.time() + if not force and (now - self._profibusLastUpdate) < 0.5: + return + if not self._ensureProfibusConnected(): + return + with self.profibusLock: + try: + self.profibusManager.readAreas() + self._profibusLastUpdate = now + except Exception as e: + print(f"读取PROFIBUS区域失败: {e}") + + def _writeProfibusVariable(self, info, rawValue): + if not self.profibusManager: + return False, None + if not self._ensureProfibusConnected(): + return False, None + device = self.profibusManager.getDevice(info['deviceName']) + if not device: + return False, None + areaIndex = info['areaIndex'] + if areaIndex >= len(device.areas): + return False, None + area = device.areas[areaIndex] + if area.type in ['AI', 'AO']: + analogValue, qualityList = self._formatAnalogValue(rawValue) + if analogValue is None: + return False, None + valuesToWrite = [analogValue] + normalizedValue = analogValue + else: + valuesToWrite = self._formatDiscreteValues(area, rawValue) + if valuesToWrite is None: + return False, None + qualityList = None + normalizedValue = valuesToWrite[:] + with self.profibusLock: + try: + if qualityList is not None: + self.profibusManager.writeAreas( + deviceName=info['deviceName'], + areaIndex=areaIndex, + values=valuesToWrite, + qualityValueList=qualityList + ) + else: + self.profibusManager.writeAreas( + deviceName=info['deviceName'], + areaIndex=areaIndex, + values=valuesToWrite + ) + if area.type in ['AI', 'AO']: + area.currentValue = [normalizedValue] + if qualityList: + area.qualityValueList = qualityList + else: + area.currentValue = normalizedValue[:] + except Exception as e: + print(f"写入PROFIBUS变量 {info['valueName']} 失败: {e}") + return False, None + with self.cacheLock: + self.variableValueCache[info['valueName']] = normalizedValue + return True, normalizedValue + + def _formatAnalogValue(self, rawValue): + qualityValue = None + targetValue = rawValue + if isinstance(rawValue, dict): + targetValue = rawValue.get('value') + qualityValue = rawValue.get('quality') or rawValue.get('qualityValue') + try: + analogValue = float(targetValue) + except (TypeError, ValueError): + return None, None + qualityList = None + if qualityValue is not None: + normalizedQuality = self._normalizeQualityValue(qualityValue) + if normalizedQuality is None: + return None, None + qualityList = [normalizedQuality] + return analogValue, qualityList + + def _normalizeQualityValue(self, qualityValue): + try: + if isinstance(qualityValue, str): + qualityValue = qualityValue.strip() + if qualityValue.lower().startswith('0x'): + qualityInt = int(qualityValue, 16) + else: + qualityInt = int(qualityValue) + else: + qualityInt = int(qualityValue) + if qualityInt < 0 or qualityInt > 255: + return None + return f"0x{qualityInt:02X}" + except (TypeError, ValueError): + return None + + def _formatDiscreteValues(self, area, rawValue): + bitLength = (area.bytes or 0) * 8 + if bitLength <= 0: + bitLength = 16 + if isinstance(getattr(area, 'currentValue', None), list) and area.currentValue: + baseValues = list(area.currentValue) + elif isinstance(getattr(area, 'forceValue', None), list) and area.forceValue: + baseValues = list(area.forceValue) + else: + baseValues = [0] * bitLength + if len(baseValues) < bitLength: + baseValues += [0] * (bitLength - len(baseValues)) + else: + baseValues = baseValues[:bitLength] + if isinstance(rawValue, dict) and 'index' in rawValue: + try: + index = int(rawValue['index']) + except (TypeError, ValueError): + return None + if index < 0 or index >= bitLength: + return None + baseValues[index] = 1 if rawValue.get('value') in [1, True, '1', 'on', 'ON'] else 0 + return baseValues + if isinstance(rawValue, (list, tuple)): + normalized = [1 if item in [1, True, '1', 'on', 'ON'] else 0 for item in rawValue] + if len(normalized) < bitLength: + normalized += [0] * (bitLength - len(normalized)) + return normalized[:bitLength] + baseValues[0] = 1 if rawValue in [1, True, '1', 'on', 'ON'] else 0 + return baseValues + + def _readProfibusVariable(self, info): + if not self.profibusManager: + return None + self._updateProfibusAreas() + device = self.profibusManager.getDevice(info['deviceName']) + if not device: + return None + areaIndex = info['areaIndex'] + if areaIndex >= len(device.areas): + return None + area = device.areas[areaIndex] + if area.type in ['AI', 'AO']: + if isinstance(area.currentValue, list) and area.currentValue: + try: + return float(area.currentValue[0]) + except (TypeError, ValueError): + return area.currentValue[0] + return None + if isinstance(area.currentValue, list) and area.currentValue: + bitLength = (area.bytes or 0) * 8 + if bitLength <= 0: + bitLength = len(area.currentValue) + return area.currentValue[:bitLength] + return None + + # ==================== 对外共享的 PROFIBUS 接口 ==================== + def hasProfibusSupport(self) -> bool: + return self.profibusEnabled and self.profibusManager is not None + + def getSharedProfibusManager(self): + return self.profibusManager + + def connectProfibus(self) -> bool: + if not self.hasProfibusSupport(): + return False + return self._ensureProfibusConnected() + + def refreshProfibusVariables(self): + if self.profibusEnabled: + self._refreshProfibusVarCache() + + def updateProfibusAreas(self, force: bool = False) -> bool: + if not self.hasProfibusSupport(): + return False + self._updateProfibusAreas(force=force) + return True + + def listProfibusVariables(self) -> Dict[str, Dict[str, Any]]: + return {name: dict(info) for name, info in self.profibusVarMap.items()} + + def getProfibusVariableInfo(self, variableName: str) -> Optional[Dict[str, Any]]: + return self.profibusVarMap.get(variableName) + + def readProfibusValue(self, variableName: str, useCache: bool = True): + if not self.hasProfibusSupport(): + return None + if useCache: + with self.cacheLock: + if variableName in self.variableValueCache: + return self.variableValueCache[variableName] + value = self.readVariableValue(variableName) + with self.cacheLock: + self.variableValueCache[variableName] = value + return value + + def writeProfibusValue(self, variableName: str, value) -> bool: + if not self.hasProfibusSupport(): + return False + return bool(self.writeVariableValue(variableName, value)) + def getAllVariableNames(self): return list(self.varInfoCache.keys()) + + def _readVariableValueOriginal(self, variableName): varInfo = self.lookupVariable(variableName) value = None @@ -359,7 +748,10 @@ class ProtocolManage(object): value = self.tcpVarManager.readValue(varType, channel, model=model) if varType in ['AI', 'AO']: value = self.getRealAI(value, info['max'], info['min']) + elif modelType == 'ProfibusVar': + value = self._readProfibusVariable(info) elif modelType == 'HartSimulateVar': + if not self.hartRtuSlaveManager: return None @@ -377,18 +769,36 @@ class ProtocolManage(object): # 默认读取主变量 value = self.hartRtuSlaveManager.readVariable('primaryVariable') if value is not None and value != 'error': + externalValue = self._prepareExternalValue(value) if self.RpcClient: - self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) - self.historyDBManage.writeVarValue(variableName, value) + self.RpcClient.setVarContent( + variableName, + externalValue, + info.get('min'), + info.get('max'), + info.get('varType') + ) + if self.historyDBManage: + self.historyDBManage.writeVarValue(variableName, externalValue) return value else: return None + except Exception as e: print(f"读取变量值失败: {str(e)}") return None + def _prepareExternalValue(self, value): + if isinstance(value, (list, dict)): + try: + return json.dumps(value, ensure_ascii=False) + except Exception: + return str(value) + return value + def readVariableValue(self, variableName): with self.cacheLock: + if variableName in self.variableValueCache: return self.variableValueCache[variableName] return self._readVariableValueOriginal(variableName)