You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

316 lines
14 KiB
Python

1 month ago
#!/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