#!/usr/bin/env python # -*- coding: utf-8 -*- from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QPushButton, QFormLayout, QMessageBox, QComboBox, QDoubleSpinBox, QCompleter) from PyQt5.QtCore import Qt, QThread, pyqtSignal from protocol.HART import common class ReadInfoThread(QThread): """读取或写入信息的线程类""" infoReceived = pyqtSignal(dict, str) errorOccurred = pyqtSignal(str, str) def __init__(self, hartComm, command_name, params=None): super().__init__() self.hartComm = hartComm self.command_name = command_name self.params = params if params is not None else {} self._is_running = True def stop(self): self._is_running = False def run(self): try: if not self.hartComm: raise Exception("HART通信对象未初始化") func = getattr(self.hartComm, self.command_name, None) if not func: raise Exception(f"未找到名为 {self.command_name} 的通信方法") response = func(**self.params) if self._is_running: msg = response[0] if isinstance(response, list) and response else response if msg and isinstance(msg, dict): if msg.get("status") != "fail": self.infoReceived.emit(msg, self.command_name) else: error_msg = msg.get("error", "未知错误") self.errorOccurred.emit(f"命令 {self.command_name} 执行失败: {error_msg}", self.command_name) else: self.errorOccurred.emit(f"收到无效响应: {response}", self.command_name) except Exception as e: self.errorOccurred.emit(str(e), self.command_name) class HartSensorConfigWidget(QWidget): def __init__(self, parent=None): super(HartSensorConfigWidget, self).__init__(parent) self.hartComm = None self.readThreads = {} self.units_map = common.UNITS_CODE self.reverse_units_map = common.REVERSE_UNITS_CODE self.transfer_fn_map = common.TRANSFER_FUNCTION_CODE self.reverse_transfer_fn_map = common.REVERSE_TRANSFER_FUNCTION_CODE self.initUI() def initUI(self): mainLayout = QVBoxLayout(self) displayLayout = QHBoxLayout() pvInfoGroup = QGroupBox("传感器信息 (Command 14)") pvInfoForm = QFormLayout() self.serialNoLabel = QLabel("--") self.sensorUpperLimitLabel = QLabel("--") self.sensorLowerLimitLabel = QLabel("--") self.sensorUnitLabel = QLabel("--") self.minSpanLabel = QLabel("--") pvInfoForm.addRow("传感器序列号:", self.serialNoLabel) pvInfoForm.addRow("传感器上限:", self.sensorUpperLimitLabel) pvInfoForm.addRow("传感器下限:", self.sensorLowerLimitLabel) pvInfoForm.addRow("传感器单位:", self.sensorUnitLabel) pvInfoForm.addRow("最小量程:", self.minSpanLabel) self.readPvInfoButton = QPushButton("读取传感器信息") self.readPvInfoButton.clicked.connect(lambda: self.readInfo('readPrimaryVariableInformation')) pvInfoForm.addRow(self.readPvInfoButton) pvInfoGroup.setLayout(pvInfoForm) outputInfoGroup = QGroupBox("输出信息 (Command 15)") outputInfoForm = QFormLayout() self.alarmCodeLabel = QLabel("--") self.transferFnLabel = QLabel("--") self.pvRangeUnitLabel = QLabel("--") self.pvUpperRangeLabel = QLabel("--") self.pvLowerRangeLabel = QLabel("--") self.dampingLabel = QLabel("--") self.writeProtectLabel = QLabel("--") self.privateLabelLabel = QLabel("--") outputInfoForm.addRow("报警动作:", self.alarmCodeLabel) outputInfoForm.addRow("输出特性:", self.transferFnLabel) outputInfoForm.addRow("PV量程单位:", self.pvRangeUnitLabel) outputInfoForm.addRow("PV上限值:", self.pvUpperRangeLabel) outputInfoForm.addRow("PV下限值:", self.pvLowerRangeLabel) outputInfoForm.addRow("阻尼值(s):", self.dampingLabel) outputInfoForm.addRow("写保护:", self.writeProtectLabel) outputInfoForm.addRow("私有标签:", self.privateLabelLabel) self.readOutputInfoButton = QPushButton("读取输出信息") self.readOutputInfoButton.clicked.connect(lambda: self.readInfo('readOutputInformation')) outputInfoForm.addRow(self.readOutputInfoButton) outputInfoGroup.setLayout(outputInfoForm) displayLayout.addWidget(pvInfoGroup) displayLayout.addWidget(outputInfoGroup) mainLayout.addLayout(displayLayout) configLayout = QHBoxLayout() writeGroup = QGroupBox("写入配置") writeForm = QFormLayout() self.rangeUnitComboBox = QComboBox() self.rangeUnitComboBox.setEditable(True) self.rangeUnitComboBox.addItems(sorted(self.units_map.values())) completer = QCompleter(self.rangeUnitComboBox.model()) completer.setFilterMode(Qt.MatchContains) completer.setCaseSensitivity(Qt.CaseInsensitive) self.rangeUnitComboBox.setCompleter(completer) self.upperRangeSpinBox = QDoubleSpinBox() self.upperRangeSpinBox.setRange(-999999.0, 999999.0) self.upperRangeSpinBox.setDecimals(4) self.lowerRangeSpinBox = QDoubleSpinBox() self.lowerRangeSpinBox.setRange(-999999.0, 999999.0) self.lowerRangeSpinBox.setDecimals(4) self.dampingSpinBox = QDoubleSpinBox() self.dampingSpinBox.setRange(0, 100) self.dampingSpinBox.setDecimals(2) self.transferFnComboBox = QComboBox() self.transferFnComboBox.addItems(self.transfer_fn_map.values()) self.setRangeButton = QPushButton("写入量程 (Cmd 35)") self.setRangeButton.clicked.connect(self.onSetRange) self.setDampingButton = QPushButton("写入阻尼 (Cmd 34)") self.setDampingButton.clicked.connect(self.onSetDamping) self.setTransferFnButton = QPushButton("写入输出函数 (Cmd 54)") self.setTransferFnButton.clicked.connect(self.onSetTransferFunction) writeForm.addRow("量程单位:", self.rangeUnitComboBox) writeForm.addRow("量程上限:", self.upperRangeSpinBox) writeForm.addRow("量程下限:", self.lowerRangeSpinBox) writeForm.addRow(self.setRangeButton) writeForm.addRow("阻尼(s):", self.dampingSpinBox) writeForm.addRow(self.setDampingButton) writeForm.addRow("输出函数:", self.transferFnComboBox) writeForm.addRow(self.setTransferFnButton) writeGroup.setLayout(writeForm) configLayout.addWidget(writeGroup) mainLayout.addLayout(configLayout) self.setHartComm(None) def setHartComm(self, hartComm): self.hartComm = hartComm is_connected = hartComm is not None self.readPvInfoButton.setEnabled(is_connected) self.readOutputInfoButton.setEnabled(is_connected) self.setRangeButton.setEnabled(is_connected) self.setDampingButton.setEnabled(is_connected) self.setTransferFnButton.setEnabled(is_connected) def readInfo(self, command_name): if not self.hartComm: QMessageBox.warning(self, "警告", "未连接到HART设备!") return if command_name in self.readThreads and self.readThreads[command_name].isRunning(): return thread = ReadInfoThread(self.hartComm, command_name) thread.infoReceived.connect(self.updateInfo) thread.errorOccurred.connect(self.handleError) thread.finished.connect(lambda: self.onReadFinished(command_name)) self.readThreads[command_name] = thread button = self.getButtonForCommand(command_name) if button: button.setProperty("original_text", button.text()) button.setText("读取中...") button.setEnabled(False) thread.start() def updateInfo(self, msg, command_name): if command_name == 'readPrimaryVariableInformation': serial_no = msg.get('serial_no') self.serialNoLabel.setText(f"{serial_no.hex().upper()}" if serial_no else "--") self.sensorUpperLimitLabel.setText(f"{msg.get('upper_limit', '--'):.4f}") self.sensorLowerLimitLabel.setText(f"{msg.get('lower_limit', '--'):.4f}") self.sensorUnitLabel.setText(common.get_unit_description(msg.get('sensor_limits_code'))) self.minSpanLabel.setText(f"{msg.get('min_span', '--'):.4f}") elif command_name == 'readOutputInformation': self.alarmCodeLabel.setText(self.getAlarmCodeDescription(msg.get('alarm_code'))) transfer_code = msg.get('transfer_fn_code') transfer_text = self.getTransferFunctionDescription(transfer_code) self.transferFnLabel.setText(transfer_text) if transfer_text in self.reverse_transfer_fn_map: self.transferFnComboBox.setCurrentText(transfer_text) unit_code = msg.get('primary_variable_range_code') unit_text = common.get_unit_description(unit_code) self.pvRangeUnitLabel.setText(unit_text) if unit_text in self.reverse_units_map: self.rangeUnitComboBox.setCurrentText(unit_text) upper_val = msg.get('upper_range_value') lower_val = msg.get('lower_range_value') self.pvUpperRangeLabel.setText(f"{upper_val:.4f}" if upper_val is not None else "--") self.pvLowerRangeLabel.setText(f"{lower_val:.4f}" if lower_val is not None else "--") if upper_val is not None: self.upperRangeSpinBox.setValue(upper_val) if lower_val is not None: self.lowerRangeSpinBox.setValue(lower_val) damping_val = msg.get('damping_value') self.dampingLabel.setText(f"{damping_val:.2f}" if damping_val is not None else "--") if damping_val is not None: self.dampingSpinBox.setValue(damping_val) self.writeProtectLabel.setText(self.getWriteProtectDescription(msg.get('write_protect'))) private_label = msg.get('private_label') self.privateLabelLabel.setText(f"{private_label}" if private_label is not None else "--") def onSetRange(self): upper = self.upperRangeSpinBox.value() lower = self.lowerRangeSpinBox.value() unit_text = self.rangeUnitComboBox.currentText() unit_code = self.reverse_units_map.get(unit_text) if unit_code is None: QMessageBox.warning(self, "输入错误", "无效的单位。请从列表中选择或正确输入。") return if upper <= lower: QMessageBox.warning(self, "输入错误", "量程上限必须大于下限。") return self.startWriteThread('writePrimaryVariableRange', {'unitsCode': unit_code, 'upperRange': upper, 'lowerRange': lower}) def onSetDamping(self): damping = self.dampingSpinBox.value() self.startWriteThread('writePrimaryVariableDamping', {'dampingTime': damping}) def onSetTransferFunction(self): fn_text = self.transferFnComboBox.currentText() fn_code = self.reverse_transfer_fn_map.get(fn_text) if fn_code is None: QMessageBox.warning(self, "输入错误", "无效的输出函数。") return self.startWriteThread('writePrimaryVariableOutputFunction', {'transferFunctionCode': fn_code}) def startWriteThread(self, command_name, params): if not self.hartComm: QMessageBox.warning(self, "警告", "未连接到HART设备!") return if command_name in self.readThreads and self.readThreads[command_name].isRunning(): return thread = ReadInfoThread(self.hartComm, command_name, params) thread.infoReceived.connect(self.onWriteSuccess) thread.errorOccurred.connect(self.handleError) thread.finished.connect(lambda: self.onReadFinished(command_name)) self.readThreads[command_name] = thread button = self.getButtonForCommand(command_name) if button: button.setProperty("original_text", button.text()) button.setText("写入中...") button.setEnabled(False) thread.start() def onWriteSuccess(self, msg, command_name): QMessageBox.information(self, "成功", f"命令 {command_name} 执行成功。") self.readInfo('readOutputInformation') def getAlarmCodeDescription(self, code): return {0: "高报", 1: "低报", 2: "保持最后输出"}.get(code, f"未知代码({code})") def getTransferFunctionDescription(self, code): return self.transfer_fn_map.get(code, f"未知代码({code})") def getWriteProtectDescription(self, code): return {0: "未保护", 1: "已保护"}.get(code, f"未知代码({code})") def handleError(self, errorMsg, command_name): QMessageBox.critical(self, "错误", f"执行 {command_name} 时出错:\\n{errorMsg}") def onReadFinished(self, command_name): button = self.getButtonForCommand(command_name) if button: original_text = button.property("original_text") if original_text: button.setText(original_text) button.setEnabled(self.hartComm is not None) def getButtonForCommand(self, command_name): if command_name == 'readPrimaryVariableInformation': return self.readPvInfoButton elif command_name == 'readOutputInformation': return self.readOutputInfoButton elif command_name == 'writePrimaryVariableRange': return self.setRangeButton elif command_name == 'writePrimaryVariableDamping': return self.setDampingButton elif command_name == 'writePrimaryVariableOutputFunction': return self.setTransferFnButton return None