From d865b9eba76dc17ffdb32b6e90ca2213eec7f36a Mon Sep 17 00:00:00 2001
From: chrin
Date: Thu, 15 Sep 2022 08:15:46 +0000
Subject: [PATCH 1/3] Delete pvgateway.py-
---
pvgateway.py- | 1690 -------------------------------------------------
1 file changed, 1690 deletions(-)
delete mode 100644 pvgateway.py-
diff --git a/pvgateway.py- b/pvgateway.py-
deleted file mode 100644
index 2d6d64d..0000000
--- a/pvgateway.py-
+++ /dev/null
@@ -1,1690 +0,0 @@
-"""
-The module provides data and metadata of a process variable through
-PyCafe.
-"""
-__author__ = 'Jan T. M. Chrin'
-
-import copy
-from enum import IntEnum
-import inspect
-import time
-
-from distutils.version import LooseVersion
-
-from qtpy.QtCore import (QEvent, QMutex, QPoint, QProcess, QSettings, Qt, QUrl,
- Signal)
-from qtpy.QtCore import __version__ as QT_VERSION_STR
-from qtpy.QtGui import QCursor, QDesktopServices, QFont
-from qtpy.QtWidgets import (QAction, QApplication, QComboBox, QDialog,
- QHBoxLayout, QLabel, QMenu, QMessageBox,
- QPushButton, QSpinBox, QVBoxLayout, QWidget)
-
-def __LINE__():
- return inspect.currentframe().f_back_f_lineno
-
-class DAQState(IntEnum):
- BS = 10
- CA = 20
- BS_STOP = 30
- CA_STOP = 40
- BS_PAUSE = 50
- CA_PAUSE = 60
-
-class PVGateway(QWidget):
- """Retrieves pv metadata through PyCafe.
-
- The PVGateway class when subclassed by Qt widgets enables their
- connectivity to channel access.
-
- Attributes:
- monid: (int) Monitor id
- units : (str) Units associated with the pv
-
- trigger_monitor_ is the signal triggered by updates
- arising from monitored pvs.
- trigger_connect is the signal triggered from changes in pv
- connection status.
- widget_handle_dict is a dictionary mapping widgets to their pv
- handle.
- A pv handle may be associated to more than one widget.
- """
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int) #pvdata, status
-
-
- trigger_connect = Signal(int, str, int)
-
- trigger_daq = Signal(object, str, int)
- trigger_daq_int = Signal(object, str, int)
- trigger_daq_str = Signal(object, str, int)
-
- #Properties, user supplied
- ACT_ON_BEAM = 'actOnBeam'
- NOT_ACT_ON_BEAM = 'notActOnBeam'
- READBACK_ALARM = 'alarm'
- READBACK_STATIC = 'static'
-
- #Properties, dynamic
- DISCONNECTED = 'disconnected'
- ALARM_SEV_MINOR = 'alarmSevMinor'
- ALARM_SEV_MAJOR = 'alarmSevMajor'
- ALARM_SEV_INVALID = 'alarmSevInvalid'
- ALARM_SEV_NO_ALARM = READBACK_ALARM
- DAQ_STOPPED = 'stopped'
- DAQ_PAUSED = 'paused'
-
- #ObjectName, defined by CAQ
- PV_CONTROLLER = "Controller"
- PV_READBACK = "Readback"
- PV_DAQ_BS = "BSRead"
- PV_DAQ_CA = "CARead"
-
- _DAQ_CAFE_SG_NAME = "gBS2CA"
-
- _alarm_severity_record_types = ["ai", "ao", "calc", "calcout", "dfanout",
- "longin", "longout", "pid", "sel",
- "steppermotor", "sub"]
-
- #parent is Gui
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units: bool = False, prefix: str = "", suffix: str = "",
- connect_callback=None, msg_label: str = "",
- connect_triggers: bool = True, notify_freq_hz: int = 0,
- notify_unison: bool = False, precision: int = 0,
- monitor_dbr_time: bool = False):
-
- super().__init__()
-
- if parent is None:
- return
-
- if not pv_name:
- return
-
- self.connect_callback = connect_callback
- self.notify_freq_hz = abs(notify_freq_hz)
- self.notify_freq_hz_default = self.notify_freq_hz
-
- self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
- 1000 / self.notify_freq_hz
-
- self.notify_unison = bool(notify_unison) and bool(self.notify_freq_hz)
-
- self.parent = parent
- self.settings = self.parent.settings
-
- self.pv_name = pv_name
-
- self.color_mode = None
-
- if color_mode is not None:
- if color_mode in (self.ACT_ON_BEAM,
- self.NOT_ACT_ON_BEAM,
- self.READBACK_ALARM,
- self.READBACK_STATIC):
- self.color_mode = color_mode
-
- self.color_mode_requested = self.color_mode
-
- if monitor_callback is not None:
- self.monitor_callback = monitor_callback
- else:
- self.monitor_callback = None
-
- self.pv_within_daq_group = pv_within_daq_group
-
- self.show_units = show_units
- self.prefix = prefix
- self.suffix = suffix
-
- self.cafe = self.parent.cafe
- self.cyca = self.parent.cyca
-
- if self.parent.settings is not None:
- self.url_archiver = self.parent.settings.data["url"]["archiver"]
- self.url_databuffer = self.parent.settings.data["url"]["databuffer"]
- self.bg_readback = self.parent.settings.data["StyleGuide"][
- "bgReadback"]
- self.fg_alarm_major = self.parent.settings.data["StyleGuide"][
- "fgAlarmMajor"]
- self.fg_alarm_minor = self.parent.settings.data["StyleGuide"][
- "fgAlarmMinor"]
- self.fg_alarm_invalid = self.parent.settings.data["StyleGuide"][
- "fgAlarmInvalid"]
- self.fg_alarm_noalarm = self.parent.settings.data["StyleGuide"][
- "fgAlarmNoAlarm"]
- else:
- #self.settings = ReadJSON(self.parent.appname)
- self.url_archiver = ("https://ui-data-api.psi.ch/prepare?channel=" +
- "sf-archiverappliance/")
- self.url_databuffer \
- = "https://ui-data-api.psi.ch/prepare?channel=sf-databuffer/"
-
- self.daq_group_name = self._DAQ_CAFE_SG_NAME
- self.desc = None
- self.handle = None
- self.initialize_complete = False
- self.initialize_again = False
-
- self.msg_label = msg_label
- self.msg_press_value = None
- self.msg_release_value = None
-
- self.monitor_id = None
- self.monitor_dbr_time = monitor_dbr_time
- self.mutex_post_display = QMutex()
-
- self.precision_user = precision
- self.has_precision_user = bool(precision)
- self.precision_pv = 3
-
- self.precision = (self.precision_user if self.has_precision_user else
- self.precision_pv)
-
- self.pvd = None
- self.pv_ctrl = None
- self.pv_info = None
- self.record_type = None
-
- #if 'show_log_message' in dir(self.parent):
- # self.show_log_message = self.parent.show_log_message
- #else:
- # self.show_log_message = None
-
- self.qt_object_name = None
-
- self.qt_property_controller = {
- self.DISCONNECTED: False,
- self.ACT_ON_BEAM: False, self.NOT_ACT_ON_BEAM: False
- }
-
- self.qt_property_readback = {
- self.DISCONNECTED: False,
- self.READBACK_ALARM: False, self.READBACK_STATIC: False,
- self.ALARM_SEV_MINOR: False, self.ALARM_SEV_MAJOR: False,
- self.ALARM_SEV_INVALID: False
- }
-
- self.qt_property_daq_bs = {
- self.DISCONNECTED: False,
- self.READBACK_ALARM: False, self.READBACK_STATIC: False,
- self.ALARM_SEV_MINOR: False, self.ALARM_SEV_MAJOR: False,
- self.ALARM_SEV_INVALID: False,
- self.DAQ_STOPPED: False, self.DAQ_PAUSED: False
- }
-
- self.qt_property_daq_ca = {
- self.DISCONNECTED: False,
- self.READBACK_ALARM: False, self.READBACK_STATIC: False,
- self.ALARM_SEV_MINOR: False, self.ALARM_SEV_MAJOR: False,
- self.ALARM_SEV_INVALID: False,
- self.DAQ_STOPPED: False, self.DAQ_PAUSED: False
- }
-
- self.qt_object_to_property = {
- self.PV_CONTROLLER: self.qt_property_controller,
- self.PV_READBACK: self.qt_property_readback,
- self.PV_DAQ_BS: self.qt_property_daq_bs,
- self.PV_DAQ_CA: self.qt_property_daq_ca
- }
-
- self._qt_property_selected = {}
-
- self.status_tip = None
- self.suggested_text = ""
- self.time_monotonic = time.monotonic()
- self.pvd_previous = None
- self.timeout = 0.2
- self.units = ""
-
- self.widget = self
-
- _widget_name_part = str(self.widget.__class__).split("\'")[1].split(".")
- #_widget_class_part = _widget_name_part[1].split(".")
- self.widget_class = _widget_name_part[len(_widget_name_part)-1]
-
- if pv_within_daq_group:
- self.trigger_daq_int.connect(self.receive_daq_update)
- self.trigger_daq.connect(self.receive_daq_update)
- self.trigger_daq_str.connect(self.receive_daq_update)
-
- elif connect_triggers:
- self.trigger_monitor.connect(self.receive_monitor_dbr_time)
- self.trigger_monitor_str.connect(self.receive_monitor_update)
- self.trigger_monitor_int.connect(self.receive_monitor_update)
- self.trigger_monitor_float.connect(self.receive_monitor_update)
- self.trigger_connect.connect(self.receive_connect_update)
-
- self.context_menu = QMenu()
- self.context_menu.setObjectName("contextMenu")
- self.context_menu.setWindowModality(Qt.NonModal) #ApplicationModal
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.0"):
- self.context_menu.addSection("PV: {0}".format(self.pv_name))
-
- action1 = QAction("Text Info", self)
- action1.triggered.connect(self.pv_status_text)
-
- action2 = QAction("Lookup in Archiver", self)
- action2.triggered.connect(self.lookup_archiver)
-
- action3 = QAction("Lookup in Databuffer", self)
- action3.triggered.connect(self.lookup_databuffer)
-
- action4 = QAction("Strip Chart (PShell)", self)
- action4.triggered.connect(self.strip_chart)
-
- action6 = QAction("Configure Display Parameters", self)
- action6.triggered.connect(self.display_parameters)
-
- self.context_menu.addAction(action1)
- self.context_menu.addAction(action2)
- self.context_menu.addAction(action3)
- self.context_menu.addAction(action4)
-
- action5 = QAction("Reconnect: {0}".format(self.pv_name), self)
- action5.triggered.connect(self.reconnect_channel)
- _font = QFont()
- _font.setPixelSize(12)
- action5.setFont(_font)
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.context_menu.addSection("")
-
- #return action6 and 5 code here eventually
- self.context_menu.addAction(action6)
- self.context_menu.addAction(action5)
-
- self.pv_message_in_a_box = QMessageBox()
- self.pv_message_in_a_box.setObjectName("pvinfo")
-
- #Qt.ApplicationModal not used as it blocks input to all windows
- self.pv_message_in_a_box.setWindowModality(Qt.NonModal)
- self.pv_message_in_a_box.setIcon(QMessageBox.Information)
- self.pv_message_in_a_box.setStandardButtons(QMessageBox.Close)
- self.pv_message_in_a_box.setDefaultButton(QMessageBox.Close)
-
- self.initialize()
-
- #return self - previously used by pvgateway
-
-
- def initialize(self):
- '''Initialze class attributes and connect to ca if required.'''
-
- _handle_within_group_flag = False
- if self.pv_within_daq_group:
- self.handle = self.cafe.getHandleFromPVWithinGroup(
- self.pv_name, self.daq_group_name)
- if self.handle > 0:
- self.cafe.addWidget(self.handle, self.widget)
- _handle_within_group_flag = True
- #Callback already invoked to emit signal here!!
- _channel_info = self.cafe.getChannelInfo(self.handle)
-
- #wgts = self.cafe.getWidgets(self.handle)
-
- self.trigger_connect.emit(
- int(self.handle), str(self.pv_name),
- int(_channel_info.cafeConnectionState))
- #In case user is misinformed
- if not _handle_within_group_flag:
- self.handle = self.cafe.getHandleFromPV(self.pv_name)
- if self.connect_callback is None:
- self.connect_callback = self.py_connect_callback
-
- if self.handle > 0:
- #The second time round, widget is gateway rather than parent,
- #Why is that?
- self.cafe.setPyConnectCallbackFn(self.handle,
- self.connect_callback)
-
- self.cafe.addWidget(self.handle, self.widget)
-
- _channel_info = self.cafe.getChannelInfo(self.handle)
- self.trigger_connect.emit(
- self.handle, self.pv_name,
- int(_channel_info.cafeConnectionState))
-
- else:
- self.cafe.openPrepare()
- self.handle = self.cafe.open(self.pv_name,
- self.connect_callback)
- self.cafe.addWidget(self.handle, self.widget)
- self.cafe.openNowAndWait(self.timeout, self.handle)
-
- self.initialize_meta_data()
-
- self.pv_message_in_a_box.setWindowTitle(self.pv_name)
-
-
- def initialize_meta_data(self):
-
- _current_value = ""
-
- if self.cafe.isConnected(self.handle) and \
- self.cafe.initCallbackComplete(self.handle):
-
- if self.pvd is None:
- self.pvd = self.cafe.getPVCache(self.handle)
-
- if self.pv_ctrl is None:
- self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
- self.set_precision_and_units()
-
- if self.pv_info is None:
- self.pv_info = self.cafe.getChannelInfo(self.pv_name)
- if "Not Supported" in self.pv_info.className:
- _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
- self.record_type = _rtype if _rtype is not None else \
- self.pv_info.className
- _rtype = self.cafe.close(self.pv_name.split(".")[0] +
- ".RTYP")
- else:
- self.record_type = self.pv_info.className
-
- _current_value = self.cafe.getCache(self.handle)
- if isinstance(_current_value, (int, float)):
- #space for positive numbers
- _value_form = ("{:<+.%sf}" % self.precision)
- _current_value = _value_form.format(
- round(_current_value, self.precision))
-
- #Reset
- self.initialize_complete = True
-
- #verify user input
- if self.show_units is True:
- if self.suffix == self.units and self.units != "":
- self.show_units = False
-
- self.suggested_text = self.prefix
- if self.prefix:
- self.suggested_text += " "
-
- _suggested_text_from_value = " "
-
- _max_control_abs = 0
-
- if self.pv_ctrl is not None:
- _lower_control_abs = abs(int(self.pv_ctrl.lowerControlLimit))
- _upper_control_abs = abs(int(self.pv_ctrl.upperControlLimit))
- _max_control_abs = max(_lower_control_abs, _upper_control_abs)
- if _max_control_abs is None:
- _max_control_abs = 0
-
- _enum_list = self.pv_ctrl.enumStrings
-
- if _enum_list:
- _enum_list_member_max_length = 0
- _enum_list_member_max_index = 0
-
- for i in range(0, len(_enum_list)):
- if len(_enum_list[i]) > _enum_list_member_max_length:
- _enum_list_member_max_length = len(_enum_list[i])
- _enum_list_member_max_index = i
- _suggested_text_from_value += \
- _enum_list[_enum_list_member_max_index] + "."
- else:
- if self.pv_ctrl.lowerControlLimit < 0:
- _suggested_text_from_value += "-"
- _suggested_text_from_value += str(_max_control_abs) + "."
-
- self.precision = min(9, self.precision) #safety net
- for i in range(0, self.precision):
- _suggested_text_from_value += "0"
-
- if len(_current_value) > len(_suggested_text_from_value):
- _suggested_text_from_value = _current_value
-
- self.suggested_text += _suggested_text_from_value
-
- if self.show_units:
- self.suggested_text += " " + self.units
- self.suggested_text += self.suffix
-
- _suggested_text_length = len(self.suggested_text)
- self.suggested_text = self.suggested_text.center(
- _suggested_text_length+2)
-
- self.max_control_abs_str = str(_max_control_abs)
-
- _max_control_abs_length = len(self.max_control_abs_str)
- _offset = 9
- self.max_control_abs_str = self.max_control_abs_str.center(
- _max_control_abs_length + _offset)
-
- qsettings = QSettings()
- qsettings.beginGroup("Widget")
- qsettings.beginGroup(self.pv_name)
- qsettings.beginGroup(self.widget_class)
-
- _var_text = "suggested_text"
- _ctrl_abs = "max_control_abs_str"
-
- if self.cafe.isConnected(self.handle) and \
- self.cafe.initCallbackComplete(self.handle):
- qsettings.setValue(_var_text, self.suggested_text)
- qsettings.setValue(_ctrl_abs, self.max_control_abs_str)
- else:
- if qsettings.value(_var_text) is not None:
- self.suggested_text = qsettings.value(_var_text)
- if qsettings.value(_ctrl_abs) is not None:
- self.max_control_abs_str = qsettings.value(_ctrl_abs)
-
- qsettings.endGroup()
- qsettings.endGroup()
- qsettings.endGroup()
-
-
- def is_initialize_complete(self):
- icount = 0
- while not self.initialize_complete:
- time.sleep(0.01)
- self.initialize_meta_data()
- icount += 1
- if icount > 50:
- return False
- return True
-
- def cleanup(self, close_pv=True):
- '''Clean up the widget.'''
-
- #Make sure mon id is valid
- if self.handle > 0:
- _monID_list = self.cafe.getMonitorIDs(self.handle)
- if self.monitor_id in _monID_list:
- self.cafe.monitorStop(self.handle, self.monitor_id)
-
- #Do not close of there are other monitors
- if self.cafe.getNoMonitors(self.handle) > 0:
- if close_pv is True:
- self.cafe.close(self.pv_name)
- self.widget.deleteLater()
-
-
- def format_display_value(self, value):
-
- if value is None:
- print(self, self.pv_name, ">>>>format_display_value is None")
- #return
-
- if isinstance(value, str):
- _value_str = value
- elif isinstance(value, int):
- _value_str = str(value)
- else:
- _value_form = ("{:< .%sf}" % self.precision)
- _rounded_value = round(value, self.precision)
- _value_str = _value_form.format(_rounded_value)
-
- if self.show_units:
- _value_str += " " + self.units + " "
- if self.suffix:
- _value_str += " " + self.suffix + " "
-
- if self.prefix:
- _space = ""
- if self.pv_ctrl is not None:
- if self.pv_ctrl.lowerDisplayLimit < 0:
- _space = " "
- _value_str = self.prefix + _space + _value_str
-
- return _value_str
-
- def post_display_value(self, value):
-
- _value_str = self.format_display_value(value)
-
- if "setText" in dir(self):
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
- self.setText(_value_str)
- self.blockSignals(False)
- else:
- self.setText(_value_str)
-
- else:
- print("setText method does not exist for this widget class:\n",
- self.widget.__class__)
- print("sender was: ", self.sender())
-
-
- def py_connect_callback(self, handle, pvname, status):
- '''Callback function to be invoked on change of pv connection status.
- Checks for existence of widget. Waits up to a maximun of 100 ms.
- '''
- pv_name = pvname
- self.trigger_connect.emit(int(handle), str(pv_name), int(status))
-
- def receive_connect_update(self, handle, pv_name, status,
- post_display=True):
- '''Triggered by connect signal. For Widget to overload.'''
-
- if pv_name is not None:
- if pv_name != self.pv_name:
- print(("pv_name {0} in receive_connect_update " +
- "does not match: {1}").format(pv_name, self.pv_name))
-
- if status == self.cyca.ICAFE_CS_CONN:
- self.initialize_connect = True
- self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
- self.pv_info = self.cafe.getChannelInfo(self.handle)
- if self.pv_info is not None and self.record_type is None:
- if "Not Supported" in self.pv_info.className:
- _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
- self.record_type = _rtype if _rtype is not None else \
- self.pv_info.className
- _rtype = self.cafe.close(self.pv_name.split(".")[0] +
- ".RTYP")
- else:
- self.record_type = self.pv_info.className
-
- self.set_precision_and_units(reconnectFlag=True)
-
- if not self.msg_label:
- _value = self.cafe.getCache(handle, dt='native')
- #Another reconnection in progress!!!
-
- if _value is None:
- return
- else:
- _value = self.msg_label
-
- if post_display:
- self.post_display_value(_value)
- self.qt_property_reconnect()
-
- else:
- self.qt_property_disconnect()
-
-
- if status == self.cyca.ICAFE_CS_CLOSED:
- self.initialize_again = True
-
- elif self.initialize_again:
- #monitos_id informs whether or not widget has a monitor
- #CAQMessageButton for instance does not have a monitor
-
- if not self.pv_within_daq_group and self.monitor_id is not None:
- self.monitor_start()
- self.initialize_again = False
-
- return
-
-
- def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
- ''' DAQ mode is widget specific.
- DAQ may be in BS mode, but channels within DAQ stream that
- are not BS enabled will be flagged as CA Mode, i.e., CARead
- '''
-
- _current_qt_dynamic_property = self.qt_dynamic_property_get()
-
- alarm_severity = daq_pvd.alarmSeverity
- self.pvd = daq_pvd
-
- if daq_mode != self.qt_object_name:
- self.qt_object_name = daq_mode
- self.setObjectName(self.qt_object_name)
- self.qt_style_polish()
-
- if daq_state in (self.cyca.ICAFE_DAQ_STOPPED,):
- if _current_qt_dynamic_property != self.DAQ_STOPPED:
- self.qt_property_daq_stopped()
-
- elif daq_state in (self.cyca.ICAFE_DAQ_PAUSED,):
- if _current_qt_dynamic_property != self.DAQ_PAUSED:
- self.qt_property_daq_paused()
-
- elif daq_state in (self.cyca.ICAFE_DAQ_RUN,):
- if daq_mode == self.PV_DAQ_BS and \
- _current_qt_dynamic_property != self.READBACK_STATIC:
- self.qt_property_static()
-
- elif daq_mode == self.PV_DAQ_CA:
- if self.color_mode != self.color_mode_requested:
- self.color_mode = self.color_mode_requested
-
- if self.cafe.isEnum(self.handle) and \
- isinstance(daq_pvd.value[0], int):
- _value = self.cafe.getStringFromEnum(self.handle,
- daq_pvd.value[0])
- else:
- _value = daq_pvd.value[0]
-
- if daq_pvd.status == self.cyca.ICAFE_NORMAL:
- if self.msg_label == "":
- self.post_display_value(_value)
- if daq_mode == self.PV_DAQ_BS:
- return
-
- #Check if color settings are correct
- if alarm_severity > self.cyca.SEV_NO_ALARM:
- self.color_mode = self.READBACK_ALARM
- self.color_mode_requested = self.READBACK_ALARM
-
- if self.color_mode == self.READBACK_ALARM:
- if alarm_severity == self.cyca.SEV_MINOR:
- if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
- self.qt_property_alarm_sev_minor()
-
- elif alarm_severity == self.cyca.SEV_MAJOR:
- if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
- self.qt_property_alarm_sev_major()
-
- elif alarm_severity == self.cyca.SEV_INVALID:
- if _current_qt_dynamic_property != \
- self.ALARM_SEV_INVALID:
- self.qt_property_alarm_sev_invalid()
-
- elif alarm_severity == self.cyca.SEV_NO_ALARM:
- if _current_qt_dynamic_property != \
- self.ALARM_SEV_NO_ALARM:
- self.qt_property_alarm_sev_no_alarm()
-
- elif _current_qt_dynamic_property != self.READBACK_STATIC:
- self.qt_property_static()
-
- else:
- if _current_qt_dynamic_property != self.DISCONNECTED:
- self.qt_property_disconnect()
-
-
- def receive_monitor_dbr_time(self, pvdata, alarm_severity):
- print("called from gateway", self.pv_name, alarm_severity)
- pvdata.show()
-
- def receive_monitor_update(self, value, status, alarm_severity):
- '''Triggered by monitor signal. For Widget to overload.'''
-
- self.mutex_post_display.lock()
- _current_qt_dynamic_property = self.qt_dynamic_property_get()
-
- if status == self.cyca.ICAFE_NORMAL:
-
- if self.msg_label == "":
- self.post_display_value(value)
-
- #For DAQ when channel connects after application start-up
- if _current_qt_dynamic_property == self.DISCONNECTED:
- self.qt_property_initial_values(qt_object_name=self.PV_READBACK)
-
- #Check if color settings are correct
- elif _current_qt_dynamic_property == self.READBACK_STATIC:
- if alarm_severity > self.cyca.SEV_NO_ALARM:
- if alarm_severity < self.cyca.SEV_INVALID:
- self.color_mode = self.READBACK_ALARM
- self.status_tip = ("Widget color mode is dynamic, " +
- "pv with alarm limits")
- elif alarm_severity == self.cyca.SEV_INVALID:
- if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
- self.qt_property_alarm_sev_invalid()
-
- if self.color_mode == self.READBACK_ALARM:
- if alarm_severity == self.cyca.SEV_MINOR:
- if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
- self.qt_property_alarm_sev_minor()
-
- elif alarm_severity == self.cyca.SEV_MAJOR:
- if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
- self.qt_property_alarm_sev_major()
-
- elif alarm_severity == self.cyca.SEV_INVALID:
- if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
- self.qt_property_alarm_sev_invalid()
-
- elif alarm_severity == self.cyca.SEV_NO_ALARM:
- if _current_qt_dynamic_property != self.ALARM_SEV_NO_ALARM:
- self.qt_property_alarm_sev_no_alarm()
-
- else:
- if _current_qt_dynamic_property != self.DISCONNECTED:
- self.qt_property_disconnect()
-
- self.mutex_post_display.unlock()
-
- def py_monitor_callback(self, handle, pvname, pvdata):
-
- '''Callback function to be invoked on change of pv value.
- cafe.getCache and cafe.set operations permitted within callback.
- '''
-
- pv_name = pvname
- pvd = pvdata
-
- if not hasattr(self, 'cafe'):
- print("py_monitor_callback: name/handle self cafe is NONE",
- pv_name, handle)
- return
-
- self.pvd = pvd
-
- if pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
- print("initialize again")
- self.initialize()
-
- elif pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
- _alarm_severity = self.cyca.ICAFE_CA_OP_CONN_DOWN
- else:
- _alarm_severity = pvd.alarmSeverity
-
- if self.monitor_dbr_time:
- self.trigger_monitor.emit(pvd, _alarm_severity)
- elif isinstance(pvd.value[0], str):
- self.trigger_monitor_str.emit((pvd.value[0]), pvd.status,
- _alarm_severity)
- elif isinstance(pvd.value[0], int):
- self.trigger_monitor_int.emit((pvd.value[0]), pvd.status,
- _alarm_severity)
- else:
- self.trigger_monitor_float.emit(float(pvd.value[0]), pvd.status,
- _alarm_severity)
-
-
- def monitor_start(self):
- '''Initiate monitor on pv.'''
- if self.handle > 0:
- #Is monitor in waiting - now deleted with monitor_stop
- if self.notify_unison:
- self.monitor_id = self.cafe.monitorStart(
- self.handle, dbr=self.cyca.CY_DBR_TIME)
- #start with gateway supplied monitor callback handler
- elif self.monitor_callback is None:
- self.monitor_id = self.cafe.monitorStart(
- self.handle, cb=self.py_monitor_callback,
- dbr=self.cyca.CY_DBR_TIME,
- notify_milliseconds=self.notify_milliseconds)
- else:
- self.monitor_id = self.cafe.monitorStart(
- self.handle, cb=self.monitor_callback,
- dbr=self.cyca.CY_DBR_TIME,
- notify_milliseconds=self.notify_milliseconds)
-
- def monitor_stop(self):
- if self.handle > 0:
- _monID_list = self.cafe.getMonitorIDs(self.handle)
- _monID_inwaiting_list = self.cafe.getMonitorIDsInWaiting(
- self.handle)
- _monID_all = _monID_list + _monID_inwaiting_list
-
- if self.monitor_id in _monID_all:
- self.cafe.monitorStop(self.handle, self.monitor_id)
- #Is monitor in waiting?
- #remove monitors in waiting - to do
-
- def reconnect_channel(self):
- self.cafe.reconnect([self.handle]) #list
-
- def set_desc(self):
- '''Set description of pv from pv.DESC'''
-
- if self.cafe.hasDescription(self.handle):
- self.desc = self.cafe.getDescription(self.handle)
- return
- elif self.desc is not None:
- return
- else:
- self.cafe.supplementHandle(self.handle)
- if self.cafe.hasDescription(self.handle):
- self.desc = self.cafe.getDescription(self.handle)
-
- if self.desc is not None:
- return
-
- ###Back-up solution
- _found = str(self.pv_name).find(".")
- if _found != -1:
- _pv_desc = str(self.pv_name)[0:_found] +".DESC"
- else:
- _pv_desc = self.pv_name +".DESC"
- _handle_desc = self.cafe.getHandleFromPVName(_pv_desc)
-
- _handle_desc_already_open = False
-
- if _handle_desc == 0:
- self.cafe.openPrepare()
- _handle_desc = self.cafe.open(_pv_desc)
- self.cafe.openNowAndWait(self.timeout, _handle_desc)
- time.sleep(0.001)
- else:
- _handle_desc_already_open = True
-
- if self.cafe.isConnected(_handle_desc):
- self.desc = self.cafe.getCache(_handle_desc, 'str')
- if self.desc is None:
- self.desc = self.cafe.get(_handle_desc, 'str')
- else:
- self.desc = None
-
- if not _handle_desc_already_open:
- self.cafe.close(_handle_desc)
-
- def set_precision_and_units(self, reconnectFlag: bool = False):
- '''Set the pv precision and units.'''
- if self.pv_ctrl is None or reconnectFlag is True:
- self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
-
- if self.pv_ctrl is not None:
- if not self.has_precision_user:
- self.precision = self.pv_ctrl.precision
- if self.pv_ctrl.units is not None:
- self.units = str(self.pv_ctrl.units)
- else:
- self.units = ""
-
- if reconnectFlag is True:
- #verify user input
- if self.show_units is True and self.suffix is not None:
- if self.suffix == self.units:
- self.show_units = False
-
-
- def _qt_readback_color_mode(self):
- '''Color mode is determined from CAFE and depends on whether the pv:
- has alarm limits (self.color_mode = 'readbackAlarm')
- or is without alarm limits (self.color_mode = 'readbackStatic')
- '''
-
- #Already set by user
- if self.color_mode is self.READBACK_ALARM:
- return
-
- if self.cafe.isConnected(self.handle):
- pvd = self.cafe.getPVCache(self.handle)
- if pvd.alarmSeverity in (self.cyca.SEV_MINOR, self.cyca.SEV_MAJOR) \
- or self.cafe.hasAlarmStatusSeverity(self.handle):
- self.color_mode = self.READBACK_ALARM
- self.status_tip = ("Widget color mode is dynamic, " +
- "pv with alarm limits")
- else:
- self.color_mode = self.READBACK_STATIC
- self.status_tip = ("Widget color mode is static, " +
- "pv without alarm limits")
-
-
- def qt_property_initial_values(self, qt_object_name: str = None,
- tool_tip: bool = True):
-
- '''Set Qt property values.'''
- self.qt_object_name = qt_object_name
- if tool_tip:
- self.setToolTip(self.pv_name)
- self.setObjectName(self.qt_object_name)
- if self.qt_object_name in self.qt_object_to_property.keys():
- self._qt_property_selected = copy.deepcopy(
- self.qt_object_to_property[self.qt_object_name])
- else:
- print("qt_property_initial_values: Object not found in dictionary")
-
- if self.cafe.isConnected(self.handle):
-
- if self.qt_object_name == self.PV_READBACK:
- self._qt_readback_color_mode()
- #self.setStatusTip(self.status_tip)
-
- elif self.qt_object_name == self.PV_CONTROLLER:
- if self.color_mode == self.ACT_ON_BEAM:
- #self.setStatusTip("PV setting acts directly on beam")
- pass
- else:
- self.color_mode = self.NOT_ACT_ON_BEAM
- #self.setStatusTip("PV setting does not influence beam")
-
- elif self.qt_object_name == self.PV_DAQ_CA:
- self._qt_readback_color_mode()
-
- elif self.qt_object_name == self.PV_DAQ_BS:
- self.color_mode = self.READBACK_STATIC
-
- self._qt_dynamic_property_set(self.color_mode)
-
- else:
- self.qt_property_disconnect()
-
-
- def qt_dynamic_property_get(self, property_state: str = None):
- '''Retrieves the requested property value
- else that which is currently true'''
-
- for _property, _value in self._qt_property_selected.items():
- if property_state is not None:
- if _property == property_state:
- return _value
- elif _value:
- return _property
-
- def _qt_dynamic_property_set(self, property_state: str = None):
- '''
- Set the Input property to true, and the remainder to False
- If None is given then all dynamic properties are set to False
- '''
-
- for _property in self._qt_property_selected.keys():
- if _property == property_state:
- self.setProperty(_property, True)
- self._qt_property_selected[_property] = True
- else:
- self.setProperty(_property, False)
- self._qt_property_selected[_property] = False
-
- def qt_property_disconnect(self):
- '''Set Qt disconnect property value.'''
- self._qt_dynamic_property_set(self.DISCONNECTED)
- self.qt_style_polish()
-
- def qt_property_reconnect(self):
- '''Set Qt connected property value.'''
-
- if self.qt_object_name == self.PV_READBACK:
- self._qt_readback_color_mode()
- #self.setStatusTip(self.status_tip)
-
-
- elif self.qt_object_name == self.PV_CONTROLLER:
- if self.color_mode == self.ACT_ON_BEAM:
- #self.setStatusTip("PV setting acts directly on beam")
- pass
- else:
- self.color_mode = self.NOT_ACT_ON_BEAM
- #self.setStatusTip("PV setting does not influence beam")
-
-
- #self._qt_property_selected =
- self._qt_dynamic_property_set(self.color_mode)
-
- self.qt_style_polish()
-
- def qt_property_alarm_sev_major(self):
- '''Set Qt MAJOR property value.'''
-
- self._qt_dynamic_property_set(self.ALARM_SEV_MAJOR)
- self.setStatusTip("{0} reports value in MAJOR alarm state!".format(
- self.pv_name))
- self.qt_style_polish()
-
- def qt_property_alarm_sev_minor(self):
- '''Set Qt MINOR property value.'''
- self._qt_dynamic_property_set(self.ALARM_SEV_MINOR)
- self.setStatusTip("{0} reports value in MINOR alarm state!".format(
- self.pv_name))
- self.qt_style_polish()
-
- def qt_property_alarm_sev_no_alarm(self):
- '''Set Qt READBACK_ALARM property value.'''
- #self._qt_property_selected =
- self._qt_dynamic_property_set(self.READBACK_ALARM)
- self.setStatusTip("{0} reports value in normal state".format(
- self.pv_name))
- self.qt_style_polish()
-
- def qt_property_alarm_sev_invalid(self):
- '''Set Qt INVALID property value.'''
- self._qt_dynamic_property_set(self.ALARM_SEV_INVALID)
- self.setStatusTip("PV={0} reports an INVALID value!".format(
- self.pv_name))
- self.qt_style_polish()
-
- def qt_property_static(self):
- '''Set Qt STATIC property value.'''
- self._qt_dynamic_property_set(self.READBACK_STATIC)
- self.setStatusTip("PV={0} does not have an alarm state".format(
- self.pv_name))
- self.qt_style_polish()
-
- def qt_property_daq_stopped(self):
- '''Set Qt STOPPED property value.'''
- self._qt_dynamic_property_set(self.DAQ_STOPPED)
- self.setStatusTip("PV={0} reports DAQ has stopped".format(
- self.pv_name))
- self.qt_style_polish()
-
- def qt_property_daq_paused(self):
- '''Set Qt STOPPED property value.'''
- self._qt_dynamic_property_set(self.DAQ_PAUSED)
- self.setStatusTip("PV={0} reports DAQ has paused".format(
- self.pv_name))
- self.qt_style_polish()
-
- def qt_style_polish(self, redraw=False):
- if redraw:
- self.style().unpolish(self)
- self.style().polish(self)
- event = QEvent(QEvent.StyleChange)
- QApplication.sendEvent(self, event)
- self.update()
- self.updateGeometry()
- else:
- self.style().polish(self)
- QApplication.processEvents()
-
- def pv_status_text_header(self, source="Channel Access"):
- _source = source
- _source_separator = "----------------------------------------"
- _text = """
-
- Widget: {0} ({1}, {2})
- """.format(self.widget_class, self.qt_object_name, self.color_mode)
-
- if self.msg_press_value is not None:
- _text += """
- On press, sends value: {0}
- """.format(self.msg_press_value, "DarkOrchid")
-
- if self.msg_release_value is not None:
- _text += """
- On release, sends value: {0}
- """.format(self.msg_release_value, "DarkOrchid")
-
- if self.pv_within_daq_group:
- if self.qt_object_name in self.PV_DAQ_BS:
- _ds_color = "Navy Blue"
- else:
- _ds_color = "Black"
- else:
- _ds_color = "Black"
-
- _text += """
- {0}
- Data source: {1}
- {0}
- PV: {2}
- """.format(_source_separator, _source, self.pv_name, "DarkOrchid",
- _ds_color)
-
- if self.desc is None:
- self.set_desc()
-
- if self.desc == "":
- _text += """
- """
- return _text
-
- _text += """
-
- Description: {0}
-
- """.format(self.desc, "DarkOrchid")
-
- return _text
-
-
- def pv_status_text_enum(self):
-
- _val_enum = None
- _value = self.pvd.value[0]
- if isinstance(_value, str):
- _val_enum = self.cafe.getEnumFromString(self.handle, _value)
- elif _value is not None:
- _val_enum = self.cafe.getStringFromEnum(self.handle, _value)
-
- _color = "Blue"
-
- #To catch case where channel is called by user
-
-
- #To catch DAQ case
- if self.pv_within_daq_group:
- if self.qt_object_name in self.PV_DAQ_BS:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DAQ_PAUSED,
- self.DISCONNECTED):
- _color = "White"
- elif self.qt_object_name in self.PV_DAQ_CA:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DISCONNECTED):
- _color = "White"
-
- elif not self.cafe.isConnected(self.handle):
- _color = "White"
- elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
- _color = "White"
-
- _text = """
-
- Value: {1} [{2}]
- """.format(_color, _value, _val_enum)
-
- return _text
-
- def pv_status_text_data(self):
-
- _value_str = ""
- _first_end = 9
- _end_range = min(self.pvd.nelem, _first_end)
- if _end_range > 1:
- _value_str = "[ "
- for i in range(0, _end_range):
- _value = self.pvd.value[i]
- if _value is None:
- _value = '0'
- if isinstance(_value, str):
- _value_str += _value
- elif isinstance(_value, int):
- _value_str += str(_value)
- else:
- if self.pv_ctrl is not None:
- _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
- _value_str += _value_form.format(
- round(_value, self.pv_ctrl.precision))
- if i < (_end_range-1):
- _value_str += " "
-
- if self.pvd.nelem > _first_end:
- _value_str += " ... "
- _value = self.pvd.value[self.pvd.nelem-1]
- if isinstance(_value, str):
- _value_str += _value
- elif isinstance(_value, int):
- _value_str += str(_value)
- else:
- if self.pv_ctrl is not None:
- _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
- _value_str += _value_form.format(
- round(_value, self.pv_ctrl.precision))
- _value_str += " "
- if _end_range > 1:
- _value_str += "]"
-
- _color = "Blue"
-
-
- #To catch DAQ case
- if self.pv_within_daq_group:
-
- if self.qt_object_name in self.PV_DAQ_BS:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DAQ_PAUSED,
- self.DISCONNECTED):
- _color = "White"
- elif self.qt_object_name in self.PV_DAQ_CA:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DISCONNECTED):
- _color = "White"
-
- elif not self.cafe.isConnected(self.handle):
- _color = "White"
- elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
- _color = "White"
-
- _text = """
-
- Value: {1} {2}
- """.format(_color, _value_str, self.units)
-
- return _text
-
-
- def pv_status_text_timestamp(self):
- _status_not_ok_color = "IndianRed"
- _status_ok_color = "DimGray"
- _ts_color = "Blue"
- _color = _status_ok_color
-
- #To catch DAQ case
- if self.pv_within_daq_group:
- if self.qt_object_name in self.PV_DAQ_BS:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DAQ_PAUSED,
- self.DISCONNECTED):
- _ts_color = "White"
- _color = "White"
- elif self.qt_object_name in self.PV_DAQ_CA:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DISCONNECTED):
- _ts_color = "White"
- _color = "White"
-
- elif not self.cafe.isConnected(self.handle):
- _ts_color = "White"
- elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
- _ts_color = "White"
-
- if self.pvd.status != self.cyca.ICAFE_NORMAL:
- _color = _status_not_ok_color
- _text = """
- Timestamp: {2}
- Status: {3}
{4}
- """.format(_ts_color, _color, self.pvd.tsDateAsString,
- self.pvd.statusAsString,
- self.cafe.getStatusInfo(self.pvd.status))
-
- return _text
-
-
- def pv_status_text_alarm(self):
- _text = """
- """
- _color = "DimGray"
-
- #To catch DAQ case
- if self.pv_within_daq_group:
-
- if self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
- _color = "Yellow"
- elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
- _color = "Red"
- elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
- _color = "White"
-
- if self.qt_object_name in self.PV_DAQ_BS:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DAQ_PAUSED,
- self.DISCONNECTED):
- _color = "White"
- elif self.qt_object_name in self.PV_DAQ_CA:
- if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
- self.DISCONNECTED):
- _color = "White"
-
-
- elif not self.cafe.isConnected(self.handle):
- _color = "White"
-
- elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
- _color = "White"
-
- elif self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
- _color = "Yellow"
- elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
- _color = "Red"
- elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
- _color = "White"
-
- _text += """
- Alarm status: {1}
- Alarm severity: {2}
- """.format(_color, self.pvd.alarmStatusAsString,
- self.pvd.alarmSeverityAsString)
-
- return _text
-
- def pv_access(self):
- _accessIs = ""
- if self.pv_info is None:
- self.pv_info = self.cafe.getChannelInfo(self.handle)
- if self.pv_info.accessRead:
- _accessIs += "Read"
- if self.pv_info.accessWrite:
- _accessIs += "Write"
- return _accessIs
-
- def pv_status_text_enum_metadata(self):
- _text = """
- ENUM strings: {2}
- Data type (native): {3}
- Record type: {4}
- RW Access: {5}
- IOC: {6}
- """.format("MediumBlue", "DarkOrchid", self.pvc.enumStrings,
- self.pv_info.dataTypeAsString,
- self.record_type, self.pv_access(),
- self.pv_info.hostName)
- return _text
-
- def pv_status_text_metadata(self):
-
- if self.pv_info is None:
- self.pv_info = self.cafe.getChannelInfo(self.handle)
- if self.pv_info is not None and self.record_type is None:
- if "Not Supported" in self.pv_info.className:
- _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
- self.record_type = _rtype if _rtype is not None else \
- self.pv_info.className
- self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
- else:
- self.record_type = self.pv_info.className
-
- if self.record_type in ["stringin", "stringout"]:
- _text = """
- Data type (native): {1}
- Record type: {2}
- RW Access: {3}
- IOC: {4}
- """.format("MediumBlue", self.pv_info.dataTypeAsString,
- self.record_type, self.pv_access(),
- self.pv_info.hostName)
- return _text
-
- _text = """
- """
- if self.pvd.nelem > 1:
- _text += """
- Nelem: {1}
- """.format("MediumBlue", self.pvd.nelem)
-
- _text += """
- Precision (PV): {1}
- Data type (native): {2}
- Record type: {3}
- RW Access: {4}
- IOC: {5}
- """.format("MediumBlue", self.pvc.precision,
- self.pv_info.dataTypeAsString,
- self.record_type, self.pv_access(),
- self.pv_info.hostName)
- return _text
-
-
- def pv_status_text_alarm_limits(self):
-
- if self.pv_info is None:
- self.pv_info = self.cafe.getChannelInfo(self.handle)
- if self.pv_info is not None and self.record_type is None:
- if "Not Supported" in self.pv_info.className:
- _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
- self.record_type = _rtype if _rtype is not None else \
- self.pv_info.className
- self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
- else:
- self.record_type = self.pv_info.className
-
- _text = """
- """
-
- #No all record types have alarms
- #className is not supported at psi since introduction of the
- #linux ca gateway
- #Not Supported by Gateway
-
- if "Not Supported" in str(self.record_type):
- pass
- elif self.record_type not in self._alarm_severity_record_types:
- return _text
-
- if self.pvc.lowerAlarmLimit == 0 and self.pvc.upperAlarmLimit == 0 and \
- self.pvc.lowerWarningLimit == 0 and self.pvc.upperWarningLimit == 0:
- return _text
-
- if self.cafe.hasAlarmStatusSeverity(self.handle):
- _text = """
- Lower/Upper alarm limit:
- {1} / {4}
- Lower/Upper warning limit:
- {2} / {3}
-
- """.format("MediumBlue",
- self.pvc.lowerAlarmLimit, self.pvc.lowerWarningLimit,
- self.pvc.upperWarningLimit, self.pvc.upperAlarmLimit)
- return _text
-
- def pv_status_text_display_limits(self):
- _text = """
- """
- if self.pvc.lowerDisplayLimit == 0 and \
- self.pvc.upperDisplayLimit == 0 and \
- self.pvc.lowerControlLimit == 0 and self.pvc.upperControlLimit == 0:
- return _text
- _text = """
- Lower/Upper control limit:
- {3} / {4}
- Lower/Upper display limit:
- {1} / {2}
-
- """.format("MediumBlue",
- self.pvc.lowerDisplayLimit, self.pvc.upperDisplayLimit,
- self.pvc.lowerControlLimit, self.pvc.upperControlLimit)
- return _text
-
-
- def pv_status_text(self):
- '''pv metadata to accompany widget's dialog box.'''
- QApplication.processEvents()
- _source = "Channel Access"
-
- if self.pv_within_daq_group:
- if self.qt_object_name == self.PV_DAQ_BS:
- _source = "DAQ (Beam Synchronous)"
- #self.pvd written to in receive_daq_update
- elif self.qt_object_name == self.PV_DAQ_CA:
- _source = "DAQ (Channel Access)"
- self.pvd = self.cafe.getPVCache(self.handle)
- if self.pvd.pulseID > 0:
- _source += "
Pulse ID: {0}".format(self.pvd.pulseID)
- else:
- self.pvd = self.cafe.getPVCache(self.handle)
-
- self.pvc = self.cafe.getCtrlCache(self.handle)
-
- _text_data = """
- """
- if self.pvd.status == self.cyca.ECAFE_INVALID_HANDLE:
- _text_data = """ Status: {1}
{2}
- """.format("Blue",
- "Channel closed while DAQ in STOP state.",
- ("PV info requires DAQ to be in " +
- "RUN/PAUSED state"))
-
-
- elif self.pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
- _text_data = """ Status: {1}
{2}
- """.format("Red", self.pvd.statusAsString,
- self.cafe.getStatusInfo(self.pvd.status))
-
- elif self.pvc.noEnumStrings > 0:
- _text_data = (self.pv_status_text_enum() +
- self.pv_status_text_timestamp() +
- self.pv_status_text_alarm() +
- self.pv_status_text_enum_metadata())
-
- else:
- _text_data = (self.pv_status_text_data() +
- self.pv_status_text_timestamp() +
- self.pv_status_text_alarm() +
- self.pv_status_text_metadata() +
- self.pv_status_text_alarm_limits() +
- self.pv_status_text_display_limits())
-
- self.pv_message_in_a_box.setText(
- self.pv_status_text_header(source=_source) + _text_data
- )
- QApplication.processEvents()
- self.pv_message_in_a_box.exec()
-
-
- def lookup_archiver(self):
- '''Plot pvdata from archiver.'''
- #"https://ui-data-api.psi.ch/prepare?
- #channel=sf-archiverappliance/"
- urlIs = self.url_archiver
- urlIs = urlIs + self.pv_name
-
- if not QDesktopServices.openUrl(QUrl(urlIs)):
- print("URL FOR ARCHIVER NOT FOUND", urlIs)
- #if self.show_log_message is not None:
- # self.show_log_message(MsgSeverity.ERROR, __pymodule__, _line(),
- # "Failed to open URL {0}".format(urlIs))
-
- def lookup_databuffer(self):
- '''Plot beam synchronous pvdata from databuffer.'''
- #""https://ui-data-api.psi.ch/prepare?channel = sf-databuffer/"
- urlIs = self.url_databuffer
- urlIs = urlIs + self.pv_name
-
- if not QDesktopServices.openUrl(QUrl(urlIs)):
- print("URL FOR DATA BUFFER NOT FOUND", urlIs)
- #if self.show_log_message is not None:
- # self.show_log_message(MsgSeverity.ERROR, __pymodule__, _line(),
- # "Failed to open URL {0}".format(urlIs))
- QApplication.processEvents()
-
- def strip_chart(self):
- '''PShell strip chart.'''
- configStr = ("-config = [[[true,\"" + self.pv_name +
- "\",\"Channel\",1,1]]]")
- commandStr = "/sf/op/bin/strip_chart"
- argStr = ["-nlaf", "-start", configStr, "&"]
- QProcess.startDetached(commandStr, argStr)
-
-
- def display_parameters(self):
- display_wgt = QDialog(self)
-
- _rect = display_wgt.geometry() #
- _parentRect = self.context_menu.geometry()
-
- _rect.moveTo(display_wgt.mapToGlobal(
- QPoint(_parentRect.x() + _parentRect.width() - _rect.width(),
- _parentRect.y())))
-
- display_wgt.setGeometry(_rect)
- display_wgt.setWindowTitle(self.pv_name)
- layout = QVBoxLayout()
-
- precision_flag = True
- if self.pv_ctrl is not None:
- if self.pv_ctrl.precision <= 0:
- precision_flag = False
-
- if self.cafe.getDataTypeNative(self.handle) in (
- self.cyca.CY_DBR_FLOAT,
- self.cyca.CY_DBR_DOUBLE) and precision_flag:
- #precision user
- _hbox_wgt = QWidget()
- _hbox = QHBoxLayout()
- precision_user_label = QLabel("Precision (user):")
- self.precision_user_wgt = QSpinBox(self)
- self.precision_user_wgt.setFocusPolicy(Qt.NoFocus)
- self.precision_user_wgt.setValue(int(self.precision))
- if self.pv_ctrl is not None:
- _max = self.pv_ctrl.precision
- else:
- _max = 6
- self.precision_user_wgt.setMaximum(_max)
- self.precision_user_wgt.valueChanged.connect(
- self.precision_user_changed)
- _hbox.addWidget(precision_user_label)
- _hbox.addWidget(self.precision_user_wgt)
- _hbox_wgt.setLayout(_hbox)
-
- precision_user_label.setFixedWidth(110)
- self.precision_user_wgt.setFixedWidth(35)
- _hbox_wgt.setFixedWidth(160)
-
- #precision ioc
- _hbox2_wgt = QWidget()
- _hbox2 = QHBoxLayout()
- precision_ioc_label = QLabel("Precision (ioc): ")
- precision_ioc = QPushButton(self)
- precision_ioc.setText(" {} ".format(_max))
- precision_ioc.clicked.connect(self.precision_ioc_reset)
-
- _hbox2.addWidget(precision_ioc_label)
- _hbox2.addWidget(precision_ioc)
- _hbox2_wgt.setLayout(_hbox2)
-
- precision_ioc_label.setFixedWidth(110)
- precision_ioc.setFixedWidth(20)
- _hbox2_wgt.setFixedWidth(145)
-
- layout.addWidget(_hbox_wgt)
- layout.addWidget(_hbox2_wgt)
-
- #precision refresh rate
- _hbox3_wgt = QWidget()
- _hbox3 = QHBoxLayout()
- refresh_freq_label = QLabel("Refresh rate: ")
- _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
- self.notify_freq_hz_default
-
- self.refresh_freq_combox_idx_dict = {0: 0, 1: 10, 2: 5, 3: 2, 4: 1,
- 5: 0.5, 6: _default_refresh_val}
- refresh_freq = QComboBox(self)
- refresh_freq.addItem('direct')
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[1]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[2]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[3]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[4]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[5]))
-
- _default_text = 'default (direct)' if _default_refresh_val == 0 else \
- 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
-
- refresh_freq.addItem(_default_text)
-
- for key, value in self.refresh_freq_combox_idx_dict.items():
- if value == self.notify_freq_hz:
- refresh_freq.setCurrentIndex(key)
- break
- refresh_freq.currentIndexChanged.connect(self.refresh_rate_changed)
-
- _hbox3.addWidget(refresh_freq_label)
- _hbox3.addWidget(refresh_freq)
- _hbox3_wgt.setLayout(_hbox3)
-
- refresh_freq_label.setFixedWidth(110)
- refresh_freq.setFixedWidth(115)
- _hbox3_wgt.setFixedWidth(235)
-
- layout.addWidget(_hbox3_wgt)
-
- layout.setAlignment(Qt.AlignLeft)
- layout.setContentsMargins(10, 0, 0, 0)
- layout.setSpacing(0)
-
- display_wgt.setMinimumWidth(340)
- display_wgt.setLayout(layout)
-
- display_wgt.exec()
- QApplication.processEvents()
-
- def precision_ioc_reset(self):
- if self.pv_ctrl is not None:
- self.precision_user = self.pv_ctrl.precision
- self.precision = self.pv_ctrl.precision
- if self.precision is not None:
- self.precision_user_wgt.setValue(self.precision)
-
- def precision_user_changed(self, new_value):
- self.precision_user = new_value
- self.precision = new_value
-
- _pvd = self.cafe.getPVCache(self.handle)
-
- if _pvd.value[0] is not None:
- if isinstance(_pvd.value[0], float):
- self.trigger_monitor_float.emit(
- _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
-
- def refresh_rate_changed(self, new_idx):
- _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
- self.notify_milliseconds = 0 if _notify_freq_hz == 0 else \
- 1000 / _notify_freq_hz
- self.notify_freq_hz = _notify_freq_hz
-
- if self.notify_unison:
- self.notify_unison = False
- self.monitor_stop()
- self.monitor_start()
-
- else:
- self.cafe.updateMonitorPolicyDeltaMS(
- self.handle, self.monitor_id, self.notify_milliseconds)
-
- #https://doc.qt.io/qt-5.9/qtwidgets-mainwindows-menus-example.html
- #Since Qt5 this has to be implemented in order to avoid the Select
- #All dialogue button appearing..
- def contextMenuEvent(self, event):
- return
-
- def showContextMenu(self):
- self.context_menu.exec(QCursor.pos())
-
- def mousePressEvent(self, event):
- '''Action on mouse press event.'''
- button = event.button()
- if button == Qt.RightButton:
- self.context_menu.exec(QCursor.pos())
- self.clearFocus()
-
- def mouseReleaseEvent(self, event):
- event.ignore()
-
From d9f098582f80fe7bd91f2e0dbd65e821af080293 Mon Sep 17 00:00:00 2001
From: chrin
Date: Thu, 15 Sep 2022 08:15:54 +0000
Subject: [PATCH 2/3] Delete pvwidgets.py-
---
pvwidgets.py- | 3112 -------------------------------------------------
1 file changed, 3112 deletions(-)
delete mode 100644 pvwidgets.py-
diff --git a/pvwidgets.py- b/pvwidgets.py-
deleted file mode 100644
index 3e05f6d..0000000
--- a/pvwidgets.py-
+++ /dev/null
@@ -1,3112 +0,0 @@
-''' Module with channel access enabled QtWidgets.'''
-__author__ = 'Jan T. M. Chrin'
-
-import re
-import time
-
-import collections
-import numpy as np
-from sklearn.linear_model import LinearRegression
-from distutils.version import LooseVersion
-from functools import reduce as func_reduce
-
-from qtpy.QtCore import QEventLoop, QPoint, Qt, QThread, QTimer, Signal, Slot
-from qtpy.QtGui import (QCloseEvent, QColor, QCursor, QFont, QFontMetricsF,
- QIcon, QKeySequence)
-from qtpy.QtCore import __version__ as QT_VERSION_STR
-from qtpy.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QAction,
- QApplication, QBoxLayout, QCheckBox, QComboBox,
- QDialog, QDockWidget, QDoubleSpinBox, QFrame,
- QGroupBox, QHBoxLayout, QLabel, QLineEdit,
- QListWidget, QMenu, QMessageBox, QPushButton,
- QSpinBox, QStyle, QStyleOptionSpinBox, QTableWidget,
- QTableWidgetItem, QVBoxLayout, QWidget)
-
-import pyqtgraph as pg
-from pyqtgraph import PlotWidget
-from caqtwidgets.pvgateway import PVGateway
-
-class QTaggedLineEdit(QWidget):
- def __init__(self, label_text=str(""), value="",
- position="LEFT", parent=None):
- super(QTaggedLineEdit, self).__init__(parent)
- self.parameter = str(value)
- self.label = QLabel(label_text)
- self.label.setObjectName("Tagged")
- self.label.setFixedHeight(24)
- self.label.setContentsMargins(10, 0, 0, 0)
- #self.label.setFixedWidth(80)
- self.line_edit = QLineEdit(self.parameter)
- self.line_edit.setObjectName("Write")
- self.line_edit.setFixedHeight(24)
- font = QFont("sans serif", 16)
- fm = QFontMetricsF(font)
- self.line_edit.setMaximumWidth(fm.width(self.parameter)+20)
- self.label.setBuddy(self.line_edit)
- layout = QBoxLayout(
- QBoxLayout.LeftToRight if position == "LEFT" else \
- QBoxLayout.TopToBottom)
- layout.addWidget(self.label)
- layout.addWidget(self.line_edit)
- layout.addStretch()
- layout.setSpacing(2)
- layout.setContentsMargins(0, 0, 0, 0)
- self.setLayout(layout)
-
-class QHLine(QFrame):
- def __init__(self):
- super(QHLine, self).__init__()
- self.setFrameShape(QFrame.HLine)
- self.setFrameShadow(QFrame.Sunken)
-
-class QVLine(QFrame):
- def __init__(self):
- super(QVLine, self).__init__()
- self.setFrameShape(QFrame.VLine)
- self.setFrameShadow(QFrame.Sunken)
-
-class AppQLineEdit(QLineEdit):
- def __init__(self, parent=None):
- #super().__init__(parent)
- pass
- def leaveEvent(self, event):
- self.clearFocus()
- del event
-
-class CAQLineEdit(QLineEdit, PVGateway):
- '''Channel access enabled QLineEdit widget'''
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int)
- trigger_connect = Signal(int, str, int)
-
- trigger_daq = Signal(object, str, int)
- trigger_daq_int = Signal(object, str, int)
- trigger_daq_str = Signal(object, str, int)
-
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units: bool = False, prefix: str = "", suffix: str = "",
- notify_freq_hz: int = 0, precision: int = 0):
-
- super().__init__(parent, pv_name, monitor_callback,
- pv_within_daq_group, color_mode, show_units, prefix,
- suffix, connect_callback=self.py_connect_callback,
- notify_freq_hz=notify_freq_hz, precision=precision)
-
- self.is_initialize_complete()
- self.configure_widget()
-
- if not self.pv_within_daq_group:
- self.monitor_start()
-
- def py_connect_callback(self, handle, pvname, status):
- '''Callback function to be invoked on change of pv connection status.
- '''
- self.trigger_connect.emit(int(handle), str(pvname), int(status))
-
- @Slot(object, str, int)
- def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
- PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
- PVGateway.receive_monitor_update(self, value, status, alarm_severity)
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- PVGateway.receive_connect_update(self, handle, pv_name, status)
-
- def configure_widget(self):
- self.setFocusPolicy(Qt.NoFocus)
-
- fm = QFontMetricsF(QFont("Sans Serif", 10))
- qrect = fm.boundingRect(self.suggested_text)
- _width_scaling_factor = 1.15
- self.setFixedHeight((fm.lineSpacing()*1.8))
- self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
-
- if self.pv_within_daq_group:
- self.qt_property_initial_values(qt_object_name=self.PV_DAQ_CA)
- else:
- self.qt_property_initial_values(qt_object_name=self.PV_READBACK)
-
- #renove highlighting which persists after mouse leaves
- def mouseMoveEvent(self, event):
- #event.ignore()
- pass
-
- def leaveEvent(self, event):
- self.clearFocus()
- del event
-
-class CAQLabel(QLabel, PVGateway):
- '''Channel access enabled QLabel widget'''
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int)
-
- trigger_connect = Signal(int, str, int)
-
- trigger_daq = Signal(object, str, int)
- trigger_daq_int = Signal(object, str, int)
- trigger_daq_str = Signal(object, str, int)
-
-
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units: bool = False, prefix: str = "", suffix: str = "",
- notify_freq_hz: int = 0, precision: int = 0):
-
- super().__init__(parent, pv_name, monitor_callback, pv_within_daq_group,
- color_mode, show_units, prefix, suffix,
- connect_callback=self.py_connect_callback,
- notify_freq_hz=notify_freq_hz, precision=precision)
-
- self.is_initialize_complete()
-
- self.configure_widget()
-
- if self.pv_within_daq_group is False:
- self.monitor_start()
-
- def py_connect_callback(self, handle, pvname, status):
- '''Callback function to be invoked on change of
- pv connection status.
- '''
- self.trigger_connect.emit(int(handle), str(pvname), int(status))
-
- @Slot(object, str, int)
- def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
- PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
- PVGateway.receive_monitor_update(self, value, status, alarm_severity)
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- PVGateway.receive_connect_update(self, handle, pv_name, status)
-
- def configure_widget(self):
- self.setFocusPolicy(Qt.NoFocus)
-
- fm = QFontMetricsF(QFont("Sans Serif", 10))
- qrect = fm.boundingRect(self.suggested_text)
- _width_scaling_factor = 1.15
-
- self.setFixedHeight((fm.lineSpacing()*1.8))
- self.setFixedWidth((qrect.width() * _width_scaling_factor))
-
- if self.pv_within_daq_group:
- self.qt_property_initial_values(qt_object_name=self.PV_DAQ_CA)
- else:
- self.qt_property_initial_values(qt_object_name=self.PV_READBACK)
-
-#For use with CAQMenu
-class QLineEditExtended(QLineEdit):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.parent = parent
-
- def mousePressEvent(self, event):
- button = event.button()
- if button == Qt.RightButton:
- self.parent.showContextMenu()
- elif button == Qt.LeftButton:
- self.parent.mousePressEvent(event)
-
-class CAQMenu(QComboBox, PVGateway):
- '''Channel access enabled QMenu widget'''
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int)
- trigger_connect = Signal(int, str, int)
-
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units=False, prefix: str = "", suffix: str = ""):
-
- super().__init__(parent, pv_name, monitor_callback,
- pv_within_daq_group, color_mode, show_units, prefix,
- suffix, connect_callback=self.py_connect_callback)
-
- self.is_initialize_complete()
-
- self.configure_widget()
-
- #After configure:widget
- self.currentIndexChanged.connect(self.value_change)
-
- if self.pv_within_daq_group is False:
- self.monitor_start()
-
- def py_connect_callback(self, handle, pvname, status):
- '''Callback function to be invoked on change of
- pv connection status.
- '''
- self.trigger_connect.emit(int(handle), str(pvname), int(status))
-
- def configure_widget(self):
-
- self.previousIndex = None
-
- self.setFocusPolicy(Qt.NoFocus)
- self.setEditable(True)
- self.setLineEdit(QLineEditExtended(self))
- self.lineEdit().setReadOnly(True)
- self.lineEdit().setAlignment(Qt.AlignCenter)
-
- enumStringList = self.cafe.getEnumStrings(self.handle)
-
- self.addItems(enumStringList)
- for i in range(0, self.count()):
- self.setItemData(i, Qt.AlignCenter, Qt.TextAlignmentRole)
-
- fm = QFontMetricsF(QFont("Sans Serif", 10))
- qrect = fm.boundingRect(self.suggested_text)
-
- _width_scaling_factor = 1.1
-
- self.setFixedHeight(fm.lineSpacing()*1.8)
- self.setFixedWidth((qrect.width()+40) * _width_scaling_factor)
-
- self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
-
- def post_display_value(self, value):
- '''Convert value to index'''
- if "setCurrentIndex" in dir(self):
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
-
- if isinstance(value, str):
- self.setCurrentIndex(self.cafe.getEnumFromString(self.handle,
- value))
-
- elif isinstance(value, int):
- self.setCurrentIndex(value)
- #Should not happen
- elif isinstance(value, float):
- self.setCurrentIndex(int(value))
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(False)
-
-
- #self.previousIndex = self.currentIndex()
- return
- else:
- print(("ERROR: overloaded post_display_value: 'setCurrentIndex' "
- "method does not exist!"))
-
-
- def value_change(self, indx):
-
- status = self.cafe.set(self.handle, indx)
-
- if status != self.cyca.ICAFE_NORMAL:
- #self.showSetErrorMsg(status)
-
- value = self.cafe.getCache(self.handle, 'int')
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
-
- if value is not None:
- self.setCurrentIndex(value)
- else:
- if self.previousIndex is not None:
- self.setCurrentIndex(self.previousIndex)
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(False)
-
- self.pv_message_in_a_box.setText(
- "CAQMenu set operation reports error:\n{0}".format(
- self.cafe.getStatusCodeAsString(status)))
- self.pv_message_in_a_box.exec()
-
- def mousePressEvent(self, event):
-
- button = event.button()
- if button == Qt.RightButton:
- PVGateway.mousePressEvent(self, event)
-
- elif self.pv_info is not None:
- if self.pv_info.accessWrite == 0:
- event.ignore()
- return
- else:
- QComboBox.mousePressEvent(self, event)
-
- self.previousIndex = self.currentIndex()
-
- def enterEvent(self, event):
- if self.pv_info is not None:
- if self.pv_info.accessWrite == 0:
- for i in range(0, self.count()):
- self.setItemIcon(i, QIcon(":/forbidden.png"))
- self.setStyleSheet(
- ("QComboBox {background: transparent}" +
- "QComboBox::drop-down {image: url(:/forbidden.png)}"))
-
- def leaveEvent(self, event):
- if self.pv_info is not None:
- if self.pv_info.accessWrite == 0:
- for i in range(0, self.count()):
- self.setItemIcon(i, QIcon())
- self.setStyleSheet(
- "QComboBox::drop-down {background: transparent}")
-
-
- #The widget should not gain focus by using the mouse wheel.
- #This is accomplished by setting the focus policy to Qt.StrongFocus.
- #The widget should only accept wheel events if it already has the
- #focus. This is accomplished by reimplementing QWidget.wheelEvent
- #within a QSpinBox subclass:
- def wheelEvent(self, event):
- if self.hasFocus() is False:
- event.ignore()
- else:
- QComboBox.wheelEvent(self, event)
-
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
- '''Triggered by monitor signal'''
-
- PVGateway.receive_monitor_update(self, value, status, alarm_severity)
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- PVGateway.receive_connect_update(self, handle, pv_name, status)
-
-
-class CAQMessageButton(QPushButton, PVGateway):
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int)
- trigger_connect = Signal(int, str, int)
-
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- notify_freq_hz: int = 0,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units=False, msg_label: str = "",
- msg_press_value=None, msg_release_value=None,
- start_monitor=False):
- super().__init__(parent=parent, pv_name=pv_name,
- monitor_callback=monitor_callback,
- notify_freq_hz=notify_freq_hz,
- pv_within_daq_group=pv_within_daq_group,
- color_mode=color_mode, show_units=show_units,
- msg_label=msg_label,
- connect_callback=self.py_connect_callback)
-
- self.msg_press_value = msg_press_value
- self.msg_release_value = msg_release_value
-
- if self.msg_press_value is not None:
- self.pressed.connect(self.act_on_pressed)
- if self.msg_release_value is not None:
- self.released.connect(self.act_on_released)
-
- self.msg_label = msg_label
- self.suggested_text = self.msg_label
- _suggested_text_length = len(self.suggested_text)+3
- self.suggested_text = self.suggested_text.rjust(_suggested_text_length,
- "^")
-
- self.configure_widget()
-
- self.msg_press_status = self.cyca.ICAFE_NORMAL
- self.msg_release_status = self.cyca.ICAFE_NORMAL
- self.msg_report_status = "PV={0}\n".format(self.pv_name)
- self.msg_has_error = False
-
- if not self.pv_within_daq_group and start_monitor:
- self.monitor_start()
-
- def py_connect_callback(self, handle, pvname, status):
- '''Callback function to be invoked on change of
- pv connection status.
- '''
- self.trigger_connect.emit(int(handle), str(pvname), int(status))
-
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
- PVGateway.receive_monitor_update(self, value, status, alarm_severity)
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- PVGateway.receive_connect_update(self, handle, pv_name, status)
-
- def configure_widget(self):
- self.setFocusPolicy(Qt.StrongFocus)
- self.setCheckable(True) #Recognizes press and release states
-
- fm = QFontMetricsF(QFont("Sans Serif", 12))
- qrect = fm.boundingRect(self.suggested_text)
-
- _width_scaling_factor = 1.0
-
- self.setText(self.msg_label)
- self.setFixedHeight((fm.lineSpacing()*2.0))
- self.setFixedWidth((qrect.width() * _width_scaling_factor))
-
- self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
-
-
- def enterEvent(self, event):
- if self.pv_info is not None:
- if self.pv_info.accessWrite == 0:
- self.setProperty("readOnly", True)
- self.qt_style_polish()
-
- def leaveEvent(self, event):
- if self.property("readOnly"):
- self.setProperty(self.qt_dynamic_property_get(), True)
- self.qt_style_polish()
-
- def mouseReleaseEvent(self, event):
- if self.msg_release_value is not None:
- time.sleep(0.1)
- QPushButton.mouseReleaseEvent(self, event)
-
- def mousePressEvent(self, event):
- if self.pv_info is not None:
- if self.pv_info.accessWrite == 1:
- QPushButton.mousePressEvent(self, event)
- if event.button() == Qt.RightButton:
- PVGateway.mousePressEvent(self, event)
-
- def act_on_pressed(self):
- if self.msg_press_value is not None:
- self.msg_press_status = self.cafe.set(self.handle,
- self.msg_press_value)
- if self.msg_press_status != self.cyca.ICAFE_NORMAL:
- self.msg_report_status += (
- "Error in set operation (at press button):\n{0}\n".format(
- self.cafe.getStatusCodeAsString(self.msg_press_status)))
- self.msg_has_error = True
- qm = QMessageBox()
- qm.setText(self.msg_report_status)
- qm.exec()
- QApplication.processEvents()
-
- def act_on_released(self):
- if self.msg_release_value is not None:
- self.msg_release_status = self.cafe.set(self.handle,
- self.msg_release_value)
- if self.msg_release_status != self.cyca.ICAFE_NORMAL:
- self.msg_report_status += (
- "Error in set operation (at release button):\n{0}\n".format(
- self.cafe.getStatusCodeAsString(self.msg_release_status)))
- self.msg_has_error = True
-
- if self.msg_has_error:
- self.msg_has_error = False
- self.pv_message_in_a_box.setText(self.msg_report_status)
- self.pv_message_in_a_box.exec()
- self.msg_report_status = "PV={0}\n".format(self.pv_name)
- qm = QMessageBox()
- qm.setText(self.msg_report_status)
- qm.exec()
- QApplication.processEvents()
-
-class CAQTextEntry(QLineEdit, PVGateway):
- '''Channel access enabled QTextEntry widget'''
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int)
- trigger_connect = Signal(int, str, int)
-
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units=False, prefix: str = "", suffix: str = ""):
- super().__init__(parent, pv_name, monitor_callback, pv_within_daq_group,
- color_mode, show_units, prefix, suffix,
- connect_callback=self.py_connect_callback)
-
- self.is_initialize_complete() #waits a fraction of a second
-
- self.currentText = ""
- self.returnPressed.connect(self.valuechange)
- self.configure_widget()
- if self.pv_within_daq_group is False:
- self.monitor_start()
-
- def py_connect_callback(self, handle, pvname, status):
- '''Callback function to be invoked on change of
- pv connection status.
- '''
- self.trigger_connect.emit(int(handle), str(pvname), int(status))
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
- PVGateway.receive_monitor_update(self, value, status, alarm_severity)
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- PVGateway.receive_connect_update(self, handle, pv_name, status)
-
- def configure_widget(self):
- self.setFocusPolicy(Qt.StrongFocus)
-
- fm = QFontMetricsF(QFont("Sans Serif", 12))
- qrect = fm.boundingRect(self.suggested_text)
-
- _width_scaling_factor = 1.15
-
- self.setFixedHeight((fm.lineSpacing()*1.8))
- self.setFixedWidth(((qrect.width()+10) * _width_scaling_factor))
-
- self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
-
- def valuechange(self):
- status = self.cafe.set(self.handle, self.text())
- if status != self.cyca.ICAFE_NORMAL:
- if self.cafe.getNoMonitors(self.handle) == 0:
- val = self.cafe.get(self.handle, 'native')
- else:
- val = self.cafe.getCache(self.handle, 'native')
-
- if val is not None:
- if isinstance(val, str):
- strText = val
- else:
- valStr = ("{: .%sf}" % self.precision)
- strText = valStr.format(round(val, self.precision))
- print(strText, " precision ", self.precision)
- self.setText(strText)
- else:
- #Do this for TextInfo cache
- if self.cafe.getNoMonitors(self.handle) == 0:
- val = self.cafe.get(self.handle, 'native')
-
- def setText(self, value):
- QLineEdit.setText(self, value)
- self.currentText = self.text()
-
- def enterEvent(self, event):
- if self.pv_info is not None:
- if self.pv_info.accessWrite == 0:
- self.setProperty("readOnly", True)
- self.qt_style_polish()
- self.setReadOnly(True)
- self.setFocusPolicy(Qt.StrongFocus)
-
- def leaveEvent(self, event):
-
- if self.isReadOnly():
- self.setReadOnly(False)
- self.setProperty(self.qt_dynamic_property_get(), True)
- self.qt_style_polish()
-
- if self.text() != self.currentText:
- QLineEdit.setText(self, self.currentText)
-
- self.setCursorPosition(100)
- self.clearFocus()
- self.setFocusPolicy(Qt.NoFocus)
- del event
-
- def mousePressEvent(self, event):
- if event.button() == Qt.RightButton:
- PVGateway.mousePressEvent(self, event)
- self.clearFocus()
- return
- local_event_position = QPoint(event.x(), event.y())
- local_cursor_position = self.cursorPositionAt(local_event_position)
- self.setCursorPosition(local_cursor_position)
-
-
-class CAQSpinBox(QSpinBox, PVGateway):
- '''Channel access enabled QTextEntry widget'''
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int)
- trigger_connect = Signal(int, str, int)
-
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units=False, prefix: str = "", suffix: str = ""):
- super().__init__(parent, pv_name, monitor_callback, pv_within_daq_group,
- color_mode, show_units, prefix, suffix,
- connect_callback=self.py_connect_callback)
-
- self.is_initialize_complete()
-
- self.valueChanged.connect(self.value_change)
- self.configure_widget()
- if not self.pv_within_daq_group:
- self.monitor_start()
-
-
- def py_connect_callback(self, handle, pvname, status):
- '''
- Callback function to be invoked on change of pv connection
- status.
- '''
- self.trigger_connect.emit(int(handle), str(pvname), int(status))
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
- PVGateway.receive_monitor_update(self, value, status, alarm_severity)
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- PVGateway.receive_connect_update(self, handle, pv_name, status)
-
- def configure_widget(self):
- self.previousValue = None
- self.currentValue = None
- self.setFocusPolicy(Qt.StrongFocus)
- self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
- self.setAccelerated(False)
- self.setLineEdit(QLineEditExtended(self))
- self.lineEdit().setEnabled(True)
- self.lineEdit().setReadOnly(False)
- self.lineEdit().setAlignment(Qt.AlignLeft)
- self.lineEdit().setFont(QFont("Sans Serif", 16))
-
- fm = QFontMetricsF(QFont("Sans Serif", 12))
-
- _suggested_text = self.max_control_abs_str
- _added_text = ""
-
- if self.show_units:
- _added_text += " " + self.units
- _suggested_text += self.units
- if self.suffix:
- _added_text += " " + self.suffix
- _suggested_text += self.suffix
-
- self.setSuffix(_added_text)
-
- qrect = fm.boundingRect(_suggested_text)
- _width_scaling_factor = 1.0
-
- self.setFixedHeight((fm.lineSpacing()*1.8))
- self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
-
- self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
-
- if self.pv_ctrl is not None:
- self.setRange(int(self.pv_ctrl.lowerControlLimit),
- int(self.pv_ctrl.upperControlLimit))
-
-
- def post_display_value(self, value):
- '''Convert value to index'''
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
- self.setValue(int(round(value)))
- self.blockSignals(False)
- else:
- self.setValue(int(round(value)))
-
-
- def mousePressEvent(self, event):
- _opt = QStyleOptionSpinBox()
- self.initStyleOption(_opt)
- _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
- QStyle.SC_SpinBoxUp, self)
- _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
- QStyle.SC_SpinBoxDown, self)
-
- self.previousValue = self.value()
-
- if event.button() == Qt.LeftButton:
- if _rect_up.contains(event.pos(), proper=True) or \
- _rect_down.contains(event.pos(), proper=True):
-
- if not self.cafe.isConnected(self.handle):
- self.pv_message_in_a_box.setText(
- ("Spinbox change value events currently suspended\n" +
- "as channel {0} is disconnected.").format(
- self.pv_name))
- self.pv_message_in_a_box.exec()
- return
-
- QSpinBox.mousePressEvent(self, event)
- #Clear Focus: only one step per mouse click.
- self.clearFocus()
-
- local_event_position = QPoint(event.x(), event.y())
- local_cursor_position = self.lineEdit().cursorPositionAt(
- local_event_position)
-
- self.lineEdit().setCursorPosition(local_cursor_position)
-
- PVGateway.mousePressEvent(self, event)
-
- def setValue(self, intVal):
- QSpinBox.setValue(self, intVal)
- self.currentValue = self.value()
-
- def value_change(self, intVal):
-
- status = self.cafe.set(self.handle, intVal)
- if status != self.cyca.ICAFE_NORMAL:
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
-
- if self.previousValue is not None:
- self.setValue(self.previousValue)
- else:
- _value = self.cafe.getCache(self.handle, 'int')
-
- if _value is not None:
- self.setValue(_value)
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(False)
-
- self.pv_message_in_a_box.setText(
- ("Spinbox set operation reports error:\n{0}"
- .format(self.cafe.getStatusCodeAsString(status))))
- self.pv_message_in_a_box.exec()
-
- else:
- if self.previousValue is not None:
- self.setValue(self.previousValue)
- else:
- _value = self.cafe.getCache(self.handle, 'int')
-
- if _value is not None:
- self.setValue(_value)
-
- self.parent.statusbar.showMessage(
- (self.widget_class + " " +
- self.cafe.getStatusCodeAsString(status)))
-
-
- def enterEvent(self, event):
- if self.pv_info is not None:
- if self.pv_info.accessWrite == 0:
- self.setProperty("readOnly", True)
- self.qt_style_polish()
- self.setReadOnly(True)
- self.setFocusPolicy(Qt.StrongFocus)
-
- def leaveEvent(self, event):
- if self.isReadOnly():
- self.setReadOnly(False)
- self.setProperty(self.qt_dynamic_property_get(), True)
- self.qt_style_polish()
-
- self.clearFocus()
- self.setFocusPolicy(Qt.NoFocus)
- del event
-
-
- def keyPressEvent(self, event):
- if event.key() in (Qt.Key_Return, Qt.Key_Enter):
- QSpinBox.keyPressEvent(self, event)
- self.clearFocus()
- elif event.key() in (Qt.Key_Up, Qt.Key_Down):
- QSpinBox.keyPressEvent(self, event)
- else:
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
- QSpinBox.keyPressEvent(self, event)
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(False)
-
-
- # The spin box should not gain focus by using the mouse wheel.
- # This is accomplished by setting the focus policy to Qt.StrongFocus.
- # The spin box should only accept wheel events if it already has the focus.
- # This is accomplished by reimplementing QWidget.wheelEvent within a
- # QSpinBox subclass:
- def wheelEvent(self, event):
- #print("wheelEvent", self.hasFocus())
- if self.hasFocus() is False:
- event.ignore()
- else:
- QSpinBox.wheelEvent(self, event)
-
-
-class CAQDoubleSpinBox(QDoubleSpinBox, PVGateway):
- '''Channel access enabled QDoubleSpinBox widget'''
- trigger_monitor_float = Signal(float, int, int)
- trigger_monitor_int = Signal(int, int, int)
- trigger_monitor_str = Signal(str, int, int)
- trigger_monitor = Signal(object, int)
- trigger_connect = Signal(int, str, int)
-
- def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
- pv_within_daq_group: bool = False, color_mode=None,
- show_units: bool = False, prefix: str = "", suffix: str = ""):
- super().__init__(parent=parent, pv_name=pv_name,
- monitor_callback=monitor_callback,
- pv_within_daq_group=pv_within_daq_group,
- color_mode=color_mode, show_units=show_units,
- prefix=prefix, suffix=suffix,
- connect_callback=self.py_connect_callback)
-
- self.is_initialize_complete()
- self.valueChanged.connect(self.valuechange)
- self.configure_widget()
-
- if self.pv_within_daq_group is False:
- self.monitor_start()
-
-
- def py_connect_callback(self, handle, pvname, status):
- '''
- Callback function to be invoked on change of pv connection
- status.
- '''
- self.trigger_connect.emit(int(handle), str(pvname), int(status))
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
- PVGateway.receive_monitor_update(self, value, status, alarm_severity)
-
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- PVGateway.receive_connect_update(self, handle, pv_name, status)
-
- def configure_widget(self):
- self.previousValue = None
- self.currentValue = None
- self.setFocusPolicy(Qt.StrongFocus)
- self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
- self.setAccelerated(False)
- self.setLineEdit(QLineEditExtended(self))
- self.lineEdit().setReadOnly(False)
- self.lineEdit().setAlignment(Qt.AlignRight)
- self.lineEdit().setFont(QFont("Sans Serif", 12))
-
- _stepsize = 10**(self.precision * -1)
- self.setSingleStep(_stepsize)
- self.setDecimals(self.precision)
-
- fm = QFontMetricsF(QFont("Sans Serif", 12))
-
- _suggested_text = self.suggested_text
- _added_text = ""
-
- if self.show_units:
- _added_text += " " + self.units
- _suggested_text += self.units
- if self.suffix:
- _added_text += " " + self.suffix
- _suggested_text += self.suffix
-
- self.setSuffix(_added_text)
-
- qrect = fm.boundingRect(_suggested_text)
-
- _width_scaling_factor = 1.15
-
- self.setFixedHeight((fm.lineSpacing()*1.8))
- self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
-
- self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
-
- if self.pv_ctrl is not None:
- self.setRange(int(self.pv_ctrl.lowerControlLimit),
- int(self.pv_ctrl.upperControlLimit))
-
-
- def post_display_value(self, value):
- '''set value from monitor'''
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
- self.setValue(value)
- self.blockSignals(False)
- else:
- self.setValue(value)
-
- def mousePressEvent(self, event):
-
- _opt = QStyleOptionSpinBox()
- self.initStyleOption(_opt)
- _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
- QStyle.SC_SpinBoxUp, self)
- _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
- QStyle.SC_SpinBoxDown, self)
- self.previousValue = self.value()
-
- if event.button() == Qt.LeftButton:
- if _rect_up.contains(event.pos(), proper=False) or \
- _rect_down.contains(event.pos(), proper=False):
-
- if not self.cafe.isConnected(self.handle):
- self.pv_message_in_a_box.setText(
- ("Spinbox change value events currently suspended\n" +
- "as channel {0} is disconnected.").format(
- self.pv_name))
- self.pv_message_in_a_box.exec()
- return
-
- QDoubleSpinBox.mousePressEvent(self, event)
-
- local_event_position = QPoint(event.x(), event.y())
- local_cursor_position = self.lineEdit().cursorPositionAt(
- local_event_position)
-
- self.lineEdit().setCursorPosition(local_cursor_position)
-
- PVGateway.mousePressEvent(self, event)
-
- def mouseReleaseEvent(self, event):
- self.clearFocus()
-
- def setValue(self, value):
- self.currentValue = self.value()
- QDoubleSpinBox.setValue(self, value)
-
- def valuechange(self, fval):
- status = self.cafe.set(self.handle, fval)
-
- if status != self.cyca.ICAFE_NORMAL:
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
- if self.previousValue is not None:
- self.setValue(self.previousValue)
- else:
- _value = self.cafe.getCache(self.handle, 'float')
-
- if _value is not None:
- self.setValue(_value)
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(False)
-
- self.pv_message_in_a_box.setText(
- ("Spinbox set operation reports error:\n{0}"
- .format(self.cafe.getStatusCodeAsString(status))))
- self.pv_message_in_a_box.exec()
-
- else:
- if self.previousValue is not None:
- self.setValue(self.previousValue)
- else:
- _value = self.cafe.getCache(self.handle, 'float')
-
- if _value is not None:
- self.setValue(_value)
-
- self.parent.statusbar.showMessage(
- (self.widget_class + " " +
- self.cafe.getStatusCodeAsString(status)))
-
-
- def enterEvent(self, event):
- self.setFocusPolicy(Qt.StrongFocus)
- if self.pv_info is not None:
- if self.pv_info.accessWrite == 0:
- self.setProperty("readOnly", True)
- self.qt_style_polish()
- self.setReadOnly(True)
-
- def leaveEvent(self, event):
- if self.isReadOnly():
- self.setReadOnly(False)
- self.setProperty(self.qt_dynamic_property_get(), True)
- self.qt_style_polish()
-
- self.clearFocus()
- self.setFocusPolicy(Qt.NoFocus)
- del event
-
- def keyPressEvent(self, event):
-
- if event.key() in (Qt.Key_Return, Qt.Key_Enter):
- QDoubleSpinBox.keyPressEvent(self, event)
- self.clearFocus()
- elif event.key() in (Qt.Key_Up, Qt.Key_Down):
- QDoubleSpinBox.keyPressEvent(self, event)
- else:
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(True)
- QDoubleSpinBox.keyPressEvent(self, event)
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.blockSignals(False)
-
- # The spin box should not gain focus by using the mouse wheel.
- # This is accomplished by setting the focus policy to Qt.StrongFocus.
- # The spin box should only accept wheel events if it already has the focus.
- # This is accomplished by reimplementing QWidget.wheelEvent within a
- # QSpinBox subclass:
- def wheelEvent(self, event):
- if self.hasFocus() is False:
- event.ignore()
- else:
- QDoubleSpinBox.wheelEvent(self, event)
-
-
-class reconnectQPushButton(QPushButton, QThread):
- def __init__(self, parent=None):
- super().__init__()
- self.parent = parent
- self.clicked.connect(self.onClicked)
- self.isdirty = False
- self._handles_to_reconnect = []
- self.reconnectThread = None
-
- def onClicked(self, event):
-
- self._handles_to_reconnect = []
-
- for i in range(0, len(self.parent.pv_gateway)):
- if self.parent.item(
- i, self.parent.no_columns-1).checkState() == Qt.Checked:
- self._handles_to_reconnect.append(
- self.parent.pv_gateway[i].handle)
-
- self.reconnect()
- QApplication.processEvents()
-
- def reconnect(self):
- QApplication.processEvents()
-
- self.isdirty = True
- if self._handles_to_reconnect:
- self.parent.cafe.reconnect(self._handles_to_reconnect)
- self.isdirty = False
- #Uncheck reconnected channels
- for i in range(0, len(self.parent.pv_gateway)):
- if self.parent.item(
- i, self.parent.no_columns-1).checkState() == Qt.Checked:
- if self.parent.cafe.isConnected(
- self.parent.pv_gateway[i].handle):
- self.parent.item(
- i, self.parent.no_columns-1).setCheckState(False)
-
- #Uncheck global reconnect check box
- self.parent.cb_item_all.setCheckState(Qt.Unchecked)
-
-
-class CAQTableWidget(QTableWidget):
- '''Channel access enabled QTableWidget widget'''
- #trigger_monitor_float = Signal(float, int, int)
- #trigger_monitor_int = Signal(int, int, int)
- #trigger_monitor_str = Signal(str, int, int)
- #trigger_connect = Signal(int, str, int)
-
- def hasNewData(self, _row, pv_data):
-
- if self.pv_gateway[_row].pvd_previous is None:
- return True
-
- newDataFlag = False
-
- if self.pv_gateway[_row].pvd_previous.ts[1] != pv_data.ts[1]:
- newDataFlag = True
- elif self.pv_gateway[_row].pvd_previous.ts[0] != pv_data.ts[0]:
- newDataFlag = True
- # Catch disconnect events(!!) and set newDataFlag only
- elif self.pv_gateway[_row].pvd_previous.status != pv_data.status:
- newDataFlag = True
- return newDataFlag
-
-
- def paint_rows(self, row_range: list = [], reset=False, last_row=[" ", " "],
- columns=[0]):
-
- _qcolor_last_line = QColor("#d1e8e9")
- self.font_pts11 = QTableWidgetItem().font()
- self.font_pts11.setPixelSize(11)
- if reset:
- _qcolor = self.item(0, self.columnCount()-1).background()
- _start = 0
- _end = self.rowCount()-1
- else:
- _qcolor = _qcolor_last_line
- _start = row_range[0]
- _end = row_range[1]
-
- for _row in range(_start, _end):
- _cell = QTableWidgetItem("{0}".format(_row+1))
- if not reset:
- _cell.setFont(self.font_pts11)
- _cell.setBackground(_qcolor)
-
- if 1 in columns:
- self.item(_row, 0).setBackground(_qcolor)
- self.item(_row, 0).setFont(self.font_pts11)
- if 0 in columns:
- self.setVerticalHeaderItem(_row, _cell)
-
-
- #last row
-
- if reset and 0 in columns:
- _cell = QTableWidgetItem("{0}".format(last_row[0]))
- _cell.setFont(self.font_pts11)
- self.setVerticalHeaderItem(self.rowCount()-1, _cell)
-
- self.item(self.rowCount()-1, 0).setTextAlignment(Qt.AlignCenter)
- self.item(self.rowCount()-1, 0).setText(str(last_row[1]))
- self.item(self.rowCount()-1, 0).setBackground(_qcolor)
- self.item(self.rowCount()-1, 0).setFont(self.font_pts11)
- elif last_row[0] != " ":
- _cell = QTableWidgetItem("{0}".format(last_row[0]))
- _cell.setBackground(_qcolor_last_line)
- _cell.setFont(self.font_pts11)
- self.setVerticalHeaderItem(self.rowCount()-1, _cell)
-
- if columns:
- self.item(self.rowCount()-1, 0).setTextAlignment(Qt.AlignCenter)
- self.item(self.rowCount()-1, 0).setText(str(last_row[1]))
- self.item(self.rowCount()-1, 0).setBackground(_qcolor_last_line)
- self.item(self.rowCount()-1, 0).setFont(self.font_pts11)
-
-
- def widget_update(self):
-
- for _row, pvgate in enumerate(self.pv_gateway):
- #for _row in range(0, len(self.pv_gateway)):
- if not pvgate.notify_unison:
- continue
- _handle = pvgate.handle
- _pvd = pvgate.cafe.getPVCache(_handle)
-
- if _pvd.status in (self.cyca.ICAFE_CS_NEVER_CONN,
- self.cyca.ICAFE_CA_OP_CONN_DOWN):
- pvgate.pvd_previous = _pvd
- continue
-
- pvgate.pvd_previous = _pvd
-
- #if timestamps the same - then skip
- _value = _pvd.value[0]
- _value = pvgate.format_display_value(_value)
-
- qtwi = QTableWidgetItem(str(_value)+ " ")
- f = qtwi.font()
- f.setPointSize(8)
- qtwi.setFont(f)
-
- self.setItem(_row, self.no_columns-3,
- QTableWidgetItem(qtwi))
- self.item(_row, self.no_columns-3).setTextAlignment(Qt.AlignRight |
- Qt.AlignVCenter)
-
- _ts_date = _pvd.tsDateAsString
- _ts_str_len = len(_ts_date)
- _ilength_target = self.format_ts_nano
-
- while _ts_str_len < _ilength_target:
- _ts_date += "0"
- _ilength_target = _ilength_target - 1
- _ts_str_len = len(_ts_date)
- _ts_str = _ts_date[0: _ts_str_len - (self.format_ts_nano -
- self.format_ts_decimal_part)]
- _ts_str_len = len(_ts_str)
- _ilength_target = self.format_ts_decimal_part
- if self.format_ts_decimal_part == self.format_ts_deci:
- if _ts_str_len == self.format_ts_sec:
- _ts_str += "."
- while _ts_str_len < _ilength_target:
- _ts_str += "0"
- _ilength_target = _ilength_target -1
-
- qtwi = QTableWidgetItem(_ts_str)
- f = qtwi.font()
- f.setPointSize(8)
- qtwi.setFont(f)
-
- self.setItem(_row, self.no_columns-2, QTableWidgetItem(qtwi))
- self.item(_row, self.no_columns-2).setTextAlignment(Qt.AlignCenter)
-
- _prop = pvgate.qt_dynamic_property_get()
-
- alarm_severity = _pvd.alarmSeverity
-
- if _prop == pvgate.READBACK_ALARM:
-
- if alarm_severity == pvgate.cyca.SEV_MAJOR:
- _bgcolor = pvgate.fg_alarm_major
- _fgcolor = "black"
- elif alarm_severity == pvgate.cyca.SEV_MINOR:
- _bgcolor = pvgate.fg_alarm_minor
- _fgcolor = "black"
- elif alarm_severity == pvgate.cyca.SEV_INVALID:
- _bgcolor = pvgate.fg_alarm_invalid
- _fgcolor = "#777777"
- else:
- _bgcolor = pvgate.fg_alarm_noalarm
- _fgcolor = "black"
-
- #Colors for bg/fg reversed as is the old norm
- self.item(_row, self.no_columns-3).setBackground(
- QColor(_bgcolor))
- self.item(_row, self.no_columns-2).setBackground(
- QColor(_bgcolor))
- self.item(_row, self.no_columns-3).setForeground(
- QColor(_fgcolor))
- self.item(_row, self.no_columns-2).setForeground(
- QColor(_fgcolor))
-
- elif _prop == pvgate.READBACK_STATIC:
-
- self.item(_row, self.no_columns-3).setBackground(
- QColor(pvgate.bg_readback))
- self.item(_row, self.no_columns-2).setBackground(
- QColor(pvgate.bg_readback))
-
- elif _prop == pvgate.DISCONNECTED:
- self.item(_row, self.no_columns-3).setBackground(
- QColor("#ffffff"))
- self.item(_row, self.no_columns-2).setBackground(
- QColor("#ffffff"))
- self.item(_row, self.no_columns-3).setForeground(
- QColor("#777777"))
- self.item(_row, self.no_columns-2).setForeground(
- QColor("#777777"))
-
- else:
- print(_prop, "widget_update unknown in element/row", _row,
- _row+1)
-
- QApplication.processEvents()
-
- def __init__(self, parent=None, pv_list: list = ["PV_NAME_NOT_GIVEN"],
- monitor_callback=None, pv_within_daq_group: bool = False,
- color_mode=None, show_units: bool = True, prefix: str = "",
- suffix: str = "", ts_res: str = "milli",
- init_column: bool = False, init_list: list = [],
- notify_freq_hz: int = 0, notify_unison: bool = True,
- precision: int = 0, scale_factor: float = 1,
- show_timestamp: bool = True, pv_list_show: list = None):
-
- super().__init__()
- self.columns_dict = {}
- _column_dict_value = 0
- self.columns_dict['PV'] = _column_dict_value
- if init_column:
- _column_dict_value += 1
- self.columns_dict['Init'] = _column_dict_value
- _column_dict_value += 1
- self.columns_dict['Value'] = _column_dict_value
- if show_timestamp:
- _column_dict_value += 1
- self.columns_dict['Timestamp'] = _column_dict_value
- _column_dict_value += 1
- self.columns_dict['Reconnect'] = _column_dict_value
-
- self.setWindowModality(Qt.ApplicationModal)
- self.no_columns = _column_dict_value + 1
-
- self.init_column = init_column
-
- self.init_list = init_list
- if self.init_column and not self.init_list:
- self.init_list = pv_list
-
- self.icount = 0
- self.notify_freq_hz = abs(notify_freq_hz)
- self.notify_freq_hz_default = self.notify_freq_hz
- self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
- 1000 / self.notify_freq_hz
-
- self.notify_unison = bool(notify_unison) and bool(self.notify_freq_hz)
-
- self.precision = precision
- self.scale_factor = scale_factor
- self.show_timestamp = show_timestamp
-
- self.format_ts_nano = 31 #max length of date
- self.format_ts_micro = 28
- self.format_ts_milli = 25
- self.format_ts_deci = 23 #-8
- self.format_ts_sec = 21
- if "nano" in ts_res.lower():
- self.format_ts_decimal_part = self.format_ts_nano
- elif "micro" in ts_res.lower():
- self.format_ts_decimal_part = self.format_ts_micro
- elif "milli" in ts_res.lower():
- self.format_ts_decimal_part = self.format_ts_milli
- elif "deci" in ts_res.lower():
- self.format_ts_decimal_part = self.format_ts_deci
- elif "sec" in ts_res.lower():
- self.format_ts_decimal_part = self.format_ts_sec
- else:
- self.format_ts_decimal_part = self.format_ts_milli
-
- self.pv2item_dict = {}
-
- self.pv_list = pv_list
- self.pv_gateway = [None] * len(self.pv_list)
-
- self.pv_list_show = pv_list_show
- if self.pv_list_show is None:
- self.pv_list_show = self.pv_list
-
- _color_mode = [None] * len(self.pv_list)
-
- if isinstance(color_mode, list):
- for i in range(0, len(color_mode)):
- _color_mode[i] = color_mode[i]
-
- for i in range(0, len(self.pv_list)):
-
- self.pv_gateway[i] = PVGateway(
- parent, self.pv_list[i], monitor_callback,
- pv_within_daq_group, _color_mode[i], show_units, prefix, suffix,
- connect_triggers=False, notify_freq_hz=self.notify_freq_hz,
- notify_unison=self.notify_unison, precision=self.precision)
-
- self.pv_gateway[i].is_initialize_complete()
- self.pv_gateway[i].trigger_connect.connect(
- self.receive_connect_update)
- self.pv_gateway[i].trigger_monitor_str.connect(
- self.receive_monitor_update)
- self.pv_gateway[i].trigger_monitor_int.connect(
- self.receive_monitor_update)
- self.pv_gateway[i].trigger_monitor_float.connect(
- self.receive_monitor_update)
-
- self.pv_gateway[i].widget_class = "QTableWidgetItem"
-
- self.pv_gateway[i].qt_property_initial_values(
- qt_object_name=self.pv_gateway[i].PV_READBACK, tool_tip=False)
-
- #required for reconnect
- self.cafe = self.pv_gateway[0].cafe
- self.cyca = self.pv_gateway[0].cyca
-
- self.timer = None
- if self.notify_unison:
- self.timer = QTimer()
- self.timer.timeout.connect(self.widget_update)
- self.timer.singleShot(0, self.widget_update)
- self.timer.start(self.notify_milliseconds)
-
- self.configure_widget()
-
- #Connect only deals with colours - only helps on reconnect
- # In any case monitors take over
- #Got to do this earlier or emit immediately after!!
- for i in range(0, len(self.pv_gateway)):
- if self.cafe.isConnected(self.pv_gateway[i].pv_name):
- self.pv_gateway[i].trigger_connect.emit(
- self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
- self.pv_gateway[i].cyca.ICAFE_CS_CONN)
-
- for i in range(0, len(self.pv_gateway)):
- if not self.pv_gateway[i].pv_within_daq_group:
- self.pv_gateway[i].monitor_start()
-
- self.update_init_values()
-
- self.configure_context_menu()
-
-
- def configure_context_menu(self):
- self.table_context_menu = QMenu()
- self.table_context_menu.setObjectName("contextMenu")
- self.table_context_menu.setWindowModality(Qt.NonModal)
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.table_context_menu.addSection("---")
-
- action1 = QAction("Configure Table PVs", self)
- action1.triggered.connect(self.display_table_parameters)
- self.table_context_menu.addAction(action1)
-
- if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
- self.table_context_menu.addSection("---")
-
- QApplication.processEvents()
-
-
- def restore_init_values(self, pv_list: list = []):
- _set_values_dict = self.get_init_values()
-
- if not pv_list:
- _pvs_to_set, _values_to_set = zip(*_set_values_dict.items())
- #zip returns tuples
- _pvs_to_set = list(_pvs_to_set)
- _values_to_set = list(_values_to_set)
- else:
- _pvs_to_set = []
- _values_to_set = []
- for pv in pv_list:
- if pv in _set_values_dict.keys():
- _pvs_to_set.append(pv)
- _values_to_set.append(_set_values_dict[pv])
-
- status, status_list = self.cafe.setScalarList(_pvs_to_set,
- _values_to_set)
-
- if status != self.cyca.ICAFE_NORMAL:
- _mess = ("The following device(s) reported an error " +
- "in 'set' operation:")
- for i, status_value in enumerate(status_list):
- if status_value != self.cyca.ICAFE_NORMAL:
- _mess += ("\n" + _pvs_to_set[i] + " has status = " +
- str(status_value) + " " +
- self.cafe.getStatusCodeAsString(status_value) +
- " " + self.cafe.getStatusInfo(status_value))
- qm = QMessageBox()
- qm.setText(_mess)
-
- qm.exec()
- QApplication.processEvents()
-
- self.init_value_button.setEnabled(True)
-
-
- def is_same_as_init_values(self):
- _init_values_dict = self.get_column_values(self.columns_dict['Init'])
- _pvs, _init_values = zip(*_init_values_dict.items())
- _current_values_dict = self.get_column_values(
- self.columns_dict['Value'])
- _pvs, _current_values = zip(*_current_values_dict.items())
- #zip returns tuples
-
- return bool(func_reduce(lambda i, j: i and j, map(
- lambda m, k: m == k, _init_values, _current_values), True))
-
- #if func_reduce(lambda i, j: i and j, map(
- # lambda m, k: m == k, _init_values, _current_values), True):
- # return True
- #else:
- # return False
-
-
- def get_column_values(self, column_no):
- _values_dict = {}
- _start = 0
- _end = len(self.pv_gateway)
- _pvs = [None] * _end
- _values_str = [None] * _end
- _values = [None] * _end
-
- for _row in range(_start, _end):
- _values_str[_row] = self.item(_row, column_no).text()
- _pvs[_row] = self.item(_row, 0).text()
-
- _value_list = [float(_value_list) for _value_list in re.findall(
- r'-?\d+\.?\d*', _values_str[_row])]
-
- if not _value_list:
- print("row", _row, "values", _values_str[_row], _pvs[_row])
- _values[_row] = _values_str[_row] #Can be enum string
- else:
- _values[_row] = _value_list[0]
-
- if _pvs[_row] in self.pv_list_show:
- _values_dict[self.pv_gateway[_row].pv_name] = _values[_row]
-
- return _values_dict #_pvs_to_set, _values_to_set
-
-
- def get_init_values(self):
- return self.get_column_values(self.columns_dict['Init'])
-
- def get_init_values_previous(self):
- _set_values_dict = {}
- _start = 0
- _end = len(self.pv_gateway)
- _pvs_to_set = [None] * _end
- _values_to_set_str = [None] * _end
- _values_to_set = [None] * _end
- for _row in range(_start, _end):
- _values_to_set_str[_row] = self.item(
- _row, self.columns_dict['Init']).text()
- _pvs_to_set[_row] = self.item(_row, self.columns_dict['PV']).text()
-
- _value_list = [float(_value_list) for _value_list in re.findall(
- r'-?\d+\.?\d*', _values_to_set_str[_row])]
-
- if not _value_list:
- print("//row", _row, "values", _values_to_set_str[_row],
- _pvs_to_set[_row])
- _values_to_set[_row] = _values_to_set_str[_row] #Can be enum str
- else:
- _values_to_set[_row] = _value_list[0]
-
-
- if _pvs_to_set[_row] in self.init_list:
- _set_values_dict[
- self.pv_gateway[_row].pv_name] = _values_to_set[_row]
-
- return _set_values_dict
-
-
- def update_init_values(self):
- _start = 0
- _end = len(self.pv_gateway)
-
- for _row in range(_start, _end):
- _handle = self.pv_gateway[_row].handle
- _value = self.pv_gateway[_row].cafe.getCache(_handle)
-
- if _value is not None:
- if self.scale_factor != 1:
- _value = _value * self.scale_factor
- _value = self.pv_gateway[_row].format_display_value(_value)
-
- qtwi = QTableWidgetItem(str(_value)+ " ")
- _f = qtwi.font()
- _f.setPointSize(8)
- qtwi.setFont(_f)
- self.setItem(_row, 1, qtwi)
- self.item(_row, 1).setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
-
-
- def configure_widget(self):
-
- _column_width_pvname = 180
- _column_width_value = 90
- _column_width_timestamp = 210
- _column_width_checkbox = 22
-
- self.setRowCount(len(self.pv_gateway)+1)
- self.setColumnCount(self.no_columns)
- self.setEditTriggers(QAbstractItemView.NoEditTriggers)
- self.resizeColumnsToContents()
- self.resizeRowsToContents()
- #self.horizontalHeader().setStretchLastSection(True);
- self.setColumnWidth(self.columns_dict['PV'], _column_width_pvname)
-
- self.setColumnWidth(self.columns_dict['Value'], _column_width_value)
- if 'Init' in self.columns_dict.keys():
- self.setColumnWidth(self.columns_dict['Init'], _column_width_value)
- if 'Timestamp' in self.columns_dict.keys():
- self.setColumnWidth(self.columns_dict['Timestamp'],
- _column_width_timestamp)
- self.setColumnWidth(self.columns_dict['Reconnect'],
- _column_width_checkbox)
-
- _pv_column = self.columns_dict['PV']
-
- for i in range(0, len(self.pv_gateway)):
- qtwt = QTableWidgetItem(self.pv_list_show[i])
- f = qtwt.font()
- f.setPointSize(8)
- qtwt.setFont(f)
-
- self.setItem(i, _pv_column, qtwt)
- self.item(i, _pv_column).setTextAlignment(Qt.AlignHCenter |
- Qt.AlignVCenter)
- for i_column in range(1, self.no_columns-1):
- self.setItem(i, i_column, QTableWidgetItem(str("")))
- self.item(i, i_column).setTextAlignment(Qt.AlignHCenter |
- Qt.AlignVCenter)
- self.pv2item_dict[self.pv_gateway[i]] = i
-
- cb_item = QTableWidgetItem()
- cb_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
- cb_item.setCheckState(Qt.Unchecked)
- cb_item.setTextAlignment(Qt.AlignCenter)
- cb_item.setToolTip(self.pv_gateway[i].pv_name)
-
- self.setItem(i, self.no_columns-1, cb_item)
- self.item(i, self.no_columns-1).setTextAlignment(Qt.AlignCenter)
-
- if self.init_column:
- self.init_widget = QWidget()
- _init_layout = QHBoxLayout(self.init_widget)
- self.init_value_button = QPushButton()
- self.init_value_button.setText("Update")
- _f = self.init_value_button.font()
- _f.setPointSize(8)
- self.init_value_button.setFont(_f)
- self.init_value_button.setFixedWidth(80)
- self.init_value_button.clicked.connect(self.update_init_values)
- self.init_value_button.setToolTip(
- ("Stores initial, pre-measurement value. Update is also " +
- "typically executed automatically before new optics are set."))
- _init_layout.addWidget(self.init_value_button)
- _init_layout.setAlignment(Qt.AlignRight)
- _init_layout.setContentsMargins(1, 1, 0, 0) #Required
- self.init_widget.setLayout(_init_layout)
- self.setCellWidget(len(self.pv_gateway), 1, self.init_widget)
-
- _restore_widget = QWidget()
- _restore_layout = QHBoxLayout(_restore_widget)
- self.restore_value_button = QPushButton()
- self.restore_value_button.setStyleSheet(
- "QPushButton{background-color: rgb(212, 219, 157);}")
- self.restore_value_button.setText("Restore")
- _f = self.restore_value_button.font()
- _f.setPointSize(8)
- self.restore_value_button.setFont(_f)
- self.restore_value_button.setFixedWidth(80)
- self.restore_value_button.clicked.connect(self.restore_init_values)
- self.restore_value_button.setToolTip(
- ("Restore devices to their pre-measurement values"))
- _restore_layout.addWidget(self.restore_value_button)
- _restore_layout.setAlignment(Qt.AlignRight)
- _restore_layout.setContentsMargins(1, 1, 0, 0)
- _restore_widget.setLayout(_restore_layout)
- self.setCellWidget(len(self.pv_gateway), 2, _restore_widget)
-
- #Do not display no for last row (Reconnect button)
- _row_digit_last_cell = QTableWidgetItem(str(""))
- self.setVerticalHeaderItem(len(self.pv_gateway), _row_digit_last_cell)
- self.setItem(len(self.pv_gateway), 0, QTableWidgetItem(str("")))
-
- _qwb = QWidget()
-
- self.reconnect_button = reconnectQPushButton(self) #self required
-
- f = self.reconnect_button.font()
-
- if 'Timestamp' in self.columns_dict.keys():
- f.setPointSize(8)
- self.reconnect_button.setFixedWidth(100)
- else:
- f.setPointSize(6)
- self.reconnect_button.setFixedWidth(58)
-
- self.reconnect_button.setFont(f)
-
- self.reconnect_button.setText("Reconnect")
-
- _layout = QHBoxLayout(_qwb)
- _layout.addWidget(self.reconnect_button)
- _layout.setAlignment(Qt.AlignCenter)
- _layout.setContentsMargins(0, 0, 0, 0) #Required
-
- #_reconnect_button
- self.setCellWidget(len(self.pv_gateway), self.no_columns-2, _qwb)
-
- self.cb_item_all = QCheckBox()
- self.cb_item_all.setCheckState(Qt.Unchecked)
- self.cb_item_all.stateChanged.connect(self.reconnectStateChanged)
- self.cb_item_all.setObjectName("Reconnect")
-
- self.setCellWidget(len(self.pv_gateway), self.no_columns-1,
- self.cb_item_all)
-
- header_item = QTableWidgetItem("Process Variable")
-
- self.setHorizontalHeaderItem(self.columns_dict['PV'], header_item)
-
- if 'Init' in self.columns_dict.keys():
- self.setHorizontalHeaderItem(self.columns_dict['Init'],
- QTableWidgetItem("Initial Value"))
-
- self.setHorizontalHeaderItem(self.columns_dict['Value'],
- QTableWidgetItem("Value"))
-
- if 'Timestamp' in self.columns_dict.keys():
- self.setHorizontalHeaderItem(self.columns_dict['Timestamp'],
- QTableWidgetItem("Timestamp"))
- self.setHorizontalHeaderItem(self.columns_dict['Reconnect'],
- QTableWidgetItem("R"))
- self.setFocusPolicy(Qt.NoFocus)
- self.setEditTriggers(QAbstractItemView.NoEditTriggers)
- self.setSelectionMode(QAbstractItemView.NoSelection)
-
- self.verticalHeader().setDefaultAlignment(Qt.AlignRight)
- self.verticalHeader().setFixedWidth(22)
-
- _fm_font = QFont("Sans Serif")
- _fm_font.setPointSize(12)
- fm = QFontMetricsF(_fm_font)
-
- _factor = 1
- if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
- _factor = 1.18
-
- self.setFixedHeight(
- int(fm.lineSpacing() * _factor * (len(self.pv_gateway)+3)))
- _min_table_width = 620 if not self.init_column else 650
- self.setMinimumWidth(_min_table_width)
-
- for _row in range(0, len(self.pv_gateway)):
- self.item(_row, _pv_column).setForeground(QColor("#000000"))
-
- for i_column in range(1, self.no_columns-2):
- self.item(_row, i_column).setForeground(QColor("#000000"))
- self.item(_row, i_column).setTextAlignment(Qt.AlignRight |
- Qt.AlignVCenter)
-
- self.item(_row, self.columns_dict['Value']).setBackground(
- QColor("#ffffff"))
- if 'Timestamp' in self.columns_dict.keys():
- self.item(_row,
- self.columns_dict['Timestamp']).setTextAlignment(
- Qt.AlignCenter)
- self.item(_row,
- self.columns_dict['Timestamp']).setBackground(
- QColor("#ffffff"))
-
- @Slot(int)
- def reconnectStateChanged(self, state):
- if state == Qt.Unchecked:
- for i in range(0, len(self.pv_gateway)):
- self.item(i, self.columns_dict['Reconnect']).setCheckState(
- Qt.Unchecked)
- else:
- for i in range(0, len(self.pv_gateway)):
- self.item(i, self.columns_dict['Reconnect']).setCheckState(
- Qt.Checked)
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
-
- _row = self.pv2item_dict[self.sender()]
- self.pv_gateway[_row].time_monotonic = time.monotonic()
- if self.scale_factor != 1:
- value = value * self.scale_factor
- _value = self.pv_gateway[_row].format_display_value(value)
-
- qtwi = QTableWidgetItem(str(_value) + " ")
- f = qtwi.font()
- f.setPointSize(8)
- qtwi.setFont(f)
- self.setItem(_row, self.columns_dict['Value'], qtwi)
- self.item(_row, self.columns_dict['Value']).setTextAlignment(
- Qt.AlignRight | Qt.AlignVCenter)
-
- if 'Timestamp' in self.columns_dict.keys():
- _handle = self.pv_gateway[_row].handle
- _pvd = self.pv_gateway[_row].cafe.getPVCache(_handle)
- _ts_date = _pvd.tsDateAsString
- _ts_str_len = len(_ts_date)
- _ilength_target = self.format_ts_nano
-
- while _ts_str_len < _ilength_target:
- _ts_date += "0"
- _ilength_target = _ilength_target -1
-
- ##ts_str_len = len(_ts_date)
- _ts_str = _ts_date[0: _ts_str_len-(
- self.format_ts_nano-self.format_ts_decimal_part)]
- _ts_str_len = len(_ts_str)
-
- _ilength_target = self.format_ts_decimal_part
- if self.format_ts_decimal_part == self.format_ts_deci:
- if _ts_str_len == self.format_ts_sec:
- _ts_str += "."
- while _ts_str_len < _ilength_target:
- _ts_str += "0"
- _ilength_target = _ilength_target -1
-
- qtwi = QTableWidgetItem(_ts_str)
- f = qtwi.font()
- f.setPointSize(8)
- qtwi.setFont(f)
-
- self.setItem(_row, self.columns_dict['Timestamp'], qtwi)
- self.item(_row, self.columns_dict['Timestamp']).setTextAlignment(
- Qt.AlignCenter)
-
- _prop = self.pv_gateway[_row].qt_dynamic_property_get()
-
- if _prop == self.pv_gateway[_row].READBACK_ALARM:
-
- if alarm_severity == self.pv_gateway[_row].cyca.SEV_MAJOR:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmMajor
- _fgcolor = "black"
- elif alarm_severity == self.pv_gateway[_row].cyca.SEV_MINOR:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmMinor
- _fgcolor = "black"
- elif alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmInvalid
- _fgcolor = "#777777"
- else:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmNoAlarm
- _fgcolor = "black"
-
- #Colors for bg/fg reversed as is the old norm
- self.item(_row, self.columns_dict['Value']).setBackground(
- QColor(_bgcolor))
- self.item(_row, self.columns_dict['Value']).setForeground(
- QColor(_fgcolor))
- if 'Timestamp' in self.columns_dict.keys():
- self.item(_row, self.columns_dict['Timestamp']).setBackground(
- QColor(_bgcolor))
- self.item(_row, self.columns_dict['Timestamp']).setForeground(
- QColor(_fgcolor))
-
-
- elif _prop == self.pv_gateway[_row].DISCONNECTED or \
- alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
- self.item(_row, self.columns_dict['Value']).setBackground(
- QColor("#ffffff"))
- self.item(_row, self.columns_dict['Value']).setForeground(
- QColor("#777777"))
-
- if 'Timestamp' in self.columns_dict.keys():
- self.item(_row, self.columns_dict['Timestamp']).setBackground(
- QColor("#ffffff"))
- self.item(_row, self.columns_dict['Timestamp']).setForeground(
- QColor("#777777"))
-
-
- elif _prop == self.pv_gateway[_row].READBACK_STATIC:
- self.item(_row, self.columns_dict['Value']).setBackground(
- QColor(self.pv_gateway[_row].bg_readback))
- if 'Timestamp' in self.columns_dict.keys():
- self.item(_row, self.columns_dict['Timestamp']).setBackground(
- QColor(self.pv_gateway[_row].bg_readback))
- else:
-
- print(_prop, self.pv_gateway[_row].DISCONNECTED,
- "(in monitor) unknown in element/row no.", _row, _row+1)
-
- QApplication.processEvents(QEventLoop.AllEvents, 10)
-
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- _row = self.pv2item_dict[self.sender()]
-
- self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
- post_display=False)
-
- _prop = self.pv_gateway[_row].qt_dynamic_property_get()
-
- #self.post_display_value(status)
- if _prop == self.pv_gateway[_row].DISCONNECTED:
- self.item(_row, self.columns_dict['Value']).setBackground(
- QColor("#ffffff"))
- self.item(_row, self.columns_dict['Value']).setForeground(
- QColor("#777777"))
- if 'Timestamp' in self.columns_dict.keys():
- self.item(_row, self.columns_dict['Timestamp']).setBackground(
- QColor("#ffffff"))
- self.item(_row, self.columns_dict['Timestamp']).setForeground(
- QColor("#777777"))
-
- QApplication.processEvents()
-
- def table_precision_user_changed(self, new_value):
- self.pvgateway_precision = new_value
-
- for pvgate in self.pv_gateway:
- if pvgate.pv_ctrl is not None:
- self.pvgateway_precision = min(pvgate.pv_ctrl.precision,
- new_value)
-
- pvgate.precision_user = self.pvgateway_precision
- pvgate.precision = self.pvgateway_precision
-
- _pvd = self.cafe.getPVCache(pvgate.handle)
-
- if _pvd.value[0] is not None:
- if isinstance(_pvd.value[0], float):
- pvgate.trigger_monitor_float.emit(
- _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
-
-
- def table_precision_ioc_reset(self):
- if self.max_precision_value == self.table_precision_user_wgt.value():
- self.table_precision_user_changed(self.max_precision_value)
- else:
- self.table_precision_user_wgt.setValue(self.max_precision_value)
-
- def table_refresh_rate_changed(self, new_idx):
-
- _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
- _notify_milliseconds = 0 if _notify_freq_hz == 0 else \
- 1000 / _notify_freq_hz
-
- self.notify_freq_hz = _notify_freq_hz
-
- if _notify_milliseconds == 0:
- for pvgate in self.pv_gateway:
- pvgate.notify_unison = False
- pvgate.notify_milliseconds = _notify_milliseconds
- pvgate.notify_freq_hz = self.notify_freq_hz
- pvgate.monitor_stop()
- time.sleep(0.01)
- for pvgate in self.pv_gateway:
- pvgate.monitor_start()
-
- else:
- for pvgate in self.pv_gateway:
- if not pvgate.notify_unison:
- pvgate.monitor_stop()
-
- for pvgate in self.pv_gateway:
- pvgate.notify_milliseconds = _notify_milliseconds
- pvgate.notify_freq_hz = self.notify_freq_hz
-
- if not pvgate.notify_unison:
- pvgate.notify_unison = True
- pvgate.monitor_start()
- else:
-
- self.cafe.updateMonitorPolicyDeltaMS(
- pvgate.handle, pvgate.monitor_id,
- pvgate.notify_milliseconds)
-
- if self.timer is not None:
- self.timer.stop()
- else:
- self.timer = QTimer()
- self.timer.timeout.connect(self.widget_update)
- self.timer.singleShot(0, self.widget_update)
-
- if _notify_milliseconds > 0:
- self.timer.start(_notify_milliseconds)
-
- def table_ts_resolution_changed(self, new_idx):
-
- for i, ts_res in enumerate(self.ts_combox_idx_dict.values()):
- if i == new_idx:
- self.format_ts_decimal_part = ts_res
- break
-
- for pvgate in self.pv_gateway:
- _pvd = self.cafe.getPVCache(pvgate.handle)
- if _pvd.value[0] is not None:
- if isinstance(_pvd.value[0], float):
- pvgate.trigger_monitor_float.emit(
- _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
- elif isinstance(_pvd.value[0], int):
- pvgate.trigger_monitor_int.emit(
- _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
- else:
- pvgate.trigger_monitor_str.emit(
- str(_pvd.value[0]), _pvd.status, _pvd.alarmSeverity)
-
-
- def display_table_parameters(self):
- display_wgt = QDialog(self)
- display_wgt.setWindowTitle("PV Parameters")
- layout = QVBoxLayout()
- common_label_width = 120
- common_wgt_width = 160
- common_hbox_width = common_label_width + common_wgt_width + 20
-
- self.initial_value = 0
- self.max_precision_value = 0
- for i, pvgate in enumerate(self.pv_gateway):
- if pvgate.pv_ctrl is not None:
- if pvgate.pv_ctrl.precision > 0:
- self.max_precision_value = max(self.max_precision_value,
- pvgate.pv_ctrl.precision)
- self.initial_value = max(self.initial_value,
- pvgate.precision)
-
- if self.max_precision_value > 0:
- #precision user
- _hbox_wgt = QWidget()
- _hbox = QHBoxLayout()
- precision_user_label = QLabel("Precision (user):")
- self.table_precision_user_wgt = QSpinBox(self)
- self.table_precision_user_wgt.setFocusPolicy(Qt.NoFocus)
- self.table_precision_user_wgt.setValue(self.initial_value)
- self.table_precision_user_wgt.setMaximum(self.max_precision_value)
- self.table_precision_user_wgt.valueChanged.connect(
- self.table_precision_user_changed)
- precision_user_label.setAlignment(Qt.AlignLeft)
- self.table_precision_user_wgt.setAlignment(Qt.AlignLeft)
- _hbox.addWidget(precision_user_label)
- _hbox.addWidget(self.table_precision_user_wgt)
- _hbox.setAlignment(Qt.AlignLeft)
- _hbox_wgt.setLayout(_hbox)
-
- precision_user_label.setFixedWidth(common_label_width)
- self.table_precision_user_wgt.setFixedWidth(40)
- _hbox_wgt.setFixedWidth(common_hbox_width)
-
- #precision ioc
- _hbox2_wgt = QWidget()
- _hbox2 = QHBoxLayout()
- precision_ioc_label = QLabel("Precision (ioc): ")
- precision_ioc = QPushButton(self)
- precision_ioc.setText("Reset")
- precision_ioc.clicked.connect(self.table_precision_ioc_reset)
- precision_ioc_label.setAlignment(Qt.AlignLeft)
-
- _hbox2.addWidget(precision_ioc_label)
- _hbox2.addWidget(precision_ioc)
- _hbox2.setAlignment(Qt.AlignLeft)
-
- _hbox2_wgt.setLayout(_hbox2)
-
- precision_ioc_label.setFixedWidth(common_label_width)
- precision_ioc.setFixedWidth(50)
-
- _hbox2_wgt.setFixedWidth(common_hbox_width)
-
- layout.addWidget(_hbox_wgt)
- layout.addWidget(_hbox2_wgt)
-
- if 'Timestamp' in self.columns_dict.keys():
- #time-stamp
- _hbox4_wgt = QWidget()
- _hbox4 = QHBoxLayout()
- ts_label = QLabel("Timestamp: ")
-
- self.ts_combox_idx_dict = {
- 'second (s)': self.format_ts_sec,
- 'decisecond (ds)': self.format_ts_deci,
- 'millisecond (ms)': self.format_ts_milli,
- 'microsecond (\u03bcs)': self.format_ts_micro,
- 'nanosecond (ns)': self.format_ts_nano}
-
- ts_resolution = QComboBox(self)
- for key, ts_res in self.ts_combox_idx_dict.items():
- ts_resolution.addItem(key)
-
- _current_idx = 0
-
- for i, (key, ts_res) in enumerate(self.ts_combox_idx_dict.items()):
- if ts_res == self.format_ts_decimal_part:
- _current_idx = i
- break
-
- ts_resolution.setCurrentIndex(_current_idx)
- ts_resolution.currentIndexChanged.connect(
- self.table_ts_resolution_changed)
-
- _hbox4.addWidget(ts_label)
- _hbox4.addWidget(ts_resolution)
- _hbox4_wgt.setLayout(_hbox4)
-
- ts_label.setFixedWidth(common_label_width)
- ts_resolution.setFixedWidth(common_wgt_width)
- _hbox4_wgt.setFixedWidth(common_hbox_width)
-
- layout.addWidget(_hbox4_wgt)
-
- #precision refresh rate
- _hbox3_wgt = QWidget()
- _hbox3 = QHBoxLayout()
- refresh_freq_label = QLabel("Refresh rate: ")
- #_default_refresh_val = 0 if self.notify_freq_hz <= 0 else \
- # self.notify_freq_hz
- _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
- self.notify_freq_hz_default
-
- self.refresh_freq_combox_idx_dict = {0: 0, 1: 10, 2: 5, 3: 2, 4: 1,
- 5: 0.5, 6: _default_refresh_val}
- refresh_freq = QComboBox(self)
- refresh_freq.addItem('direct')
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[1]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[2]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[3]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[4]))
- refresh_freq.addItem('{0} Hz'.format(
- self.refresh_freq_combox_idx_dict[5]))
-
- _default_text = 'default (direct)' if _default_refresh_val == 0 else \
- 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
-
- refresh_freq.addItem(_default_text)
-
- for key, value in self.refresh_freq_combox_idx_dict.items():
- if value == self.notify_freq_hz:
- refresh_freq.setCurrentIndex(key)
- break
-
- refresh_freq.currentIndexChanged.connect(
- self.table_refresh_rate_changed)
-
- _hbox3.addWidget(refresh_freq_label)
- _hbox3.addWidget(refresh_freq)
- _hbox3_wgt.setLayout(_hbox3)
-
- refresh_freq_label.setFixedWidth(common_label_width)
- refresh_freq.setFixedWidth(common_wgt_width)
- _hbox3_wgt.setFixedWidth(common_hbox_width)
-
- layout.addWidget(_hbox3_wgt)
-
- layout.setAlignment(Qt.AlignLeft)
- layout.setContentsMargins(10, 0, 0, 0)
- layout.setSpacing(0)
-
- display_wgt.setMinimumWidth(340)
- display_wgt.setLayout(layout)
-
- display_wgt.exec()
-
-
- def mousePressEvent(self, event):
- row = self.indexAt(event.pos()).row()
-
- if row > -1:
- if row < len(self.pv_list):
- self.pv_gateway[row].mousePressEvent(event)
- else:
- button = event.button()
- if button == Qt.RightButton:
- self.table_context_menu.exec(QCursor.pos())
- self.clearFocus()
-
- #remove highlighting which persists after mouse leaves
- def mouseMoveEvent(self, event):
- pass
-
- def leaveEvent(self, event):
- self.clearSelection()
- self.clearFocus()
- del event
-
-
-class QMessageWidget(QListWidget):
- """Log message window."""
- def __init__(self, parent=None):
- super(QMessageWidget, self).__init__(parent)
- self.myItem = None
- self.setSelectionMode(QAbstractItemView.ExtendedSelection)
- self.setFocusPolicy(Qt.StrongFocus)
-
- def leaveEvent(self, event):
- if self.myItem:
- self.clearSelection()
- self.clearFocus()
- del event
-
- def mousePressEvent(self, event):
- item = self.itemAt(event.x(), event.y())
- if item:
- self.myItem = item
- self.setCurrentItem(self.myItem)
-
- def keyPressEvent(self, event):
- if event.matches(QKeySequence.Copy):
- nitem = event.count()
- if nitem:
- if self.myItem is not None:
- _str = self.myItem.text()
- QApplication.clipboard().setText(_str)
-
-
-
-class QResultsWidget:
- """Results table"""
- def __init__(self, summary_dict=None, table_dict=None):
-
- self.summary_dict = summary_dict
- self.table_dict = table_dict
- self._group_box = None
-
- def group_box(self, title=""):
- self._group_box = QGroupBox(title)
- self._group_box.setObjectName("OUTERLEFT")
- _vbox = QVBoxLayout()
- _qspace = QFrame()
- _qspace.setFixedHeight(10)
- _vbox.addWidget(_qspace)
-
- _font = QFont("Sans Serif", 10)
-
- longest_str_item1 = ""
- longest_str_item2 = ""
-
- for i, (label, text) in enumerate(self.summary_dict.items()):
- if len(str(label)) > len(longest_str_item1):
- longest_str_item1 = str(label)
- if len(str(text)) > len(longest_str_item2):
- longest_str_item2 = str(text)
-
- fm = QFontMetricsF(_font)
-
- _factor = 1.15
-
- if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
- _factor = 1.18
-
- qrect1 = fm.boundingRect(longest_str_item1)
- qrect2 = fm.boundingRect(longest_str_item2)
- _width_scaling_factor = 1.5
- _width_scaling_factor_le = 1.15
- _widget_height = 25
- for i, (label, text) in enumerate(self.summary_dict.items()):
- #print(label, text)
- qlabel = QLabel(label)
- qle = QLineEdit(text)
- qlabel.setFont(_font)
- qlabel.setStyleSheet(("QLabel{color:black;" +
- "margin:0px; padding:2px;}"))
- qlabel.setFixedWidth(qrect1.width() * _width_scaling_factor)
- qlabel.setFixedHeight(_widget_height)
-
- qle.setFocusPolicy(Qt.NoFocus)
- qle.setFont(_font)
- qle.setStyleSheet(("QLineEdit{color:blue;" +
- "background-color: lightgray;" +
- "qproperty-readOnly: true;" +
- "margin:0px; padding:2px;}"))
- qle.setFixedWidth(qrect2.width() * _width_scaling_factor_le)
- qle.setFixedHeight(_widget_height)
- qle.setAlignment(Qt.AlignRight)
-
- _hbox_widget = QWidget()
- _hbox = QHBoxLayout()
- _hbox.addWidget(qlabel)
- _hbox.addWidget(qle)
- _hbox_widget.setLayout(_hbox)
- _hbox.setAlignment(Qt.AlignCenter)
- _hbox.setContentsMargins(0, 2, 0, 0)
- _vbox.addWidget(_hbox_widget)
-
- _vbox.setContentsMargins(0, 0, 0, 0)
- _vbox.setAlignment(Qt.AlignCenter|Qt.AlignTop)
-
- _vbox2_widget = QWidget()
- _vbox2 = QVBoxLayout()
- _vbox2.setContentsMargins(0, 20, 0, 40)
- table = QTableWidget(len(self.table_dict)-1, 2)
- table.verticalHeader().setVisible(False)
- table.setFocusPolicy(Qt.NoFocus)
- #table.setFont(_font)
-
- longest_str_item1 = ""
- longest_str_item2 = ""
-
- for i, (label, text) in enumerate(self.table_dict.items()):
- item1 = QTableWidgetItem(str(label))
- item2 = QTableWidgetItem(str(text))
- item1.setTextAlignment(Qt.AlignCenter)
- item2.setTextAlignment(Qt.AlignCenter)
- item1.setForeground(QColor("black"))
- item2.setForeground(QColor("black"))
- if i%2 == 0:
- item1.setBackground(QColor("lightgray"))
- item2.setBackground(QColor("lightgray"))
-
- if len(str(label)) > len(longest_str_item1):
- longest_str_item1 = str(label)
- if len(str(text)) > len(longest_str_item2):
- longest_str_item2 = str(text)
-
- if i == 0:
- #item1.setFont(_font)
- #item2.setFont(_font)
- table.setHorizontalHeaderItem(0, item1)
- table.setHorizontalHeaderItem(1, item2)
- else:
- table.setItem(i-1, 0, item1)
- table.setItem(i-1, 1, item2)
-
- fm = QFontMetricsF(_font)
-
- _factor = 1.2
-
- if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
- _factor = 1.18
-
- qrect = fm.boundingRect(longest_str_item1 + longest_str_item2)
-
- _width_scaling_factor = 1.04
- table.resizeColumnsToContents()
- table.resizeRowsToContents()
-
- table.setFixedHeight((fm.lineSpacing() * _factor * len(
- self.table_dict)) + fm.lineSpacing()*2)
-
- table.setFixedWidth(((qrect.width()) * _width_scaling_factor))
-
- _vbox2.addWidget(table)
- _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
- _vbox2_widget.setLayout(_vbox2)
-
- _vbox.addWidget(_vbox2_widget)
-
- self._group_box.setLayout(_vbox)
- self._group_box.setContentsMargins(20, 20, 20, 20)
- self._group_box.setAlignment(Qt.AlignTop)
- self._group_box.setFixedHeight(
- table.height() + (_widget_height*len(self.summary_dict)))
- self._group_box.setFixedWidth(table.width() + 20)
- return self._group_box
-
-
-class QResultsTableWidget():
- """Results table"""
- def __init__(self, column_headings=None):
-
- self.column_headings = column_headings
- self._group_box = None
-
- def group_box(self, title="Table of Results"):
- self._group_box = QGroupBox(title)
- self._group_box.setObjectName("OUTER")
-
- _font = QFont("Sans Serif", 10)
-
- _vbox2_widget = QWidget()
- _vbox2 = QVBoxLayout()
- _vbox2.setContentsMargins(0, 20, 0, 40)
- table = QTableWidget(1, len(self.column_headings))
- table.verticalHeader().setVisible(True)
- table.setFocusPolicy(Qt.NoFocus)
- table.setFont(_font)
-
- for i, heading in enumerate(self.column_headings):
- _item = QTableWidgetItem(str(heading))
- table.setHorizontalHeaderItem(i, _item)
-
- table.resizeColumnsToContents()
- table.resizeRowsToContents()
- table.setFixedHeight(400)
-
- _vbox2.addWidget(table)
- _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
- _vbox2_widget.setLayout(_vbox2)
-
- self._group_box.setLayout(_vbox2)
- self._group_box.setContentsMargins(20, 20, 20, 20)
- self._group_box.setAlignment(Qt.AlignTop)
-
- self._group_box.setFixedWidth(table.width() + 20)
- return self._group_box
-
-
-class QHDFDockWidget(QDockWidget):
-
- def __init__(self, title=None, parent=None):
- super().__init__(title, parent)
- self.parent = parent
- self.is_docked = True
- self.geometry_from_qsettings = self.parent.application_geometry
- self.topLevelChanged.connect(self._top_level_changed)
- self.setVisible(False)
- self.setFloating(False)
- self.geometry_from_qsettings = self.parent.geometry()
-
- def closeEvent(self, event: QCloseEvent):
- super().closeEvent(event)
-
- self.parent.setGeometry(self.geometry_from_qsettings)
- self.setGeometry(self.geometry_from_qsettings)
- QApplication.processEvents()
-
- self.parent.setGeometry(self.geometry_from_qsettings)
-
- def changeEvent(self, event):
- pass
-
- def _top_level_changed(self, is_floating):
- pass
-
-
-class QNoDockWidget(QDockWidget):
-
- def __init__(self, title=None, parent=None):
- super().__init__(title, parent)
- self.parent = parent
- self.is_docked = True
- self.geometry_from_qsettings = self.parent.application_geometry
- self.topLevelChanged.connect(self._top_level_changed)
- self.setVisible(False)
- self.setFloating(True)
-
- def changeEvent(self, event):
- if "QAbstractButton" in str(self.sender()):
- self.geometry_from_qsettings = self.parent.geometry()
-
- def _top_level_changed(self): #, is_floating):
- self.setVisible(False)
- self.setFloating(True)
- #ResetGeometry
- self.parent.setGeometry(self.geometry_from_qsettings)
- QApplication.processEvents()
-
-
-
-class CAQStripChart(PlotWidget):
- '''Channel access enabled pyqtgraph.PlotWidget'''
-
- def __init__(self, parent=None, pv_list: list = ['PV_NAME_NOT_GIVEN'],
- monitor_callback=None, pv_within_daq_group: bool = False,
- color_mode=None, show_units: bool = False, prefix: str = "",
- suffix: str = "", notify_freq_hz: int = 0, title: str = "",
- ylabel: str = ""):
- super().__init__()
-
- self.no_channels = len(pv_list)
-
- self.found = False
- self.time_zero = [0] * self.no_channels
- self.time_delta = [0] * self.no_channels
- self.pv_list = pv_list
- self.pv2item_dict = {}
- self.pv_gateway = [None] * self.no_channels
-
- self.pvd_previous_list = [None] * self.no_channels
- self.val_previous = [None] * self.no_channels
-
- self.curve = [None] * self.no_channels
-
- for i in range(0, len(self.pv_list)):
- self.pv_gateway[i] = PVGateway(
- parent, pv_list[i], monitor_callback, pv_within_daq_group,
- color_mode, show_units, prefix, suffix,
- #connect_callback=self.py_connect_callback,
- connect_triggers=False, notify_freq_hz=notify_freq_hz,
- monitor_dbr_time=True)
-
- self.pv_gateway[i].is_initialize_complete()
-
- self.pvd_previous_list[i] = self.pv_gateway[i].pvd
-
- self.pv_gateway[i].trigger_connect.connect(
- self.receive_connect_update)
-
- self.pv_gateway[i].trigger_monitor_str.connect(
- self.receive_monitor_update)
- self.pv_gateway[i].trigger_monitor_int.connect(
- self.receive_monitor_update)
- self.pv_gateway[i].trigger_monitor_float.connect(
- self.receive_monitor_update)
- self.pv_gateway[i].trigger_monitor.connect(
- self.receive_monitor_dbr_time)
-
- self.pv_gateway[i].widget_class = "PlotWidget"
- self.pv2item_dict[self.pv_gateway[i]] = i
-
- self.cafe = self.pv_gateway[0].cafe
- self.cyca = self.pv_gateway[0].cyca
- for i in range(0, len(self.pv_gateway)):
- if self.cafe.isConnected(self.pv_gateway[i].pv_name):
- self.pv_gateway[i].trigger_connect.emit(
- self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
- self.pv_gateway[i].cyca.ICAFE_CS_CONN)
-
- for i in range(0, len(self.pv_gateway)):
- if not self.pv_gateway[i].pv_within_daq_group:
- self.pv_gateway[i].monitor_start()
-
- sampleinterval = 0.2
- ##timewindow = 1800.0
-
- self.ts_delta_max = 0.6
-
- # Data stuff
- self._interval = int(sampleinterval*1000)
- self._bufsize = 9000 #int(timewindow/0.33)
- self._bufsize2 = 9000 # int(timewindow/1.33)
- self.databuffer = [None] * self.no_channels
- self.timebuffer = [None] * self.no_channels
- self.x = [None] * self.no_channels
- self.y = [None] * self.no_channels
- self.x_shifted = [None] * self.no_channels
-
- self.idx = [0] * self.no_channels
-
- for i in range(0, self.no_channels):
- bsize = self._bufsize if i == 0 else self._bufsize2
- self.databuffer[i] = collections.deque([None]*bsize, bsize)
- self.timebuffer[i] = collections.deque([0]*bsize, bsize)
- self.x[i] = np.zeros(bsize, dtype=np.float)
- self.y[i] = np.zeros(bsize, dtype=np.float)
-
- ##_long_size=20
- #self.data_series_buffer = collections.deque([0]*_long_size, _long_size)
- #self.time_series_buffer = collections.deque([0]*_long_size, _long_size)
-
- #self.data_series = [] * self.no_channels
- #self.time_series = [] * self.no_channels
-
- self.iflag_series = 0
-
- #self.x = np.linspace(-timewindow, 0.0, self._bufsize)
- #self.x_series = np.zeros(_long_size, dtype=np.float)
- #self.y_series = np.zeros(_long_size, dtype=np.float)
- if title is not None:
- self.setTitle(str(title)) #self.pv_gateway[0].pv_name)
- self.showGrid(x=True, y=True)
- self.setLabel('left', ylabel, self.pv_gateway[0].units)
- self.setLabel('bottom', 'time', 's')
- self.setBackground((60, 60, 60)) #247, 236, 249))
- self.setLimits(yMin=-0.11)
-
- self.plotItem.setMouseEnabled(y=False) # Only allow zoom in X-axis
- self.plotItem.setMouseEnabled(x=True) # Only allow zoom in Y-axis
-
- pen_list = [(125, 249, 255), (255, 255, 0)]
-
- for i in range(0, len(self.pv_gateway)):
- self.curve[i] = self.plot(self.x[0], self.y[0], pen=pen_list[i])
-
- l = pg.LegendItem(offset=(0., 0.5), colCount=1)
- l.setParentItem(self.graphicsItem())
-
- l.setLabelTextColor((255, 255, 255))
-
- for curv, pv in zip(self.curve, self.pv_gateway):
- l.addItem(curv, pv.pv_name)
-
- QApplication.processEvents()
-
- @Slot(object, int)
- def receive_monitor_dbr_time(self, pvdata, alarm_severity):
-
- #Check on alarm_severity??
-
- _row = self.pv2item_dict[self.sender()]
-
- ts_now = pvdata.ts[0] + pvdata.ts[1] * 10**(-9)
- ts_previous = (self.pvd_previous_list[_row].ts[0] +
- self.pvd_previous_list[_row].ts[1] * 10**(-9))
- ##ts_delta = ts_now - ts_previous
-
- if (pvdata.ts[0] == self.pvd_previous_list[_row].ts[0]) and (
- pvdata.ts[1] == self.pvd_previous_list[_row].ts[1]):
- pvdata.show()
- self.pvd_previous_list[_row].show()
- return
-
- value = pvdata.value[0]
- #discard first callbacks
- #if ts_delta > 2.0:
- # self.pvd_previous_list[_row] = _pvd
- # return;
- self.pvd_previous_list[_row] = pvdata
- self.val_previous[_row] = value
- #self.pvd_previous_list[_row].ts[0] = _pvd.ts[0]
- #self.pvd_previous_list[_row].ts[1] = _pvd.ts[1]
-
- self.databuffer[_row].append(value)
- self.timebuffer[_row].append(self.time_delta[_row])
-
- highest_ts = self.timebuffer[0][0] \
- if self.timebuffer[0][0] is not None else 0
- for i in range(1, len(self.timebuffer)):
- if self.timebuffer[i][0] is None:
- continue
- elif self.timebuffer[i][0] > highest_ts:
- highest_ts = self.timebuffer[i][0]
-
- if self.timebuffer[_row][0] is not None:
- for i, val in enumerate(self.timebuffer[_row]):
- if val > highest_ts:
- self.idx[_row] = i - 1
- break
-
- self.y[_row][:] = self.databuffer[_row]
- self.x[_row][:] = self.timebuffer[_row]
-
- idx = self.idx[_row]
- self.x_shifted[_row] = list(
- map(lambda m: (m - self.time_delta[_row]), self.x[_row][idx:]))
-
- self.curve[_row].setData(self.x_shifted[_row], self.y[_row][idx:])
-
- self.time_delta[_row] = (
- pvdata.ts[0] + pvdata.ts[1]*10**(-9)) - self.time_zero[0]
-
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
-
- #self.pv_gateway.receive_monitor_update(value, status, alarm_severity)
- _row = self.pv2item_dict[self.sender()]
-
- #print("row, value===>", _row, value, self.pv_gateway[_row].pv_name)
- _pvd = self.pv_gateway[_row].cafe.getPVCache(
- self.pv_gateway[_row].handle)
-
- #print("value", _pvd.value[0], self.pvd_previous_list[_row].value[0])
-
- _pvd2 = self.pv_gateway[_row].pvd
-
- print("val", value, _pvd2.value[0], _pvd.value[0],
- self.pvd_previous_list[_row].value[0])
-
- ts_now = _pvd.ts[0] + _pvd.ts[1] * 10**(-9)
-
- ts_previous = (self.pvd_previous_list[_row].ts[0] +
- self.pvd_previous_list[_row].ts[1] * 10**(-9))
- ts_delta = ts_now - ts_previous
-
- if value == self.val_previous[_row]:
- _pvd.show()
-
- return
-
-
- #discard first callbacks
- #if ts_delta > 2.0:
- # self.pvd_previous_list[_row] = _pvd
- # return;
- self.pvd_previous_list[_row] = _pvd2
- self.val_previous[_row] = value
- #self.pvd_previous_list[_row].ts[0] = _pvd.ts[0]
- #self.pvd_previous_list[_row].ts[1] = _pvd.ts[1]
-
- self.databuffer[_row].append(value)
- self.timebuffer[_row].append(self.time_delta[_row])
-
- highest_ts = self.timebuffer[0][0] \
- if self.timebuffer[0][0] is not None else 0
- for i in range(1, len(self.timebuffer)):
- if self.timebuffer[i][0] is None:
- continue
- elif self.timebuffer[i][0] > highest_ts:
- highest_ts = self.timebuffer[i][0]
-
-
- if self.timebuffer[_row][0] is not None:
- for i, val in enumerate(self.timebuffer[_row]):
- if val > highest_ts:
- self.idx[_row] = i - 1
- break
-
-
- #for i in range(1, self.timebuffer):
- # if self.timebuffer[i][0] is not None:
- # a = self.timebuffer[0][0]
- # for i, val in enumerate(self.timebuffer[_row]):
- # if val > a:
- # idx = i - 1
- # break
-
-
- self.y[_row][:] = self.databuffer[_row]
- self.x[_row][:] = self.timebuffer[_row]
-
-
- #self.y[_row][:] = self.databuffer[_row]
- #self.x[_row][:] = self.timebuffer[_row]
-
- '''
- #print(ts_delta, value, self.pvd_previous.value[0])
- #if (ts_delta < self.ts_delta_max) and (value <
- #self.pvd_previous.value[0]) :
- if (value < self.pvd_previous.value[0]) :
- self.data_series_buffer.append(value)
- self.time_series_buffer.append(ts_now - self.time_zero )
- self.y_series[:] = self.data_series_buffer
- self.x_series[:] = self.time_series_buffer
- #print(self.x_series, self.y_series)
- #elif ts_delta < 1.0:
- if len(self.data_series_buffer) > 15:
- #x_series = np.array(self.time_series, dtype=np.float)
- #y_series = np.array(self.data_series, dtype=np.float)
- _x=self.x_series.reshape((-1, 1))
-
- model = LinearRegression()
- model.fit(_x, self.y_series)
- r_sq = model.score(_x, self.y_series)
- ###JCprint('coefficient of determination:',
- ##r_sq, "slope", model.coef_ , "lifetime:",
- ###self.y_series[0]/model.coef_ / 3600)
- #print('intercept:', model.intercept_)
- #print('slope:', model.coef_)
- #print('max value', y_series[0], y_series[1])
- if r_sq > 0.995:
- _I = self.y_series[0]
- ###JCprint("lifetime:", _I/model.coef_ / 3600)
-
-
- y_pred = model.predict(_x)
- #print("len, y_pred, _x", len(y_pred),
- #len(self.y_series), len(_x))
- #print('predicted response:', y_pred, sep='\n')
- m_sq_error = mean_squared_error(self.y_series, y_pred)
- #print('Mean squared error: {0:.9f}'.format(
- # mean_squared_error(y_series, y_pred)))
- #print('Coefficient of determination: {0:.9f}'.format(
- # r2_score(y_series, y_pred)))
-
-
-
- self.trigger_series_sequence.emit(self.x_series,
- self.y_series)
- #print("emit")
- self.data_series = []
- self.time_series = []
- #print(len(self.x_series), len(self.y_series))
- else:
- self.data_series = []
- self.time_series = []
-
- '''
-
-
- #dt = (self.x[-1] - self.x[-2])
- #print("dt", dt)
- #Lowet IPCT before trigger is set to t=0
- idx = self.idx[_row]
- self.x_shifted[_row] = list(
- map(lambda m: (m - self.time_delta[_row]), self.x[_row][idx:]))
-
- ##self.y = np.where(self.y != self.y, 0, self.y) #test for nan
-
- #print("row len len ", _row, self.time_delta[0], self.time_delta[1])
-
-
- self.curve[_row].setData(self.x_shifted[_row], self.y[_row][idx:])
-
- self.time_delta[_row] = (
- _pvd.ts[0] + _pvd.ts[1]*10**(-9)) - self.time_zero[0]
-
-
- '''
- LOOK_BACK = -800
- if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name:
- LOOK_BACK = -250
-
- if value > self.y[-2]:
- if not self.found:
- #print(x_shifted[-240:], self.y[-240:])
- #self.y = np.where(self.y != self.y, 0, self.y) #test for nan
- max_index = self.y[LOOK_BACK:].argmax()
-
- if max_index == 0:
- return
- print("max index=", max_index)
-
- #print(x_shifted[-600+max_index:], self.x[-600+max_index:])
- #print(self.y[-600+max_index:-2])
- self.found = True
- #print("Are Signals blocked??", self.signalsBlocked())
- self.trigger_decay_sequence.emit(np.array(
- x_shifted[LOOK_BACK+max_index+9:-2]),
- self.y[LOOK_BACK+max_index+9:-2])
- else:
- self.found = False
- '''
-
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- print("pv_name==>", pv_name)
-
- _row = self.pv2item_dict[self.sender()]
- self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
- post_display=False)
-
- #self.pv_gateway.receive_connect_update(handle, pv_name, status)
- _pvd = self.pv_gateway[_row].cafe.getPVCache(
- self.pv_gateway[_row].handle)
- if self.time_zero[_row] == 0:
- self.time_zero[_row] = _pvd.ts[0] + _pvd.ts[1]*10**(-9)
-
- self.pvd_previous = _pvd
-
-
- #renove highlighting which persists after mouse leaves
- def mouseMoveEvent(self, event):
- pass
-
- def leaveEvent(self, event):
- self.clearFocus()
- del event
-
-
-class CAQPCTChart(PlotWidget):
- '''Channel access enabled pyqtgraph.PlotWidget'''
- #trigger_monitor_float = Signal(float, int, int)
- #trigger_monitor_int = Signal(int, int, int)
- #trigger_monitor_str = Signal(str, int, int)
-
- #trigger_connect = Signal(int, str, int)
-
- trigger_decay_sequence = Signal(np.ndarray, np.ndarray)
- trigger_series_sequence = Signal(np.ndarray, np.ndarray)
- #def py_connect_callback(self, handle, pvname, status):
- # self.trigger_connect.emit(int(handle), str(pvname), int(status))
- # print("py connect callback", handle, pvname, status)
-
- def daq_start(self):
- self.blockSignals(False)
-
- def daq_pause(self):
- self.blockSignals(True)
-
- def daq_stop(self):
- self.blockSignals(True)
-
- def __init__(self, parent=None, pv_list: list = ['PV_NAME_NOT_GIVEN'],
- monitor_callback=None, pv_within_daq_group: bool = False,
- color_mode=None, show_units: bool = False, prefix: str = "",
- suffix: str = "", notify_freq_hz: int = 0):
- super().__init__()
-
- self.found = False
- self.time_zero = 0
- self.time_delta = 0
- self.pv_list = pv_list
- self.pv2item_dict = {}
- self.pv_gateway = [None] * len(self.pv_list)
- self.pvd_previous = None
-
- for i in range(0, len(self.pv_list)):
- self.pv_gateway[i] = PVGateway(
- parent, pv_list[i], monitor_callback, pv_within_daq_group,
- color_mode, show_units, prefix, suffix,
- #connect_callback=self.py_connect_callback,
- connect_triggers=False, notify_freq_hz=notify_freq_hz)
-
- self.pv_gateway[i].is_initialize_complete()
-
- self.pv_gateway[i].trigger_connect.connect(
- self.receive_connect_update)
-
- self.pv_gateway[i].trigger_monitor_str.connect(
- self.receive_monitor_update)
- self.pv_gateway[i].trigger_monitor_int.connect(
- self.receive_monitor_update)
- self.pv_gateway[i].trigger_monitor_float.connect(
- self.receive_monitor_update)
-
- self.pv_gateway[i].widget_class = "PlotWidget"
-
- self.pv2item_dict[self.pv_gateway[i]] = i
-
- self.cafe = self.pv_gateway[0].cafe
- self.cyca = self.pv_gateway[0].cyca
- for i in range(0, len(self.pv_gateway)):
- if self.cafe.isConnected(self.pv_gateway[i].pv_name):
- self.pv_gateway[i].trigger_connect.emit(
- self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
- self.pv_gateway[i].cyca.ICAFE_CS_CONN)
-
- for i in range(0, len(self.pv_gateway)):
- if not self.pv_gateway[i].pv_within_daq_group:
- self.pv_gateway[i].monitor_start()
-
- sampleinterval = 0.333
- timewindow = 1800.0
-
- self.ts_delta_max = 0.6
-
- # Data stuff
- self._interval = int(sampleinterval*1000)
- self._bufsize = int(timewindow/sampleinterval)
- self.databuffer = collections.deque([None]*self._bufsize, self._bufsize)
- self.timebuffer = collections.deque([0]*self._bufsize, self._bufsize)
-
- _long_size = 20
- self.data_series_buffer = collections.deque([0]*_long_size, _long_size)
- self.time_series_buffer = collections.deque([0]*_long_size, _long_size)
-
- self.data_series = []
- self.time_series = []
-
- self.iflag_series = 0
-
- #self.x = np.linspace(-timewindow, 0.0, self._bufsize)
- self.x = np.zeros(self._bufsize, dtype=np.float)
- self.y = np.zeros(self._bufsize, dtype=np.float)
-
- self.x_series = np.zeros(_long_size, dtype=np.float)
- self.y_series = np.zeros(_long_size, dtype=np.float)
-
- self.setTitle("PCT(t)") #self.pv_gateway[0].pv_name)
- self.showGrid(x=True, y=True)
- self.setLabel('left', 'I', 'mA')
- self.setLabel('bottom', 'time', 's')
- self.setBackground((60, 60, 60)) #247, 236, 249))
- self.setLimits(yMin=-0.11)
-
- self.plotItem.setMouseEnabled(y=False) # Only allow zoom in X-axis
- self.plotItem.setMouseEnabled(x=True) # Only allow zoom in Y-axis
-
- self.curve = self.plot(self.x, self.y, pen=(125, 249, 255))
- #self.curve2 = self.plot(self.x, self.y, pen=(255,255,0))
-
- l = pg.LegendItem(offset=(0., 0.5))
- l.setParentItem(self.graphicsItem())
- l.setLabelTextColor((125, 249, 255))
-
- l.addItem(self.curve, str(self.pv_gateway[0].pv_name))
- '''
- l2=self.addLegend()
- l2.setLabelTextColor('g')
- l2.setOffset(10)
- l2.addItem(self.curve2, str(self.pv_gateway[0].pv_name))
- '''
- self.daq_stop()
- print(self._bufsize)
- print(len(self.x), len(self.y))
-
-
-
- @Slot(str, int, int)
- @Slot(int, int, int)
- @Slot(float, int, int)
- def receive_monitor_update(self, value, status, alarm_severity):
-
- #self.pv_gateway.receive_monitor_update(value, status, alarm_severity)
- _row = self.pv2item_dict[self.sender()]
- #print("value===>", value, self.pv_gateway[_row].pv_name)
- _pvd = self.pv_gateway[_row].cafe.getPVCache(
- self.pv_gateway[_row].handle)
-
- ts_now = _pvd.ts[0] + _pvd.ts[1] * 10**(-9)
- ts_previous = self.pvd_previous.ts[0] + self.pvd_previous.ts[1]*10**(-9)
- ts_delta = ts_now - ts_previous
-
- if (_pvd.ts[0] == self.pvd_previous.ts[0]) and (
- _pvd.ts[1] == self.pvd_previous.ts[1]):
- #_pvd.show()
- return
-
- #discard first callbacks
- if ts_delta > 2.0:
- self.pvd_previous = _pvd
- return
-
- self.databuffer.append(value)
- self.y[:] = self.databuffer
- self.timebuffer.append(self.time_delta)
- self.x[:] = self.timebuffer
-
- #print(ts_delta, value, self.pvd_previous.value[0])
- #if (ts_delta < self.ts_delta_max) and (value <
- # self.pvd_previous.value[0]):
- if value < self.pvd_previous.value[0]:
- self.data_series_buffer.append(value)
- self.time_series_buffer.append(ts_now - self.time_zero)
- self.y_series[:] = self.data_series_buffer
- self.x_series[:] = self.time_series_buffer
- #print(self.x_series, self.y_series)
- #elif ts_delta < 1.0:
- if len(self.data_series_buffer) > 15:
- #x_series = np.array(self.time_series, dtype=np.float)
- #y_series = np.array(self.data_series, dtype=np.float)
- _x = self.x_series.reshape((-1, 1))
-
- model = LinearRegression()
- model.fit(_x, self.y_series)
- r_sq = model.score(_x, self.y_series)
- ###JCprint('coefficient of determination:',
- ###r_sq, "slope", model.coef_ , "lifetime:",
- ###self.y_series[0]/model.coef_ / 3600)
- #print('intercept:', model.intercept_)
- #print('slope:', model.coef_)
- #print('max value', y_series[0], y_series[1])
- if r_sq > 0.995:
- #_I = self.y_series[0]
-
-
- ###JCprint("lifetime:", _I/model.coef_ / 3600)
-
- ####y_pred = model.predict(_x)
- #print("len, y_pred, _x", len(y_pred), len(self.y_series),
- # len(_x))
- #print('predicted response:', y_pred, sep='\n')
- ##m_sq_error = mean_squared_error(self.y_series, y_pred)
- #print('Mean squared error: {0:.9f}'.format(
- # mean_squared_error(y_series, y_pred)))
- #print('Coefficient of determination: {0:.9f}'.format(
- # r2_score(y_series, y_pred)))
-
-
- self.trigger_series_sequence.emit(self.x_series,
- self.y_series)
- #print("emit")
- self.data_series = []
- self.time_series = []
- #print(len(self.x_series), len(self.y_series))
- else:
- self.data_series = []
- self.time_series = []
-
-
- self.pvd_previous = _pvd
-
- #dt = (self.x[-1] - self.x[-2])
- #print("dt", dt)
- #Lowet IPCT before trigger is set to t=0
- x_shifted = list(map(lambda m: (m - self.time_delta), self.x))
-
- ##self.y = np.where(self.y != self.y, 0, self.y) #test for nan
- self.curve.setData(x_shifted, self.y)
-
- self.time_delta = (
- _pvd.ts[0] + _pvd.ts[1]*10**(-9)) - self.time_zero
- #x_shifted2= list(map(lambda m : m -self.time_delta-1 , self.x))
- #self.curve2.setData(x_shifted2, self.y)
- #QApplication.processEvents()
- #print(type(x_shifted), type(self.y), type([1.1]), type(1.1))
-
- LOOK_BACK = -800
- if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name:
- LOOK_BACK = -250
-
- if value > self.y[-2]:
- if not self.found:
- #print(x_shifted[-240:], self.y[-240:])
- #self.y = np.where(self.y != self.y, 0, self.y) #test for nan
- max_index = self.y[LOOK_BACK:].argmax()
-
- if max_index == 0:
- return
- print("max index=", max_index)
-
- #print(x_shifted[-600+max_index:], self.x[-600+max_index:])
- #print(self.y[-600+max_index:-2])
- self.found = True
- #print("Are Signals blocked??", self.signalsBlocked())
- self.trigger_decay_sequence.emit(
- np.array(x_shifted[LOOK_BACK+max_index+9:-2]),
- self.y[LOOK_BACK+max_index+9:-2])
- else:
- self.found = False
-
-
-
-
- @Slot(int, str, int)
- def receive_connect_update(self, handle: int, pv_name: str, status: int):
- '''Triggered by connect signal'''
- print("pv_name==>", pv_name)
-
- _row = self.pv2item_dict[self.sender()]
- self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
- post_display=False)
-
- #self.pv_gateway.receive_connect_update(handle, pv_name, status)
- _pvd = self.pv_gateway[_row].cafe.getPVCache(
- self.pv_gateway[_row].handle)
- if self.time_zero == 0:
- self.time_zero = _pvd.ts[0] + _pvd.ts[1]*10**(-9)
- #print(self.time_zero)
- self.pvd_previous = _pvd
-
-
- #renove highlighting which persists after mouse leaves
- def mouseMoveEvent(self, event):
- pass
-
- def leaveEvent(self, event):
- self.clearFocus()
- del event
From 7f1981aac7ef5e4e684c33153285769c4093f86b Mon Sep 17 00:00:00 2001
From: chrin
Date: Fri, 14 Oct 2022 13:32:15 +0200
Subject: [PATCH 3/3] fixed alarm colors in QTableWidget
---
__pycache__/pvgateway.cpython-37.pyc | Bin 37266 -> 37418 bytes
__pycache__/pvwidgets.cpython-37.pyc | Bin 71261 -> 72208 bytes
pvgateway.py | 14 +-
pvgateway.py- | 1696 +++++++++++++
pvgateway.py:2.9 | 1910 ++++++++++++++
pvwidgets.py | 201 +-
pvwidgets.py- | 3142 +++++++++++++++++++++++
pvwidgets.py:2.9 | 3461 ++++++++++++++++++++++++++
8 files changed, 10361 insertions(+), 63 deletions(-)
create mode 100644 pvgateway.py-
create mode 100644 pvgateway.py:2.9
create mode 100644 pvwidgets.py-
create mode 100644 pvwidgets.py:2.9
diff --git a/__pycache__/pvgateway.cpython-37.pyc b/__pycache__/pvgateway.cpython-37.pyc
index af6ecc4a657c33cc640fd0256aa7f4f751e6fbbc..cb5cbe417ac06d91bd93910b3450ea78e9dbb886 100644
GIT binary patch
delta 7548
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
wEh4u_pF~rXPL9l
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
AlK=n!
delta 27261
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 0:
@@ -942,6 +950,8 @@ class PVGateway(QWidget):
elif self.qt_object_name == self.PV_DAQ_BS:
self.color_mode = self.READBACK_STATIC
+ if 'READ' in self.pv_name:
+ print('color mode',self.pv_name, self.color_mode)
self._qt_dynamic_property_set(self.color_mode)
else:
diff --git a/pvgateway.py- b/pvgateway.py-
new file mode 100644
index 0000000..64a856c
--- /dev/null
+++ b/pvgateway.py-
@@ -0,0 +1,1696 @@
+"""
+The module provides data and metadata of a process variable through
+PyCafe.
+"""
+__author__ = 'Jan T. M. Chrin'
+
+import copy
+from enum import IntEnum
+import inspect
+import time
+
+from distutils.version import LooseVersion
+
+from qtpy.QtCore import (QEvent, QMutex, QPoint, QProcess, QSettings, Qt, QUrl,
+ Signal)
+from qtpy.QtCore import __version__ as QT_VERSION_STR
+from qtpy.QtGui import QCursor, QDesktopServices, QFont
+from qtpy.QtWidgets import (QAction, QApplication, QComboBox, QDialog,
+ QHBoxLayout, QLabel, QMenu, QMessageBox,
+ QPushButton, QSpinBox, QVBoxLayout, QWidget)
+
+def __LINE__():
+ return inspect.currentframe().f_back_f_lineno
+
+class DAQState(IntEnum):
+ BS = 10
+ CA = 20
+ BS_STOP = 30
+ CA_STOP = 40
+ BS_PAUSE = 50
+ CA_PAUSE = 60
+
+class PVGateway(QWidget):
+ """Retrieves pv metadata through PyCafe.
+
+ The PVGateway class when subclassed by Qt widgets enables their
+ connectivity to channel access.
+
+ Attributes:
+ monid: (int) Monitor id
+ units : (str) Units associated with the pv
+
+ trigger_monitor_ is the signal triggered by updates
+ arising from monitored pvs.
+ trigger_connect is the signal triggered from changes in pv
+ connection status.
+ widget_handle_dict is a dictionary mapping widgets to their pv
+ handle.
+ A pv handle may be associated to more than one widget.
+ """
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int) #pvdata, status
+
+
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+ #Properties, user supplied
+ ACT_ON_BEAM = 'actOnBeam'
+ NOT_ACT_ON_BEAM = 'notActOnBeam'
+ READBACK_ALARM = 'alarm'
+ READBACK_STATIC = 'static'
+
+ #Properties, dynamic
+ DISCONNECTED = 'disconnected'
+ ALARM_SEV_MINOR = 'alarmSevMinor'
+ ALARM_SEV_MAJOR = 'alarmSevMajor'
+ ALARM_SEV_INVALID = 'alarmSevInvalid'
+ ALARM_SEV_NO_ALARM = READBACK_ALARM
+ DAQ_STOPPED = 'stopped'
+ DAQ_PAUSED = 'paused'
+
+ #ObjectName, defined by CAQ
+ PV_CONTROLLER = "Controller"
+ PV_READBACK = "Readback"
+ PV_DAQ_BS = "BSRead"
+ PV_DAQ_CA = "CARead"
+
+ _DAQ_CAFE_SG_NAME = "gBS2CA"
+
+ _alarm_severity_record_types = ["ai", "ao", "calc", "calcout", "dfanout",
+ "longin", "longout", "pid", "sel",
+ "steppermotor", "sub"]
+
+ #parent is Gui
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ connect_callback=None, msg_label: str = "",
+ connect_triggers: bool = True, notify_freq_hz: int = 0,
+ notify_unison: bool = False, precision: int = 0,
+ monitor_dbr_time: bool = False):
+
+
+ super().__init__()
+
+
+ if parent is None:
+ return
+
+ if not pv_name:
+ return
+
+
+ self.connect_callback = connect_callback
+ self.notify_freq_hz = abs(notify_freq_hz)
+ self.notify_freq_hz_default = self.notify_freq_hz
+
+ self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
+ 1000 / self.notify_freq_hz
+
+ self.notify_unison = bool(notify_unison) and bool(self.notify_freq_hz)
+
+ self.parent = parent
+ self.settings = self.parent.settings
+
+ self.pv_name = pv_name
+
+ self.color_mode = None
+
+ if color_mode is not None:
+ if color_mode in (self.ACT_ON_BEAM,
+ self.NOT_ACT_ON_BEAM,
+ self.READBACK_ALARM,
+ self.READBACK_STATIC):
+ self.color_mode = color_mode
+
+ self.color_mode_requested = self.color_mode
+
+ if monitor_callback is not None:
+ self.monitor_callback = monitor_callback
+ else:
+ self.monitor_callback = None
+
+ self.pv_within_daq_group = pv_within_daq_group
+
+ self.show_units = show_units
+ self.prefix = prefix
+ self.suffix = suffix
+
+ self.cafe = self.parent.cafe
+ self.cyca = self.parent.cyca
+
+ if self.parent.settings is not None:
+ self.url_archiver = self.parent.settings.data["url"]["archiver"]
+ self.url_databuffer = self.parent.settings.data["url"]["databuffer"]
+ self.bg_readback = self.parent.settings.data["StyleGuide"][
+ "bgReadback"]
+ self.fg_alarm_major = self.parent.settings.data["StyleGuide"][
+ "fgAlarmMajor"]
+ self.fg_alarm_minor = self.parent.settings.data["StyleGuide"][
+ "fgAlarmMinor"]
+ self.fg_alarm_invalid = self.parent.settings.data["StyleGuide"][
+ "fgAlarmInvalid"]
+ self.fg_alarm_noalarm = self.parent.settings.data["StyleGuide"][
+ "fgAlarmNoAlarm"]
+ else:
+ #self.settings = ReadJSON(self.parent.appname)
+ self.url_archiver = ("https://ui-data-api.psi.ch/prepare?channel=" +
+ "sf-archiverappliance/")
+ self.url_databuffer \
+ = "https://ui-data-api.psi.ch/prepare?channel=sf-databuffer/"
+
+ self.daq_group_name = self._DAQ_CAFE_SG_NAME
+ self.desc = None
+ self.handle = None
+ self.initialize_complete = False
+ self.initialize_again = False
+
+ self.msg_label = msg_label
+ self.msg_press_value = None
+ self.msg_release_value = None
+
+ self.monitor_id = None
+ self.monitor_dbr_time = monitor_dbr_time
+ self.mutex_post_display = QMutex()
+
+ self.precision_user = precision
+ self.has_precision_user = bool(precision)
+ self.precision_pv = 3
+
+ self.precision = (self.precision_user if self.has_precision_user else
+ self.precision_pv)
+
+ self.pvd = None
+ self.pv_ctrl = None
+ self.pv_info = None
+ self.record_type = None
+
+ #if 'show_log_message' in dir(self.parent):
+ # self.show_log_message = self.parent.show_log_message
+ #else:
+ # self.show_log_message = None
+
+ self.qt_object_name = None
+
+ self.qt_property_controller = {
+ self.DISCONNECTED: False,
+ self.ACT_ON_BEAM: False, self.NOT_ACT_ON_BEAM: False
+ }
+
+ self.qt_property_readback = {
+ self.DISCONNECTED: False,
+ self.READBACK_ALARM: False, self.READBACK_STATIC: False,
+ self.ALARM_SEV_MINOR: False, self.ALARM_SEV_MAJOR: False,
+ self.ALARM_SEV_INVALID: False
+ }
+
+ self.qt_property_daq_bs = {
+ self.DISCONNECTED: False,
+ self.READBACK_ALARM: False, self.READBACK_STATIC: False,
+ self.ALARM_SEV_MINOR: False, self.ALARM_SEV_MAJOR: False,
+ self.ALARM_SEV_INVALID: False,
+ self.DAQ_STOPPED: False, self.DAQ_PAUSED: False
+ }
+
+ self.qt_property_daq_ca = {
+ self.DISCONNECTED: False,
+ self.READBACK_ALARM: False, self.READBACK_STATIC: False,
+ self.ALARM_SEV_MINOR: False, self.ALARM_SEV_MAJOR: False,
+ self.ALARM_SEV_INVALID: False,
+ self.DAQ_STOPPED: False, self.DAQ_PAUSED: False
+ }
+
+ self.qt_object_to_property = {
+ self.PV_CONTROLLER: self.qt_property_controller,
+ self.PV_READBACK: self.qt_property_readback,
+ self.PV_DAQ_BS: self.qt_property_daq_bs,
+ self.PV_DAQ_CA: self.qt_property_daq_ca
+ }
+
+ self._qt_property_selected = {}
+
+ self.status_tip = None
+ self.suggested_text = ""
+ self.time_monotonic = time.monotonic()
+ self.pvd_previous = None
+ self.timeout = 0.2
+ self.units = ""
+
+ self.widget = self
+
+ _widget_name_part = str(self.widget.__class__).split("\'")[1].split(".")
+ #_widget_class_part = _widget_name_part[1].split(".")
+ self.widget_class = _widget_name_part[len(_widget_name_part)-1]
+
+ if pv_within_daq_group:
+ self.trigger_daq_int.connect(self.receive_daq_update)
+ self.trigger_daq.connect(self.receive_daq_update)
+ self.trigger_daq_str.connect(self.receive_daq_update)
+
+ elif connect_triggers:
+ self.trigger_monitor.connect(self.receive_monitor_dbr_time)
+ self.trigger_monitor_str.connect(self.receive_monitor_update)
+ self.trigger_monitor_int.connect(self.receive_monitor_update)
+ self.trigger_monitor_float.connect(self.receive_monitor_update)
+ self.trigger_connect.connect(self.receive_connect_update)
+
+ self.context_menu = QMenu()
+ self.context_menu.setObjectName("contextMenu")
+ self.context_menu.setWindowModality(Qt.NonModal) #ApplicationModal
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.0"):
+ self.context_menu.addSection("PV: {0}".format(self.pv_name))
+
+ action1 = QAction("Text Info", self)
+ action1.triggered.connect(self.pv_status_text)
+
+ action2 = QAction("Lookup in Archiver", self)
+ action2.triggered.connect(self.lookup_archiver)
+
+ action3 = QAction("Lookup in Databuffer", self)
+ action3.triggered.connect(self.lookup_databuffer)
+
+ action4 = QAction("Strip Chart (PShell)", self)
+ action4.triggered.connect(self.strip_chart)
+
+ action6 = QAction("Configure Display Parameters", self)
+ action6.triggered.connect(self.display_parameters)
+
+ self.context_menu.addAction(action1)
+ self.context_menu.addAction(action2)
+ self.context_menu.addAction(action3)
+ self.context_menu.addAction(action4)
+
+ action5 = QAction("Reconnect: {0}".format(self.pv_name), self)
+ action5.triggered.connect(self.reconnect_channel)
+ _font = QFont()
+ _font.setPixelSize(12)
+ action5.setFont(_font)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.context_menu.addSection("")
+
+ #return action6 and 5 code here eventually
+ self.context_menu.addAction(action6)
+ self.context_menu.addAction(action5)
+
+ self.pv_message_in_a_box = QMessageBox()
+ self.pv_message_in_a_box.setObjectName("pvinfo")
+
+ #Qt.ApplicationModal not used as it blocks input to all windows
+ self.pv_message_in_a_box.setWindowModality(Qt.NonModal)
+ self.pv_message_in_a_box.setIcon(QMessageBox.Information)
+ self.pv_message_in_a_box.setStandardButtons(QMessageBox.Close)
+ self.pv_message_in_a_box.setDefaultButton(QMessageBox.Close)
+
+ self.initialize()
+ #The __init__ method of a class is used to initialize new objects,
+ #not create them. As such, it should not return any value.
+
+
+ return #self # used by pvgateway in CAQStripChart
+
+
+ def initialize(self):
+ '''Initialze class attributes and connect to ca if required.'''
+
+ _handle_within_group_flag = False
+ if self.pv_within_daq_group:
+ self.handle = self.cafe.getHandleFromPVWithinGroup(
+ self.pv_name, self.daq_group_name)
+ if self.handle > 0:
+ self.cafe.addWidget(self.handle, self.widget)
+ _handle_within_group_flag = True
+ #Callback already invoked to emit signal here!!
+ _channel_info = self.cafe.getChannelInfo(self.handle)
+
+ #wgts = self.cafe.getWidgets(self.handle)
+
+ self.trigger_connect.emit(
+ int(self.handle), str(self.pv_name),
+ int(_channel_info.cafeConnectionState))
+ #In case user is misinformed
+ if not _handle_within_group_flag:
+ self.handle = self.cafe.getHandleFromPV(self.pv_name)
+ if self.connect_callback is None:
+ self.connect_callback = self.py_connect_callback
+
+ if self.handle > 0:
+ #The second time round, widget is gateway rather than parent,
+ #Why is that?
+ self.cafe.setPyConnectCallbackFn(self.handle,
+ self.connect_callback)
+
+ self.cafe.addWidget(self.handle, self.widget)
+
+ _channel_info = self.cafe.getChannelInfo(self.handle)
+ self.trigger_connect.emit(
+ self.handle, self.pv_name,
+ int(_channel_info.cafeConnectionState))
+
+ else:
+ self.cafe.openPrepare()
+ self.handle = self.cafe.open(self.pv_name,
+ self.connect_callback)
+ self.cafe.addWidget(self.handle, self.widget)
+ self.cafe.openNowAndWait(self.timeout, self.handle)
+
+ self.initialize_meta_data()
+
+ self.pv_message_in_a_box.setWindowTitle(self.pv_name)
+
+
+ def initialize_meta_data(self):
+
+ _current_value = ""
+
+ if self.cafe.isConnected(self.handle) and \
+ self.cafe.initCallbackComplete(self.handle):
+
+ if self.pvd is None:
+ self.pvd = self.cafe.getPVCache(self.handle)
+
+ if self.pv_ctrl is None:
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+ self.set_precision_and_units()
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.pv_name)
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else \
+ self.pv_info.className
+ _rtype = self.cafe.close(self.pv_name.split(".")[0] +
+ ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ _current_value = self.cafe.getCache(self.handle)
+ if isinstance(_current_value, (int, float)):
+ #space for positive numbers
+ _value_form = ("{:<+.%sf}" % self.precision)
+ _current_value = _value_form.format(
+ round(_current_value, self.precision))
+
+ #Reset
+ self.initialize_complete = True
+
+ #verify user input
+ if self.show_units is True:
+ if self.suffix == self.units and self.units != "":
+ self.show_units = False
+
+ self.suggested_text = self.prefix
+ if self.prefix:
+ self.suggested_text += " "
+
+ _suggested_text_from_value = " "
+
+ _max_control_abs = 0
+
+ if self.pv_ctrl is not None:
+ _lower_control_abs = abs(int(self.pv_ctrl.lowerControlLimit))
+ _upper_control_abs = abs(int(self.pv_ctrl.upperControlLimit))
+ _max_control_abs = max(_lower_control_abs, _upper_control_abs)
+ if _max_control_abs is None:
+ _max_control_abs = 0
+
+ _enum_list = self.pv_ctrl.enumStrings
+
+ if _enum_list:
+ _enum_list_member_max_length = 0
+ _enum_list_member_max_index = 0
+
+ for i in range(0, len(_enum_list)):
+ if len(_enum_list[i]) > _enum_list_member_max_length:
+ _enum_list_member_max_length = len(_enum_list[i])
+ _enum_list_member_max_index = i
+ _suggested_text_from_value += \
+ _enum_list[_enum_list_member_max_index] + "."
+ else:
+ if self.pv_ctrl.lowerControlLimit < 0:
+ _suggested_text_from_value += "-"
+ _suggested_text_from_value += str(_max_control_abs) + "."
+
+ self.precision = min(9, self.precision) #safety net
+ for i in range(0, self.precision):
+ _suggested_text_from_value += "0"
+
+ if len(_current_value) > len(_suggested_text_from_value):
+ _suggested_text_from_value = _current_value
+
+ self.suggested_text += _suggested_text_from_value
+
+ if self.show_units:
+ self.suggested_text += " " + self.units
+ self.suggested_text += self.suffix
+
+ _suggested_text_length = len(self.suggested_text)
+ self.suggested_text = self.suggested_text.center(
+ _suggested_text_length+2)
+
+ self.max_control_abs_str = str(_max_control_abs)
+
+ _max_control_abs_length = len(self.max_control_abs_str)
+ _offset = 9
+ self.max_control_abs_str = self.max_control_abs_str.center(
+ _max_control_abs_length + _offset)
+
+ qsettings = QSettings()
+ qsettings.beginGroup("Widget")
+ qsettings.beginGroup(self.pv_name)
+ qsettings.beginGroup(self.widget_class)
+
+ _var_text = "suggested_text"
+ _ctrl_abs = "max_control_abs_str"
+
+ if self.cafe.isConnected(self.handle) and \
+ self.cafe.initCallbackComplete(self.handle):
+ qsettings.setValue(_var_text, self.suggested_text)
+ qsettings.setValue(_ctrl_abs, self.max_control_abs_str)
+ else:
+ if qsettings.value(_var_text) is not None:
+ self.suggested_text = qsettings.value(_var_text)
+ if qsettings.value(_ctrl_abs) is not None:
+ self.max_control_abs_str = qsettings.value(_ctrl_abs)
+
+ qsettings.endGroup()
+ qsettings.endGroup()
+ qsettings.endGroup()
+
+
+ def is_initialize_complete(self):
+ icount = 0
+ while not self.initialize_complete:
+ time.sleep(0.01)
+ self.initialize_meta_data()
+ icount += 1
+ if icount > 50:
+ return False
+ return True
+
+ def cleanup(self, close_pv=True):
+ '''Clean up the widget.'''
+
+ #Make sure mon id is valid
+ if self.handle > 0:
+ _monID_list = self.cafe.getMonitorIDs(self.handle)
+ if self.monitor_id in _monID_list:
+ self.cafe.monitorStop(self.handle, self.monitor_id)
+
+ #Do not close of there are other monitors
+ if self.cafe.getNoMonitors(self.handle) > 0:
+ if close_pv is True:
+ self.cafe.close(self.pv_name)
+ self.widget.deleteLater()
+
+
+ def format_display_value(self, value):
+
+ if value is None:
+ print(self, self.pv_name, ">>>>format_display_value is None")
+ #return
+
+ if isinstance(value, str):
+ _value_str = value
+ elif isinstance(value, int):
+ _value_str = str(value)
+ else:
+ _value_form = ("{:< .%sf}" % self.precision)
+ _rounded_value = round(value, self.precision)
+ _value_str = _value_form.format(_rounded_value)
+
+ if self.show_units:
+ _value_str += " " + self.units + " "
+ if self.suffix:
+ _value_str += " " + self.suffix + " "
+
+ if self.prefix:
+ _space = ""
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.lowerDisplayLimit < 0:
+ _space = " "
+ _value_str = self.prefix + _space + _value_str
+
+ return _value_str
+
+ def post_display_value(self, value):
+
+ _value_str = self.format_display_value(value)
+
+ if "setText" in dir(self):
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ self.setText(_value_str)
+ self.blockSignals(False)
+ else:
+ self.setText(_value_str)
+
+ else:
+ print("setText method does not exist for this widget class:\n",
+ self.widget.__class__)
+ print("sender was: ", self.sender())
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ Checks for existence of widget. Waits up to a maximun of 100 ms.
+ '''
+ pv_name = pvname
+ self.trigger_connect.emit(int(handle), str(pv_name), int(status))
+
+ def receive_connect_update(self, handle, pv_name, status,
+ post_display=True):
+ '''Triggered by connect signal. For Widget to overload.'''
+
+ if pv_name is not None:
+ if pv_name != self.pv_name:
+ print(("pv_name {0} in receive_connect_update " +
+ "does not match: {1}").format(pv_name, self.pv_name))
+
+ if status == self.cyca.ICAFE_CS_CONN:
+ self.initialize_connect = True
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else \
+ self.pv_info.className
+ _rtype = self.cafe.close(self.pv_name.split(".")[0] +
+ ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ self.set_precision_and_units(reconnectFlag=True)
+
+ if not self.msg_label:
+ _value = self.cafe.getCache(handle, dt='native')
+ #Another reconnection in progress!!!
+
+ if _value is None:
+ return
+ else:
+ _value = self.msg_label
+
+ if post_display:
+ self.post_display_value(_value)
+ self.qt_property_reconnect()
+
+ else:
+ self.qt_property_disconnect()
+
+
+ if status == self.cyca.ICAFE_CS_CLOSED:
+ self.initialize_again = True
+
+ elif self.initialize_again:
+ #monitos_id informs whether or not widget has a monitor
+ #CAQMessageButton for instance does not have a monitor
+
+ if not self.pv_within_daq_group and self.monitor_id is not None:
+ self.monitor_start()
+ self.initialize_again = False
+
+ return
+
+
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ ''' DAQ mode is widget specific.
+ DAQ may be in BS mode, but channels within DAQ stream that
+ are not BS enabled will be flagged as CA Mode, i.e., CARead
+ '''
+
+ _current_qt_dynamic_property = self.qt_dynamic_property_get()
+
+ alarm_severity = daq_pvd.alarmSeverity
+ self.pvd = daq_pvd
+
+ if daq_mode != self.qt_object_name:
+ self.qt_object_name = daq_mode
+ self.setObjectName(self.qt_object_name)
+ self.qt_style_polish()
+
+ if daq_state in (self.cyca.ICAFE_DAQ_STOPPED,):
+ if _current_qt_dynamic_property != self.DAQ_STOPPED:
+ self.qt_property_daq_stopped()
+
+ elif daq_state in (self.cyca.ICAFE_DAQ_PAUSED,):
+ if _current_qt_dynamic_property != self.DAQ_PAUSED:
+ self.qt_property_daq_paused()
+
+ elif daq_state in (self.cyca.ICAFE_DAQ_RUN,):
+ if daq_mode == self.PV_DAQ_BS and \
+ _current_qt_dynamic_property != self.READBACK_STATIC:
+ self.qt_property_static()
+
+ elif daq_mode == self.PV_DAQ_CA:
+ if self.color_mode != self.color_mode_requested:
+ self.color_mode = self.color_mode_requested
+
+ if self.cafe.isEnum(self.handle) and \
+ isinstance(daq_pvd.value[0], int):
+ _value = self.cafe.getStringFromEnum(self.handle,
+ daq_pvd.value[0])
+ else:
+ _value = daq_pvd.value[0]
+
+ if daq_pvd.status == self.cyca.ICAFE_NORMAL:
+ if self.msg_label == "":
+ self.post_display_value(_value)
+ if daq_mode == self.PV_DAQ_BS:
+ return
+
+ #Check if color settings are correct
+ if alarm_severity > self.cyca.SEV_NO_ALARM:
+ self.color_mode = self.READBACK_ALARM
+ self.color_mode_requested = self.READBACK_ALARM
+
+ if self.color_mode == self.READBACK_ALARM:
+ if alarm_severity == self.cyca.SEV_MINOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
+ self.qt_property_alarm_sev_minor()
+
+ elif alarm_severity == self.cyca.SEV_MAJOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
+ self.qt_property_alarm_sev_major()
+
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != \
+ self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ elif alarm_severity == self.cyca.SEV_NO_ALARM:
+ if _current_qt_dynamic_property != \
+ self.ALARM_SEV_NO_ALARM:
+ self.qt_property_alarm_sev_no_alarm()
+
+ elif _current_qt_dynamic_property != self.READBACK_STATIC:
+ self.qt_property_static()
+
+ else:
+ if _current_qt_dynamic_property != self.DISCONNECTED:
+ self.qt_property_disconnect()
+
+
+ def receive_monitor_dbr_time(self, pvdata, alarm_severity):
+ print("called from gateway", self.pv_name, alarm_severity)
+ pvdata.show()
+
+ def receive_monitor_update(self, value, status, alarm_severity):
+ '''Triggered by monitor signal. For Widget to overload.'''
+
+ self.mutex_post_display.lock()
+ _current_qt_dynamic_property = self.qt_dynamic_property_get()
+
+ if status == self.cyca.ICAFE_NORMAL:
+
+ if self.msg_label == "":
+ self.post_display_value(value)
+
+ #For DAQ when channel connects after application start-up
+ if _current_qt_dynamic_property == self.DISCONNECTED:
+ self.qt_property_initial_values(qt_object_name=self.PV_READBACK)
+
+ #Check if color settings are correct
+ elif _current_qt_dynamic_property == self.READBACK_STATIC:
+ if alarm_severity > self.cyca.SEV_NO_ALARM:
+ if alarm_severity < self.cyca.SEV_INVALID:
+ self.color_mode = self.READBACK_ALARM
+ self.status_tip = ("Widget color mode is dynamic, " +
+ "pv with alarm limits")
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ if self.color_mode == self.READBACK_ALARM:
+ if alarm_severity == self.cyca.SEV_MINOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
+ self.qt_property_alarm_sev_minor()
+
+ elif alarm_severity == self.cyca.SEV_MAJOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
+ self.qt_property_alarm_sev_major()
+
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ elif alarm_severity == self.cyca.SEV_NO_ALARM:
+ if _current_qt_dynamic_property != self.ALARM_SEV_NO_ALARM:
+ self.qt_property_alarm_sev_no_alarm()
+
+ else:
+ if _current_qt_dynamic_property != self.DISCONNECTED:
+ self.qt_property_disconnect()
+
+ self.mutex_post_display.unlock()
+
+ def py_monitor_callback(self, handle, pvname, pvdata):
+
+ '''Callback function to be invoked on change of pv value.
+ cafe.getCache and cafe.set operations permitted within callback.
+ '''
+
+ pv_name = pvname
+ pvd = pvdata
+
+ if not hasattr(self, 'cafe'):
+ print("py_monitor_callback: name/handle self cafe is NONE",
+ pv_name, handle)
+ return
+
+ self.pvd = pvd
+
+ if pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
+ print("initialize again")
+ self.initialize()
+
+ elif pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _alarm_severity = self.cyca.ICAFE_CA_OP_CONN_DOWN
+ else:
+ _alarm_severity = pvd.alarmSeverity
+
+ if self.monitor_dbr_time:
+ self.trigger_monitor.emit(pvd, _alarm_severity)
+ elif isinstance(pvd.value[0], str):
+ self.trigger_monitor_str.emit((pvd.value[0]), pvd.status,
+ _alarm_severity)
+ elif isinstance(pvd.value[0], int):
+ self.trigger_monitor_int.emit((pvd.value[0]), pvd.status,
+ _alarm_severity)
+ else:
+ self.trigger_monitor_float.emit(float(pvd.value[0]), pvd.status,
+ _alarm_severity)
+
+
+ def monitor_start(self):
+ '''Initiate monitor on pv.'''
+ if self.handle > 0:
+ #Is monitor in waiting - now deleted with monitor_stop
+ if self.notify_unison:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, dbr=self.cyca.CY_DBR_TIME)
+ #start with gateway supplied monitor callback handler
+ elif self.monitor_callback is None:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, cb=self.py_monitor_callback,
+ dbr=self.cyca.CY_DBR_TIME,
+ notify_milliseconds=self.notify_milliseconds)
+ else:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, cb=self.monitor_callback,
+ dbr=self.cyca.CY_DBR_TIME,
+ notify_milliseconds=self.notify_milliseconds)
+
+ def monitor_stop(self):
+ if self.handle > 0:
+ _monID_list = self.cafe.getMonitorIDs(self.handle)
+ _monID_inwaiting_list = self.cafe.getMonitorIDsInWaiting(
+ self.handle)
+ _monID_all = _monID_list + _monID_inwaiting_list
+
+ if self.monitor_id in _monID_all:
+ self.cafe.monitorStop(self.handle, self.monitor_id)
+ #Is monitor in waiting?
+ #remove monitors in waiting - to do
+
+ def reconnect_channel(self):
+ self.cafe.reconnect([self.handle]) #list
+
+ def set_desc(self):
+ '''Set description of pv from pv.DESC'''
+
+ if self.cafe.hasDescription(self.handle):
+ self.desc = self.cafe.getDescription(self.handle)
+ return
+ elif self.desc is not None:
+ return
+ else:
+ self.cafe.supplementHandle(self.handle)
+ if self.cafe.hasDescription(self.handle):
+ self.desc = self.cafe.getDescription(self.handle)
+
+ if self.desc is not None:
+ return
+
+ ###Back-up solution
+ _found = str(self.pv_name).find(".")
+ if _found != -1:
+ _pv_desc = str(self.pv_name)[0:_found] +".DESC"
+ else:
+ _pv_desc = self.pv_name +".DESC"
+ _handle_desc = self.cafe.getHandleFromPVName(_pv_desc)
+
+ _handle_desc_already_open = False
+
+ if _handle_desc == 0:
+ self.cafe.openPrepare()
+ _handle_desc = self.cafe.open(_pv_desc)
+ self.cafe.openNowAndWait(self.timeout, _handle_desc)
+ time.sleep(0.001)
+ else:
+ _handle_desc_already_open = True
+
+ if self.cafe.isConnected(_handle_desc):
+ self.desc = self.cafe.getCache(_handle_desc, 'str')
+ if self.desc is None:
+ self.desc = self.cafe.get(_handle_desc, 'str')
+ else:
+ self.desc = None
+
+ if not _handle_desc_already_open:
+ self.cafe.close(_handle_desc)
+
+ def set_precision_and_units(self, reconnectFlag: bool = False):
+ '''Set the pv precision and units.'''
+ if self.pv_ctrl is None or reconnectFlag is True:
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+
+ if self.pv_ctrl is not None:
+ if not self.has_precision_user:
+ self.precision = self.pv_ctrl.precision
+ if self.pv_ctrl.units is not None:
+ self.units = str(self.pv_ctrl.units)
+ else:
+ self.units = ""
+
+ if reconnectFlag is True:
+ #verify user input
+ if self.show_units is True and self.suffix is not None:
+ if self.suffix == self.units:
+ self.show_units = False
+
+
+ def _qt_readback_color_mode(self):
+ '''Color mode is determined from CAFE and depends on whether the pv:
+ has alarm limits (self.color_mode = 'readbackAlarm')
+ or is without alarm limits (self.color_mode = 'readbackStatic')
+ '''
+
+ #Already set by user
+ if self.color_mode is self.READBACK_ALARM:
+ return
+
+ if self.cafe.isConnected(self.handle):
+ pvd = self.cafe.getPVCache(self.handle)
+ if pvd.alarmSeverity in (self.cyca.SEV_MINOR, self.cyca.SEV_MAJOR) \
+ or self.cafe.hasAlarmStatusSeverity(self.handle):
+ self.color_mode = self.READBACK_ALARM
+ self.status_tip = ("Widget color mode is dynamic, " +
+ "pv with alarm limits")
+ else:
+ self.color_mode = self.READBACK_STATIC
+ self.status_tip = ("Widget color mode is static, " +
+ "pv without alarm limits")
+
+
+ def qt_property_initial_values(self, qt_object_name: str = None,
+ tool_tip: bool = True):
+
+ '''Set Qt property values.'''
+ self.qt_object_name = qt_object_name
+ if tool_tip:
+ self.setToolTip(self.pv_name)
+ self.setObjectName(self.qt_object_name)
+ if self.qt_object_name in self.qt_object_to_property.keys():
+ self._qt_property_selected = copy.deepcopy(
+ self.qt_object_to_property[self.qt_object_name])
+ else:
+ print("qt_property_initial_values: Object not found in dictionary")
+
+ if self.cafe.isConnected(self.handle):
+
+ if self.qt_object_name == self.PV_READBACK:
+ self._qt_readback_color_mode()
+ #self.setStatusTip(self.status_tip)
+
+ elif self.qt_object_name == self.PV_CONTROLLER:
+ if self.color_mode == self.ACT_ON_BEAM:
+ #self.setStatusTip("PV setting acts directly on beam")
+ pass
+ else:
+ self.color_mode = self.NOT_ACT_ON_BEAM
+ #self.setStatusTip("PV setting does not influence beam")
+
+ elif self.qt_object_name == self.PV_DAQ_CA:
+ self._qt_readback_color_mode()
+
+ elif self.qt_object_name == self.PV_DAQ_BS:
+ self.color_mode = self.READBACK_STATIC
+
+ self._qt_dynamic_property_set(self.color_mode)
+
+ else:
+ self.qt_property_disconnect()
+
+
+ def qt_dynamic_property_get(self, property_state: str = None):
+ '''Retrieves the requested property value
+ else that which is currently true'''
+
+ for _property, _value in self._qt_property_selected.items():
+ if property_state is not None:
+ if _property == property_state:
+ return _value
+ elif _value:
+ return _property
+
+ def _qt_dynamic_property_set(self, property_state: str = None):
+ '''
+ Set the Input property to true, and the remainder to False
+ If None is given then all dynamic properties are set to False
+ '''
+
+ for _property in self._qt_property_selected.keys():
+ if _property == property_state:
+ self.setProperty(_property, True)
+ self._qt_property_selected[_property] = True
+ else:
+ self.setProperty(_property, False)
+ self._qt_property_selected[_property] = False
+
+ def qt_property_disconnect(self):
+ '''Set Qt disconnect property value.'''
+ self._qt_dynamic_property_set(self.DISCONNECTED)
+ self.qt_style_polish()
+
+ def qt_property_reconnect(self):
+ '''Set Qt connected property value.'''
+
+ if self.qt_object_name == self.PV_READBACK:
+ self._qt_readback_color_mode()
+ #self.setStatusTip(self.status_tip)
+
+
+ elif self.qt_object_name == self.PV_CONTROLLER:
+ if self.color_mode == self.ACT_ON_BEAM:
+ #self.setStatusTip("PV setting acts directly on beam")
+ pass
+ else:
+ self.color_mode = self.NOT_ACT_ON_BEAM
+ #self.setStatusTip("PV setting does not influence beam")
+
+
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.color_mode)
+
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_major(self):
+ '''Set Qt MAJOR property value.'''
+
+ self._qt_dynamic_property_set(self.ALARM_SEV_MAJOR)
+ self.setStatusTip("{0} reports value in MAJOR alarm state!".format(
+ self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_minor(self):
+ '''Set Qt MINOR property value.'''
+ self._qt_dynamic_property_set(self.ALARM_SEV_MINOR)
+ self.setStatusTip("{0} reports value in MINOR alarm state!".format(
+ self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_no_alarm(self):
+ '''Set Qt READBACK_ALARM property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.READBACK_ALARM)
+ self.setStatusTip("{0} reports value in normal state".format(
+ self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_invalid(self):
+ '''Set Qt INVALID property value.'''
+ self._qt_dynamic_property_set(self.ALARM_SEV_INVALID)
+ self.setStatusTip("PV={0} reports an INVALID value!".format(
+ self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_static(self):
+ '''Set Qt STATIC property value.'''
+ self._qt_dynamic_property_set(self.READBACK_STATIC)
+ self.setStatusTip("PV={0} does not have an alarm state".format(
+ self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_daq_stopped(self):
+ '''Set Qt STOPPED property value.'''
+ self._qt_dynamic_property_set(self.DAQ_STOPPED)
+ self.setStatusTip("PV={0} reports DAQ has stopped".format(
+ self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_daq_paused(self):
+ '''Set Qt STOPPED property value.'''
+ self._qt_dynamic_property_set(self.DAQ_PAUSED)
+ self.setStatusTip("PV={0} reports DAQ has paused".format(
+ self.pv_name))
+ self.qt_style_polish()
+
+ def qt_style_polish(self, redraw=False):
+ if redraw:
+ self.style().unpolish(self)
+ self.style().polish(self)
+ event = QEvent(QEvent.StyleChange)
+ QApplication.sendEvent(self, event)
+ self.update()
+ self.updateGeometry()
+ else:
+ self.style().polish(self)
+ QApplication.processEvents()
+
+ def pv_status_text_header(self, source="Channel Access"):
+ _source = source
+ _source_separator = "----------------------------------------"
+ _text = """
+
+ Widget: {0} ({1}, {2})
+ """.format(self.widget_class, self.qt_object_name, self.color_mode)
+
+ if self.msg_press_value is not None:
+ _text += """
+ On press, sends value: {0}
+ """.format(self.msg_press_value, "DarkOrchid")
+
+ if self.msg_release_value is not None:
+ _text += """
+ On release, sends value: {0}
+ """.format(self.msg_release_value, "DarkOrchid")
+
+ if self.pv_within_daq_group:
+ if self.qt_object_name in self.PV_DAQ_BS:
+ _ds_color = "Navy Blue"
+ else:
+ _ds_color = "Black"
+ else:
+ _ds_color = "Black"
+
+ _text += """
+ {0}
+ Data source: {1}
+ {0}
+ PV: {2}
+ """.format(_source_separator, _source, self.pv_name, "DarkOrchid",
+ _ds_color)
+
+ if self.desc is None:
+ self.set_desc()
+
+ if self.desc == "":
+ _text += """
+ """
+ return _text
+
+ _text += """
+
+ Description: {0}
+
+ """.format(self.desc, "DarkOrchid")
+
+ return _text
+
+
+ def pv_status_text_enum(self):
+
+ _val_enum = None
+ _value = self.pvd.value[0]
+ if isinstance(_value, str):
+ _val_enum = self.cafe.getEnumFromString(self.handle, _value)
+ elif _value is not None:
+ _val_enum = self.cafe.getStringFromEnum(self.handle, _value)
+
+ _color = "Blue"
+
+ #To catch case where channel is called by user
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+ if self.qt_object_name in self.PV_DAQ_BS:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in self.PV_DAQ_CA:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ _text = """
+
+ Value: {1} [{2}]
+ """.format(_color, _value, _val_enum)
+
+ return _text
+
+ def pv_status_text_data(self):
+
+ _value_str = ""
+ _first_end = 9
+ _end_range = min(self.pvd.nelem, _first_end)
+ if _end_range > 1:
+ _value_str = "[ "
+ for i in range(0, _end_range):
+ _value = self.pvd.value[i]
+ if _value is None:
+ _value = '0'
+ if isinstance(_value, str):
+ _value_str += _value
+ elif isinstance(_value, int):
+ _value_str += str(_value)
+ else:
+ if self.pv_ctrl is not None:
+ _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
+ _value_str += _value_form.format(
+ round(_value, self.pv_ctrl.precision))
+ if i < (_end_range-1):
+ _value_str += " "
+
+ if self.pvd.nelem > _first_end:
+ _value_str += " ... "
+ _value = self.pvd.value[self.pvd.nelem-1]
+ if isinstance(_value, str):
+ _value_str += _value
+ elif isinstance(_value, int):
+ _value_str += str(_value)
+ else:
+ if self.pv_ctrl is not None:
+ _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
+ _value_str += _value_form.format(
+ round(_value, self.pv_ctrl.precision))
+ _value_str += " "
+ if _end_range > 1:
+ _value_str += "]"
+
+ _color = "Blue"
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+
+ if self.qt_object_name in self.PV_DAQ_BS:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in self.PV_DAQ_CA:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ _text = """
+
+ Value: {1} {2}
+ """.format(_color, _value_str, self.units)
+
+ return _text
+
+
+ def pv_status_text_timestamp(self):
+ _status_not_ok_color = "IndianRed"
+ _status_ok_color = "DimGray"
+ _ts_color = "Blue"
+ _color = _status_ok_color
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+ if self.qt_object_name in self.PV_DAQ_BS:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _ts_color = "White"
+ _color = "White"
+ elif self.qt_object_name in self.PV_DAQ_CA:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _ts_color = "White"
+ _color = "White"
+
+ elif not self.cafe.isConnected(self.handle):
+ _ts_color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _ts_color = "White"
+
+ if self.pvd.status != self.cyca.ICAFE_NORMAL:
+ _color = _status_not_ok_color
+ _text = """
+ Timestamp: {2}
+ Status: {3}
{4}
+ """.format(_ts_color, _color, self.pvd.tsDateAsString,
+ self.pvd.statusAsString,
+ self.cafe.getStatusInfo(self.pvd.status))
+
+ return _text
+
+
+ def pv_status_text_alarm(self):
+ _text = """
+ """
+ _color = "DimGray"
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+
+ if self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
+ _color = "Yellow"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
+ _color = "Red"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
+ _color = "White"
+
+ if self.qt_object_name in self.PV_DAQ_BS:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in self.PV_DAQ_CA:
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
+ _color = "Yellow"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
+ _color = "Red"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
+ _color = "White"
+
+ _text += """
+ Alarm status: {1}
+ Alarm severity: {2}
+ """.format(_color, self.pvd.alarmStatusAsString,
+ self.pvd.alarmSeverityAsString)
+
+ return _text
+
+ def pv_access(self):
+ _accessIs = ""
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info.accessRead:
+ _accessIs += "Read"
+ if self.pv_info.accessWrite:
+ _accessIs += "Write"
+ return _accessIs
+
+ def pv_status_text_enum_metadata(self):
+ _text = """
+ ENUM strings: {2}
+ Data type (native): {3}
+ Record type: {4}
+ RW Access: {5}
+ IOC: {6}
+ """.format("MediumBlue", "DarkOrchid", self.pvc.enumStrings,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+ def pv_status_text_metadata(self):
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else \
+ self.pv_info.className
+ self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ if self.record_type in ["stringin", "stringout"]:
+ _text = """
+ Data type (native): {1}
+ Record type: {2}
+ RW Access: {3}
+ IOC: {4}
+ """.format("MediumBlue", self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+ _text = """
+ """
+ if self.pvd.nelem > 1:
+ _text += """
+ Nelem: {1}
+ """.format("MediumBlue", self.pvd.nelem)
+
+ _text += """
+ Precision (PV): {1}
+ Data type (native): {2}
+ Record type: {3}
+ RW Access: {4}
+ IOC: {5}
+ """.format("MediumBlue", self.pvc.precision,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+
+ def pv_status_text_alarm_limits(self):
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else \
+ self.pv_info.className
+ self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ _text = """
+ """
+
+ #No all record types have alarms
+ #className is not supported at psi since introduction of the
+ #linux ca gateway
+ #Not Supported by Gateway
+
+ if "Not Supported" in str(self.record_type):
+ pass
+ elif self.record_type not in self._alarm_severity_record_types:
+ return _text
+
+ if self.pvc.lowerAlarmLimit == 0 and self.pvc.upperAlarmLimit == 0 and \
+ self.pvc.lowerWarningLimit == 0 and self.pvc.upperWarningLimit == 0:
+ return _text
+
+ if self.cafe.hasAlarmStatusSeverity(self.handle):
+ _text = """
+ Lower/Upper alarm limit:
+ {1} / {4}
+ Lower/Upper warning limit:
+ {2} / {3}
+
+ """.format("MediumBlue",
+ self.pvc.lowerAlarmLimit, self.pvc.lowerWarningLimit,
+ self.pvc.upperWarningLimit, self.pvc.upperAlarmLimit)
+ return _text
+
+ def pv_status_text_display_limits(self):
+ _text = """
+ """
+ if self.pvc.lowerDisplayLimit == 0 and \
+ self.pvc.upperDisplayLimit == 0 and \
+ self.pvc.lowerControlLimit == 0 and self.pvc.upperControlLimit == 0:
+ return _text
+ _text = """
+ Lower/Upper control limit:
+ {3} / {4}
+ Lower/Upper display limit:
+ {1} / {2}
+
+ """.format("MediumBlue",
+ self.pvc.lowerDisplayLimit, self.pvc.upperDisplayLimit,
+ self.pvc.lowerControlLimit, self.pvc.upperControlLimit)
+ return _text
+
+
+ def pv_status_text(self):
+ '''pv metadata to accompany widget's dialog box.'''
+ QApplication.processEvents()
+ _source = "Channel Access"
+
+ if self.pv_within_daq_group:
+ if self.qt_object_name == self.PV_DAQ_BS:
+ _source = "DAQ (Beam Synchronous)"
+ #self.pvd written to in receive_daq_update
+ elif self.qt_object_name == self.PV_DAQ_CA:
+ _source = "DAQ (Channel Access)"
+ self.pvd = self.cafe.getPVCache(self.handle)
+ if self.pvd.pulseID > 0:
+ _source += "
Pulse ID: {0}".format(self.pvd.pulseID)
+ else:
+ self.pvd = self.cafe.getPVCache(self.handle)
+
+ self.pvc = self.cafe.getCtrlCache(self.handle)
+
+ _text_data = """
+ """
+ if self.pvd.status == self.cyca.ECAFE_INVALID_HANDLE:
+ _text_data = """ Status: {1}
{2}
+ """.format("Blue",
+ "Channel closed while DAQ in STOP state.",
+ ("PV info requires DAQ to be in " +
+ "RUN/PAUSED state"))
+
+
+ elif self.pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
+ _text_data = """ Status: {1}
{2}
+ """.format("Red", self.pvd.statusAsString,
+ self.cafe.getStatusInfo(self.pvd.status))
+
+ elif self.pvc.noEnumStrings > 0:
+ _text_data = (self.pv_status_text_enum() +
+ self.pv_status_text_timestamp() +
+ self.pv_status_text_alarm() +
+ self.pv_status_text_enum_metadata())
+
+ else:
+ _text_data = (self.pv_status_text_data() +
+ self.pv_status_text_timestamp() +
+ self.pv_status_text_alarm() +
+ self.pv_status_text_metadata() +
+ self.pv_status_text_alarm_limits() +
+ self.pv_status_text_display_limits())
+
+ self.pv_message_in_a_box.setText(
+ self.pv_status_text_header(source=_source) + _text_data
+ )
+ QApplication.processEvents()
+ self.pv_message_in_a_box.exec()
+
+
+ def lookup_archiver(self):
+ '''Plot pvdata from archiver.'''
+ #"https://ui-data-api.psi.ch/prepare?
+ #channel=sf-archiverappliance/"
+ urlIs = self.url_archiver
+ urlIs = urlIs + self.pv_name
+
+ if not QDesktopServices.openUrl(QUrl(urlIs)):
+ print("URL FOR ARCHIVER NOT FOUND", urlIs)
+ #if self.show_log_message is not None:
+ # self.show_log_message(MsgSeverity.ERROR, __pymodule__, _line(),
+ # "Failed to open URL {0}".format(urlIs))
+
+ def lookup_databuffer(self):
+ '''Plot beam synchronous pvdata from databuffer.'''
+ #""https://ui-data-api.psi.ch/prepare?channel = sf-databuffer/"
+ urlIs = self.url_databuffer
+ urlIs = urlIs + self.pv_name
+
+ if not QDesktopServices.openUrl(QUrl(urlIs)):
+ print("URL FOR DATA BUFFER NOT FOUND", urlIs)
+ #if self.show_log_message is not None:
+ # self.show_log_message(MsgSeverity.ERROR, __pymodule__, _line(),
+ # "Failed to open URL {0}".format(urlIs))
+ QApplication.processEvents()
+
+ def strip_chart(self):
+ '''PShell strip chart.'''
+ configStr = ("-config = [[[true,\"" + self.pv_name +
+ "\",\"Channel\",1,1]]]")
+ commandStr = "/sf/op/bin/strip_chart"
+ argStr = ["-nlaf", "-start", configStr, "&"]
+ QProcess.startDetached(commandStr, argStr)
+
+
+ def display_parameters(self):
+ display_wgt = QDialog(self)
+
+ _rect = display_wgt.geometry() #
+ _parentRect = self.context_menu.geometry()
+
+ _rect.moveTo(display_wgt.mapToGlobal(
+ QPoint(_parentRect.x() + _parentRect.width() - _rect.width(),
+ _parentRect.y())))
+
+ display_wgt.setGeometry(_rect)
+ display_wgt.setWindowTitle(self.pv_name)
+ layout = QVBoxLayout()
+
+ precision_flag = True
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.precision <= 0:
+ precision_flag = False
+
+ if self.cafe.getDataTypeNative(self.handle) in (
+ self.cyca.CY_DBR_FLOAT,
+ self.cyca.CY_DBR_DOUBLE) and precision_flag:
+ #precision user
+ _hbox_wgt = QWidget()
+ _hbox = QHBoxLayout()
+ precision_user_label = QLabel("Precision (user):")
+ self.precision_user_wgt = QSpinBox(self)
+ self.precision_user_wgt.setFocusPolicy(Qt.NoFocus)
+ self.precision_user_wgt.setValue(int(self.precision))
+ if self.pv_ctrl is not None:
+ _max = self.pv_ctrl.precision
+ else:
+ _max = 6
+ self.precision_user_wgt.setMaximum(_max)
+ self.precision_user_wgt.valueChanged.connect(
+ self.precision_user_changed)
+ _hbox.addWidget(precision_user_label)
+ _hbox.addWidget(self.precision_user_wgt)
+ _hbox_wgt.setLayout(_hbox)
+
+ precision_user_label.setFixedWidth(110)
+ self.precision_user_wgt.setFixedWidth(35)
+ _hbox_wgt.setFixedWidth(160)
+
+ #precision ioc
+ _hbox2_wgt = QWidget()
+ _hbox2 = QHBoxLayout()
+ precision_ioc_label = QLabel("Precision (ioc): ")
+ precision_ioc = QPushButton(self)
+ precision_ioc.setText(" {} ".format(_max))
+ precision_ioc.clicked.connect(self.precision_ioc_reset)
+
+ _hbox2.addWidget(precision_ioc_label)
+ _hbox2.addWidget(precision_ioc)
+ _hbox2_wgt.setLayout(_hbox2)
+
+ precision_ioc_label.setFixedWidth(110)
+ precision_ioc.setFixedWidth(20)
+ _hbox2_wgt.setFixedWidth(145)
+
+ layout.addWidget(_hbox_wgt)
+ layout.addWidget(_hbox2_wgt)
+
+ #precision refresh rate
+ _hbox3_wgt = QWidget()
+ _hbox3 = QHBoxLayout()
+ refresh_freq_label = QLabel("Refresh rate: ")
+ _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
+ self.notify_freq_hz_default
+
+ self.refresh_freq_combox_idx_dict = {0: 0, 1: 10, 2: 5, 3: 2, 4: 1,
+ 5: 0.5, 6: _default_refresh_val}
+ refresh_freq = QComboBox(self)
+ refresh_freq.addItem('direct')
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[1]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[2]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[3]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[4]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[5]))
+
+ _default_text = 'default (direct)' if _default_refresh_val == 0 else \
+ 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
+
+ refresh_freq.addItem(_default_text)
+
+ for key, value in self.refresh_freq_combox_idx_dict.items():
+ if value == self.notify_freq_hz:
+ refresh_freq.setCurrentIndex(key)
+ break
+ refresh_freq.currentIndexChanged.connect(self.refresh_rate_changed)
+
+ _hbox3.addWidget(refresh_freq_label)
+ _hbox3.addWidget(refresh_freq)
+ _hbox3_wgt.setLayout(_hbox3)
+
+ refresh_freq_label.setFixedWidth(110)
+ refresh_freq.setFixedWidth(115)
+ _hbox3_wgt.setFixedWidth(235)
+
+ layout.addWidget(_hbox3_wgt)
+
+ layout.setAlignment(Qt.AlignLeft)
+ layout.setContentsMargins(10, 0, 0, 0)
+ layout.setSpacing(0)
+
+ display_wgt.setMinimumWidth(340)
+ display_wgt.setLayout(layout)
+
+ display_wgt.exec()
+ QApplication.processEvents()
+
+ def precision_ioc_reset(self):
+ if self.pv_ctrl is not None:
+ self.precision_user = self.pv_ctrl.precision
+ self.precision = self.pv_ctrl.precision
+ if self.precision is not None:
+ self.precision_user_wgt.setValue(self.precision)
+
+ def precision_user_changed(self, new_value):
+ self.precision_user = new_value
+ self.precision = new_value
+
+ _pvd = self.cafe.getPVCache(self.handle)
+
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ self.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+
+ def refresh_rate_changed(self, new_idx):
+ _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
+ self.notify_milliseconds = 0 if _notify_freq_hz == 0 else \
+ 1000 / _notify_freq_hz
+ self.notify_freq_hz = _notify_freq_hz
+
+ if self.notify_unison:
+ self.notify_unison = False
+ self.monitor_stop()
+ self.monitor_start()
+
+ else:
+ self.cafe.updateMonitorPolicyDeltaMS(
+ self.handle, self.monitor_id, self.notify_milliseconds)
+
+ #https://doc.qt.io/qt-5.9/qtwidgets-mainwindows-menus-example.html
+ #Since Qt5 this has to be implemented in order to avoid the Select
+ #All dialogue button appearing..
+ def contextMenuEvent(self, event):
+ return
+
+ def showContextMenu(self):
+ self.context_menu.exec(QCursor.pos())
+
+ def mousePressEvent(self, event):
+ '''Action on mouse press event.'''
+ button = event.button()
+ if button == Qt.RightButton:
+ self.context_menu.exec(QCursor.pos())
+ self.clearFocus()
+
+ def mouseReleaseEvent(self, event):
+ event.ignore()
+
diff --git a/pvgateway.py:2.9 b/pvgateway.py:2.9
new file mode 100644
index 0000000..3464c85
--- /dev/null
+++ b/pvgateway.py:2.9
@@ -0,0 +1,1910 @@
+"""The module provides data and metadata of a process variable through PyCafe."""
+__author__ = 'Jan T. M. Chrin'
+
+import copy
+import inspect
+import sys
+import time
+
+from datetime import datetime
+from distutils.version import LooseVersion
+
+from qtpy.QtCore import (QEvent, QObject, QMutex, QPoint, QProcess, QRect,
+ QSettings, Qt, QUrl, Signal, Slot)
+from qtpy.QtCore import __version__ as QT_VERSION_STR
+from qtpy.QtGui import (QColor, QCursor, QDesktopServices, QFont, QPainter,
+ QPalette)
+from qtpy.QtWidgets import (QAction, QApplication, QComboBox, QDialog,
+ QHBoxLayout, QLabel, QLineEdit, QMenu, QMessageBox,
+ QPushButton, QSpinBox, QVBoxLayout, QWidget)
+
+from bdbase.readjson import Rjson
+from bdbase.enumkind import DAQState
+
+def __LINE__():
+ return inspect.currentframe().f_back_f_lineno
+
+
+class PVGateway(QWidget):
+ """Retrieves pv metadata through PyCafe.
+
+ The PVGateway class when subclassed by Qt widgets enables their connectivity
+ to channel access.
+
+ Attributes:
+ monid: (int) Monitor id
+ units : (str) Units associated with the pv
+
+ trigger_monitor_ is the signal triggered by updates arising from
+ monitored pvs.
+ trigger_connect is the signal triggered from changes in pv connection status.
+ widget_handle_dict is a dictionary mapping widgets to their pv handle.
+ A pv handle may be associated to more than one widget.
+ """
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int) #pvdata, status
+
+
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+ #Properties, user supplied
+ ACT_ON_BEAM = 'actOnBeam'
+ NOT_ACT_ON_BEAM = 'notActOnBeam'
+ READBACK_ALARM = 'alarm'
+ READBACK_STATIC = 'static'
+
+ #Properties, dynamic
+ DISCONNECTED = 'disconnected'
+ ALARM_SEV_MINOR = 'alarmSevMinor'
+ ALARM_SEV_MAJOR = 'alarmSevMajor'
+ ALARM_SEV_INVALID = 'alarmSevInvalid'
+ ALARM_SEV_NO_ALARM = READBACK_ALARM
+ DAQ_STOPPED = 'stopped'
+ DAQ_PAUSED = 'paused'
+
+ #ObjectName, defined by CAQ
+ PV_CONTROLLER = "Controller"
+ PV_READBACK = "Readback"
+ PV_DAQ_BS = "BSRead"
+ PV_DAQ_CA = "CARead"
+
+ _DAQ_CAFE_SG_NAME = "gBS2CA"
+
+ _alarm_severity_record_types = ["ai", "ao", "calc", "calcout", "dfanout",
+ "longin", "longout", "pid", "sel",
+ "steppermotor", "sub"]
+
+ #parent is Gui
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ connect_callback=None, msg_label: str = "",
+ connect_triggers: bool = True, notify_freq_hz: int = 0,
+ notify_unison: bool = False, precision: int = 0,
+ monitor_dbr_time: bool = False):
+
+ #super(PVGateway, self).__init__() # do NOT use parent
+ #It turned out a widget was created with the main window as a parent, but incorrectly placed.
+ #Parent must not be QMainWindow. This interferes with the toolbar!! 16 Aug. 2020
+ super().__init__()
+
+ if parent is None:
+ return
+
+ if pv_name is "":
+ return
+
+ self.connect_callback = connect_callback
+ self.notify_freq_hz = abs(notify_freq_hz)
+ self.notify_freq_hz_default = self.notify_freq_hz
+
+ self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
+ 1000 / self.notify_freq_hz
+
+ self.notify_unison = True if notify_unison and \
+ self.notify_freq_hz > 0 else False
+
+ self.parent = parent
+ self.pv_name = pv_name
+
+ self.color_mode = None
+
+ if color_mode is not None:
+ if color_mode in (self.ACT_ON_BEAM,
+ self.NOT_ACT_ON_BEAM,
+ self.READBACK_ALARM,
+ self.READBACK_STATIC):
+ self.color_mode = color_mode
+
+ self.color_mode_requested = self.color_mode
+
+ if monitor_callback is not None:
+ self.monitor_callback = monitor_callback
+ else:
+ self.monitor_callback = None
+
+ self.pv_within_daq_group = pv_within_daq_group
+
+ self.show_units = show_units
+ self.prefix = prefix
+ self.suffix = suffix
+
+ self.cafe = self.parent.cafe
+ self.cyca = self.parent.cyca
+ if self.parent.settings is not None:
+ self.settings = self.parent.settings
+ else:
+ self.settings = Rjson(self.parent.appname)
+
+ self.daq_group_name = self._DAQ_CAFE_SG_NAME
+ self.desc = None
+ self.handle = None
+ self.initialize_complete = False
+ self.initialize_again = False
+
+ self.msg_label = msg_label
+ self.msg_press_value = None
+ self.msg_release_value = None
+
+ self.monitor_id = None
+ self.monitor_dbr_time = monitor_dbr_time
+
+ self.mutex_post_display = QMutex()
+
+
+ self.precision_user = precision
+ self.has_precision_user = True if precision > 0 else False
+ self.precision_pv = 3
+
+ self.precision = (self.precision_user if self.has_precision_user else
+ self.precision_pv)
+
+ self.pvd = None
+ self.pv_ctrl = None
+ self.pv_info = None
+ self.record_type = None
+
+ if self.parent.showMessage:
+ self.showMessage = self.parent.showMessage
+ else:
+ self.showMessage = None
+
+ self.qt_object_name = None
+
+ self.qt_property_controller = {
+ self.DISCONNECTED : False,
+ self.ACT_ON_BEAM : False, self.NOT_ACT_ON_BEAM : False
+ }
+
+ self.qt_property_readback = {
+ self.DISCONNECTED : False,
+ self.READBACK_ALARM : False, self.READBACK_STATIC : False,
+ self.ALARM_SEV_MINOR : False, self.ALARM_SEV_MAJOR : False,
+ self.ALARM_SEV_INVALID : False
+ }
+
+ self.qt_property_daq_bs = {
+ self.DISCONNECTED : False,
+ self.READBACK_ALARM : False, self.READBACK_STATIC : False,
+ self.ALARM_SEV_MINOR : False, self.ALARM_SEV_MAJOR : False,
+ self.ALARM_SEV_INVALID : False,
+ self.DAQ_STOPPED : False,
+ self.DAQ_PAUSED : False
+ }
+
+ self.qt_property_daq_ca = {
+ self.DISCONNECTED : False,
+ self.READBACK_ALARM : False, self.READBACK_STATIC : False,
+ self.ALARM_SEV_MINOR : False, self.ALARM_SEV_MAJOR : False,
+ self.ALARM_SEV_INVALID : False,
+ self.DAQ_STOPPED : False,
+ self.DAQ_PAUSED : False
+ }
+
+ self.qt_object_to_property = {
+ self.PV_CONTROLLER : self.qt_property_controller,
+ self.PV_READBACK : self.qt_property_readback,
+ self.PV_DAQ_BS : self.qt_property_daq_bs,
+ self.PV_DAQ_CA : self.qt_property_daq_ca
+ }
+
+ self._qt_property_selected = {}
+
+ self.status_tip = None
+ self.suggested_text = ""
+ self.time_monotonic = time.monotonic()
+ self.pvd_previous = None
+ self.timeout = 0.2
+ self.units = ""
+
+ self.widget = self
+
+ _widget_name_part = str(self.widget.__class__).split("\'")[1].split(".")
+ _widget_class_part = _widget_name_part[1].split(".")
+ self.widget_class = _widget_name_part[len(_widget_name_part)-1]
+
+ if pv_within_daq_group:
+ self.trigger_daq_int.connect(self.receive_daq_update)
+ self.trigger_daq.connect(self.receive_daq_update)
+ self.trigger_daq_str.connect(self.receive_daq_update)
+
+ elif connect_triggers:
+ self.trigger_monitor.connect(self.receive_monitor_dbr_time)
+ self.trigger_monitor_str.connect(self.receive_monitor_update)
+ self.trigger_monitor_int.connect(self.receive_monitor_update)
+ self.trigger_monitor_float.connect(self.receive_monitor_update)
+ self.trigger_connect.connect(self.receive_connect_update)
+
+ self.context_menu = QMenu()
+ self.context_menu.setObjectName("contextMenu")
+ self.context_menu.setWindowModality(Qt.NonModal) #ApplicationModal
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.0"):
+ self.context_menu.addSection("PV: {0}".format(self.pv_name))
+
+ action1 = QAction("Text Info", self)
+ action1.triggered.connect(self.pv_status_text)
+
+ action2 = QAction("Lookup in Archiver", self)
+ action2.triggered.connect(self.lookup_archiver)
+
+ action3 = QAction("Lookup in Databuffer", self)
+ action3.triggered.connect(self.lookup_databuffer)
+
+ action4 = QAction("Strip Chart (PShell)", self)
+ action4.triggered.connect(self.strip_chart)
+
+ action6 = QAction("Configure Display Parameters", self)
+ action6.triggered.connect(self.display_parameters)
+
+ self.context_menu.addAction(action1)
+ self.context_menu.addAction(action2)
+ self.context_menu.addAction(action3)
+ self.context_menu.addAction(action4)
+
+ action5 = QAction("Reconnect: {0}".format(self.pv_name), self)
+ action5.triggered.connect(self.reconnect_channel)
+ _font = QFont()
+ _font.setPixelSize(12)
+ action5.setFont(_font)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.context_menu.addSection("")
+
+ #return action6 and 5 code here eventually
+ self.context_menu.addAction(action6)
+ self.context_menu.addAction(action5)
+
+ self.pv_message_in_a_box = QMessageBox()
+ self.pv_message_in_a_box.setObjectName("pvinfo")
+
+ #Qt.NonModal often causes harmless QXcbConnection: XCB error: 3 (BadWindow), sequence:
+ #but only if the window is closed too quickly(!)
+ #Qt.ApplicationModal not used as it blocks input to all windows
+ self.pv_message_in_a_box.setWindowModality(Qt.NonModal) #Qt.ApplicationModal
+ self.pv_message_in_a_box.setIcon(QMessageBox.Information)
+ self.pv_message_in_a_box.setStandardButtons(QMessageBox.Close) # Shows QMessageBox.Close shows Close
+ self.pv_message_in_a_box.setDefaultButton(QMessageBox.Close) #Show OK
+
+ self.initialize()
+ '''
+ #temporary code position
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.precision > 0:
+ self.context_menu.addAction(action6)
+
+ self.context_menu.addAction(action5)
+ '''
+ return self
+
+
+ def initialize(self):
+ '''Initialze class attributes and connect to ca if required.'''
+
+ _handle_within_group_flag = False
+ if self.pv_within_daq_group:
+ self.handle = self.cafe.getHandleFromPVWithinGroup(
+ self.pv_name, self.daq_group_name)
+ if self.handle > 0:
+ self.cafe.addWidget(self.handle, self.widget)
+ _handle_within_group_flag = True
+ #Callback already invoked to emit signal here!!
+ _channel_info = self.cafe.getChannelInfo(self.handle)
+ print(self.pv_name, self.handle)
+ w = self.cafe.getWidgets(self.handle)
+ print("widget list", w)
+ #_channel_info.show()
+
+ self.trigger_connect.emit(
+ int(self.handle), str(self.pv_name),
+ int(_channel_info.cafeConnectionState))
+ #In case user is misinformed
+ if not _handle_within_group_flag:
+ self.handle = self.cafe.getHandleFromPV(self.pv_name)
+ if self.connect_callback is None:
+ self.connect_callback = self.py_connect_callback
+
+ if self.handle > 0:
+
+ #The second time round, widget is gateway rather than parent, Why is that?
+ self.cafe.setPyConnectCallbackFn(self.handle,
+ self.connect_callback)
+
+ self.cafe.addWidget(self.handle, self.widget)
+
+ _channel_info = self.cafe.getChannelInfo(self.handle)
+ self.trigger_connect.emit(
+ self.handle, self.pv_name,
+ int(_channel_info.cafeConnectionState))
+
+
+ #print("====OLD===============", self.handle, self.widget, self.parent)
+ else:
+
+ self.cafe.openPrepare()
+ self.handle = self.cafe.open(self.pv_name,
+ self.connect_callback)
+ self.cafe.addWidget(self.handle, self.widget)
+ self.cafe.openNowAndWait(self.timeout, self.handle)
+ #self.cafe.openNow()
+ #_channel_info = self.cafe.getChannelInfo(self.handle)
+ #self.trigger_connect.emit(int(self.handle), str(self.pv_name), int(_channel_info.cafeConnectionState))
+ #print("====NEW============ ==", self.handle, self.widget)
+
+ self.initialize_meta_data()
+
+ self.pv_message_in_a_box.setWindowTitle(self.pv_name)
+
+
+ def initialize_meta_data(self):
+
+ _current_value = ""
+
+ if self.cafe.isConnected(self.handle) and \
+ self.cafe.initCallbackComplete(self.handle):
+
+ if self.pvd is None:
+ self.pvd = self.cafe.getPVCache(self.handle)
+
+ if self.pv_ctrl is None:
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+ self.set_precision_and_units()
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.pv_name)
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ _rtype = self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+ #print ("record_type", self.record_type)
+
+
+ _current_value = self.cafe.getCache(self.handle)
+ if isinstance(_current_value, (int, float)):
+ #space for positive numbers
+ _value_form = ("{:<+.%sf}" % self.precision)
+ _current_value = _value_form.format(
+ round(_current_value, self.precision))
+ #if self.desc is None:
+ # self.set_desc()
+
+ #Reset
+ self.initialize_complete = True
+
+ #verify user input
+ if self.show_units is True:
+ if self.suffix == self.units and self.units != "":
+ self.show_units = False
+
+ self.suggested_text = self.prefix
+ if len(self.prefix) > 0:
+ self.suggested_text += " "
+
+ _suggested_text_from_value = " "
+
+ _max_control_abs = 0
+
+ if self.pv_ctrl is not None:
+ _lower_control_abs = abs(int(self.pv_ctrl.lowerControlLimit))
+ _upper_control_abs = abs(int(self.pv_ctrl.upperControlLimit))
+ _max_control_abs = max(_lower_control_abs, _upper_control_abs)
+ if _max_control_abs is None:
+ _max_control_abs = 0
+
+ _enum_list = self.pv_ctrl.enumStrings
+
+ if len(_enum_list) > 0:
+ _enum_list_member_max_length = 0
+ _enum_list_member_max_index = 0
+
+ for i in range(0, len(_enum_list)):
+ if len(_enum_list[i]) > _enum_list_member_max_length:
+ _enum_list_member_max_length = len(_enum_list[i])
+ _enum_list_member_max_index = i
+ _suggested_text_from_value += \
+ _enum_list[_enum_list_member_max_index] + "." #Add extra space
+ else:
+ if self.pv_ctrl.lowerControlLimit < 0:
+ _suggested_text_from_value += "-"
+ _suggested_text_from_value += str(_max_control_abs) + "."
+
+
+ #print("precision", self.precision, self.pv_name)
+ self.precision = min(9, self.precision) #safety net
+ for i in range (0, self.precision):
+ _suggested_text_from_value += "0"
+
+ if len(_current_value) > len(_suggested_text_from_value):
+ _suggested_text_from_value = _current_value
+
+ self.suggested_text += _suggested_text_from_value
+
+ if self.show_units:
+ self.suggested_text += " " + self.units
+ self.suggested_text += self.suffix
+
+ _suggested_text_length = len(self.suggested_text)
+ self.suggested_text = self.suggested_text.center(
+ _suggested_text_length+2)
+
+ self.max_control_abs_str = str(_max_control_abs)
+
+
+ _max_control_abs_length = len(self.max_control_abs_str)
+ _offset = 9
+ self.max_control_abs_str = self.max_control_abs_str.center(
+ _max_control_abs_length + _offset)
+
+
+ qsettings = QSettings()
+ qsettings.beginGroup("Widget")
+ qsettings.beginGroup(self.pv_name)
+ qsettings.beginGroup(self.widget_class)
+ #_var_base = "Widget/" + self.pv_name + "/" + self.widget_class + "/"
+ _var_text = "suggested_text"
+ _ctrl_abs = "max_control_abs_str"
+
+ if self.cafe.isConnected(self.handle) and \
+ self.cafe.initCallbackComplete(self.handle):
+ qsettings.setValue(_var_text, self.suggested_text)
+ qsettings.setValue(_ctrl_abs, self.max_control_abs_str)
+ else:
+ if qsettings.value(_var_text) is not None:
+ self.suggested_text = qsettings.value(_var_text)
+ if qsettings.value(_ctrl_abs) is not None:
+ self.max_control_abs_str = qsettings.value(_ctrl_abs)
+
+
+ qsettings.endGroup()
+ qsettings.endGroup()
+ qsettings.endGroup()
+
+
+ def is_initialize_complete(self):
+ icount = 0;
+ while not self.initialize_complete :
+ time.sleep(0.01)
+ self.initialize_meta_data()
+ icount += 1
+ if icount > 50:
+ return False
+ return True
+
+ def cleanup(self, close_pv = True):
+ '''Clean up the widget.'''
+ ##Check for monitors
+ ##QWidget::close() is basically a combination of QWidget::closeEvent(), QWidget::hide(), and QObject::deleteLater() (if Qt::WA_DeleteOnClose is set).
+
+ #Make sure mon id is valid
+ if self.handle > 0:
+ _monID_list = self.cafe.getMonitorIDs(self.handle)
+ if self.monitor_id in _monID_list:
+ self.cafe.monitorStop(self.handle, self.monitor_id)
+ #self.cafe.monitorStop(self.handle, self.monitor_id)
+ #Do not close of there are other monitors
+ if self.cafe.getNoMonitors(self.handle) > 0:
+ if close_pv is True:
+ self.cafe.close(self.pv_name)
+ self.widget.deleteLater()
+
+
+ def format_display_value(self, value):
+
+ if value is None:
+ print(self, self.pv_name, ">>>>>>>>>>>>format_display_value is None>>>>>>")
+ #return
+
+ if isinstance(value, str):
+ _value_str = value
+ elif isinstance(value, int):
+ _value_str = str(value)
+ else:
+ _value_form = ("{:< .%sf}" % self.precision) #space for positive numbers
+ #print("v/prec", value, self.precision, flush=True)
+
+ _rounded_value = round(value, self.precision)
+ #print(_rounded_value, flush=True)
+ _value_str = _value_form.format(_rounded_value)
+ #print(_value_str, flush=True)
+
+ if self.show_units:
+ _value_str += " " + self.units + " "
+ if self.suffix is not "":
+ _value_str += " " + self.suffix + " "
+
+ if self.prefix is not "":
+ _space = ""
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.lowerDisplayLimit < 0:
+ _space = " "
+ _value_str = self.prefix + _space + _value_str
+
+ return _value_str
+
+ def post_display_value(self, value):
+
+ #self.mutex_post_display.lock()
+
+ _value_str = self.format_display_value(value)
+
+ if "setText" in dir(self):
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ self.setText(_value_str)
+ self.blockSignals(False)
+ else:
+ #print("value =", _value_str, flush=True)
+ self.setText(_value_str)
+
+ else:
+ print("setText method does not exist for this widget class:\n", self.widget.__class__)
+ print("sender was: ", self.sender())
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ Checks for existence of widget. Waits up to a maximun of 100 ms.
+ '''
+ #print(" py_connect_callback:: START ")
+ #print(" py_connect_callback:: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", pvname)
+ #print(handle, pvname, status, self.cafe.getStatusCodeAsString(status))
+
+ pv_name = pvname
+ _widget = None
+ #_widgetList =self.cafe.getWidgets(handle)
+ #for i in range(0, len(_widgetList)):
+ #_widget = _widgetList[i]
+ #_widget.trigger_connect.emit(int(handle), str(pv_name), int(status))
+ #print (i, "widget at connect>>>>>>>>>>>>>>>>>>>>>>", _widget.__class__)
+ self.trigger_connect.emit(int(handle), str(pv_name), int(status))
+ #print(" py_connect_callback:: END ")
+
+
+ def receive_connect_update(self, handle, pv_name, status, post_display=True):
+ '''Triggered by connect signal. For Widget to overload.'''
+
+ #print(" receive _connect_callback:: START ")
+ #print ("RRReceive_connect triggered for widget", self, "with status", status)
+ #print ("RRReceive_connect triggered for widget", handle, pv_name, status)
+ _alarm_severity = None
+ if status == self.cyca.ICAFE_CS_CONN:
+ self.initialize_connect = True
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ _rtype = self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ #print(self.pv_name)
+ #print("record type====>", self.record_type)
+ else:
+ self.record_type = self.pv_info.className
+
+ self.set_precision_and_units(reconnectFlag=True)
+ #THis will connect to a new channel
+ #if self.desc is None:
+ # self.set_desc()
+ #print("msg_lab", self.msg_label, "/", len(self.msg_label))
+
+ if self.msg_label == "":
+ _value = self.cafe.getCache(handle, dt='native')
+ #Another reconnection in progress!!!
+
+ if _value == None:
+ return
+ else:
+ _value = self.msg_label
+ #print("_value", _value, "/", len(_value))
+
+ #print("_value", _value, "/")
+ if post_display:
+ self.post_display_value(_value)
+ self.qt_property_reconnect()
+
+ else:
+ self.qt_property_disconnect()
+
+
+ if status == self.cyca.ICAFE_CS_CLOSED:
+ self.initialize_again = True
+
+ elif self.initialize_again:
+ #monitos_id informs whether or not widget has a monitor
+ #CAQMessageButton for instance does not have a monitor
+
+ if not self.pv_within_daq_group and self.monitor_id is not None:
+ self.monitor_start()
+ #print("RESTART MONITOR FOR THIS WIDGET", flush=True)
+
+ self.initialize_again = False
+
+ #print(" receive _connect_callback:: END ")
+
+ return
+
+
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ ''' DAQ mode is widget specific.
+ DAQ may be in BS mode, but channels within DAQ stream that
+ are not BS enabled will be flagged as CA Mode, i.e., CARead
+ '''
+
+ _current_qt_dynamic_property = self.qt_dynamic_property_get()
+
+ alarm_severity = daq_pvd.alarmSeverity
+ self.pvd = daq_pvd
+
+ #print("BEFORE mode, object_name, daqState", daq_mode, self.qt_object_name, daq_state)
+
+ if daq_mode != self.qt_object_name:
+ self.qt_object_name = daq_mode
+ self.setObjectName(self.qt_object_name)
+ self.qt_style_polish()
+
+ #print("AFTER mode, object_name, daqState", daq_mode, self.qt_object_name, daq_state)
+
+ if daq_state in (self.cyca.ICAFE_DAQ_STOPPED,):
+ #if daq_state in (DAQState.CA_STOP, DAQState.BS_STOP, DAQState.CA_PAUSE,
+ # DAQState.BS_PAUSE):
+ if _current_qt_dynamic_property != self.DAQ_STOPPED:
+ self.qt_property_daq_stopped()
+
+ elif daq_state in (self.cyca.ICAFE_DAQ_PAUSED,):
+ if _current_qt_dynamic_property != self.DAQ_PAUSED:
+ self.qt_property_daq_paused()
+
+ elif daq_state in (self.cyca.ICAFE_DAQ_RUN,):
+ #if _current_qt_dynamic_property != self.READBACK_ALARM:
+ # self.qt_property_alarm_sev_no_alarm()
+ #print ("before", daq_mode, _current_qt_dynamic_property)
+
+ if daq_mode == self.PV_DAQ_BS and \
+ _current_qt_dynamic_property != self.READBACK_STATIC:
+ self.qt_property_static()
+
+ elif daq_mode == self.PV_DAQ_CA:
+ #if _current_qt_dynamic_property not in (self.READBACK_STATIC,
+ # self.READBACK_ALARM,):
+ if self.color_mode != self.color_mode_requested:
+ self.color_mode == self.color_mode_requested
+ #print("new colore mode")
+
+ if self.cafe.isEnum(self.handle) and \
+ isinstance(daq_pvd.value[0], int):
+ _value = self.cafe.getStringFromEnum(self.handle,
+ daq_pvd.value[0])
+ else:
+ _value = daq_pvd.value[0]
+
+ if daq_pvd.status == self.cyca.ICAFE_NORMAL:
+ if self.msg_label == "":
+ self.post_display_value(_value)
+
+ if daq_mode == self.PV_DAQ_BS:
+ return
+
+ #Fro DAQ when channel connects after application start-up
+ #if _current_qt_dynamic_property == self.DISCONNECTED:
+ # self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+ #Check if color settings are correct
+ ##if _current_qt_dynamic_property == self.READBACK_STATIC and \
+ if alarm_severity > self.cyca.SEV_NO_ALARM:
+ self.color_mode = self.READBACK_ALARM
+ self.color_mode_requested = self.READBACK_ALARM
+
+ if self.color_mode == self.READBACK_ALARM:
+ if alarm_severity == self.cyca.SEV_MINOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
+ self.qt_property_alarm_sev_minor()
+
+ elif alarm_severity == self.cyca.SEV_MAJOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
+ self.qt_property_alarm_sev_major()
+
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ elif alarm_severity == self.cyca.SEV_NO_ALARM:
+ if _current_qt_dynamic_property != self.ALARM_SEV_NO_ALARM:
+ self.qt_property_alarm_sev_no_alarm()
+
+ elif _current_qt_dynamic_property != self.READBACK_STATIC:
+ self.qt_property_static()
+
+ #print ("after", daq_mode, self.qt_dynamic_property_get() )
+ else:
+ if _current_qt_dynamic_property != self.DISCONNECTED:
+ self.qt_property_disconnect()
+
+
+ def receive_monitor_dbr_time(self, pvdata, alarm_severity):
+ print("in gateway", self.pv_name)
+ #pvdata.show()
+
+ def receive_monitor_update(self, value, status, alarm_severity):
+ '''Triggered by monitor signal. For Widget to overload.'''
+
+ self.mutex_post_display.lock()
+ _current_qt_dynamic_property = self.qt_dynamic_property_get()
+ #print(self.pv_name, value, status, alarm_severity)
+ #if isinstance(value, (int, float)):
+ # if value < 100:
+ # print("CURRENT PROPERY VALUE", self.pv_name, _current_qt_dynamic_property, value, status, alarm_severity )
+ #print ("sender //2//", self.sender(), value)
+
+ #print("receive monitor update/1", self.pv_name, self.qt_object_name, self._qt_property_selected)
+
+ if status == self.cyca.ICAFE_NORMAL:
+ '''
+ if isinstance(value, (int, float)):
+
+ if value < -20:
+ alarm_severity = self.cyca.SEV_INVALID
+ elif value < -1:
+ alarm_severity = self.cyca.SEV_MAJOR
+ elif value < 5:
+ alarm_severity = self.cyca.SEV_MINOR
+ else:
+ alarm_severity = self.cyca.SEV_NO_ALARM
+ '''
+
+ if self.msg_label == "":
+ self.post_display_value(value)
+
+ #For DAQ when channel connects after application start-up
+ if _current_qt_dynamic_property == self.DISCONNECTED:
+ self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+ #Check if color settings are correct
+ elif _current_qt_dynamic_property == self.READBACK_STATIC:
+ if alarm_severity > self.cyca.SEV_NO_ALARM and \
+ alarm_severity < self.cyca.SEV_INVALID:
+ self.color_mode = self.READBACK_ALARM
+ self.status_tip = "Widget color mode is dynamic, pv with alarm limits"
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ if self.color_mode == self.READBACK_ALARM:
+
+ if alarm_severity == self.cyca.SEV_MINOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
+ self.qt_property_alarm_sev_minor()
+
+ elif alarm_severity == self.cyca.SEV_MAJOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
+ self.qt_property_alarm_sev_major()
+
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ elif alarm_severity == self.cyca.SEV_NO_ALARM:
+ if _current_qt_dynamic_property != self.ALARM_SEV_NO_ALARM:
+ self.qt_property_alarm_sev_no_alarm()
+
+ else:
+ if _current_qt_dynamic_property != self.DISCONNECTED:
+ self.qt_property_disconnect()
+
+ self.mutex_post_display.unlock()
+
+ #print("receive monitor update/2", self.pv_name, self.qt_object_name, self._qt_property_selected)
+
+ def py_monitor_callback(self, handle, pvname, pvdata):
+
+
+ '''Callback function to be invoked on change of pv value.
+ cafe.getCache and cafe.set operations permitted within callback.
+ '''
+ '''
+ if "PULSEID" in pvname:
+ pass
+ else:
+ print ("py_monitor_callback: name/handle ",pvname, handle )
+ '''
+ pv_name = pvname
+ pvd = pvdata
+ #print("===================================")
+ #print("pvname/handle in mon callback ", pv_name, handle)
+
+
+ if not hasattr(self, 'cafe'):
+ print ("py_monitor_callback: name/handle self cafe is NONE =>>>>>>>>>>>> ",
+ pv_name, handle)
+ return
+ #pv_name = self.cafe.getPVNameFromHandle(self.handle)
+ #pvd = self.cafe.getPVCache(self.handle)
+
+ self.pvd = pvd
+ '''
+ if pvname != pv_name:
+ print ("py_monitor_callback: name/handle/monid =>>>>>>>>>>>> ",
+ pv_name, handle, self.cafe.getMonitorIDInCallback(handle))
+ print ("PV NAME NOT THE SAME **** WIDGET in monitor callback ", self)
+ '''
+ #_pvc = self.cafe.getCtrlCache(handle)
+ #print("_pvc.nelem",_pvc.nelem)
+ #_pvc.show()
+ #print(_pvc.nelem)
+
+ #_pvd = self.cafe.getPVCache(handle)
+ #pvd.showMax(4000) #set no of elemets to 1 in pvctrlCache!
+ #print(pvd.nelem)
+
+ '''
+ _info = self.cafe.getChannelInfo(handle)
+ _info.show()
+ '''
+
+
+
+ #_widgetList =self.cafe.getWidgets(handle)
+
+ _widget = None
+
+ #print ("END monid =>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ", self.cafe.getMonitorIDInCallback(handle))
+ #print(self, self.pv_name)
+ #print(_widgetList)
+ '''
+ self.mutex.lock()
+
+ for _widget, _handle in self.widget_handle_dict.items():
+ if _handle == handle:
+ '''
+ for i in range(0, 1): #len(_widgetList)):
+ #_widget = _widgetList[i]
+ if pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
+ print("initialize again")
+ self.initialize()
+
+ elif pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _alarm_severity = self.cyca.ICAFE_CA_OP_CONN_DOWN
+ #print("COMPARE ALARM SEVERITIES ", _alarm_severity, pvd.alarmSeverity)
+ else:
+ _alarm_severity = pvd.alarmSeverity
+
+
+
+ if self.monitor_dbr_time:
+ self.trigger_monitor.emit(pvd, _alarm_severity)
+
+ elif isinstance(pvd.value[0], str):
+ self.trigger_monitor_str.emit((pvd.value[0]), pvd.status, _alarm_severity) #, _widget)
+ #print("emitted str value", pvd.value[0])
+ elif isinstance(pvd.value[0], int):
+ self.trigger_monitor_int.emit((pvd.value[0]), pvd.status, _alarm_severity)
+
+ else:
+ #print(dir(self.receivers(self, self.trigger_monitor_float)))
+ self.trigger_monitor_float.emit(float(pvd.value[0]), pvd.status, _alarm_severity)
+ #print("emitted float value", pvd.value[0])
+ pass
+
+
+
+
+ #if _widget is None:
+ # print("NO WIDGET FOR THIS PV!!!! pv = ", pv_name)
+ #self.mutex.unlock()
+
+
+
+ def monitor_start(self):
+ '''Initiate monitor on pv.'''
+ #print(self, self.pv_name, "Initiate monitor on pv:",
+ # self.monitor_callback, self.py_monitor_callback)
+ if self.handle > 0:
+ #Is monitor in waiting - now deleted with monitor_stop
+ if self.notify_unison:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, dbr=self.cyca.CY_DBR_TIME)
+ #start with gateway supplied monitor callback handler
+ elif self.monitor_callback is None:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, cb=self.py_monitor_callback,
+ dbr=self.cyca.CY_DBR_TIME,
+ notify_milliseconds=self.notify_milliseconds)
+ else:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, cb=self.monitor_callback,
+ dbr=self.cyca.CY_DBR_TIME,
+ notify_milliseconds=self.notify_milliseconds)
+
+
+ def monitor_stop(self):
+ #print("monitor_stopped")
+ if self.handle > 0:
+ _monID_list = self.cafe.getMonitorIDs(self.handle)
+ _monID_inwaiting_list = self.cafe.getMonitorIDsInWaiting(self.handle)
+ _monID_all = _monID_list + _monID_inwaiting_list
+
+ if self.monitor_id in _monID_all:
+ #print("stopping in monitor_stop for handle", self.monitor_id)
+ self.cafe.monitorStop(self.handle, self.monitor_id)
+ #print("stopped in monitor_stop for handle", self.monitor_id)
+ #Is monitor in waiting?
+ #remove monitors in waiting
+
+
+
+ def reconnect_channel(self):
+ self.cafe.reconnect([self.handle]) #list
+
+ def set_desc(self):
+ '''Set description of pv from pv.DESC'''
+
+ if self.cafe.hasDescription(self.handle):
+ self.desc = self.cafe.getDescription(self.handle)
+ return
+ elif self.desc is not None:
+ return
+ else:
+ self.cafe.supplementHandle(self.handle)
+ if self.cafe.hasDescription(self.handle):
+ self.desc = self.cafe.getDescription(self.handle)
+
+ if self.desc is not None:
+ return
+
+ ###Back-up solution
+ _found = str(self.pv_name).find(".")
+ if _found != -1:
+ _pv_desc = str(self.pv_name)[0:_found] +".DESC"
+ else:
+ _pv_desc = self.pv_name +".DESC"
+ _handle_desc = self.cafe.getHandleFromPVName(_pv_desc)
+
+ _handle_desc_already_open = False
+
+ if _handle_desc == 0:
+ self.cafe.openPrepare()
+ _handle_desc = self.cafe.open(_pv_desc)
+ self.cafe.openNowAndWait(self.timeout, _handle_desc)
+ time.sleep(0.001)
+ else:
+ _handle_desc_already_open = True
+
+ if self.cafe.isConnected(_handle_desc):
+ self.desc = self.cafe.getCache(_handle_desc, 'str')
+ if self.desc is None:
+ self.desc = self.cafe.get(_handle_desc, 'str')
+ else:
+ self.desc = None
+
+ if not _handle_desc_already_open:
+ self.cafe.close(_handle_desc)
+
+ def set_precision_and_units(self, reconnectFlag: bool = False):
+ '''Set the pv precision and units.'''
+ if self.pv_ctrl is None or reconnectFlag is True:
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+
+ if self.pv_ctrl is not None:
+ if not self.has_precision_user:
+ self.precision = self.pv_ctrl.precision
+ if self.pv_ctrl.units is not None:
+ #print(self.pv_ctrl.units)
+ #print(type(self.pv_ctrl.units))
+ self.units = str(self.pv_ctrl.units)
+ else:
+ self.units = ""
+
+ if reconnectFlag is True:
+ #verify user input
+ if self.show_units is True and self.suffix is not None:
+ if self.suffix == self.units:
+ self.show_units = False
+
+
+ def _qt_readback_color_mode(self):
+ '''Color mode is determined from CAFE and depends on whether the pv:
+ has alarm limits (self.color_mode = 'readbackAlarm')
+ or is without alarm limits (self.color_mode = 'readbackStatic')
+ '''
+
+
+ #Already set by user
+ if self.color_mode is self.READBACK_ALARM:
+ return
+
+
+ if self.cafe.isConnected(self.handle):
+ pvd = self.cafe.getPVCache(self.handle)
+ if pvd.alarmSeverity in (self.cyca.SEV_MINOR, self.cyca.SEV_MAJOR) or \
+ self.cafe.hasAlarmStatusSeverity(self.handle):
+ self.color_mode = self.READBACK_ALARM
+ self.status_tip = "Widget color mode is dynamic, pv with alarm limits"
+ #print(self.pv_name, "has alarm svev", self.cafe.hasAlarmStatusSeverity(self.handle))
+
+
+ else:
+ self.color_mode = self.READBACK_STATIC
+ self.status_tip = "Widget color mode is static, pv without alarm limits"
+
+
+ def qt_property_initial_values(self, qt_object_name: str = None, tool_tip: bool = True):
+
+ '''Set Qt property values.'''
+ self.qt_object_name = qt_object_name
+ if tool_tip:
+ self.setToolTip(self.pv_name)
+ self.setObjectName(self.qt_object_name)
+ if self.qt_object_name in self.qt_object_to_property.keys():
+ self._qt_property_selected = copy.deepcopy(self.qt_object_to_property[self.qt_object_name])
+ else:
+ print ("qt_property_initial_values: Object not found in dictionary")
+
+ #print("qt_property_initial_values", self.qt_object_name, self._qt_property_selected)
+
+
+ if self.cafe.isConnected(self.handle):
+
+ if self.qt_object_name == self.PV_READBACK:
+ self._qt_readback_color_mode()
+ #self.setStatusTip(self.status_tip)
+
+ elif self.qt_object_name == self.PV_CONTROLLER:
+ if self.color_mode == self.ACT_ON_BEAM:
+ #self.setStatusTip("PV setting acts directly on beam")
+ pass
+ else:
+ self.color_mode = self.NOT_ACT_ON_BEAM
+ #self.setStatusTip("PV setting does not influence beam")
+
+ elif self.qt_object_name == self.PV_DAQ_CA:
+ self._qt_readback_color_mode()
+
+ elif self.qt_object_name == self.PV_DAQ_BS:
+ self.color_mode = self.READBACK_STATIC
+
+ #print("qt_property_initial_values//", self.pv_name, self.qt_object_name, self._qt_property_selected)
+ self._qt_dynamic_property_set(self.color_mode)
+ #print("qt_property_initial_values///", self.pv_name, self.qt_object_name, self._qt_property_selected)
+
+ else:
+ self.qt_property_disconnect()
+
+ #print("qt_property_initial_values", self.pv_name, self.qt_object_name, self.color_mode)
+
+ '''
+ meta_obj = self.metaObject()
+ count = meta_obj.propertyCount()
+ for i in range(0, count):
+ meta_prop = meta_obj.property(i)
+ name = meta_prop.name()
+ print(i, name, self.property(name))
+ '''
+
+ def qt_dynamic_property_get(self, property_state : str = None):
+ '''Retrieves the requested property value'''
+ '''else that which is currently true'''
+
+ for _property, _value in self._qt_property_selected.items(): #states.items():
+ if property_state is not None:
+ if _property == property_state:
+ return _value
+ elif _value:
+ #print(self, _property, "SELECTED")
+ return _property
+
+ def _qt_dynamic_property_set(self, property_state : str = None):
+ '''Set the Input property to true, and the remainder to False'''
+ '''If None is given then all dynamic properties are set to False'''
+
+ #print("qt_property_set/", property_state, self.pv_name, self.qt_object_name, self.color_mode)
+ #if property_state in self.qt_property_states.keys():
+ for _property, _value in self._qt_property_selected.items(): #states.items():
+ if _property == property_state:
+ self.setProperty(_property, True)
+ self._qt_property_selected[_property] = True
+ else:
+ self.setProperty(_property, False)
+ self._qt_property_selected[_property] = False
+
+ #print("qt_property_set//", self.pv_name, self.qt_object_name, self.color_mode)
+
+ #l = self.dynamicPropertyNames()
+ #for i in range (0, len(l)):
+ # print(i, l[i])
+ #return self._qt_property_selected
+
+ def qt_property_disconnect(self, redraw=False):
+ '''Set Qt disconnect property value.'''
+
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.DISCONNECTED)
+
+ '''
+ if not self.initialize_complete:
+ self.setStatusTip("PV={0} was never connected".format(self.pv_name))
+ else:
+ self.setStatusTip("PV={0} is presently disconnected".format(self.pv_name))
+ '''
+ #print("qt_property_disconnect", self.pv_name, self.qt_object_name, self.color_mode)
+ self.qt_style_polish()
+
+ return #self._qt_property_selected
+
+
+ def qt_property_reconnect(self, redraw=False):
+ '''Set Qt connected property value.'''
+ #self.setObjectName("PyCafe")
+ #self.setToolTip(self.pv_name)
+ #l = self.dynamicPropertyNames()
+ #for i in range (0, len(l)-1):
+ # print(i, l[i])
+ #self.setProperty(str(l[i],'utf-8'), False)
+
+ if self.qt_object_name == self.PV_READBACK:
+ self._qt_readback_color_mode()
+ #self.setStatusTip(self.status_tip)
+
+
+ elif self.qt_object_name == self.PV_CONTROLLER:
+ if self.color_mode == self.ACT_ON_BEAM:
+ #self.setStatusTip("PV setting acts directly on beam")
+ pass
+ else:
+ self.color_mode = self.NOT_ACT_ON_BEAM
+ #self.setStatusTip("PV setting does not influence beam")
+
+
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.color_mode)
+
+ #print("qt_property_reconnect", self.pv_name, self.qt_object_name, self.color_mode)
+
+ #l = self.dynamicPropertyNames()
+ #for i in range (0, len(l)):
+ # print(i, l[i])
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_major(self, redraw=False):
+ '''Set Qt MAJOR property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.ALARM_SEV_MAJOR)
+ self.setStatusTip("{0} reports value in MAJOR alarm state!".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_minor(self, redraw=False):
+ '''Set Qt MINOR property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.ALARM_SEV_MINOR)
+ self.setStatusTip("{0} reports value in MINOR alarm state!".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_no_alarm(self, redraw=False):
+ '''Set Qt READBACK_ALARM property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.READBACK_ALARM)
+ self.setStatusTip("{0} reports value in normal state".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_invalid(self, redraw=False):
+ '''Set Qt INVALID property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.ALARM_SEV_INVALID)
+ self.setStatusTip("PV={0} reports an INVALID value!".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_static(self, redraw=False):
+ '''Set Qt STATIC property value.'''
+ self._qt_dynamic_property_set(self.READBACK_STATIC)
+ self.setStatusTip("PV={0} does not have an alarm state".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_daq_stopped(self, redraw=False):
+ '''Set Qt STOPPED property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.DAQ_STOPPED)
+ self.setStatusTip("PV={0} reports DAQ has stopped".format(self.pv_name))
+ self.qt_style_polish()
+
+
+ def qt_property_daq_paused(self, redraw=False):
+ '''Set Qt STOPPED property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.DAQ_PAUSED)
+ self.setStatusTip("PV={0} reports DAQ has paused".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_style_polish(self, redraw=False):
+ if redraw:
+ self.style().unpolish(self)
+ self.style().polish(self)
+ event=QEvent(QEvent.StyleChange)
+ QApplication.sendEvent(self, event);
+ self.update()
+ self.updateGeometry()
+ else:
+ #self.style().unpolish(self)
+ self.style().polish(self)
+ QApplication.processEvents()
+
+ def pv_status_text_header(self, source="Channel Access"):
+ _source = source
+ _source_separator = "----------------------------------------"
+ _text = """
+
+ Widget: {0} ({1}, {2})
+
+ """.format(self.widget_class, self.qt_object_name, self.color_mode)
+
+ if self.msg_press_value is not None:
+ _text += """
+ On press, sends value: {0}
+ """.format(self.msg_press_value, "DarkOrchid")
+
+ if self.msg_release_value is not None:
+ _text += """
+ On release, sends value: {0}
+ """.format(self.msg_release_value, "DarkOrchid")
+
+ if self.pv_within_daq_group:
+ if self.qt_object_name in (self.PV_DAQ_BS,):
+ _ds_color = "Navy Blue"
+ else:
+ _ds_color = "Black"
+ else:
+ _ds_color = "Black"
+
+ _text += """
+ {0}
+ Data source: {1}
+ {0}
+ PV: {2}
+ """.format(_source_separator, _source, self.pv_name, "DarkOrchid",
+ _ds_color)
+
+ if self.desc is None:
+ self.set_desc()
+
+ if self.desc == "":
+ _text += """
+ """
+ return _text
+
+ _text += """
+
+ Description: {6}
+
+ """.format(self.widget_class, self.qt_object_name, \
+ self.color_mode, _source_separator, _source, \
+ self.pv_name, self.desc, "DarkOrchid"
+ )
+ return _text
+
+ def pv_status_text_enum(self):
+
+ _val_enum = None
+ _value = self.pvd.value[0]
+ if isinstance(_value, str):
+ _val_enum = self.cafe.getEnumFromString(self.handle, _value)
+ elif _value is not None:
+ _val_enum = self.cafe.getStringFromEnum(self.handle, _value)
+
+ _color = "Blue"
+
+ #To catch case where channel is called by user
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ _text = """
+
+ Value: {1} [{2}]
+ """.format(_color, _value , _val_enum
+ )
+
+ return _text
+
+ def pv_status_text_data(self):
+
+ _value_str = ""
+ _first_end = 9
+ _end_range = min(self.pvd.nelem, _first_end)
+ if _end_range > 1:
+ _value_str = "[ "
+ for i in range (0, _end_range):
+ _value = self.pvd.value[i]
+ if _value is None:
+ _value = '0'
+ if isinstance(_value, str):
+ _value_str += _value
+ elif isinstance(_value, int):
+ _value_str += str(_value)
+ else:
+ if self.pv_ctrl is not None:
+ _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
+ _value_str += _value_form.format(
+ round(_value, self.pv_ctrl.precision))
+ if i < (_end_range-1):
+ _value_str += " "
+
+ if self.pvd.nelem > _first_end:
+ _value_str += " ... "
+ _value = self.pvd.value[self.pvd.nelem-1]
+ if isinstance(_value, str):
+ _value_str += _value
+ elif isinstance(_value, int):
+ _value_str += str(value)
+ else:
+ if self.pv_ctrl is not None:
+ _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
+ _value_str += _value_form.format(
+ round(_value, self.pv_ctrl.precision))
+ _value_str += " "
+ if _end_range > 1:
+ _value_str += "]"
+
+ _color = "Blue"
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+
+
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ _text = """
+
+ Value: {1} {2}
+ """.format(_color, _value_str, self.units)
+
+ return _text
+
+
+ def pv_status_text_timestamp(self):
+ _status_not_ok_color = "IndianRed"
+ _status_ok_color = "DimGray"
+ _ts_color = "Blue"
+ _color = _status_ok_color
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _ts_color = "White"
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _ts_color = "White"
+ _color = "White"
+
+ elif not self.cafe.isConnected(self.handle):
+ _ts_color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _ts_color = "White"
+
+
+ if self.pvd.status != self.cyca.ICAFE_NORMAL:
+ _color = _status_not_ok_color
+ _text = """
+ Timestamp: {2}
+ Status: {3}
{4}
+ """.format( _ts_color, _color, self.pvd.tsDateAsString, \
+ self.pvd.statusAsString, \
+ self.cafe.getStatusInfo(self.pvd.status))
+
+ return _text
+
+
+ def pv_status_text_alarm(self):
+ _text ="""
+ """
+ _color = "DimGray"
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+
+
+ if self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
+ _color = "Yellow"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
+ _color = "Red"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
+ _color = "White"
+
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
+ _color = "Yellow"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
+ _color = "Red"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
+ _color = "White"
+
+
+ _text += """
+ Alarm status: {1}
+ Alarm severity: {2}
+ """.format(_color, self.pvd.alarmStatusAsString,
+ self.pvd.alarmSeverityAsString)
+
+ return _text
+
+ def pv_access(self):
+ _accessIs = ""
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info.accessRead:
+ _accessIs += "Read"
+ if self.pv_info.accessWrite:
+ _accessIs += "Write"
+ return _accessIs
+
+ def pv_status_text_enum_metadata(self):
+ _text = """
+ ENUM strings: {2}
+ Data type (native): {3}
+ Record type: {4}
+ RW Access: {5}
+ IOC: {6}
+ """.format( "MediumBlue", "DarkOrchid", self.pvc.enumStrings,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+ def pv_status_text_metadata(self):
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ if self.record_type in ["stringin", "stringout"]:
+ _text = """
+ Data type (native): {3}
+ Record type: {4}
+ RW Access: {5}
+ IOC: {6}
+ """.format("MediumBlue", self.pvd.nelem, self.pvc.precision,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+ _text = """
+ """
+ if self.pvd.nelem > 1:
+ _text += """
+ Nelem: {1}
+ """.format("MediumBlue", self.pvd.nelem)
+
+ _text += """
+ Precision (PV): {1}
+ Data type (native): {2}
+ Record type: {3}
+ RW Access: {4}
+ IOC: {5}
+ """.format("MediumBlue", self.pvc.precision,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+
+ def pv_status_text_alarm_limits(self, ):
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ _text ="""
+ """
+
+ #No all record types have alarms
+ #className is not supported at psi since introduction of the linux ca gateway
+ #Not Supported by Gateway
+
+ #self.pv_info.show()
+ #self.pv_ctrl.show()
+ #print(self.record_type)
+ #print(self._alarm_severity_record_types)
+
+ if "Not Supported" in str(self.record_type):
+ pass
+ elif self.record_type not in self._alarm_severity_record_types:
+ return _text
+
+ if self.pvc.lowerAlarmLimit == 0 and self.pvc.upperAlarmLimit == 0 and \
+ self.pvc.lowerWarningLimit == 0 and self.pvc.upperWarningLimit == 0:
+
+ return _text
+
+ if self.cafe.hasAlarmStatusSeverity(self.handle): # or "Not Supported" in self.pv_info.className:
+ _text = """
+ Lower/Upper alarm limit: {1} / {4}
+ Lower/Upper warning limit: {2} / {3}
+
+ """.format("MediumBlue",
+ self.pvc.lowerAlarmLimit, self.pvc.lowerWarningLimit,
+ self.pvc.upperWarningLimit, self.pvc.upperAlarmLimit)
+ return _text
+
+ def pv_status_text_display_limits(self):
+ _text ="""
+ """
+ if self.pvc.lowerDisplayLimit == 0 and self.pvc.upperDisplayLimit == 0 and \
+ self.pvc.lowerControlLimit == 0 and self.pvc.upperControlLimit == 0:
+ return _text
+ _text = """
+ Lower/Upper control limit: {3} / {4}
+ Lower/Upper display limit: {1} / {2}
+
+ """.format("MediumBlue",
+ self.pvc.lowerDisplayLimit, self.pvc.upperDisplayLimit,
+ self.pvc.lowerControlLimit, self.pvc.upperControlLimit)
+ return _text
+
+
+
+
+ def pv_status_text(self):
+ '''pv metadata to accompany widget's dialog box.'''
+ QApplication.processEvents()
+ _source = "Channel Access"
+
+ if self.pv_within_daq_group:
+ if self.qt_object_name == self.PV_DAQ_BS:
+ _source = "DAQ (Beam Synchronous)"
+ #self.pvd written to in receive_daq_update
+ elif self.qt_object_name == self.PV_DAQ_CA:
+ _source = "DAQ (Channel Access)"
+ self.pvd = self.cafe.getPVCache(self.handle)
+ if self.pvd.pulseID > 0:
+ _source += "
Pulse ID: {0}".format(self.pvd.pulseID)
+ else:
+ self.pvd = self.cafe.getPVCache(self.handle)
+
+ ##For testing...
+ ##self.pvd.status = self.cyca.ICAFE_CA_OP_CONN_DOWN
+ ##self.pvd.statusAsString = 'ICAFE_CA_OP_CONN_DOWN'
+ #i, pvd, pvc = self.cafe.getChannelDataStore(self.handle)
+ self.pvc = self.cafe.getCtrlCache(self.handle)
+ #self.pvc.show()
+
+ _text_data ="""
+ """
+
+ if self.pvd.status == self.cyca.ECAFE_INVALID_HANDLE:
+ _text_data = """ Status: {1}
{2}
+ """.format("Blue", "Channel closed while DAQ in STOP state.",
+ "PV info requires DAQ to be in RUN/PAUSED state" )
+
+
+ elif self.pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
+ _text_data = """ Status: {1}
{2}
+ """.format("Red", self.pvd.statusAsString, self.cafe.getStatusInfo(self.pvd.status))
+
+ elif self.pvc.noEnumStrings > 0:
+ _text_data = self.pv_status_text_enum() + \
+ self.pv_status_text_timestamp() + \
+ self.pv_status_text_alarm() + \
+ self.pv_status_text_enum_metadata()
+
+ else:
+ _text_data = self.pv_status_text_data()+ \
+ self.pv_status_text_timestamp() + \
+ self.pv_status_text_alarm() + \
+ self.pv_status_text_metadata() + \
+ self.pv_status_text_alarm_limits() + \
+ self.pv_status_text_display_limits()
+
+ self.pv_message_in_a_box.setText(
+ self.pv_status_text_header(source=_source) + _text_data
+ )
+ QApplication.processEvents()
+ self.pv_message_in_a_box.exec()
+
+
+ def lookup_archiver(self):
+ '''Plot pvdata from archiver.'''
+ #"https://ui-data-api.psi.ch/prepare?channel = sf-archiverappliance/"
+ urlIs = self.settings.urlArchiver
+ urlIs = urlIs + self.pv_name
+
+ if not QDesktopServices.openUrl(QUrl(urlIs)): #, QUrl.TolerantMode)
+ if self.showMessage is not None:
+ self.showMessage(MsgSeverity.ERROR, __pymodule__, _line(),
+ "Failed to open URL {0}".format(urlIs))
+
+ def lookup_databuffer(self):
+ '''Plot beam synchronous pvdata from databuffer.'''
+ #""https://ui-data-api.psi.ch/prepare?channel = sf-databuffer/"
+ urlIs = self.settings.urlDatabuffer
+ urlIs = urlIs + self.pv_name
+
+ if not QDesktopServices.openUrl(QUrl(urlIs)): #, QUrl.TolerantMode)
+ if self.showMessage is not None:
+ self.showMessage(MsgSeverity.ERROR, __pymodule__, _line(),
+ "Failed to open URL {0}".format(urlIs))
+ QApplication.processEvents()
+
+ def strip_chart(self):
+ '''PShell strip chart.'''
+ configStr = "-config = [[[true,\"" + self.pv_name + "\",\"Channel\",1,1]]]"
+ commandStr = "/sf/op/bin/strip_chart"
+ argStr = ["-nlaf", "-start", configStr, "&"]
+ QProcess.startDetached(commandStr, argStr)
+
+
+ def display_parameters(self):
+ display_wgt = QDialog(self)
+
+ _rect = display_wgt.geometry() #get current geometry of help window
+ _parentRect = self.context_menu.geometry() # QRect(100, 1000, 640, 480) #get current geometry of this window
+ #print(_rect, _parentRect)
+ _rect.moveTo(display_wgt.mapToGlobal(
+ QPoint(_parentRect.x() + _parentRect.width() - _rect.width(),
+ _parentRect.y())))
+
+ display_wgt.setGeometry(_rect)
+
+ #This has no effect
+ #display_wgt.setWindowModality(Qt.WindowModal) #Qt.ApplicationModal Qt.ApplicationModal
+ display_wgt.setWindowTitle(self.pv_name)
+ layout = QVBoxLayout()
+ #print("sender==================>", self.sender(), self)
+ #self.the_gw = self
+ #print("getNativeDataType", self.cafe.getDataTypeNative(self.handle))
+
+ precision_flag = True
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.precision <= 0:
+ precision_flag = False
+ if self.cafe.getDataTypeNative(self.handle) in (
+ self.cyca.CY_DBR_FLOAT, self.cyca.CY_DBR_DOUBLE) and precision_flag:
+ #precision user
+ _hbox_wgt = QWidget()
+ _hbox = QHBoxLayout()
+ precision_user_label = QLabel("Precision (user):")
+ self.precision_user_wgt = QSpinBox(self)
+ self.precision_user_wgt.setFocusPolicy(Qt.NoFocus)
+ self.precision_user_wgt.setValue(int(self.precision))
+ if self.pv_ctrl is not None:
+ _max = self.pv_ctrl.precision
+ else:
+ _max = 6
+ self.precision_user_wgt.setMaximum(_max)
+ self.precision_user_wgt.valueChanged.connect(
+ self.precision_user_changed)
+ _hbox.addWidget(precision_user_label)
+ _hbox.addWidget(self.precision_user_wgt)
+ _hbox_wgt.setLayout(_hbox)
+
+ precision_user_label.setFixedWidth(110)
+ self.precision_user_wgt.setFixedWidth(35)
+ _hbox_wgt.setFixedWidth(160)
+
+ #precision ioc
+ _hbox2_wgt = QWidget()
+ _hbox2 = QHBoxLayout()
+ precision_ioc_label = QLabel("Precision (ioc): ")
+ precision_ioc = QPushButton(self)
+ precision_ioc.setText(" {} ".format(_max))
+ precision_ioc.clicked.connect(self.precision_ioc_reset)
+
+ _hbox2.addWidget(precision_ioc_label)
+ _hbox2.addWidget(precision_ioc)
+ _hbox2_wgt.setLayout(_hbox2)
+
+ precision_ioc_label.setFixedWidth(110)
+ precision_ioc.setFixedWidth(20)
+ _hbox2_wgt.setFixedWidth(145)
+
+ layout.addWidget(_hbox_wgt)
+ layout.addWidget(_hbox2_wgt)
+
+ #precision refresh rate
+ _hbox3_wgt = QWidget()
+ _hbox3 = QHBoxLayout()
+ refresh_freq_label = QLabel("Refresh rate: ")
+ _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
+ self.notify_freq_hz_default
+
+ self.refresh_freq_combox_idx_dict = {0:0, 1:10, 2:5, 3:2, 4:1, 5:0.5,
+ 6:_default_refresh_val}
+ refresh_freq = QComboBox(self)
+ refresh_freq.addItem('direct')
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[1]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[2]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[3]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[4]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[5]))
+
+ _default_text = 'default (direct)' if _default_refresh_val == 0 else \
+ 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
+
+ refresh_freq.addItem(_default_text)
+
+
+ for key, value in self.refresh_freq_combox_idx_dict.items():
+ if value == self.notify_freq_hz:
+ refresh_freq.setCurrentIndex(key)
+ break
+ refresh_freq.currentIndexChanged.connect(self.refresh_rate_changed)
+
+
+ _hbox3.addWidget(refresh_freq_label)
+ _hbox3.addWidget(refresh_freq)
+ _hbox3_wgt.setLayout(_hbox3)
+
+ refresh_freq_label.setFixedWidth(110)
+ refresh_freq.setFixedWidth(115)
+ _hbox3_wgt.setFixedWidth(235)
+
+ layout.addWidget(_hbox3_wgt)
+
+ layout.setAlignment(Qt.AlignLeft)
+ layout.setContentsMargins(10, 0, 0, 0)
+ layout.setSpacing(0)
+
+ display_wgt.setMinimumWidth(340)
+ display_wgt.setLayout(layout)
+
+ display_wgt.exec()
+ QApplication.processEvents()
+
+ def precision_ioc_reset(self):
+ if self.pv_ctrl is not None:
+ self.precision_user = self.pv_ctrl.precision
+ self.precision = self.pv_ctrl.precision
+ if self.precision is not None:
+ self.precision_user_wgt.setValue(self.precision)
+ #self.precision_user_changed(self.precision)
+ #_value = self.cafe.getCache(self.handle)
+ #self.trigger_monitor_float.emit(_value, 1, 0)
+
+ def precision_user_changed(self, new_value):
+ self.precision_user = new_value
+ self.precision = new_value
+
+ _pvd = self.cafe.getPVCache(self.handle)
+
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ self.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+
+ '''
+ _value = self.cafe.getCache(self.handle)
+ #print("widget", self.widget, self.widget.sender())
+ if _value is not None:
+ #self.post_display_value(_value)
+ self.trigger_monitor_float.emit(_value, 1, 0)
+ '''
+
+ def refresh_rate_changed(self, new_idx):
+ _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
+ self.notify_milliseconds = 0 if _notify_freq_hz == 0 else \
+ 1000 / _notify_freq_hz
+ self.notify_freq_hz = _notify_freq_hz
+
+ if self.notify_unison:
+ self.notify_unison = False
+ self.monitor_stop()
+ self.monitor_start()
+
+ else:
+ self.cafe.updateMonitorPolicyDeltaMS(self.handle,
+ self.monitor_id,
+ self.notify_milliseconds)
+
+ #https://doc.qt.io/qt-5.9/qtwidgets-mainwindows-menus-example.html
+ #Since Qt5 this has to be implemented in order to avoid the Select All dialogue button appearing..
+ def contextMenuEvent(self, event):
+ return
+
+ def showContextMenu(self):
+ self.context_menu.exec(QCursor.pos())
+
+ def mousePressEvent(self, event):
+ '''Action on mouse press event.'''
+ button = event.button()
+ if button == Qt.RightButton:
+ #contextMenu.exec(event.globalPos())
+ self.context_menu.exec(QCursor.pos())
+ self.clearFocus()
+
+ def mouseReleaseEvent(self, event):
+ event.ignore()
+
diff --git a/pvwidgets.py b/pvwidgets.py
index adb3104..df9f610 100644
--- a/pvwidgets.py
+++ b/pvwidgets.py
@@ -1,16 +1,19 @@
''' Module with channel access enabled QtWidgets.'''
__author__ = 'Jan T. M. Chrin'
-import re
-import time
-
import collections
import numpy as np
+import re
+from threading import Lock
+import time
+
+
from sklearn.linear_model import LinearRegression
from distutils.version import LooseVersion
from functools import reduce as func_reduce
-from qtpy.QtCore import QEventLoop, QPoint, Qt, QThread, QTimer, Signal, Slot
+from qtpy.QtCore import (QEventLoop, QMutex, QPoint, Qt, QThread, QTimer,
+ Signal, Slot)
from qtpy.QtGui import (QCloseEvent, QColor, QCursor, QFont, QFontMetricsF,
QIcon, QKeySequence)
from qtpy.QtCore import __version__ as QT_VERSION_STR
@@ -462,13 +465,13 @@ class CAQMessageButton(QPushButton, PVGateway):
self.setFocusPolicy(Qt.StrongFocus)
self.setCheckable(True) #Recognizes press and release states
- fm = QFontMetricsF(QFont("Sans Serif", 12))
+ fm = QFontMetricsF(QFont("Sans Serif", 10))
qrect = fm.boundingRect(self.suggested_text)
_width_scaling_factor = 1.0
self.setText(self.msg_label)
- self.setFixedHeight((fm.lineSpacing()*2.0))
+ self.setFixedHeight((fm.lineSpacing()*1.8))
self.setFixedWidth((qrect.width() * _width_scaling_factor))
self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
@@ -1312,20 +1315,30 @@ class CAQTableWidget(QTableWidget):
super().__init__()
self.columns_dict = {}
_column_dict_value = 0
- self.columns_dict['PV'] = _column_dict_value
- if init_column:
+
+ if pv_list_show is not None:
+ if pv_list_show[0]:
+ self.columns_dict['PV'] = _column_dict_value
+ _column_dict_value += 1
+ else:
+ self.columns_dict['PV'] = _column_dict_value
_column_dict_value += 1
+
+ if init_column:
self.columns_dict['Init'] = _column_dict_value
- _column_dict_value += 1
+ _column_dict_value += 1
+
self.columns_dict['Value'] = _column_dict_value
+
if show_timestamp:
_column_dict_value += 1
self.columns_dict['Timestamp'] = _column_dict_value
+
_column_dict_value += 1
self.columns_dict['Reconnect'] = _column_dict_value
self.setWindowModality(Qt.ApplicationModal)
- self.no_columns = _column_dict_value + 1
+ self.no_columns = _column_dict_value + 1
self.init_column = init_column
@@ -1374,9 +1387,13 @@ class CAQTableWidget(QTableWidget):
_color_mode = [None] * len(self.pv_list)
- if isinstance(color_mode, list):
- for i in range(0, len(color_mode)):
- _color_mode[i] = color_mode[i]
+ if color_mode is not None:
+ if isinstance(color_mode, list):
+ for i in range(0, len(color_mode)):
+ _color_mode[i] = color_mode[i]
+ else:
+ for i in range(0, len(_color_mode)):
+ _color_mode[i] = color_mode
for i in range(0, len(self.pv_list)):
@@ -1427,7 +1444,8 @@ class CAQTableWidget(QTableWidget):
if not self.pv_gateway[i].pv_within_daq_group:
self.pv_gateway[i].monitor_start()
- self.update_init_values()
+ if init_column:
+ self.update_init_values()
self.configure_context_menu()
@@ -1568,6 +1586,10 @@ class CAQTableWidget(QTableWidget):
def update_init_values(self):
_start = 0
_end = len(self.pv_gateway)
+ if 'Init' in self.columns_dict:
+ _column_no = self.columns_dict['Init']
+ else:
+ return
for _row in range(_start, _end):
_handle = self.pv_gateway[_row].handle
@@ -1583,7 +1605,8 @@ class CAQTableWidget(QTableWidget):
_f.setPointSize(8)
qtwi.setFont(_f)
self.setItem(_row, 1, qtwi)
- self.item(_row, 1).setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
+ self.item(_row, _column_no).setTextAlignment(Qt.AlignRight |
+ Qt.AlignVCenter)
def configure_widget(self):
@@ -1599,8 +1622,10 @@ class CAQTableWidget(QTableWidget):
self.resizeColumnsToContents()
self.resizeRowsToContents()
#self.horizontalHeader().setStretchLastSection(True);
- self.setColumnWidth(self.columns_dict['PV'], _column_width_pvname)
-
+ if 'PV' in self.columns_dict.keys():
+ self.setColumnWidth(self.columns_dict['PV'], _column_width_pvname)
+ _pv_column = self.columns_dict['PV']
+
self.setColumnWidth(self.columns_dict['Value'], _column_width_value)
if 'Init' in self.columns_dict.keys():
self.setColumnWidth(self.columns_dict['Init'], _column_width_value)
@@ -1610,18 +1635,22 @@ class CAQTableWidget(QTableWidget):
self.setColumnWidth(self.columns_dict['Reconnect'],
_column_width_checkbox)
- _pv_column = self.columns_dict['PV']
-
+
for i in range(0, len(self.pv_gateway)):
- qtwt = QTableWidgetItem(self.pv_list_show[i])
- f = qtwt.font()
- f.setPointSize(8)
- qtwt.setFont(f)
+ istart = 1
+ if 'PV' in self.columns_dict.keys():
+ qtwt = QTableWidgetItem(self.pv_list_show[i])
+ f = qtwt.font()
+ f.setPointSize(8)
+ qtwt.setFont(f)
- self.setItem(i, _pv_column, qtwt)
- self.item(i, _pv_column).setTextAlignment(Qt.AlignHCenter |
- Qt.AlignVCenter)
- for i_column in range(1, self.no_columns-1):
+ self.setItem(i, _pv_column, qtwt)
+ self.item(i, _pv_column).setTextAlignment(Qt.AlignHCenter |
+ Qt.AlignVCenter)
+ else:
+ istart = 0
+
+ for i_column in range(istart, self.no_columns-1):
self.setItem(i, i_column, QTableWidgetItem(str("")))
self.item(i, i_column).setTextAlignment(Qt.AlignHCenter |
Qt.AlignVCenter)
@@ -1644,14 +1673,14 @@ class CAQTableWidget(QTableWidget):
_f = self.init_value_button.font()
_f.setPointSize(8)
self.init_value_button.setFont(_f)
- self.init_value_button.setFixedWidth(80)
+ self.init_value_button.setFixedWidth(64)
self.init_value_button.clicked.connect(self.update_init_values)
self.init_value_button.setToolTip(
("Stores initial, pre-measurement value. Update is also " +
- "typically executed automatically before new optics are set."))
+ "typically executed automatically before analysis procedure."))
_init_layout.addWidget(self.init_value_button)
- _init_layout.setAlignment(Qt.AlignRight)
- _init_layout.setContentsMargins(1, 1, 0, 0) #Required
+ _init_layout.setAlignment(Qt.AlignCenter)
+ _init_layout.setContentsMargins(1, 1, 1, 0) #Required
self.init_widget.setLayout(_init_layout)
self.setCellWidget(len(self.pv_gateway), 1, self.init_widget)
@@ -1669,10 +1698,10 @@ class CAQTableWidget(QTableWidget):
self.restore_value_button.setToolTip(
("Restore devices to their pre-measurement values"))
_restore_layout.addWidget(self.restore_value_button)
- _restore_layout.setAlignment(Qt.AlignRight)
+ _restore_layout.setAlignment(Qt.AlignCenter)
_restore_layout.setContentsMargins(1, 1, 0, 0)
_restore_widget.setLayout(_restore_layout)
- self.setCellWidget(len(self.pv_gateway), 2, _restore_widget)
+ self.setCellWidget(len(self.pv_gateway), 0, _restore_widget)
#Do not display no for last row (Reconnect button)
_row_digit_last_cell = QTableWidgetItem(str(""))
@@ -1689,8 +1718,8 @@ class CAQTableWidget(QTableWidget):
f.setPointSize(8)
self.reconnect_button.setFixedWidth(100)
else:
- f.setPointSize(6)
- self.reconnect_button.setFixedWidth(58)
+ f.setPointSize(7) #6
+ self.reconnect_button.setFixedWidth(66) #58
self.reconnect_button.setFont(f)
@@ -1712,9 +1741,10 @@ class CAQTableWidget(QTableWidget):
self.setCellWidget(len(self.pv_gateway), self.no_columns-1,
self.cb_item_all)
- header_item = QTableWidgetItem("Process Variable")
- self.setHorizontalHeaderItem(self.columns_dict['PV'], header_item)
+ if 'PV' in self.columns_dict.keys():
+ header_item = QTableWidgetItem("Process Variable")
+ self.setHorizontalHeaderItem(self.columns_dict['PV'], header_item)
if 'Init' in self.columns_dict.keys():
self.setHorizontalHeaderItem(self.columns_dict['Init'],
@@ -1749,9 +1779,13 @@ class CAQTableWidget(QTableWidget):
self.setMinimumWidth(_min_table_width)
for _row in range(0, len(self.pv_gateway)):
- self.item(_row, _pv_column).setForeground(QColor("#000000"))
+ if 'PV' in self.columns_dict.keys():
+ self.item(_row, _pv_column).setForeground(QColor("#000000"))
+ istart = 1
+ else:
+ istart = 0
- for i_column in range(1, self.no_columns-2):
+ for i_column in range(istart, self.no_columns-2):
self.item(_row, i_column).setForeground(QColor("#000000"))
self.item(_row, i_column).setTextAlignment(Qt.AlignRight |
Qt.AlignVCenter)
@@ -1834,16 +1868,16 @@ class CAQTableWidget(QTableWidget):
if _prop == self.pv_gateway[_row].READBACK_ALARM:
if alarm_severity == self.pv_gateway[_row].cyca.SEV_MAJOR:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmMajor
+ _bgcolor = self.pv_gateway[_row].fg_alarm_major
_fgcolor = "black"
elif alarm_severity == self.pv_gateway[_row].cyca.SEV_MINOR:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmMinor
+ _bgcolor = self.pv_gateway[_row].fg_alarm_minor
_fgcolor = "black"
elif alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmInvalid
+ _bgcolor = self.pv_gateway[_row].fg_alarm_invalid
_fgcolor = "#777777"
else:
- _bgcolor = self.pv_gateway[_row].settings.fgAlarmNoAlarm
+ _bgcolor = self.pv_gateway[_row].fg_alarm_noalarm
_fgcolor = "black"
#Colors for bg/fg reversed as is the old norm
@@ -2455,6 +2489,7 @@ class QNoDockWidget(QDockWidget):
class CAQStripChart(PlotWidget):
'''Channel access enabled pyqtgraph.PlotWidget'''
+
def __init__(self, parent=None, pv_list: list = ['PV_NAME_NOT_GIVEN'],
monitor_callback=None, pv_within_daq_group: bool = False,
color_mode=None, show_units: bool = False, prefix: str = "",
@@ -2477,17 +2512,18 @@ class CAQStripChart(PlotWidget):
self.val_previous = [None] * self.no_channels
self.curve = [None] * self.no_channels
-
- print (self.pv_list, flush=True)
+
+
for i in range (0, len(self.pv_list)):
- self.pv_gateway[i] = PVGateway().__init__(
- parent, pv_list[i], monitor_callback, pv_within_daq_group,
+
+ self.pv_gateway[i] = PVGateway(
+ parent, self.pv_list[i], monitor_callback, pv_within_daq_group,
color_mode, show_units, prefix, suffix,
#connect_callback=self.py_connect_callback,
connect_triggers=False, notify_freq_hz=notify_freq_hz,
monitor_dbr_time = True)
- print(i, pv_list[i], "gateway object", self.pv_gateway[i])
+
self.pv_gateway[i].is_initialize_complete()
@@ -2529,8 +2565,8 @@ class CAQStripChart(PlotWidget):
# Data stuff
self._interval = int(sampleinterval*1000)
- self._bufsize = 9000 #int(timewindow/0.33)
- self._bufsize2 = 9000 # int(timewindow/1.33)
+ self._bufsize = 10000 #int(timewindow/0.33)
+ self._bufsize2 = 10000 # int(timewindow/1.33)
self.databuffer = [None] * self.no_channels
self.timebuffer = [None] * self.no_channels
self.x = [None] * self.no_channels
@@ -2561,13 +2597,37 @@ class CAQStripChart(PlotWidget):
if title is not None:
self.setTitle(str(title)) #self.pv_gateway[0].pv_name)
self.showGrid(x=True, y=True)
- self.setLabel('left', ylabel, self.pv_gateway[0].units)
+ #self.setLabel('left', ylabel, self.pv_gateway[0].units)
self.setLabel('bottom', 'time', 's')
self.setBackground((60, 60, 60)) #247, 236, 249))
- self.setLimits(yMin=-0.11)
+ #self.setLimits(yMin=-0.11)
+ #self.setLimits(yMin=-1, yMax=1)
+
+ ax = pg.AxisItem('left')
+ ax.enableAutoSIPrefix(enable=False)
+ ax.setLabel(ylabel, self.pv_gateway[0].units)
+ ax.setGrid(155)
+
+ ay = pg.AxisItem('bottom')
+ ay.enableAutoSIPrefix(enable=False)
+ ay.setLabel('time', 'min')
+ ay.setGrid(175)
+
+ if 'BPM' in text_label:
+ ax.setTickSpacing(0.2, 0.1)
+ #ax.setRange(-1, 1)
+ #ax.setParentItem(self.graphicsItem())
+ axitems = {}
+ axitems['left'] = ax
+ axitems['bottom'] = ay
+
+ self.setAxisItems(axitems)
+
+ pg.setConfigOption('leftButtonPan', False)
self.plotItem.setMouseEnabled(y=True) # Only allow zoom in X-axis
self.plotItem.setMouseEnabled(x=True) # Only allow zoom in Y-axis
+
#(125, 249, 255)
if self.pen_color_idx == 0:
pen_list = [ (255, 155, 0), (255,255,0), (0, 180, 255) ]
@@ -2601,20 +2661,37 @@ class CAQStripChart(PlotWidget):
@Slot(object, int)
def receive_monitor_dbr_time(self, pvdata, alarm_severity):
+
+ #if not self.mutex.tryLock(): #locked():
+ # print("Event locked", pvdata.ts[0], pvdata.ts[1])
+ # return
+
+
+ #self.mutex.lock() #acquire()
+
_row = self.pv2item_dict[self.sender()]
#print("row, value from pvdata==>", _row, pvdata.value[0], self.pv_gateway[_row].pv_name)
-
+
ts_now = pvdata.ts[0] + pvdata.ts[1] * 10**(-9)
ts_previous = (self.pvd_previous_list[_row].ts[0] +
- self.pvd_previous_list[_row].ts[1] * 10**(-9))
+ self.pvd_previous_list[_row].ts[1] * 10**(-9))
ts_delta = ts_now - ts_previous
-
+
if (pvdata.ts[0] == self.pvd_previous_list[_row].ts[0]) and (
pvdata.ts[1] == self.pvd_previous_list[_row].ts[1]):
- pvdata.show()
- self.pvd_previous_list[_row].show()
+ #print("SAME TIMESTAMP")
+ #pvdata.show()
+ #self.pvd_previous_list[_row].show()
+ #print("================")
+ #self.mutex.unlock() #release()
return
-
+
+ if pvdata.ts[0] < self.pvd_previous_list[_row].ts[0]:
+ print("Funny ts value", self.pv_gateway[_row].pv_name)
+ pvdata.show()
+ #self.mutex.unlock() #release()
+ return
+
value = pvdata.value[0]
#discard first callbacks
#if ts_delta > 2.0:
@@ -2630,6 +2707,7 @@ class CAQStripChart(PlotWidget):
highest_ts = self.timebuffer[0][0] \
if self.timebuffer[0][0] is not None else 0
+
for i in range(1, len(self.timebuffer)):
if self.timebuffer[i][0] is None:
continue
@@ -2655,9 +2733,10 @@ class CAQStripChart(PlotWidget):
# self.curve[row].setData(self.x_shifted[row], self.y[row][idx:])
self.curve[_row].setData(self.x_shifted[_row], self.y[_row][idx:])
- self.time_delta[_row] = (
- pvdata.ts[0] + pvdata.ts[1]*10**(-9)) - self.time_zero[0]
-
+ self.time_delta[_row] = ((
+ pvdata.ts[0] + pvdata.ts[1]*10**(-9)) - self.time_zero[0])/60
+
+ #self.mutex.unlock() #release()
#QApplication.processEvents()
diff --git a/pvwidgets.py- b/pvwidgets.py-
new file mode 100644
index 0000000..b00eace
--- /dev/null
+++ b/pvwidgets.py-
@@ -0,0 +1,3142 @@
+''' Module with channel access enabled QtWidgets.'''
+__author__ = 'Jan T. M. Chrin'
+
+import re
+import time
+
+import collections
+import numpy as np
+from sklearn.linear_model import LinearRegression
+from distutils.version import LooseVersion
+from functools import reduce as func_reduce
+
+from qtpy.QtCore import QEventLoop, QPoint, Qt, QThread, QTimer, Signal, Slot
+from qtpy.QtGui import (QCloseEvent, QColor, QCursor, QFont, QFontMetricsF,
+ QIcon, QKeySequence)
+from qtpy.QtCore import __version__ as QT_VERSION_STR
+from qtpy.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QAction,
+ QApplication, QBoxLayout, QCheckBox, QComboBox,
+ QDialog, QDockWidget, QDoubleSpinBox, QFrame,
+ QGroupBox, QHBoxLayout, QLabel, QLineEdit,
+ QListWidget, QMenu, QMessageBox, QPushButton,
+ QSpinBox, QStyle, QStyleOptionSpinBox, QTableWidget,
+ QTableWidgetItem, QVBoxLayout, QWidget)
+
+import pyqtgraph as pg
+from pyqtgraph import PlotWidget
+from caqtwidgets.pvgateway import PVGateway
+
+class QTaggedLineEdit(QWidget):
+ def __init__(self, label_text=str(""), value="",
+ position="LEFT", parent=None):
+ super(QTaggedLineEdit, self).__init__(parent)
+ self.parameter = str(value)
+ self.label = QLabel(label_text)
+ self.label.setObjectName("Tagged")
+ self.label.setFixedHeight(24)
+ self.label.setContentsMargins(10, 0, 0, 0)
+ #self.label.setFixedWidth(80)
+ self.line_edit = QLineEdit(self.parameter)
+ self.line_edit.setObjectName("Write")
+ self.line_edit.setFixedHeight(24)
+ font = QFont("sans serif", 16)
+ fm = QFontMetricsF(font)
+ self.line_edit.setMaximumWidth(fm.width(self.parameter)+20)
+ self.label.setBuddy(self.line_edit)
+ layout = QBoxLayout(
+ QBoxLayout.LeftToRight if position == "LEFT" else \
+ QBoxLayout.TopToBottom)
+ layout.addWidget(self.label)
+ layout.addWidget(self.line_edit)
+ layout.addStretch()
+ layout.setSpacing(2)
+ layout.setContentsMargins(0, 0, 0, 0)
+ self.setLayout(layout)
+
+class QHLine(QFrame):
+ def __init__(self):
+ super(QHLine, self).__init__()
+ self.setFrameShape(QFrame.HLine)
+ self.setFrameShadow(QFrame.Sunken)
+
+class QVLine(QFrame):
+ def __init__(self):
+ super(QVLine, self).__init__()
+ self.setFrameShape(QFrame.VLine)
+ self.setFrameShadow(QFrame.Sunken)
+
+class AppQLineEdit(QLineEdit):
+ def __init__(self, parent=None):
+ #super().__init__(parent)
+ pass
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+class CAQLineEdit(QLineEdit, PVGateway):
+ '''Channel access enabled QLineEdit widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ notify_freq_hz: int = 0, precision: int = 0):
+
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback,
+ notify_freq_hz=notify_freq_hz, precision=precision)
+
+ self.is_initialize_complete()
+ self.configure_widget()
+
+ if not self.pv_within_daq_group:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(object, str, int)
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.NoFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 10))
+ qrect = fm.boundingRect(self.suggested_text)
+ _width_scaling_factor = 1.15
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ if self.pv_within_daq_group:
+ self.qt_property_initial_values(qt_object_name=self.PV_DAQ_CA)
+ else:
+ self.qt_property_initial_values(qt_object_name=self.PV_READBACK)
+
+ #renove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+class CAQLabel(QLabel, PVGateway):
+ '''Channel access enabled QLabel widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ notify_freq_hz: int = 0, precision: int = 0):
+
+ super().__init__(parent, pv_name, monitor_callback, pv_within_daq_group,
+ color_mode, show_units, prefix, suffix,
+ connect_callback=self.py_connect_callback,
+ notify_freq_hz=notify_freq_hz, precision=precision)
+
+ self.is_initialize_complete()
+
+ self.configure_widget()
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of
+ pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(object, str, int)
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.NoFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 10))
+ qrect = fm.boundingRect(self.suggested_text)
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth((qrect.width() * _width_scaling_factor))
+
+ if self.pv_within_daq_group:
+ self.qt_property_initial_values(qt_object_name=self.PV_DAQ_CA)
+ else:
+ self.qt_property_initial_values(qt_object_name=self.PV_READBACK)
+
+#For use with CAQMenu
+class QLineEditExtended(QLineEdit):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.parent = parent
+
+ def mousePressEvent(self, event):
+ button = event.button()
+ if button == Qt.RightButton:
+ self.parent.showContextMenu()
+ elif button == Qt.LeftButton:
+ self.parent.mousePressEvent(event)
+
+class CAQMenu(QComboBox, PVGateway):
+ '''Channel access enabled QMenu widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units=False, prefix: str = "", suffix: str = ""):
+
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete()
+ self.configure_widget()
+ #After configure:widget
+ self.currentIndexChanged.connect(self.value_change)
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of
+ pv connection status.
+ '''
+
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ def configure_widget(self):
+
+ self.previousIndex = None
+
+ self.setFocusPolicy(Qt.NoFocus)
+ self.setEditable(True)
+ self.setLineEdit(QLineEditExtended(self))
+ self.lineEdit().setReadOnly(True)
+ self.lineEdit().setAlignment(Qt.AlignCenter)
+
+ enumStringList = self.cafe.getEnumStrings(self.handle)
+
+ self.addItems(enumStringList)
+
+ for i in range(0, self.count()):
+ self.setItemData(i, Qt.AlignCenter, Qt.TextAlignmentRole)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 10))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.1
+
+ self.setFixedHeight(fm.lineSpacing()*1.8)
+ self.setFixedWidth((qrect.width()+40) * _width_scaling_factor)
+
+ self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
+
+ def post_display_value(self, value):
+
+
+ '''Convert value to index'''
+ if "setCurrentIndex" in dir(self):
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if isinstance(value, str):
+ self.setCurrentIndex(self.cafe.getEnumFromString(self.handle,
+ value))
+
+ elif isinstance(value, int):
+ self.setCurrentIndex(value)
+ #Should not happen
+ elif isinstance(value, float):
+ self.setCurrentIndex(int(value))
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+ #self.previousIndex = self.currentIndex()
+ return
+ else:
+ print(("ERROR: overloaded post_display_value: 'setCurrentIndex' "
+ "method does not exist!"))
+
+
+ def value_change(self, indx):
+ status = self.cafe.set(self.handle, indx)
+
+ if status != self.cyca.ICAFE_NORMAL:
+ #self.showSetErrorMsg(status)
+
+ value = self.cafe.getCache(self.handle, 'int')
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if value is not None:
+ self.setCurrentIndex(value)
+ else:
+ if self.previousIndex is not None:
+ self.setCurrentIndex(self.previousIndex)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ "CAQMenu set operation reports error:\n{0}".format(
+ self.cafe.getStatusCodeAsString(status)))
+ self.pv_message_in_a_box.exec()
+
+ def mousePressEvent(self, event):
+
+ button = event.button()
+ if button == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+
+ elif self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ event.ignore()
+ return
+
+ QComboBox.mousePressEvent(self, event)
+ self.previousIndex = self.currentIndex()
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ for i in range(0, self.count()):
+ self.setItemIcon(i, QIcon(":/forbidden.png"))
+ self.setStyleSheet(
+ ("QComboBox {background: transparent}" +
+ "QComboBox::drop-down {image: url(:/forbidden.png)}"))
+
+ def leaveEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ for i in range(0, self.count()):
+ self.setItemIcon(i, QIcon())
+ self.setStyleSheet(
+ "QComboBox::drop-down {background: transparent}")
+
+
+ #The widget should not gain focus by using the mouse wheel.
+ #This is accomplished by setting the focus policy to Qt.StrongFocus.
+ #The widget should only accept wheel events if it already has the
+ #focus. This is accomplished by reimplementing QWidget.wheelEvent
+ #within a QSpinBox subclass:
+ def wheelEvent(self, event):
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QComboBox.wheelEvent(self, event)
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ '''Triggered by monitor signal'''
+
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+
+class CAQMessageButton(QPushButton, PVGateway):
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ notify_freq_hz: int = 0,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units=False, msg_label: str = "",
+ msg_press_value=None, msg_release_value=None,
+ start_monitor=False):
+ super().__init__(parent=parent, pv_name=pv_name,
+ monitor_callback=monitor_callback,
+ notify_freq_hz=notify_freq_hz,
+ pv_within_daq_group=pv_within_daq_group,
+ color_mode=color_mode, show_units=show_units,
+ msg_label=msg_label,
+ connect_callback=self.py_connect_callback)
+
+ self.msg_press_value = msg_press_value
+ self.msg_release_value = msg_release_value
+
+ if self.msg_press_value is not None:
+ self.pressed.connect(self.act_on_pressed)
+ if self.msg_release_value is not None:
+ self.released.connect(self.act_on_released)
+
+ self.msg_label = msg_label
+ self.suggested_text = self.msg_label
+ _suggested_text_length = len(self.suggested_text)+3
+ self.suggested_text = self.suggested_text.rjust(_suggested_text_length,
+ "^")
+
+ self.configure_widget()
+
+ self.msg_press_status = self.cyca.ICAFE_NORMAL
+ self.msg_release_status = self.cyca.ICAFE_NORMAL
+ self.msg_report_status = "PV={0}\n".format(self.pv_name)
+ self.msg_has_error = False
+
+ if not self.pv_within_daq_group and start_monitor:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of
+ pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.StrongFocus)
+ self.setCheckable(True) #Recognizes press and release states
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.0
+
+ self.setText(self.msg_label)
+ self.setFixedHeight((fm.lineSpacing()*2.0))
+ self.setFixedWidth((qrect.width() * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
+
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+
+ def leaveEvent(self, event):
+ if self.property("readOnly"):
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ def mouseReleaseEvent(self, event):
+ if self.msg_release_value is not None:
+ time.sleep(0.1)
+ QPushButton.mouseReleaseEvent(self, event)
+
+ def mousePressEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 1:
+ QPushButton.mousePressEvent(self, event)
+ if event.button() == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+
+ def act_on_pressed(self):
+ if self.msg_press_value is not None:
+ self.msg_press_status = self.cafe.set(self.handle,
+ self.msg_press_value)
+ if self.msg_press_status != self.cyca.ICAFE_NORMAL:
+ self.msg_report_status += (
+ "Error in set operation (at press button):\n{0}\n".format(
+ self.cafe.getStatusCodeAsString(self.msg_press_status)))
+ self.msg_has_error = True
+ qm = QMessageBox()
+ qm.setText(self.msg_report_status)
+ qm.exec()
+ QApplication.processEvents()
+
+ def act_on_released(self):
+ if self.msg_release_value is not None:
+ self.msg_release_status = self.cafe.set(self.handle,
+ self.msg_release_value)
+ if self.msg_release_status != self.cyca.ICAFE_NORMAL:
+ self.msg_report_status += (
+ "Error in set operation (at release button):\n{0}\n".format(
+ self.cafe.getStatusCodeAsString(self.msg_release_status)))
+ self.msg_has_error = True
+
+ if self.msg_has_error:
+ self.msg_has_error = False
+ self.pv_message_in_a_box.setText(self.msg_report_status)
+ self.pv_message_in_a_box.exec()
+ self.msg_report_status = "PV={0}\n".format(self.pv_name)
+ qm = QMessageBox()
+ qm.setText(self.msg_report_status)
+ qm.exec()
+ QApplication.processEvents()
+
+class CAQTextEntry(QLineEdit, PVGateway):
+ '''Channel access enabled QTextEntry widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units=False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent, pv_name, monitor_callback, pv_within_daq_group,
+ color_mode, show_units, prefix, suffix,
+ connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete() #waits a fraction of a second
+
+ self.currentText = ""
+ self.returnPressed.connect(self.valuechange)
+ self.configure_widget()
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of
+ pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()+10) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
+
+ def valuechange(self):
+ status = self.cafe.set(self.handle, self.text())
+ if status != self.cyca.ICAFE_NORMAL:
+ if self.cafe.getNoMonitors(self.handle) == 0:
+ val = self.cafe.get(self.handle, 'native')
+ else:
+ val = self.cafe.getCache(self.handle, 'native')
+
+ if val is not None:
+ if isinstance(val, str):
+ strText = val
+ else:
+ valStr = ("{: .%sf}" % self.precision)
+ strText = valStr.format(round(val, self.precision))
+ print(strText, " precision ", self.precision)
+ self.setText(strText)
+ else:
+ #Do this for TextInfo cache
+ if self.cafe.getNoMonitors(self.handle) == 0:
+ val = self.cafe.get(self.handle, 'native')
+
+ def setText(self, value):
+ QLineEdit.setText(self, value)
+ self.currentText = self.text()
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ def leaveEvent(self, event):
+
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ if self.text() != self.currentText:
+ QLineEdit.setText(self, self.currentText)
+
+ self.setCursorPosition(100)
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+ self.clearFocus()
+ return
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.cursorPositionAt(local_event_position)
+ self.setCursorPosition(local_cursor_position)
+
+
+class CAQSpinBox(QSpinBox, PVGateway):
+ '''Channel access enabled QTextEntry widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units=False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent, pv_name, monitor_callback, pv_within_daq_group,
+ color_mode, show_units, prefix, suffix,
+ connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete()
+
+ self.valueChanged.connect(self.value_change)
+ self.configure_widget()
+ if not self.pv_within_daq_group:
+ self.monitor_start()
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''
+ Callback function to be invoked on change of pv connection
+ status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.previousValue = None
+ self.currentValue = None
+ self.setFocusPolicy(Qt.StrongFocus)
+ self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ self.setAccelerated(False)
+ self.setLineEdit(QLineEditExtended(self))
+ self.lineEdit().setEnabled(True)
+ self.lineEdit().setReadOnly(False)
+ self.lineEdit().setAlignment(Qt.AlignLeft)
+ self.lineEdit().setFont(QFont("Sans Serif", 16))
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+
+ _suggested_text = self.max_control_abs_str
+ _added_text = ""
+
+ if self.show_units:
+ _added_text += " " + self.units
+ _suggested_text += self.units
+ if self.suffix:
+ _added_text += " " + self.suffix
+ _suggested_text += self.suffix
+
+ self.setSuffix(_added_text)
+
+ qrect = fm.boundingRect(_suggested_text)
+ _width_scaling_factor = 1.0
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
+
+ if self.pv_ctrl is not None:
+ self.setRange(int(self.pv_ctrl.lowerControlLimit),
+ int(self.pv_ctrl.upperControlLimit))
+
+
+ def post_display_value(self, value):
+ '''Convert value to index'''
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ self.setValue(int(round(value)))
+ self.blockSignals(False)
+ else:
+ self.setValue(int(round(value)))
+
+
+ def mousePressEvent(self, event):
+ _opt = QStyleOptionSpinBox()
+ self.initStyleOption(_opt)
+ _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxUp, self)
+ _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxDown, self)
+
+ self.previousValue = self.value()
+
+ if event.button() == Qt.LeftButton:
+ if _rect_up.contains(event.pos(), proper=True) or \
+ _rect_down.contains(event.pos(), proper=True):
+
+ if not self.cafe.isConnected(self.handle):
+ self.pv_message_in_a_box.setText(
+ ("Spinbox change value events currently suspended\n" +
+ "as channel {0} is disconnected.").format(
+ self.pv_name))
+ self.pv_message_in_a_box.exec()
+ return
+
+ QSpinBox.mousePressEvent(self, event)
+ #Clear Focus: only one step per mouse click.
+ self.clearFocus()
+
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.lineEdit().cursorPositionAt(
+ local_event_position)
+
+ self.lineEdit().setCursorPosition(local_cursor_position)
+
+ PVGateway.mousePressEvent(self, event)
+
+ def setValue(self, intVal):
+ QSpinBox.setValue(self, intVal)
+ self.currentValue = self.value()
+
+ def value_change(self, intVal):
+
+ status = self.cafe.set(self.handle, intVal)
+ if status != self.cyca.ICAFE_NORMAL:
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'int')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("Spinbox set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+ else:
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'int')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ self.parent.statusbar.showMessage(
+ (self.widget_class + " " +
+ self.cafe.getStatusCodeAsString(status)))
+
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ def leaveEvent(self, event):
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+
+ def keyPressEvent(self, event):
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ QSpinBox.keyPressEvent(self, event)
+ self.clearFocus()
+ elif event.key() in (Qt.Key_Up, Qt.Key_Down):
+ QSpinBox.keyPressEvent(self, event)
+ else:
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ QSpinBox.keyPressEvent(self, event)
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+ # The spin box should not gain focus by using the mouse wheel.
+ # This is accomplished by setting the focus policy to Qt.StrongFocus.
+ # The spin box should only accept wheel events if it already has the focus.
+ # This is accomplished by reimplementing QWidget.wheelEvent within a
+ # QSpinBox subclass:
+ def wheelEvent(self, event):
+ #print("wheelEvent", self.hasFocus())
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QSpinBox.wheelEvent(self, event)
+
+
+class CAQDoubleSpinBox(QDoubleSpinBox, PVGateway):
+ '''Channel access enabled QDoubleSpinBox widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode=None,
+ show_units: bool = False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent=parent, pv_name=pv_name,
+ monitor_callback=monitor_callback,
+ pv_within_daq_group=pv_within_daq_group,
+ color_mode=color_mode, show_units=show_units,
+ prefix=prefix, suffix=suffix,
+ connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete()
+ self.valueChanged.connect(self.valuechange)
+ self.configure_widget()
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''
+ Callback function to be invoked on change of pv connection
+ status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.previousValue = None
+ self.currentValue = None
+ self.setFocusPolicy(Qt.StrongFocus)
+ self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ self.setAccelerated(False)
+ self.setLineEdit(QLineEditExtended(self))
+ self.lineEdit().setReadOnly(False)
+ self.lineEdit().setAlignment(Qt.AlignRight)
+ self.lineEdit().setFont(QFont("Sans Serif", 12))
+
+ _stepsize = 10**(self.precision * -1)
+ self.setSingleStep(_stepsize)
+ self.setDecimals(self.precision)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+
+ _suggested_text = self.suggested_text
+ _added_text = ""
+
+ if self.show_units:
+ _added_text += " " + self.units
+ _suggested_text += self.units
+ if self.suffix:
+ _added_text += " " + self.suffix
+ _suggested_text += self.suffix
+
+ self.setSuffix(_added_text)
+
+ qrect = fm.boundingRect(_suggested_text)
+
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
+
+ if self.pv_ctrl is not None:
+ self.setRange(int(self.pv_ctrl.lowerControlLimit),
+ int(self.pv_ctrl.upperControlLimit))
+
+
+ def post_display_value(self, value):
+ '''set value from monitor'''
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ self.setValue(value)
+ self.blockSignals(False)
+ else:
+ self.setValue(value)
+
+ def mousePressEvent(self, event):
+
+ _opt = QStyleOptionSpinBox()
+ self.initStyleOption(_opt)
+ _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxUp, self)
+ _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxDown, self)
+ self.previousValue = self.value()
+
+ if event.button() == Qt.LeftButton:
+ if _rect_up.contains(event.pos(), proper=False) or \
+ _rect_down.contains(event.pos(), proper=False):
+
+ if not self.cafe.isConnected(self.handle):
+ self.pv_message_in_a_box.setText(
+ ("Spinbox change value events currently suspended\n" +
+ "as channel {0} is disconnected.").format(
+ self.pv_name))
+ self.pv_message_in_a_box.exec()
+ return
+
+ QDoubleSpinBox.mousePressEvent(self, event)
+
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.lineEdit().cursorPositionAt(
+ local_event_position)
+
+ self.lineEdit().setCursorPosition(local_cursor_position)
+
+ PVGateway.mousePressEvent(self, event)
+
+ def mouseReleaseEvent(self, event):
+ self.clearFocus()
+
+ def setValue(self, value):
+ self.currentValue = self.value()
+ QDoubleSpinBox.setValue(self, value)
+
+ def valuechange(self, fval):
+ status = self.cafe.set(self.handle, fval)
+
+ if status != self.cyca.ICAFE_NORMAL:
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'float')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("Spinbox set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+ else:
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'float')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ self.parent.statusbar.showMessage(
+ (self.widget_class + " " +
+ self.cafe.getStatusCodeAsString(status)))
+
+
+ def enterEvent(self, event):
+ self.setFocusPolicy(Qt.StrongFocus)
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+
+ def leaveEvent(self, event):
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+ def keyPressEvent(self, event):
+
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ QDoubleSpinBox.keyPressEvent(self, event)
+ self.clearFocus()
+ elif event.key() in (Qt.Key_Up, Qt.Key_Down):
+ QDoubleSpinBox.keyPressEvent(self, event)
+ else:
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ QDoubleSpinBox.keyPressEvent(self, event)
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ # The spin box should not gain focus by using the mouse wheel.
+ # This is accomplished by setting the focus policy to Qt.StrongFocus.
+ # The spin box should only accept wheel events if it already has the focus.
+ # This is accomplished by reimplementing QWidget.wheelEvent within a
+ # QSpinBox subclass:
+ def wheelEvent(self, event):
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QDoubleSpinBox.wheelEvent(self, event)
+
+
+class reconnectQPushButton(QPushButton, QThread):
+ def __init__(self, parent=None):
+ super().__init__()
+ self.parent = parent
+ self.clicked.connect(self.onClicked)
+ self.isdirty = False
+ self._handles_to_reconnect = []
+ self.reconnectThread = None
+
+ def onClicked(self, event):
+
+ self._handles_to_reconnect = []
+
+ for i in range(0, len(self.parent.pv_gateway)):
+ if self.parent.item(
+ i, self.parent.no_columns-1).checkState() == Qt.Checked:
+ self._handles_to_reconnect.append(
+ self.parent.pv_gateway[i].handle)
+
+ self.reconnect()
+ QApplication.processEvents()
+
+ def reconnect(self):
+ QApplication.processEvents()
+
+ self.isdirty = True
+ if self._handles_to_reconnect:
+ self.parent.cafe.reconnect(self._handles_to_reconnect)
+ self.isdirty = False
+ #Uncheck reconnected channels
+ for i in range(0, len(self.parent.pv_gateway)):
+ if self.parent.item(
+ i, self.parent.no_columns-1).checkState() == Qt.Checked:
+ if self.parent.cafe.isConnected(
+ self.parent.pv_gateway[i].handle):
+ self.parent.item(
+ i, self.parent.no_columns-1).setCheckState(False)
+
+ #Uncheck global reconnect check box
+ self.parent.cb_item_all.setCheckState(Qt.Unchecked)
+
+
+class CAQTableWidget(QTableWidget):
+ '''Channel access enabled QTableWidget widget'''
+ #trigger_monitor_float = Signal(float, int, int)
+ #trigger_monitor_int = Signal(int, int, int)
+ #trigger_monitor_str = Signal(str, int, int)
+ #trigger_connect = Signal(int, str, int)
+
+ def hasNewData(self, _row, pv_data):
+
+ if self.pv_gateway[_row].pvd_previous is None:
+ return True
+
+ newDataFlag = False
+
+ if self.pv_gateway[_row].pvd_previous.ts[1] != pv_data.ts[1]:
+ newDataFlag = True
+ elif self.pv_gateway[_row].pvd_previous.ts[0] != pv_data.ts[0]:
+ newDataFlag = True
+ # Catch disconnect events(!!) and set newDataFlag only
+ elif self.pv_gateway[_row].pvd_previous.status != pv_data.status:
+ newDataFlag = True
+ return newDataFlag
+
+
+ def paint_rows(self, row_range: list = [], reset=False, last_row=[" ", " "],
+ columns=[0]):
+
+ _qcolor_last_line = QColor("#d1e8e9")
+ self.font_pts11 = QTableWidgetItem().font()
+ self.font_pts11.setPixelSize(11)
+ if reset:
+ _qcolor = self.item(0, self.columnCount()-1).background()
+ _start = 0
+ _end = self.rowCount()-1
+ else:
+ _qcolor = _qcolor_last_line
+ _start = row_range[0]
+ _end = row_range[1]
+
+ for _row in range(_start, _end):
+ _cell = QTableWidgetItem("{0}".format(_row+1))
+ if not reset:
+ _cell.setFont(self.font_pts11)
+ _cell.setBackground(_qcolor)
+
+ if 1 in columns:
+ self.item(_row, 0).setBackground(_qcolor)
+ self.item(_row, 0).setFont(self.font_pts11)
+ if 0 in columns:
+ self.setVerticalHeaderItem(_row, _cell)
+
+
+ #last row
+
+ if reset and 0 in columns:
+ _cell = QTableWidgetItem("{0}".format(last_row[0]))
+ _cell.setFont(self.font_pts11)
+ self.setVerticalHeaderItem(self.rowCount()-1, _cell)
+
+ self.item(self.rowCount()-1, 0).setTextAlignment(Qt.AlignCenter)
+ self.item(self.rowCount()-1, 0).setText(str(last_row[1]))
+ self.item(self.rowCount()-1, 0).setBackground(_qcolor)
+ self.item(self.rowCount()-1, 0).setFont(self.font_pts11)
+ elif last_row[0] != " ":
+ _cell = QTableWidgetItem("{0}".format(last_row[0]))
+ _cell.setBackground(_qcolor_last_line)
+ _cell.setFont(self.font_pts11)
+ self.setVerticalHeaderItem(self.rowCount()-1, _cell)
+
+ if columns:
+ self.item(self.rowCount()-1, 0).setTextAlignment(Qt.AlignCenter)
+ self.item(self.rowCount()-1, 0).setText(str(last_row[1]))
+ self.item(self.rowCount()-1, 0).setBackground(_qcolor_last_line)
+ self.item(self.rowCount()-1, 0).setFont(self.font_pts11)
+
+
+ def widget_update(self):
+
+ for _row, pvgate in enumerate(self.pv_gateway):
+ #for _row in range(0, len(self.pv_gateway)):
+ if not pvgate.notify_unison:
+ continue
+ _handle = pvgate.handle
+ _pvd = pvgate.cafe.getPVCache(_handle)
+
+ if _pvd.status in (self.cyca.ICAFE_CS_NEVER_CONN,
+ self.cyca.ICAFE_CA_OP_CONN_DOWN):
+ pvgate.pvd_previous = _pvd
+ continue
+
+ pvgate.pvd_previous = _pvd
+
+ #if timestamps the same - then skip
+ _value = _pvd.value[0]
+ _value = pvgate.format_display_value(_value)
+
+ qtwi = QTableWidgetItem(str(_value)+ " ")
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+ self.setItem(_row, self.no_columns-3,
+ QTableWidgetItem(qtwi))
+ self.item(_row, self.no_columns-3).setTextAlignment(Qt.AlignRight |
+ Qt.AlignVCenter)
+
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target - 1
+ _ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0: _ts_str_len - (self.format_ts_nano -
+ self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec:
+ _ts_str += "."
+ while _ts_str_len < _ilength_target:
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ qtwi = QTableWidgetItem(_ts_str)
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+ self.setItem(_row, self.no_columns-2, QTableWidgetItem(qtwi))
+ self.item(_row, self.no_columns-2).setTextAlignment(Qt.AlignCenter)
+
+ _prop = pvgate.qt_dynamic_property_get()
+
+ alarm_severity = _pvd.alarmSeverity
+
+ if _prop == pvgate.READBACK_ALARM:
+
+ if alarm_severity == pvgate.cyca.SEV_MAJOR:
+ _bgcolor = pvgate.fg_alarm_major
+ _fgcolor = "black"
+ elif alarm_severity == pvgate.cyca.SEV_MINOR:
+ _bgcolor = pvgate.fg_alarm_minor
+ _fgcolor = "black"
+ elif alarm_severity == pvgate.cyca.SEV_INVALID:
+ _bgcolor = pvgate.fg_alarm_invalid
+ _fgcolor = "#777777"
+ else:
+ _bgcolor = pvgate.fg_alarm_noalarm
+ _fgcolor = "black"
+
+ #Colors for bg/fg reversed as is the old norm
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.no_columns-2).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.no_columns-3).setForeground(
+ QColor(_fgcolor))
+ self.item(_row, self.no_columns-2).setForeground(
+ QColor(_fgcolor))
+
+ elif _prop == pvgate.READBACK_STATIC:
+
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor(pvgate.bg_readback))
+ self.item(_row, self.no_columns-2).setBackground(
+ QColor(pvgate.bg_readback))
+
+ elif _prop == pvgate.DISCONNECTED:
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.no_columns-2).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.no_columns-3).setForeground(
+ QColor("#777777"))
+ self.item(_row, self.no_columns-2).setForeground(
+ QColor("#777777"))
+
+ else:
+ print(_prop, "widget_update unknown in element/row", _row,
+ _row+1)
+
+ QApplication.processEvents()
+
+ def __init__(self, parent=None, pv_list: list = ["PV_NAME_NOT_GIVEN"],
+ monitor_callback=None, pv_within_daq_group: bool = False,
+ color_mode=None, show_units: bool = True, prefix: str = "",
+ suffix: str = "", ts_res: str = "milli",
+ init_column: bool = False, init_list: list = [],
+ notify_freq_hz: int = 0, notify_unison: bool = True,
+ precision: int = 0, scale_factor: float = 1,
+ show_timestamp: bool = True, pv_list_show: list = None):
+
+ super().__init__()
+ self.columns_dict = {}
+ _column_dict_value = 0
+ self.columns_dict['PV'] = _column_dict_value
+ if init_column:
+ _column_dict_value += 1
+ self.columns_dict['Init'] = _column_dict_value
+ _column_dict_value += 1
+ self.columns_dict['Value'] = _column_dict_value
+ if show_timestamp:
+ _column_dict_value += 1
+ self.columns_dict['Timestamp'] = _column_dict_value
+ _column_dict_value += 1
+ self.columns_dict['Reconnect'] = _column_dict_value
+
+ self.setWindowModality(Qt.ApplicationModal)
+ self.no_columns = _column_dict_value + 1
+
+ self.init_column = init_column
+
+ self.init_list = init_list
+ if self.init_column and not self.init_list:
+ self.init_list = pv_list
+
+ self.icount = 0
+ self.notify_freq_hz = abs(notify_freq_hz)
+ self.notify_freq_hz_default = self.notify_freq_hz
+ self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
+ 1000 / self.notify_freq_hz
+
+ self.notify_unison = bool(notify_unison) and bool(self.notify_freq_hz)
+
+ self.precision = precision
+ self.scale_factor = scale_factor
+ self.show_timestamp = show_timestamp
+
+ self.format_ts_nano = 31 #max length of date
+ self.format_ts_micro = 28
+ self.format_ts_milli = 25
+ self.format_ts_deci = 23 #-8
+ self.format_ts_sec = 21
+ if "nano" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_nano
+ elif "micro" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_micro
+ elif "milli" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_milli
+ elif "deci" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_deci
+ elif "sec" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_sec
+ else:
+ self.format_ts_decimal_part = self.format_ts_milli
+
+ self.pv2item_dict = {}
+
+ self.pv_list = pv_list
+ self.pv_gateway = [None] * len(self.pv_list)
+
+ self.pv_list_show = pv_list_show
+ if self.pv_list_show is None:
+ self.pv_list_show = self.pv_list
+
+ _color_mode = [None] * len(self.pv_list)
+
+ if isinstance(color_mode, list):
+ for i in range(0, len(color_mode)):
+ _color_mode[i] = color_mode[i]
+
+ for i in range(0, len(self.pv_list)):
+
+ self.pv_gateway[i] = PVGateway(
+ parent, self.pv_list[i], monitor_callback,
+ pv_within_daq_group, _color_mode[i], show_units, prefix, suffix,
+ connect_triggers=False, notify_freq_hz=self.notify_freq_hz,
+ notify_unison=self.notify_unison, precision=self.precision)
+
+ self.pv_gateway[i].is_initialize_complete()
+ self.pv_gateway[i].trigger_connect.connect(
+ self.receive_connect_update)
+ self.pv_gateway[i].trigger_monitor_str.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_int.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_float.connect(
+ self.receive_monitor_update)
+
+ self.pv_gateway[i].widget_class = "QTableWidgetItem"
+
+ self.pv_gateway[i].qt_property_initial_values(
+ qt_object_name=self.pv_gateway[i].PV_READBACK, tool_tip=False)
+
+ #required for reconnect
+ self.cafe = self.pv_gateway[0].cafe
+ self.cyca = self.pv_gateway[0].cyca
+
+ self.timer = None
+ if self.notify_unison:
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.widget_update)
+ self.timer.singleShot(0, self.widget_update)
+ self.timer.start(self.notify_milliseconds)
+
+ self.configure_widget()
+
+ #Connect only deals with colours - only helps on reconnect
+ # In any case monitors take over
+ #Got to do this earlier or emit immediately after!!
+ for i in range(0, len(self.pv_gateway)):
+ if self.cafe.isConnected(self.pv_gateway[i].pv_name):
+ self.pv_gateway[i].trigger_connect.emit(
+ self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
+ self.pv_gateway[i].cyca.ICAFE_CS_CONN)
+
+ for i in range(0, len(self.pv_gateway)):
+ if not self.pv_gateway[i].pv_within_daq_group:
+ self.pv_gateway[i].monitor_start()
+
+ self.update_init_values()
+
+ self.configure_context_menu()
+
+
+ def configure_context_menu(self):
+ self.table_context_menu = QMenu()
+ self.table_context_menu.setObjectName("contextMenu")
+ self.table_context_menu.setWindowModality(Qt.NonModal)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.table_context_menu.addSection("---")
+
+ action1 = QAction("Configure Table PVs", self)
+ action1.triggered.connect(self.display_table_parameters)
+ self.table_context_menu.addAction(action1)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.table_context_menu.addSection("---")
+
+ QApplication.processEvents()
+
+
+ def restore_init_values(self, pv_list: list = []):
+ _set_values_dict = self.get_init_values()
+
+ if not pv_list:
+ _pvs_to_set, _values_to_set = zip(*_set_values_dict.items())
+ #zip returns tuples
+ _pvs_to_set = list(_pvs_to_set)
+ _values_to_set = list(_values_to_set)
+ else:
+ _pvs_to_set = []
+ _values_to_set = []
+ for pv in pv_list:
+ if pv in _set_values_dict.keys():
+ _pvs_to_set.append(pv)
+ _values_to_set.append(_set_values_dict[pv])
+
+ status, status_list = self.cafe.setScalarList(_pvs_to_set,
+ _values_to_set)
+
+ if status != self.cyca.ICAFE_NORMAL:
+ _mess = ("The following device(s) reported an error " +
+ "in 'set' operation:")
+ for i, status_value in enumerate(status_list):
+ if status_value != self.cyca.ICAFE_NORMAL:
+ _mess += ("\n" + _pvs_to_set[i] + " has status = " +
+ str(status_value) + " " +
+ self.cafe.getStatusCodeAsString(status_value) +
+ " " + self.cafe.getStatusInfo(status_value))
+ qm = QMessageBox()
+ qm.setText(_mess)
+
+ qm.exec()
+ QApplication.processEvents()
+
+ self.init_value_button.setEnabled(True)
+
+
+ def is_same_as_init_values(self):
+ _init_values_dict = self.get_column_values(self.columns_dict['Init'])
+ _pvs, _init_values = zip(*_init_values_dict.items())
+ _current_values_dict = self.get_column_values(
+ self.columns_dict['Value'])
+ _pvs, _current_values = zip(*_current_values_dict.items())
+ #zip returns tuples
+
+ return bool(func_reduce(lambda i, j: i and j, map(
+ lambda m, k: m == k, _init_values, _current_values), True))
+
+ #if func_reduce(lambda i, j: i and j, map(
+ # lambda m, k: m == k, _init_values, _current_values), True):
+ # return True
+ #else:
+ # return False
+
+
+ def get_column_values(self, column_no):
+ _values_dict = {}
+ _start = 0
+ _end = len(self.pv_gateway)
+ _pvs = [None] * _end
+ _values_str = [None] * _end
+ _values = [None] * _end
+
+ for _row in range(_start, _end):
+ _values_str[_row] = self.item(_row, column_no).text()
+ _pvs[_row] = self.item(_row, 0).text()
+
+ _value_list = [float(_value_list) for _value_list in re.findall(
+ r'-?\d+\.?\d*', _values_str[_row])]
+
+ if not _value_list:
+ print("row", _row, "values", _values_str[_row], _pvs[_row])
+ _values[_row] = _values_str[_row] #Can be enum string
+ else:
+ _values[_row] = _value_list[0]
+
+ if _pvs[_row] in self.pv_list_show:
+ _values_dict[self.pv_gateway[_row].pv_name] = _values[_row]
+
+ return _values_dict #_pvs_to_set, _values_to_set
+
+
+ def get_init_values(self):
+ return self.get_column_values(self.columns_dict['Init'])
+
+ def get_init_values_previous(self):
+ _set_values_dict = {}
+ _start = 0
+ _end = len(self.pv_gateway)
+ _pvs_to_set = [None] * _end
+ _values_to_set_str = [None] * _end
+ _values_to_set = [None] * _end
+ for _row in range(_start, _end):
+ _values_to_set_str[_row] = self.item(
+ _row, self.columns_dict['Init']).text()
+ _pvs_to_set[_row] = self.item(_row, self.columns_dict['PV']).text()
+
+ _value_list = [float(_value_list) for _value_list in re.findall(
+ r'-?\d+\.?\d*', _values_to_set_str[_row])]
+
+ if not _value_list:
+ print("//row", _row, "values", _values_to_set_str[_row],
+ _pvs_to_set[_row])
+ _values_to_set[_row] = _values_to_set_str[_row] #Can be enum str
+ else:
+ _values_to_set[_row] = _value_list[0]
+
+
+ if _pvs_to_set[_row] in self.init_list:
+ _set_values_dict[
+ self.pv_gateway[_row].pv_name] = _values_to_set[_row]
+
+ return _set_values_dict
+
+
+ def update_init_values(self):
+ _start = 0
+ _end = len(self.pv_gateway)
+
+ for _row in range(_start, _end):
+ _handle = self.pv_gateway[_row].handle
+ _value = self.pv_gateway[_row].cafe.getCache(_handle)
+
+ if _value is not None:
+ if self.scale_factor != 1:
+ _value = _value * self.scale_factor
+ _value = self.pv_gateway[_row].format_display_value(_value)
+
+ qtwi = QTableWidgetItem(str(_value)+ " ")
+ _f = qtwi.font()
+ _f.setPointSize(8)
+ qtwi.setFont(_f)
+ self.setItem(_row, 1, qtwi)
+ self.item(_row, 1).setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
+
+
+ def configure_widget(self):
+
+ _column_width_pvname = 180
+ _column_width_value = 90
+ _column_width_timestamp = 210
+ _column_width_checkbox = 22
+
+ self.setRowCount(len(self.pv_gateway)+1)
+ self.setColumnCount(self.no_columns)
+ self.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.resizeColumnsToContents()
+ self.resizeRowsToContents()
+ #self.horizontalHeader().setStretchLastSection(True);
+ self.setColumnWidth(self.columns_dict['PV'], _column_width_pvname)
+
+ self.setColumnWidth(self.columns_dict['Value'], _column_width_value)
+ if 'Init' in self.columns_dict.keys():
+ self.setColumnWidth(self.columns_dict['Init'], _column_width_value)
+ if 'Timestamp' in self.columns_dict.keys():
+ self.setColumnWidth(self.columns_dict['Timestamp'],
+ _column_width_timestamp)
+ self.setColumnWidth(self.columns_dict['Reconnect'],
+ _column_width_checkbox)
+
+ _pv_column = self.columns_dict['PV']
+
+ for i in range(0, len(self.pv_gateway)):
+ qtwt = QTableWidgetItem(self.pv_list_show[i])
+ f = qtwt.font()
+ f.setPointSize(8)
+ qtwt.setFont(f)
+
+ self.setItem(i, _pv_column, qtwt)
+ self.item(i, _pv_column).setTextAlignment(Qt.AlignHCenter |
+ Qt.AlignVCenter)
+ for i_column in range(1, self.no_columns-1):
+ self.setItem(i, i_column, QTableWidgetItem(str("")))
+ self.item(i, i_column).setTextAlignment(Qt.AlignHCenter |
+ Qt.AlignVCenter)
+ self.pv2item_dict[self.pv_gateway[i]] = i
+
+ cb_item = QTableWidgetItem()
+ cb_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
+ cb_item.setCheckState(Qt.Unchecked)
+ cb_item.setTextAlignment(Qt.AlignCenter)
+ cb_item.setToolTip(self.pv_gateway[i].pv_name)
+
+ self.setItem(i, self.no_columns-1, cb_item)
+ self.item(i, self.no_columns-1).setTextAlignment(Qt.AlignCenter)
+
+ if self.init_column:
+ self.init_widget = QWidget()
+ _init_layout = QHBoxLayout(self.init_widget)
+ self.init_value_button = QPushButton()
+ self.init_value_button.setText("Update")
+ _f = self.init_value_button.font()
+ _f.setPointSize(8)
+ self.init_value_button.setFont(_f)
+ self.init_value_button.setFixedWidth(80)
+ self.init_value_button.clicked.connect(self.update_init_values)
+ self.init_value_button.setToolTip(
+ ("Stores initial, pre-measurement value. Update is also " +
+ "typically executed automatically before new optics are set."))
+ _init_layout.addWidget(self.init_value_button)
+ _init_layout.setAlignment(Qt.AlignRight)
+ _init_layout.setContentsMargins(1, 1, 0, 0) #Required
+ self.init_widget.setLayout(_init_layout)
+ self.setCellWidget(len(self.pv_gateway), 1, self.init_widget)
+
+ _restore_widget = QWidget()
+ _restore_layout = QHBoxLayout(_restore_widget)
+ self.restore_value_button = QPushButton()
+ self.restore_value_button.setStyleSheet(
+ "QPushButton{background-color: rgb(212, 219, 157);}")
+ self.restore_value_button.setText("Restore")
+ _f = self.restore_value_button.font()
+ _f.setPointSize(8)
+ self.restore_value_button.setFont(_f)
+ self.restore_value_button.setFixedWidth(80)
+ self.restore_value_button.clicked.connect(self.restore_init_values)
+ self.restore_value_button.setToolTip(
+ ("Restore devices to their pre-measurement values"))
+ _restore_layout.addWidget(self.restore_value_button)
+ _restore_layout.setAlignment(Qt.AlignRight)
+ _restore_layout.setContentsMargins(1, 1, 0, 0)
+ _restore_widget.setLayout(_restore_layout)
+ self.setCellWidget(len(self.pv_gateway), 2, _restore_widget)
+
+ #Do not display no for last row (Reconnect button)
+ _row_digit_last_cell = QTableWidgetItem(str(""))
+ self.setVerticalHeaderItem(len(self.pv_gateway), _row_digit_last_cell)
+ self.setItem(len(self.pv_gateway), 0, QTableWidgetItem(str("")))
+
+ _qwb = QWidget()
+
+ self.reconnect_button = reconnectQPushButton(self) #self required
+
+ f = self.reconnect_button.font()
+
+ if 'Timestamp' in self.columns_dict.keys():
+ f.setPointSize(8)
+ self.reconnect_button.setFixedWidth(100)
+ else:
+ f.setPointSize(6)
+ self.reconnect_button.setFixedWidth(58)
+
+ self.reconnect_button.setFont(f)
+
+ self.reconnect_button.setText("Reconnect")
+
+ _layout = QHBoxLayout(_qwb)
+ _layout.addWidget(self.reconnect_button)
+ _layout.setAlignment(Qt.AlignCenter)
+ _layout.setContentsMargins(0, 0, 0, 0) #Required
+
+ #_reconnect_button
+ self.setCellWidget(len(self.pv_gateway), self.no_columns-2, _qwb)
+
+ self.cb_item_all = QCheckBox()
+ self.cb_item_all.setCheckState(Qt.Unchecked)
+ self.cb_item_all.stateChanged.connect(self.reconnectStateChanged)
+ self.cb_item_all.setObjectName("Reconnect")
+
+ self.setCellWidget(len(self.pv_gateway), self.no_columns-1,
+ self.cb_item_all)
+
+ header_item = QTableWidgetItem("Process Variable")
+
+ self.setHorizontalHeaderItem(self.columns_dict['PV'], header_item)
+
+ if 'Init' in self.columns_dict.keys():
+ self.setHorizontalHeaderItem(self.columns_dict['Init'],
+ QTableWidgetItem("Initial Value"))
+
+ self.setHorizontalHeaderItem(self.columns_dict['Value'],
+ QTableWidgetItem("Value"))
+
+ if 'Timestamp' in self.columns_dict.keys():
+ self.setHorizontalHeaderItem(self.columns_dict['Timestamp'],
+ QTableWidgetItem("Timestamp"))
+ self.setHorizontalHeaderItem(self.columns_dict['Reconnect'],
+ QTableWidgetItem("R"))
+ self.setFocusPolicy(Qt.NoFocus)
+ self.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.setSelectionMode(QAbstractItemView.NoSelection)
+
+ self.verticalHeader().setDefaultAlignment(Qt.AlignRight)
+ self.verticalHeader().setFixedWidth(22)
+
+ _fm_font = QFont("Sans Serif")
+ _fm_font.setPointSize(12)
+ fm = QFontMetricsF(_fm_font)
+
+ _factor = 1
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ self.setFixedHeight(
+ int(fm.lineSpacing() * _factor * (len(self.pv_gateway)+3)))
+ _min_table_width = 620 if not self.init_column else 650
+ self.setMinimumWidth(_min_table_width)
+
+ for _row in range(0, len(self.pv_gateway)):
+ self.item(_row, _pv_column).setForeground(QColor("#000000"))
+
+ for i_column in range(1, self.no_columns-2):
+ self.item(_row, i_column).setForeground(QColor("#000000"))
+ self.item(_row, i_column).setTextAlignment(Qt.AlignRight |
+ Qt.AlignVCenter)
+
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor("#ffffff"))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row,
+ self.columns_dict['Timestamp']).setTextAlignment(
+ Qt.AlignCenter)
+ self.item(_row,
+ self.columns_dict['Timestamp']).setBackground(
+ QColor("#ffffff"))
+
+ @Slot(int)
+ def reconnectStateChanged(self, state):
+ if state == Qt.Unchecked:
+ for i in range(0, len(self.pv_gateway)):
+ self.item(i, self.columns_dict['Reconnect']).setCheckState(
+ Qt.Unchecked)
+ else:
+ for i in range(0, len(self.pv_gateway)):
+ self.item(i, self.columns_dict['Reconnect']).setCheckState(
+ Qt.Checked)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ _row = self.pv2item_dict[self.sender()]
+ self.pv_gateway[_row].time_monotonic = time.monotonic()
+ if self.scale_factor != 1:
+ value = value * self.scale_factor
+ _value = self.pv_gateway[_row].format_display_value(value)
+
+ qtwi = QTableWidgetItem(str(_value) + " ")
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+ self.setItem(_row, self.columns_dict['Value'], qtwi)
+ self.item(_row, self.columns_dict['Value']).setTextAlignment(
+ Qt.AlignRight | Qt.AlignVCenter)
+
+ if 'Timestamp' in self.columns_dict.keys():
+ _handle = self.pv_gateway[_row].handle
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(_handle)
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target -1
+
+ ##ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0: _ts_str_len-(
+ self.format_ts_nano-self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec:
+ _ts_str += "."
+ while _ts_str_len < _ilength_target:
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ qtwi = QTableWidgetItem(_ts_str)
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+ self.setItem(_row, self.columns_dict['Timestamp'], qtwi)
+ self.item(_row, self.columns_dict['Timestamp']).setTextAlignment(
+ Qt.AlignCenter)
+
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ if _prop == self.pv_gateway[_row].READBACK_ALARM:
+
+ if alarm_severity == self.pv_gateway[_row].cyca.SEV_MAJOR:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmMajor
+ _fgcolor = "black"
+ elif alarm_severity == self.pv_gateway[_row].cyca.SEV_MINOR:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmMinor
+ _fgcolor = "black"
+ elif alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmInvalid
+ _fgcolor = "#777777"
+ else:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmNoAlarm
+ _fgcolor = "black"
+
+ #Colors for bg/fg reversed as is the old norm
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.columns_dict['Value']).setForeground(
+ QColor(_fgcolor))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(
+ QColor(_fgcolor))
+
+
+ elif _prop == self.pv_gateway[_row].DISCONNECTED or \
+ alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Value']).setForeground(
+ QColor("#777777"))
+
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(
+ QColor("#777777"))
+
+
+ elif _prop == self.pv_gateway[_row].READBACK_STATIC:
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor(self.pv_gateway[_row].bg_readback))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor(self.pv_gateway[_row].bg_readback))
+ else:
+
+ print(_prop, self.pv_gateway[_row].DISCONNECTED,
+ "(in monitor) unknown in element/row no.", _row, _row+1)
+
+ QApplication.processEvents(QEventLoop.AllEvents, 10)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ _row = self.pv2item_dict[self.sender()]
+
+ self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
+ post_display=False)
+
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ #self.post_display_value(status)
+ if _prop == self.pv_gateway[_row].DISCONNECTED:
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Value']).setForeground(
+ QColor("#777777"))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(
+ QColor("#777777"))
+
+ QApplication.processEvents()
+
+ def table_precision_user_changed(self, new_value):
+ self.pvgateway_precision = new_value
+
+ for pvgate in self.pv_gateway:
+ if pvgate.pv_ctrl is not None:
+ self.pvgateway_precision = min(pvgate.pv_ctrl.precision,
+ new_value)
+
+ pvgate.precision_user = self.pvgateway_precision
+ pvgate.precision = self.pvgateway_precision
+
+ _pvd = self.cafe.getPVCache(pvgate.handle)
+
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ pvgate.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+
+
+ def table_precision_ioc_reset(self):
+ if self.max_precision_value == self.table_precision_user_wgt.value():
+ self.table_precision_user_changed(self.max_precision_value)
+ else:
+ self.table_precision_user_wgt.setValue(self.max_precision_value)
+
+ def table_refresh_rate_changed(self, new_idx):
+
+ _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
+ _notify_milliseconds = 0 if _notify_freq_hz == 0 else \
+ 1000 / _notify_freq_hz
+
+ self.notify_freq_hz = _notify_freq_hz
+
+ if _notify_milliseconds == 0:
+ for pvgate in self.pv_gateway:
+ pvgate.notify_unison = False
+ pvgate.notify_milliseconds = _notify_milliseconds
+ pvgate.notify_freq_hz = self.notify_freq_hz
+ pvgate.monitor_stop()
+ time.sleep(0.01)
+ for pvgate in self.pv_gateway:
+ pvgate.monitor_start()
+
+ else:
+ for pvgate in self.pv_gateway:
+ if not pvgate.notify_unison:
+ pvgate.monitor_stop()
+
+ for pvgate in self.pv_gateway:
+ pvgate.notify_milliseconds = _notify_milliseconds
+ pvgate.notify_freq_hz = self.notify_freq_hz
+
+ if not pvgate.notify_unison:
+ pvgate.notify_unison = True
+ pvgate.monitor_start()
+ else:
+
+ self.cafe.updateMonitorPolicyDeltaMS(
+ pvgate.handle, pvgate.monitor_id,
+ pvgate.notify_milliseconds)
+
+ if self.timer is not None:
+ self.timer.stop()
+ else:
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.widget_update)
+ self.timer.singleShot(0, self.widget_update)
+
+ if _notify_milliseconds > 0:
+ self.timer.start(_notify_milliseconds)
+
+ def table_ts_resolution_changed(self, new_idx):
+
+ for i, ts_res in enumerate(self.ts_combox_idx_dict.values()):
+ if i == new_idx:
+ self.format_ts_decimal_part = ts_res
+ break
+
+ for pvgate in self.pv_gateway:
+ _pvd = self.cafe.getPVCache(pvgate.handle)
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ pvgate.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+ elif isinstance(_pvd.value[0], int):
+ pvgate.trigger_monitor_int.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+ else:
+ pvgate.trigger_monitor_str.emit(
+ str(_pvd.value[0]), _pvd.status, _pvd.alarmSeverity)
+
+
+ def display_table_parameters(self):
+ display_wgt = QDialog(self)
+ display_wgt.setWindowTitle("PV Parameters")
+ layout = QVBoxLayout()
+ common_label_width = 120
+ common_wgt_width = 160
+ common_hbox_width = common_label_width + common_wgt_width + 20
+
+ self.initial_value = 0
+ self.max_precision_value = 0
+ for i, pvgate in enumerate(self.pv_gateway):
+ if pvgate.pv_ctrl is not None:
+ if pvgate.pv_ctrl.precision > 0:
+ self.max_precision_value = max(self.max_precision_value,
+ pvgate.pv_ctrl.precision)
+ self.initial_value = max(self.initial_value,
+ pvgate.precision)
+
+ if self.max_precision_value > 0:
+ #precision user
+ _hbox_wgt = QWidget()
+ _hbox = QHBoxLayout()
+ precision_user_label = QLabel("Precision (user):")
+ self.table_precision_user_wgt = QSpinBox(self)
+ self.table_precision_user_wgt.setFocusPolicy(Qt.NoFocus)
+ self.table_precision_user_wgt.setValue(self.initial_value)
+ self.table_precision_user_wgt.setMaximum(self.max_precision_value)
+ self.table_precision_user_wgt.valueChanged.connect(
+ self.table_precision_user_changed)
+ precision_user_label.setAlignment(Qt.AlignLeft)
+ self.table_precision_user_wgt.setAlignment(Qt.AlignLeft)
+ _hbox.addWidget(precision_user_label)
+ _hbox.addWidget(self.table_precision_user_wgt)
+ _hbox.setAlignment(Qt.AlignLeft)
+ _hbox_wgt.setLayout(_hbox)
+
+ precision_user_label.setFixedWidth(common_label_width)
+ self.table_precision_user_wgt.setFixedWidth(40)
+ _hbox_wgt.setFixedWidth(common_hbox_width)
+
+ #precision ioc
+ _hbox2_wgt = QWidget()
+ _hbox2 = QHBoxLayout()
+ precision_ioc_label = QLabel("Precision (ioc): ")
+ precision_ioc = QPushButton(self)
+ precision_ioc.setText("Reset")
+ precision_ioc.clicked.connect(self.table_precision_ioc_reset)
+ precision_ioc_label.setAlignment(Qt.AlignLeft)
+
+ _hbox2.addWidget(precision_ioc_label)
+ _hbox2.addWidget(precision_ioc)
+ _hbox2.setAlignment(Qt.AlignLeft)
+
+ _hbox2_wgt.setLayout(_hbox2)
+
+ precision_ioc_label.setFixedWidth(common_label_width)
+ precision_ioc.setFixedWidth(50)
+
+ _hbox2_wgt.setFixedWidth(common_hbox_width)
+
+ layout.addWidget(_hbox_wgt)
+ layout.addWidget(_hbox2_wgt)
+
+ if 'Timestamp' in self.columns_dict.keys():
+ #time-stamp
+ _hbox4_wgt = QWidget()
+ _hbox4 = QHBoxLayout()
+ ts_label = QLabel("Timestamp: ")
+
+ self.ts_combox_idx_dict = {
+ 'second (s)': self.format_ts_sec,
+ 'decisecond (ds)': self.format_ts_deci,
+ 'millisecond (ms)': self.format_ts_milli,
+ 'microsecond (\u03bcs)': self.format_ts_micro,
+ 'nanosecond (ns)': self.format_ts_nano}
+
+ ts_resolution = QComboBox(self)
+ for key, ts_res in self.ts_combox_idx_dict.items():
+ ts_resolution.addItem(key)
+
+ _current_idx = 0
+
+ for i, (key, ts_res) in enumerate(self.ts_combox_idx_dict.items()):
+ if ts_res == self.format_ts_decimal_part:
+ _current_idx = i
+ break
+
+ ts_resolution.setCurrentIndex(_current_idx)
+ ts_resolution.currentIndexChanged.connect(
+ self.table_ts_resolution_changed)
+
+ _hbox4.addWidget(ts_label)
+ _hbox4.addWidget(ts_resolution)
+ _hbox4_wgt.setLayout(_hbox4)
+
+ ts_label.setFixedWidth(common_label_width)
+ ts_resolution.setFixedWidth(common_wgt_width)
+ _hbox4_wgt.setFixedWidth(common_hbox_width)
+
+ layout.addWidget(_hbox4_wgt)
+
+ #precision refresh rate
+ _hbox3_wgt = QWidget()
+ _hbox3 = QHBoxLayout()
+ refresh_freq_label = QLabel("Refresh rate: ")
+ #_default_refresh_val = 0 if self.notify_freq_hz <= 0 else \
+ # self.notify_freq_hz
+ _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
+ self.notify_freq_hz_default
+
+ self.refresh_freq_combox_idx_dict = {0: 0, 1: 10, 2: 5, 3: 2, 4: 1,
+ 5: 0.5, 6: _default_refresh_val}
+ refresh_freq = QComboBox(self)
+ refresh_freq.addItem('direct')
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[1]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[2]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[3]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[4]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[5]))
+
+ _default_text = 'default (direct)' if _default_refresh_val == 0 else \
+ 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
+
+ refresh_freq.addItem(_default_text)
+
+ for key, value in self.refresh_freq_combox_idx_dict.items():
+ if value == self.notify_freq_hz:
+ refresh_freq.setCurrentIndex(key)
+ break
+
+ refresh_freq.currentIndexChanged.connect(
+ self.table_refresh_rate_changed)
+
+ _hbox3.addWidget(refresh_freq_label)
+ _hbox3.addWidget(refresh_freq)
+ _hbox3_wgt.setLayout(_hbox3)
+
+ refresh_freq_label.setFixedWidth(common_label_width)
+ refresh_freq.setFixedWidth(common_wgt_width)
+ _hbox3_wgt.setFixedWidth(common_hbox_width)
+
+ layout.addWidget(_hbox3_wgt)
+
+ layout.setAlignment(Qt.AlignLeft)
+ layout.setContentsMargins(10, 0, 0, 0)
+ layout.setSpacing(0)
+
+ display_wgt.setMinimumWidth(340)
+ display_wgt.setLayout(layout)
+
+ display_wgt.exec()
+
+
+ def mousePressEvent(self, event):
+ row = self.indexAt(event.pos()).row()
+
+ if row > -1:
+ if row < len(self.pv_list):
+ self.pv_gateway[row].mousePressEvent(event)
+ else:
+ button = event.button()
+ if button == Qt.RightButton:
+ self.table_context_menu.exec(QCursor.pos())
+ self.clearFocus()
+
+ #remove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ pass
+
+ def leaveEvent(self, event):
+ self.clearSelection()
+ self.clearFocus()
+ del event
+
+
+class QMessageWidget(QListWidget):
+ """Log message window."""
+ def __init__(self, parent=None):
+ super(QMessageWidget, self).__init__(parent)
+ self.myItem = None
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ def leaveEvent(self, event):
+ if self.myItem:
+ self.clearSelection()
+ self.clearFocus()
+ del event
+
+ def mousePressEvent(self, event):
+ item = self.itemAt(event.x(), event.y())
+ if item:
+ self.myItem = item
+ self.setCurrentItem(self.myItem)
+
+ def keyPressEvent(self, event):
+ if event.matches(QKeySequence.Copy):
+ nitem = event.count()
+ if nitem:
+ if self.myItem is not None:
+ _str = self.myItem.text()
+ QApplication.clipboard().setText(_str)
+
+
+
+class QResultsWidget:
+ """Results table"""
+ def __init__(self, summary_dict=None, table_dict=None):
+
+ self.summary_dict = summary_dict
+ self.table_dict = table_dict
+ self._group_box = None
+
+ def group_box(self, title=""):
+ self._group_box = QGroupBox(title)
+ self._group_box.setObjectName("OUTERLEFT")
+ _vbox = QVBoxLayout()
+ _qspace = QFrame()
+ _qspace.setFixedHeight(10)
+ _vbox.addWidget(_qspace)
+
+ _font = QFont("Sans Serif", 10)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, (label, text) in enumerate(self.summary_dict.items()):
+ if len(str(label)) > len(longest_str_item1):
+ longest_str_item1 = str(label)
+ if len(str(text)) > len(longest_str_item2):
+ longest_str_item2 = str(text)
+
+ fm = QFontMetricsF(_font)
+
+ _factor = 1.15
+
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ qrect1 = fm.boundingRect(longest_str_item1)
+ qrect2 = fm.boundingRect(longest_str_item2)
+ _width_scaling_factor = 1.5
+ _width_scaling_factor_le = 1.15
+ _widget_height = 25
+ for i, (label, text) in enumerate(self.summary_dict.items()):
+ #print(label, text)
+ qlabel = QLabel(label)
+ qle = QLineEdit(text)
+ qlabel.setFont(_font)
+ qlabel.setStyleSheet(("QLabel{color:black;" +
+ "margin:0px; padding:2px;}"))
+ qlabel.setFixedWidth(qrect1.width() * _width_scaling_factor)
+ qlabel.setFixedHeight(_widget_height)
+
+ qle.setFocusPolicy(Qt.NoFocus)
+ qle.setFont(_font)
+ qle.setStyleSheet(("QLineEdit{color:blue;" +
+ "background-color: lightgray;" +
+ "qproperty-readOnly: true;" +
+ "margin:0px; padding:2px;}"))
+ qle.setFixedWidth(qrect2.width() * _width_scaling_factor_le)
+ qle.setFixedHeight(_widget_height)
+ qle.setAlignment(Qt.AlignRight)
+
+ _hbox_widget = QWidget()
+ _hbox = QHBoxLayout()
+ _hbox.addWidget(qlabel)
+ _hbox.addWidget(qle)
+ _hbox_widget.setLayout(_hbox)
+ _hbox.setAlignment(Qt.AlignCenter)
+ _hbox.setContentsMargins(0, 2, 0, 0)
+ _vbox.addWidget(_hbox_widget)
+
+ _vbox.setContentsMargins(0, 0, 0, 0)
+ _vbox.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+
+ _vbox2_widget = QWidget()
+ _vbox2 = QVBoxLayout()
+ _vbox2.setContentsMargins(0, 20, 0, 40)
+ table = QTableWidget(len(self.table_dict)-1, 2)
+ table.verticalHeader().setVisible(False)
+ table.setFocusPolicy(Qt.NoFocus)
+ #table.setFont(_font)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, (label, text) in enumerate(self.table_dict.items()):
+ item1 = QTableWidgetItem(str(label))
+ item2 = QTableWidgetItem(str(text))
+ item1.setTextAlignment(Qt.AlignCenter)
+ item2.setTextAlignment(Qt.AlignCenter)
+ item1.setForeground(QColor("black"))
+ item2.setForeground(QColor("black"))
+ if i%2 == 0:
+ item1.setBackground(QColor("lightgray"))
+ item2.setBackground(QColor("lightgray"))
+
+ if len(str(label)) > len(longest_str_item1):
+ longest_str_item1 = str(label)
+ if len(str(text)) > len(longest_str_item2):
+ longest_str_item2 = str(text)
+
+ if i == 0:
+ #item1.setFont(_font)
+ #item2.setFont(_font)
+ table.setHorizontalHeaderItem(0, item1)
+ table.setHorizontalHeaderItem(1, item2)
+ else:
+ table.setItem(i-1, 0, item1)
+ table.setItem(i-1, 1, item2)
+
+ fm = QFontMetricsF(_font)
+
+ _factor = 1.2
+
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ qrect = fm.boundingRect(longest_str_item1 + longest_str_item2)
+
+ _width_scaling_factor = 1.04
+ table.resizeColumnsToContents()
+ table.resizeRowsToContents()
+
+ table.setFixedHeight((fm.lineSpacing() * _factor * len(
+ self.table_dict)) + fm.lineSpacing()*2)
+
+ table.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ _vbox2.addWidget(table)
+ _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+ _vbox2_widget.setLayout(_vbox2)
+
+ _vbox.addWidget(_vbox2_widget)
+
+ self._group_box.setLayout(_vbox)
+ self._group_box.setContentsMargins(20, 20, 20, 20)
+ self._group_box.setAlignment(Qt.AlignTop)
+ self._group_box.setFixedHeight(
+ table.height() + (_widget_height*len(self.summary_dict)))
+ self._group_box.setFixedWidth(table.width() + 20)
+ return self._group_box
+
+
+class QResultsTableWidget():
+ """Results table"""
+ def __init__(self, column_headings=None):
+
+ self.column_headings = column_headings
+ self._group_box = None
+
+ def group_box(self, title="Table of Results"):
+ self._group_box = QGroupBox(title)
+ self._group_box.setObjectName("OUTER")
+
+ _font = QFont("Sans Serif", 10)
+
+ _vbox2_widget = QWidget()
+ _vbox2 = QVBoxLayout()
+ _vbox2.setContentsMargins(0, 20, 0, 40)
+ table = QTableWidget(1, len(self.column_headings))
+ table.verticalHeader().setVisible(True)
+ table.setFocusPolicy(Qt.NoFocus)
+ table.setFont(_font)
+
+ for i, heading in enumerate(self.column_headings):
+ _item = QTableWidgetItem(str(heading))
+ table.setHorizontalHeaderItem(i, _item)
+
+ table.resizeColumnsToContents()
+ table.resizeRowsToContents()
+ table.setFixedHeight(400)
+
+ _vbox2.addWidget(table)
+ _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+ _vbox2_widget.setLayout(_vbox2)
+
+ self._group_box.setLayout(_vbox2)
+ self._group_box.setContentsMargins(20, 20, 20, 20)
+ self._group_box.setAlignment(Qt.AlignTop)
+
+ self._group_box.setFixedWidth(table.width() + 20)
+ return self._group_box
+
+
+class QHDFDockWidget(QDockWidget):
+
+ def __init__(self, title=None, parent=None):
+ super().__init__(title, parent)
+ self.parent = parent
+ self.is_docked = True
+ self.geometry_from_qsettings = self.parent.application_geometry
+ self.topLevelChanged.connect(self._top_level_changed)
+ self.setVisible(False)
+ self.setFloating(False)
+ self.geometry_from_qsettings = self.parent.geometry()
+
+ def closeEvent(self, event: QCloseEvent):
+ super().closeEvent(event)
+
+ self.parent.setGeometry(self.geometry_from_qsettings)
+ self.setGeometry(self.geometry_from_qsettings)
+ QApplication.processEvents()
+
+ self.parent.setGeometry(self.geometry_from_qsettings)
+
+ def changeEvent(self, event):
+ pass
+
+ def _top_level_changed(self, is_floating):
+ pass
+
+
+class QNoDockWidget(QDockWidget):
+
+ def __init__(self, title=None, parent=None):
+ super().__init__(title, parent)
+ self.parent = parent
+ self.is_docked = True
+ self.geometry_from_qsettings = self.parent.application_geometry
+ self.topLevelChanged.connect(self._top_level_changed)
+ self.setVisible(False)
+ self.setFloating(True)
+
+ x = self.geometry_from_qsettings.x() + 480 # 3500 #screen.width() - widget.width()
+ y = self.geometry_from_qsettings.y() + 350 #100 #screen.height() - widget.height()
+ self.move(x, y)
+
+
+ def changeEvent(self, event):
+ if "QAbstractButton" in str(self.sender()):
+ self.geometry_from_qsettings = self.parent.geometry()
+
+
+
+ def _top_level_changed(self): #, is_floating):
+ self.setVisible(False)
+ self.setFloating(True)
+ #ResetGeometry
+ self.parent.setGeometry(self.geometry_from_qsettings)
+ QApplication.processEvents()
+
+
+
+class CAQStripChart(PlotWidget):
+ '''Channel access enabled pyqtgraph.PlotWidget'''
+
+ def __init__(self, parent=None, pv_list: list = ['PV_NAME_NOT_GIVEN'],
+ monitor_callback=None, pv_within_daq_group: bool = False,
+ color_mode=None, show_units: bool = False, prefix: str = "",
+ suffix: str = "", notify_freq_hz: int = 0, title: str = "",
+ ylabel: str = "", force_ts_align = True, text_label = [],
+ pen_color_idx = 0):
+ super().__init__()
+
+ self.no_channels = len(pv_list)
+ self.pen_color_idx = pen_color_idx
+ self.text_label = text_label
+ self.found = False
+ self.time_zero = [0] * self.no_channels
+ self.time_delta = [0] * self.no_channels
+ self.pv_list = pv_list
+ self.pv2item_dict = {}
+ self.pv_gateway = [None] * self.no_channels
+
+ self.pvd_previous_list = [None] * self.no_channels
+ self.val_previous = [None] * self.no_channels
+
+ self.curve = [None] * self.no_channels
+
+
+ for i in range (0, len(self.pv_list)):
+ print("in atripchart", i, self.pv_list[i])
+ self.pv_gateway[i] = PVGateway(
+ parent, self.pv_list[i], monitor_callback, pv_within_daq_group,
+ color_mode, show_units, prefix, suffix,
+ #connect_callback=self.py_connect_callback,
+ connect_triggers=False, notify_freq_hz=notify_freq_hz,
+ monitor_dbr_time = True)
+
+
+ self.pv_gateway[i].is_initialize_complete()
+
+
+ self.pvd_previous_list[i] = self.pv_gateway[i].pvd
+
+ self.pv_gateway[i].trigger_connect.connect(
+ self.receive_connect_update)
+
+ self.pv_gateway[i].trigger_monitor_str.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_int.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_float.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor.connect(
+ self.receive_monitor_dbr_time)
+
+ self.pv_gateway[i].widget_class = "PlotWidget"
+
+ self.pv2item_dict[self.pv_gateway[i]] = i
+
+ self.cafe = self.pv_gateway[0].cafe
+ self.cyca = self.pv_gateway[0].cyca
+ for i in range(0, len(self.pv_gateway)):
+ if self.cafe.isConnected(self.pv_gateway[i].pv_name):
+ self.pv_gateway[i].trigger_connect.emit(
+ self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
+ self.pv_gateway[i].cyca.ICAFE_CS_CONN)
+
+
+ for i in range(0, len(self.pv_gateway)):
+ if not self.pv_gateway[i].pv_within_daq_group:
+ self.pv_gateway[i].monitor_start()
+
+ sampleinterval = 0.2
+ timewindow = 1800.0
+
+ self.ts_delta_max = 0.6
+
+ # Data stuff
+ self._interval = int(sampleinterval*1000)
+ self._bufsize = 9000 #int(timewindow/0.33)
+ self._bufsize2 = 9000 # int(timewindow/1.33)
+ self.databuffer = [None] * self.no_channels
+ self.timebuffer = [None] * self.no_channels
+ self.x = [None] * self.no_channels
+ self.y = [None] * self.no_channels
+ self.x_shifted = [None] * self.no_channels
+
+ self.idx = [0] * self.no_channels
+
+ for i in range(0, self.no_channels):
+ bsize = self._bufsize if i == 0 else self._bufsize2
+ self.databuffer[i] = collections.deque([None]*bsize, bsize)
+ self.timebuffer[i] = collections.deque([0]*bsize, bsize)
+ self.x[i] = np.zeros(bsize, dtype=np.float)
+ self.y[i] = np.zeros(bsize, dtype=np.float)
+
+ _long_size=20
+ #self.data_series_buffer = collections.deque([0]*_long_size, _long_size)
+ #self.time_series_buffer = collections.deque([0]*_long_size, _long_size)
+
+ #self.data_series = [] * self.no_channels
+ #self.time_series = [] * self.no_channels
+
+ self.iflag_series = 0
+
+ #self.x = np.linspace(-timewindow, 0.0, self._bufsize)
+ #self.x_series = np.zeros(_long_size, dtype=np.float)
+ #self.y_series = np.zeros(_long_size, dtype=np.float)
+ if title is not None:
+ self.setTitle(str(title)) #self.pv_gateway[0].pv_name)
+ self.showGrid(x=True, y=True)
+ self.setLabel('left', ylabel, self.pv_gateway[0].units)
+ self.setLabel('bottom', 'time', 's')
+ self.setBackground((60, 60, 60)) #247, 236, 249))
+ self.setLimits(yMin=-0.11)
+
+ self.plotItem.setMouseEnabled(y=True) # Only allow zoom in X-axis
+ self.plotItem.setMouseEnabled(x=True) # Only allow zoom in Y-axis
+ #(125, 249, 255)
+ if self.pen_color_idx == 0:
+ pen_list = [ (255, 155, 0), (255,255,0), (0, 180, 255) ]
+ elif self.pen_color_idx == 1:
+ pen_list = [ (125, 249, 255), (255,255,0), (0, 180, 255) ]
+ else:
+ pen_list = [ (0, 180, 255), (125, 249, 255), (255,255,0) ]
+
+ for i in range(0, len(self.pv_gateway)):
+ self.curve[i] = self.plot(self.x[0], self.y[0], pen=pen_list[i]) # (0, 253, 235))
+ #self.curve[1] = self.plot(self.x[1], self.y[1], pen=(255,255,0))
+ #offset=(1.0, 1.0),
+ l=pg.LegendItem() #horSpacing=20, verSpacing=0, labelTextColor=(205, 205, 205),
+ #labelTextSize='6px', colCount=1)
+
+ l.setParentItem(self.graphicsItem())
+ l.anchor((0,0), (0.08, 0.0))
+ #l.setLabelTextColor((205, 205, 205))
+ #l.setLabelTextSize(9) does not exists(!)
+ #l.setOffset(-60)
+ for curv, label in zip(self.curve, self.text_label):
+ l.addItem(curv, label)
+
+ #for curv, pv in zip(self.curve, self.pv_gateway):
+ # l.addItem(curv, self.textpv.pv_name)
+
+ #self.daq_stop()
+ #print(self._bufsize)
+ #print(len(self.x), len(self.y))
+ QApplication.processEvents()
+
+ @Slot(object, int)
+ def receive_monitor_dbr_time(self, pvdata, alarm_severity):
+ _row = self.pv2item_dict[self.sender()]
+ #print("row, value from pvdata==>", _row, pvdata.value[0], self.pv_gateway[_row].pv_name)
+
+ ts_now = pvdata.ts[0] + pvdata.ts[1] * 10**(-9)
+ ts_previous = (self.pvd_previous_list[_row].ts[0] +
+ self.pvd_previous_list[_row].ts[1] * 10**(-9))
+ ts_delta = ts_now - ts_previous
+
+ if (pvdata.ts[0] == self.pvd_previous_list[_row].ts[0]) and (
+ pvdata.ts[1] == self.pvd_previous_list[_row].ts[1]):
+ #pvdata.show()
+ self.pvd_previous_list[_row].show()
+ return
+
+ value = pvdata.value[0]
+ #discard first callbacks
+ #if ts_delta > 2.0:
+ # self.pvd_previous_list[_row] = _pvd
+ # return;
+ self.pvd_previous_list[_row] = pvdata
+ self.val_previous[_row] = value
+ #self.pvd_previous_list[_row].ts[0] = _pvd.ts[0]
+ #self.pvd_previous_list[_row].ts[1] = _pvd.ts[1]
+
+ self.databuffer[_row].append(value)
+ self.timebuffer[_row].append(self.time_delta[_row])
+
+ highest_ts = self.timebuffer[0][0] \
+ if self.timebuffer[0][0] is not None else 0
+ for i in range(1, len(self.timebuffer)):
+ if self.timebuffer[i][0] is None:
+ continue
+ elif self.timebuffer[i][0] > highest_ts:
+ highest_ts = self.timebuffer[i][0]
+
+ if self.timebuffer[_row][0] is not None:
+ for i, val in enumerate(self.timebuffer[_row]):
+ if val > highest_ts:
+ self.idx[_row] = i - 1
+ break
+
+ self.y[_row][:] = self.databuffer[_row]
+ self.x[_row][:] = self.timebuffer[_row]
+
+ idx = self.idx[_row]
+ self.x_shifted[_row] = list(map(lambda m : (m - self.time_delta[_row]), self.x[_row][idx:]))
+
+ #print("idx", self.idx)
+
+ #if 'AVG' in self.pv_gateway[_row].pv_name:
+ # for row in range(0, len(self.curve)):
+ # self.curve[row].setData(self.x_shifted[row], self.y[row][idx:])
+
+ self.curve[_row].setData(self.x_shifted[_row], self.y[_row][idx:])
+ self.time_delta[_row] = (
+ pvdata.ts[0] + pvdata.ts[1]*10**(-9)) - self.time_zero[0]
+
+ #QApplication.processEvents()
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ #self.pv_gateway.receive_monitor_update(value, status, alarm_severity)
+ _row = self.pv2item_dict[self.sender()]
+ #if _row == 1:
+ # return
+ print("row, value===>", _row, value, self.pv_gateway[_row].pv_name)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(
+ self.pv_gateway[_row].handle)
+
+ #print("value", _pvd.value[0], self.pvd_previous_list[_row].value[0])
+
+ _pvd2 = self.pv_gateway[_row].pvd
+
+ print ("val", value, _pvd2.value[0], _pvd.value[0], self.pvd_previous_list[_row].value[0])
+
+ ts_now = _pvd.ts[0] + _pvd.ts[1] * 10**(-9)
+
+ ts_previous = (self.pvd_previous_list[_row].ts[0] +
+ self.pvd_previous_list[_row].ts[1] * 10**(-9))
+ ts_delta = ts_now - ts_previous
+
+ if value == self.val_previous[_row]:
+ #if (_pvd.ts[0] == self.pvd_previous_list[_row].ts[0]) and (
+ # _pvd.ts[1] == self.pvd_previous_list[_row].ts[1]):
+ _pvd.show()
+ #self.pvd_previous_list[_row].show()
+ return
+
+
+ #discard first callbacks
+ #if ts_delta > 2.0:
+ # self.pvd_previous_list[_row] = _pvd
+ # return;
+ self.pvd_previous_list[_row] = _pvd2
+ self.val_previous[_row] = value
+ #self.pvd_previous_list[_row].ts[0] = _pvd.ts[0]
+ #self.pvd_previous_list[_row].ts[1] = _pvd.ts[1]
+
+ self.databuffer[_row].append(value)
+ self.timebuffer[_row].append(self.time_delta[_row])
+
+ highest_ts = self.timebuffer[0][0] \
+ if self.timebuffer[0][0] is not None else 0
+ for i in range(1, len(self.timebuffer)):
+ if self.timebuffer[i][0] is None:
+ continue
+ elif self.timebuffer[i][0] > highest_ts:
+ highest_ts = self.timebuffer[i][0]
+
+
+ if self.timebuffer[_row][0] is not None:
+ for i, val in enumerate(self.timebuffer[_row]):
+ if val > highest_ts:
+ self.idx[_row] = i - 1
+ break
+
+
+ '''
+ for i in range(1, self.timebuffer):
+ if self.timebuffer[i][0] is not None:
+ a = self.timebuffer[0][0]
+ for i, val in enumerate(self.timebuffer[_row]):
+ if val > a:
+ idx = i - 1
+ break
+ '''
+
+ self.y[_row][:] = self.databuffer[_row]
+ self.x[_row][:] = self.timebuffer[_row]
+
+
+ #self.y[_row][:] = self.databuffer[_row]
+ #self.x[_row][:] = self.timebuffer[_row]
+
+ '''
+ #print(ts_delta, value, self.pvd_previous.value[0])
+ #if (ts_delta < self.ts_delta_max) and (value < self.pvd_previous.value[0]) :
+ if (value < self.pvd_previous.value[0]) :
+ self.data_series_buffer.append(value)
+ self.time_series_buffer.append(ts_now - self.time_zero ) #self.time_delta)
+ self.y_series[:] = self.data_series_buffer
+ self.x_series[:] = self.time_series_buffer
+ #print(self.x_series, self.y_series)
+ #elif ts_delta < 1.0:
+ if len(self.data_series_buffer) > 15:
+ #x_series = np.array(self.time_series, dtype=np.float)
+ #y_series = np.array(self.data_series, dtype=np.float)
+ _x=self.x_series.reshape((-1, 1))
+
+ model = LinearRegression()
+ model.fit(_x, self.y_series)
+ r_sq = model.score(_x, self.y_series)
+ ###JCprint('coefficient of determination:', r_sq, "slope", model.coef_ , "lifetime:", self.y_series[0]/model.coef_ / 3600)
+ #print('intercept:', model.intercept_)
+ #print('slope:', model.coef_)
+ #print('max value', y_series[0], y_series[1])
+ if r_sq > 0.995:
+ _I = self.y_series[0]
+ ###JCprint("lifetime:", _I/model.coef_ / 3600)
+
+
+ y_pred = model.predict(_x)
+ #print("len, y_pred, _x", len(y_pred), len(self.y_series), len(_x))
+ #print('predicted response:', y_pred, sep='\n')
+ m_sq_error = mean_squared_error(self.y_series, y_pred)
+ #print('Mean squared error: {0:.9f}'.format(
+ # mean_squared_error(y_series, y_pred)))
+ #print('Coefficient of determination: {0:.9f}'.format(
+ # r2_score(y_series, y_pred)))
+
+
+
+ self.trigger_series_sequence.emit(self.x_series, self.y_series)
+ #print("emit")
+ self.data_series = []
+ self.time_series = []
+ #print(len(self.x_series), len(self.y_series))
+ else:
+ self.data_series = []
+ self.time_series = []
+
+ '''
+
+
+ #dt = (self.x[-1] - self.x[-2])
+ #print("dt", dt)
+ #Lowet IPCT before trigger is set to t=0
+ idx = self.idx[_row]
+ self.x_shifted[_row] = list(map(lambda m : (m - self.time_delta[_row]), self.x[_row][idx:]))
+
+ ##self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+
+ #print("row len len ", _row, self.time_delta[0], self.time_delta[1])
+
+
+ self.curve[_row].setData(self.x_shifted[_row], self.y[_row][idx:])
+
+ self.time_delta[_row] = (
+ _pvd.ts[0] + _pvd.ts[1]*10**(-9)) - self.time_zero[0]
+
+
+ '''
+ LOOK_BACK = -800
+ if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name:
+ LOOK_BACK = -250
+
+ if value > self.y[-2]:
+ if not self.found:
+ #print(x_shifted[-240:], self.y[-240:])
+ #self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+ max_index = self.y[LOOK_BACK:].argmax()
+
+ if max_index == 0:
+ return
+ print("max index=", max_index)
+
+ #print(x_shifted[-600+max_index:], self.x[-600+max_index:])
+ #print(self.y[-600+max_index:-2])
+ self.found = True
+ #print("Are Signals blocked??", self.signalsBlocked())
+ self.trigger_decay_sequence.emit(np.array(
+ x_shifted[LOOK_BACK+max_index+9:-2]), self.y[LOOK_BACK+max_index+9:-2])
+ else:
+ self.found = False
+ '''
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ print("pv_name==>", pv_name)
+
+ _row = self.pv2item_dict[self.sender()]
+ self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
+ post_display=False)
+
+ #self.pv_gateway.receive_connect_update(handle, pv_name, status)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(self.pv_gateway[_row].handle)
+ if self.time_zero[_row] == 0:
+ self.time_zero[_row] = _pvd.ts[0] + _pvd.ts[1]*10**(-9)
+
+ self.pvd_previous = _pvd
+
+
+ #renove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+
+class CAQPCTChart(PlotWidget):
+ '''Channel access enabled pyqtgraph.PlotWidget'''
+ #trigger_monitor_float = Signal(float, int, int)
+ #trigger_monitor_int = Signal(int, int, int)
+ #trigger_monitor_str = Signal(str, int, int)
+
+ #trigger_connect = Signal(int, str, int)
+
+ trigger_decay_sequence = Signal(np.ndarray, np.ndarray)
+ trigger_series_sequence = Signal(np.ndarray, np.ndarray)
+ #def py_connect_callback(self, handle, pvname, status):
+ # self.trigger_connect.emit(int(handle), str(pvname), int(status))
+ # print("py connect callback", handle, pvname, status)
+
+ def daq_start(self):
+ self.blockSignals(False)
+
+ def daq_pause(self):
+ self.blockSignals(True)
+
+ def daq_stop(self):
+ self.blockSignals(True)
+
+ def __init__(self, parent=None, pv_list: list = ['PV_NAME_NOT_GIVEN'],
+ monitor_callback=None, pv_within_daq_group: bool = False,
+ color_mode=None, show_units: bool = False, prefix: str = "",
+ suffix: str = "", notify_freq_hz: int = 0):
+ super().__init__()
+
+ self.found = False
+ self.time_zero = 0
+ self.time_delta = 0
+ self.pv_list = pv_list
+ self.pv2item_dict = {}
+ self.pv_gateway = [None] * len(self.pv_list)
+ self.pvd_previous = None
+
+ for i in range(0, len(self.pv_list)):
+ self.pv_gateway[i] = PVGateway(
+ parent, pv_list[i], monitor_callback, pv_within_daq_group,
+ color_mode, show_units, prefix, suffix,
+ #connect_callback=self.py_connect_callback,
+ connect_triggers=False, notify_freq_hz=notify_freq_hz)
+
+ self.pv_gateway[i].is_initialize_complete()
+
+ self.pv_gateway[i].trigger_connect.connect(
+ self.receive_connect_update)
+
+ self.pv_gateway[i].trigger_monitor_str.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_int.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_float.connect(
+ self.receive_monitor_update)
+
+ self.pv_gateway[i].widget_class = "PlotWidget"
+
+ self.pv2item_dict[self.pv_gateway[i]] = i
+
+ self.cafe = self.pv_gateway[0].cafe
+ self.cyca = self.pv_gateway[0].cyca
+ for i in range(0, len(self.pv_gateway)):
+ if self.cafe.isConnected(self.pv_gateway[i].pv_name):
+ self.pv_gateway[i].trigger_connect.emit(
+ self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
+ self.pv_gateway[i].cyca.ICAFE_CS_CONN)
+
+ for i in range(0, len(self.pv_gateway)):
+ if not self.pv_gateway[i].pv_within_daq_group:
+ self.pv_gateway[i].monitor_start()
+
+ sampleinterval = 0.333
+ timewindow = 1800.0
+
+ self.ts_delta_max = 0.6
+
+ # Data stuff
+ self._interval = int(sampleinterval*1000)
+ self._bufsize = int(timewindow/sampleinterval)
+ self.databuffer = collections.deque([None]*self._bufsize, self._bufsize)
+ self.timebuffer = collections.deque([0]*self._bufsize, self._bufsize)
+
+ _long_size = 20
+ self.data_series_buffer = collections.deque([0]*_long_size, _long_size)
+ self.time_series_buffer = collections.deque([0]*_long_size, _long_size)
+
+ self.data_series = []
+ self.time_series = []
+
+ self.iflag_series = 0
+
+ #self.x = np.linspace(-timewindow, 0.0, self._bufsize)
+ self.x = np.zeros(self._bufsize, dtype=np.float)
+ self.y = np.zeros(self._bufsize, dtype=np.float)
+
+ self.x_series = np.zeros(_long_size, dtype=np.float)
+ self.y_series = np.zeros(_long_size, dtype=np.float)
+
+ self.setTitle("PCT(t)") #self.pv_gateway[0].pv_name)
+ self.showGrid(x=True, y=True)
+ self.setLabel('left', 'I', 'mA')
+ self.setLabel('bottom', 'time', 's')
+ self.setBackground((60, 60, 60)) #247, 236, 249))
+ self.setLimits(yMin=-0.11)
+
+ self.plotItem.setMouseEnabled(y=False) # Only allow zoom in X-axis
+ self.plotItem.setMouseEnabled(x=True) # Only allow zoom in Y-axis
+
+ self.curve = self.plot(self.x, self.y, pen=(125, 249, 255))
+ #self.curve2 = self.plot(self.x, self.y, pen=(255,255,0))
+
+ l = pg.LegendItem(offset=(0., 0.5))
+ l.setParentItem(self.graphicsItem())
+ l.setLabelTextColor((125, 249, 255))
+
+ l.addItem(self.curve, str(self.pv_gateway[0].pv_name))
+ '''
+ l2=self.addLegend()
+ l2.setLabelTextColor('g')
+ l2.setOffset(10)
+ l2.addItem(self.curve2, str(self.pv_gateway[0].pv_name))
+ '''
+ self.daq_stop()
+ print(self._bufsize)
+ print(len(self.x), len(self.y))
+
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ #self.pv_gateway.receive_monitor_update(value, status, alarm_severity)
+ _row = self.pv2item_dict[self.sender()]
+ #print("value===>", value, self.pv_gateway[_row].pv_name)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(
+ self.pv_gateway[_row].handle)
+
+ ts_now = _pvd.ts[0] + _pvd.ts[1] * 10**(-9)
+ ts_previous = self.pvd_previous.ts[0] + self.pvd_previous.ts[1]*10**(-9)
+ ts_delta = ts_now - ts_previous
+
+ if (_pvd.ts[0] == self.pvd_previous.ts[0]) and (
+ _pvd.ts[1] == self.pvd_previous.ts[1]):
+ #_pvd.show()
+ return
+
+ #discard first callbacks
+ if ts_delta > 2.0:
+ self.pvd_previous = _pvd
+ return
+
+ self.databuffer.append(value)
+ self.y[:] = self.databuffer
+ self.timebuffer.append(self.time_delta)
+ self.x[:] = self.timebuffer
+
+ #print(ts_delta, value, self.pvd_previous.value[0])
+ #if (ts_delta < self.ts_delta_max) and (value <
+ # self.pvd_previous.value[0]):
+ if value < self.pvd_previous.value[0]:
+ self.data_series_buffer.append(value)
+ self.time_series_buffer.append(ts_now - self.time_zero)
+ self.y_series[:] = self.data_series_buffer
+ self.x_series[:] = self.time_series_buffer
+ #print(self.x_series, self.y_series)
+ #elif ts_delta < 1.0:
+ if len(self.data_series_buffer) > 15:
+ #x_series = np.array(self.time_series, dtype=np.float)
+ #y_series = np.array(self.data_series, dtype=np.float)
+ _x = self.x_series.reshape((-1, 1))
+
+ model = LinearRegression()
+ model.fit(_x, self.y_series)
+ r_sq = model.score(_x, self.y_series)
+ ###JCprint('coefficient of determination:',
+ ###r_sq, "slope", model.coef_ , "lifetime:",
+ ###self.y_series[0]/model.coef_ / 3600)
+ #print('intercept:', model.intercept_)
+ #print('slope:', model.coef_)
+ #print('max value', y_series[0], y_series[1])
+ if r_sq > 0.995:
+ #_I = self.y_series[0]
+
+
+ ###JCprint("lifetime:", _I/model.coef_ / 3600)
+
+ ####y_pred = model.predict(_x)
+ #print("len, y_pred, _x", len(y_pred), len(self.y_series),
+ # len(_x))
+ #print('predicted response:', y_pred, sep='\n')
+ ##m_sq_error = mean_squared_error(self.y_series, y_pred)
+ #print('Mean squared error: {0:.9f}'.format(
+ # mean_squared_error(y_series, y_pred)))
+ #print('Coefficient of determination: {0:.9f}'.format(
+ # r2_score(y_series, y_pred)))
+
+
+ self.trigger_series_sequence.emit(self.x_series,
+ self.y_series)
+ #print("emit")
+ self.data_series = []
+ self.time_series = []
+ #print(len(self.x_series), len(self.y_series))
+ else:
+ self.data_series = []
+ self.time_series = []
+
+
+ self.pvd_previous = _pvd
+
+ #dt = (self.x[-1] - self.x[-2])
+ #print("dt", dt)
+ #Lowet IPCT before trigger is set to t=0
+ x_shifted = list(map(lambda m: (m - self.time_delta), self.x))
+
+ ##self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+ self.curve.setData(x_shifted, self.y)
+
+ self.time_delta = (
+ _pvd.ts[0] + _pvd.ts[1]*10**(-9)) - self.time_zero
+ #x_shifted2= list(map(lambda m : m -self.time_delta-1 , self.x))
+ #self.curve2.setData(x_shifted2, self.y)
+ #QApplication.processEvents()
+ #print(type(x_shifted), type(self.y), type([1.1]), type(1.1))
+
+ LOOK_BACK = -800
+ if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name:
+ LOOK_BACK = -250
+
+ if value > self.y[-2]:
+ if not self.found:
+ #print(x_shifted[-240:], self.y[-240:])
+ #self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+ max_index = self.y[LOOK_BACK:].argmax()
+
+ if max_index == 0:
+ return
+ print("max index=", max_index)
+
+ #print(x_shifted[-600+max_index:], self.x[-600+max_index:])
+ #print(self.y[-600+max_index:-2])
+ self.found = True
+ #print("Are Signals blocked??", self.signalsBlocked())
+ self.trigger_decay_sequence.emit(
+ np.array(x_shifted[LOOK_BACK+max_index+9:-2]),
+ self.y[LOOK_BACK+max_index+9:-2])
+ else:
+ self.found = False
+
+
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ print("pv_name==>", pv_name)
+
+ _row = self.pv2item_dict[self.sender()]
+ self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
+ post_display=False)
+
+ #self.pv_gateway.receive_connect_update(handle, pv_name, status)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(
+ self.pv_gateway[_row].handle)
+ if self.time_zero == 0:
+ self.time_zero = _pvd.ts[0] + _pvd.ts[1]*10**(-9)
+ #print(self.time_zero)
+ self.pvd_previous = _pvd
+
+
+ #renove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ pass
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
diff --git a/pvwidgets.py:2.9 b/pvwidgets.py:2.9
new file mode 100644
index 0000000..b3e9aca
--- /dev/null
+++ b/pvwidgets.py:2.9
@@ -0,0 +1,3461 @@
+''' Module with channel access enabled QtWidgets.'''
+__author__ = 'Jan T. M. Chrin'
+
+import re
+import sys
+import time
+
+import collections
+import numpy as np
+from sklearn.linear_model import LinearRegression
+from sklearn.metrics import mean_squared_error, r2_score
+from distutils.version import LooseVersion
+from functools import reduce as func_reduce
+
+from qtpy.QtCore import (qVersion, QEvent, QEventLoop, QObject, QPoint, QSize,
+ Qt, QThread, QTimer, Signal, Slot)
+from qtpy.QtGui import (QCloseEvent, QColor, QCursor, QFont, QFontMetricsF,
+ QIcon, QKeySequence)
+from qtpy.QtCore import __version__ as QT_VERSION_STR
+from qtpy.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QAction,
+ QApplication, QCheckBox, QComboBox, QDialog,
+ QDockWidget, QDoubleSpinBox, QFrame, QGroupBox,
+ QHeaderView, QHBoxLayout, QLabel, QLineEdit,
+ QListWidget, QMenu, QMessageBox, QPushButton,
+ QSpinBox, QStyle, QStyleOptionSpinBox, QTableWidget,
+ QTableWidgetItem, QVBoxLayout, QWidget)
+
+import pyqtgraph as pg
+from pyqtgraph import PlotWidget
+
+from caqtwidgets.pvgateway import PVGateway
+
+
+class AppQLineEdit(QLineEdit):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+
+
+class CAQLineEdit(QLineEdit, PVGateway):
+ '''Channel access enabled QLineEdit widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ notify_freq_hz: int = 0):
+ #super(CAQLineEdit, self).__init__(parent)
+
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback,
+ notify_freq_hz=notify_freq_hz )
+
+ self.is_initialize_complete()
+ self.configure_widget()
+
+ if not self.pv_within_daq_group:
+ self.monitor_start()
+
+ '''
+ print("fixed width==========>", self.width())
+ print ("size", self.size()) # (100, 30)
+ print ("sizeHint", self.sizeHint()) # (190, 37)
+ print ("min Size", self.minimumSize()) #(0, 0)
+ print ("min SizeHint", self.minimumSizeHint()) # (26, 37)
+ print ("sizePolicy", self.sizePolicy()) #Object
+ '''
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(object, str, int)
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.NoFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+ _width_scaling_factor = 1.15
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ if self.pv_within_daq_group:
+ self.qt_property_initial_values(qt_object_name = self.PV_DAQ_CA)
+ else:
+ self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+ #renove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+class CAQLabel(QLabel, PVGateway):
+ '''Channel access enabled QLabel widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ notify_freq_hz: int = 0):
+ #super(CAQLabel, self).__init__(parent)
+
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback,
+ notify_freq_hz= notify_freq_hz)
+
+ self.is_initialize_complete()
+
+ self.configure_widget()
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(object, str, int)
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ #print(self, self.pv_name, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ # if (time.monotonic() - self.time_monotonic) < 0.9945:
+ # return
+ #self.time_monotonic = time.monotonic()
+ #self.lock.acquire()
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+ #self.lock.release()
+ #QApplication.processEvents()
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.NoFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth((qrect.width() * _width_scaling_factor))
+ #self.setFixedWidth(140)
+
+ if self.pv_within_daq_group:
+ self.qt_property_initial_values(qt_object_name = self.PV_DAQ_CA)
+ else:
+ self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+#For use with CAQMenu
+class QLineEditExtended(QLineEdit):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.parent = parent
+
+ def mousePressEvent(self, event):
+ button = event.button()
+ if button == Qt.RightButton:
+ self.parent.showContextMenu()
+ elif button == Qt.LeftButton:
+ self.parent.mousePressEvent(event)
+
+class CAQMenu(QComboBox, PVGateway):
+ '''Channel access enabled QMenu widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, prefix: str = "", suffix: str = ""):
+ #super(CAQMenu, self)
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete()
+
+ self.configure_widget()
+
+ #After configure:widget
+ self.currentIndexChanged.connect(self.value_change)
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+
+ def configure_widget(self):
+
+ self.previousIndex = None
+
+ self.setFocusPolicy(Qt.NoFocus)
+ self.setEditable(True)
+ self.setLineEdit(QLineEditExtended(self))
+ self.lineEdit().setReadOnly(True)
+ self.lineEdit().setAlignment(Qt.AlignCenter)
+
+ #self.lineEdit().setMouseTracking(True)
+ #self.setAttribute(Qt.WA_MouseNoMask)
+ #self.lineEdit().setAttribute(Qt.WA_NoMousePropagation)
+ #self.lineEdit().setAttribute(Qt.WA_WindowPropagation)
+
+ enumStringList = self.cafe.getEnumStrings(self.handle)
+
+ self.addItems(enumStringList)
+ for i in range(0, self.count()):
+ self.setItemData(i, Qt.AlignCenter, Qt.TextAlignmentRole);
+
+ '''
+ self.ensurePolished()
+ print(dir(self.style().property("font")))
+ f=self.style().property("font")
+ self.style().unpolish(self);
+ self.style().polish(self);
+ self.update()
+ '''
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.1
+
+ self.setFixedHeight(fm.lineSpacing()*1.8)
+ self.setFixedWidth((qrect.width()+40) * _width_scaling_factor)
+
+ self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
+
+ def post_display_value(self, value):
+ '''Convert value to index'''
+ if "setCurrentIndex" in dir(self):
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if isinstance(value, str):
+ self.setCurrentIndex(self.cafe.getEnumFromString(self.handle,
+ value))
+
+ elif isinstance(value, int):
+ self.setCurrentIndex(value)
+ #Should not happen
+ elif isinstance(value, float):
+ self.setCurrentIndex(int(value))
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+ #self.previousIndex = self.currentIndex()
+ return
+ else:
+ print(("ERROR: overloaded post_display_value: 'setCurrentIndex' "
+ "method does not exist!"))
+
+
+ def value_change(self, indx):
+
+ status = self.cafe.set(self.handle, indx)
+
+ if status != self.cyca.ICAFE_NORMAL:
+ #self.showSetErrorMsg(status)
+
+ value = self.cafe.getCache(self.handle, 'int')
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if value is not None:
+ self.setCurrentIndex(value)
+ else:
+ if self.previousIndex is not None:
+ self.setCurrentIndex(self.previousIndex)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("CAQMenu set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+
+ def mousePressEvent(self, event):
+
+ button = event.button()
+ if button == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+
+ elif self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ event.ignore()
+ return
+ else:
+ QComboBox.mousePressEvent(self, event)
+
+ self.previousIndex = self.currentIndex()
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ for i in range(0, self.count()):
+ self.setItemIcon(i, QIcon(":/forbidden.png"))
+ self.setStyleSheet(("QComboBox {background: transparent}" +
+ "QComboBox::drop-down {image: url(:/forbidden.png)}"))
+
+ def leaveEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ for i in range(0, self.count()):
+ self.setItemIcon(i, QIcon())
+ self.setStyleSheet("QComboBox::drop-down {background: transparent}")
+
+
+ #The widget should not gain focus by using the mouse wheel.
+ #This is accomplished by setting the focus policy to Qt.StrongFocus.
+ #The widget should only accept wheel events if it already has the focus.
+ #This is accomplished by reimplementing QWidget.wheelEvent within a QSpinBox subclass:
+ def wheelEvent(self, event):
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QComboBox.wheelEvent(self, event)
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ '''Triggered by monitor signal'''
+
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+
+class CAQMessageButton(QPushButton, PVGateway):
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ notify_freq_hz: int = 0,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, msg_label: str = "",
+ msg_press_value = None, msg_release_value =None,
+ start_monitor=False):
+ super().__init__(parent=parent, pv_name=pv_name,
+ monitor_callback=monitor_callback,
+ notify_freq_hz=\
+ notify_freq_hz,
+ pv_within_daq_group=pv_within_daq_group,
+ color_mode=color_mode, show_units=show_units,
+ msg_label=msg_label,
+ connect_callback=self.py_connect_callback)
+
+
+ self.msg_press_value = msg_press_value
+ self.msg_release_value = msg_release_value
+
+ if self.msg_press_value is not None:
+ self.pressed.connect(self.act_on_pressed)
+ if self.msg_release_value is not None:
+ self.released.connect(self.act_on_released)
+
+ self.msg_label = msg_label
+ self.suggested_text = self.msg_label
+ _suggested_text_length = len(self.suggested_text)+3
+ self.suggested_text = self.suggested_text.rjust(
+ _suggested_text_length,"^")
+
+ self.configure_widget()
+
+ self.msg_press_status = self.cyca.ICAFE_NORMAL
+ self.msg_release_status = self.cyca.ICAFE_NORMAL
+ self.msg_report_status = "PV={0}\n".format(self.pv_name)
+ self.msg_has_error = False
+
+ if not self.pv_within_daq_group and start_monitor:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ #print(self, self.pv_name, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.StrongFocus)
+ #self.setAutoDefault(True)
+ self.setCheckable(True) #Recognizes press and release states
+ #self.setChecked(True)
+ #self.setFlat(True)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.0
+
+ self.setText(self.msg_label)
+ self.setFixedHeight((fm.lineSpacing()*2.0))
+ self.setFixedWidth((qrect.width() * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+
+ def leaveEvent(self, event):
+ #if self.pv_info.accessWrite == 0:
+ if self.property("readOnly"):
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+ '''
+ def enterEvent(self, event):
+ if self.pv_info.accessWrite == 0:
+ self.setEnabled(False)
+
+ def leaveEvent(self, event):
+ if not self.isEnabled():
+ self.setEnabled(True)
+ '''
+
+ def mouseReleaseEvent(self, event):
+ #print("LOCAL mouseRelease = = > This Event is required so that clicked is activated!!!!!!")
+ if self.msg_release_value is not None:
+ time.sleep(0.1)
+ QPushButton.mouseReleaseEvent(self, event)
+
+ def mousePressEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 1:
+ QPushButton.mousePressEvent(self, event)
+ if event.button() == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+
+ def act_on_pressed(self):
+ #print("caQPushButton press ValueChanged--> ")
+ if self.msg_press_value is not None:
+ self.msg_press_status = self.cafe.set(self.handle, self.msg_press_value)
+ if self.msg_press_status != self.cyca.ICAFE_NORMAL:
+ self.msg_report_status += ("Error in set operation (at press button):\n{0}\n"
+ .format(self.cafe.getStatusCodeAsString(self.msg_press_status)))
+ self.msg_has_error = True
+ qm = QMessageBox()
+ qm.setText(self.msg_report_status)
+ qm.exec()
+ QApplication.processEvents()
+
+ def act_on_released(self):
+ #print("caQPushButton release ValueChanged--> ")
+ if self.msg_release_value is not None:
+ self.msg_release_status = self.cafe.set(self.handle, self.msg_release_value)
+ if self.msg_release_status != self.cyca.ICAFE_NORMAL:
+ self.msg_report_status += ("Error in set operation (at release button):\n{0}\n"
+ .format(self.cafe.getStatusCodeAsString(self.msg_release_status)))
+ self.msg_has_error = True
+
+ if self.msg_has_error:
+ self.msg_has_error = False
+ self.pv_message_in_a_box.setText(self.msg_report_status)
+ self.pv_message_in_a_box.exec()
+ self.msg_report_status = "PV={0}\n".format(self.pv_name)
+ qm = QMessageBox()
+ qm.setText(self.msg_report_status)
+ qm.exec()
+ QApplication.processEvents()
+
+class CAQTextEntry(QLineEdit, PVGateway):
+ '''Channel access enabled QTextEntry widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete() #waits a fraction of a second
+
+ self.currentText =""
+ self.returnPressed.connect(self.valuechange)
+ self.configure_widget()
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ #print(self, self.pv_name, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()+10) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+ def valuechange(self):
+ print(self.handle, self.text())
+ status = self.cafe.set(self.handle, self.text())
+ if status != self.cyca.ICAFE_NORMAL:
+ #elf.showSetErrorMsg(status)
+ if self.cafe.getNoMonitors(self.handle) == 0:
+ val = self.cafe.get(self.handle, 'native')
+ else:
+ val = self.cafe.getCache(self.handle, 'native')
+ #print("val", val)
+ if val is not None:
+ if isinstance(val, str):
+ strText = val
+ else:
+ valStr = ("{: .%sf}" % self.precision)
+ strText = valStr.format(round(val, self.precision)) ### + " " + self.units
+ print(strText, " precision ", self.precision)
+ self.setText(strText)
+ else:
+ #Do this for TextInfo cache
+ if self.cafe.getNoMonitors(self.handle) == 0:
+ val = self.cafe.get(self.handle, 'native')
+
+ def setText(self, value):
+
+ QLineEdit.setText(self, value)
+
+ #QLineEdit.setFixedWidth(self, QLineEdit.width(self)+10)
+ self.currentText = self.text()
+
+ #status = self.cafe.set(self.handle, value)
+ #if status ! = self.cyca.ICAFE_NORMAL:
+ #self.showSetErrorMsg(status)
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ def leaveEvent(self, event):
+
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ if self.text() != self.currentText:
+ QLineEdit.setText(self, self.currentText)
+
+ self.setCursorPosition(100)
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+ #def mouseMoveEvent(self, event):
+ # print("mouseMoveEvent", event.type())
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+ self.clearFocus()
+ return
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.cursorPositionAt(local_event_position)
+ self.setCursorPosition(local_cursor_position)
+
+
+
+
+
+class CAQSpinBox(QSpinBox, PVGateway):
+ '''Channel access enabled QTextEntry widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback)
+ #super().__init__(parent=parent, pv_name=pv_name, monitor_callback=monitor_callback,
+ # pv_within_daq_group= pv_within_daq_group, color_mode=color_mode, show_units=show_units, prefix=prefix,
+ # suffix=suffix, connect_callback=self.py_connect_callback)
+
+
+ self.is_initialize_complete()
+
+ self.valueChanged.connect(self.value_change)
+ self.configure_widget()
+ if not self.pv_within_daq_group:
+ self.monitor_start()
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ #print(" py_connect_callback::::::::::::::::::::::::::::::::::::::::::::::::::::::::::..: START ")
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+ #print(" py_connect_callback:: END ")
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.previousValue = None
+ self.currentValue = None
+ self.setFocusPolicy(Qt.StrongFocus)
+ self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ self.setAccelerated(False)
+ self.setLineEdit(QLineEditExtended(self))
+ self.lineEdit().setEnabled(True)
+ self.lineEdit().setReadOnly(False)
+ self.lineEdit().setAlignment(Qt.AlignLeft)
+ self.lineEdit().setFont(QFont("Sans Serif", 16))
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+
+ _suggested_text = self.max_control_abs_str
+ _added_text = ""
+
+ if self.show_units:
+ _added_text += " " + self.units
+ _suggested_text += self.units
+ if len(self.suffix) > 0:
+ _added_text += " " + self.suffix
+ _suggested_text += self.suffix
+
+ self.setSuffix(_added_text)
+
+ qrect = fm.boundingRect(_suggested_text)
+ _width_scaling_factor = 1.0
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+ if self.pv_ctrl is not None:
+ self.setRange(int(self.pv_ctrl.lowerControlLimit),
+ int(self.pv_ctrl.upperControlLimit))
+
+
+ def post_display_value(self, value):
+ '''Convert value to index'''
+ #print ("MON VALUE IN QSPINBOX ", value, " //////////// ", int(value))
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ self.setValue(int(round(value)))
+ self.blockSignals(False)
+ else:
+ self.setValue(int(round(value)))
+
+
+ def mousePressEvent(self, event):
+ _opt = QStyleOptionSpinBox()
+ self.initStyleOption(_opt)
+ _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxUp, self)
+ _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxDown, self)
+
+ #print(_rect_up.x(), _rect_down.x(), event.x(), event.y(), event.localPos(), event.pos())
+
+ self.previousValue = self.value()
+
+ if event.button() == Qt.LeftButton:
+ if _rect_up.contains(event.pos(), proper=True) or \
+ _rect_down.contains(event.pos(), proper=True):
+
+ if not self.cafe.isConnected(self.handle):
+ self.pv_message_in_a_box.setText(("Spinbox change value "
+ "events currently suspended\n"
+ "as channel {0} is disconnected.".format(self.pv_name)))
+ self.pv_message_in_a_box.exec()
+
+ return
+
+ QSpinBox.mousePressEvent(self, event)
+ #Clear Focus: only one step per mouse click.
+ self.clearFocus()
+
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.lineEdit().cursorPositionAt(local_event_position)
+
+ #print(local_event_position, " QSPINBOX VALUE POS ", local_cursor_position)
+ self.lineEdit().setCursorPosition(local_cursor_position)
+
+
+ PVGateway.mousePressEvent(self, event)
+
+ #Clear Focus: only one step per mouse click.
+ #self.clearFocus()
+
+ def setValue(self, intVal):
+ #print( QSpinBox.value(self), intVal)
+ #print( "setValue called//1//", intVal)
+ QSpinBox.setValue(self, intVal)
+ self.currentValue = self.value()
+ #print( "setValue called//2//", intVal)
+
+
+ def value_change(self, intVal):
+ #print("valuechange called", intVal, QSpinBox.value(self))
+ status = self.cafe.set(self.handle, intVal)
+
+ if status != self.cyca.ICAFE_NORMAL:
+
+ #print("previous current", self.previousValue, self.currentValue)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'int')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("Spinbox set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+ else:
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'int')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ self.parent.statusbar.showMessage(
+ (self.widget_class + " " +
+ self.cafe.getStatusCodeAsString(status)))
+
+
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ def leaveEvent(self, event):
+ #if self.value() != self.currentValue:
+ # self.setValue(self.currentValue)
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+
+ def keyPressEvent(self, event):
+ #print("key press event", event.type(), hex(event.key()))
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ QSpinBox.keyPressEvent(self, event)
+ self.clearFocus()
+ elif event.key() in (Qt.Key_Up, Qt.Key_Down):
+ QSpinBox.keyPressEvent(self, event)
+ else:
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ QSpinBox.keyPressEvent(self, event)
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+ # The spin box should not gain focus by using the mouse wheel.
+ # This is accomplished by setting the focus policy to Qt.StrongFocus.
+ # The spin box should only accept wheel events if it already has the focus.
+ # This is accomplished by reimplementing QWidget.wheelEvent within a QSpinBox subclass:
+ def wheelEvent(self, event):
+ #print("wheelEvent", self.hasFocus())
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QSpinBox.wheelEvent(self, event)
+
+ # def enterEvent(self,event):
+ # print("EnterEvent set focusPolicy to Strong")
+ # self.setFocusPolicy(Qt.StrongFocus)
+
+
+
+class CAQDoubleSpinBox(QDoubleSpinBox, PVGateway):
+ '''Channel access enabled QDoubleSpinBox widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent=parent, pv_name=pv_name,
+ monitor_callback=monitor_callback,
+ pv_within_daq_group= pv_within_daq_group,
+ color_mode=color_mode, show_units=show_units,
+ prefix=prefix, suffix=suffix,
+ connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete()
+ self.valueChanged.connect(self.valuechange)
+ self.configure_widget()
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ #print(" py_connect_callback::::::::::::::::::::::::::::::::::::::::::::::::::::::::::..: START ")
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+ #print(" py_connect_callback:: END ")
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+ #self.configure_widget()
+ #self.setSingleStep(0.1)
+
+
+ def configure_widget(self):
+ self.previousValue = None
+ self.currentValue = None
+ self.setFocusPolicy(Qt.StrongFocus)
+ self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ self.setAccelerated(False)
+ self.setLineEdit(QLineEditExtended(self))
+ #self.lineEdit().setObjectName("Controller")
+ #self.lineEdit().setProperty("notActOnBeam", True)
+ #self.lineEdit().setEnabled(False)
+ self.lineEdit().setReadOnly(False)
+ self.lineEdit().setAlignment(Qt.AlignRight)
+ self.lineEdit().setFont(QFont("Sans Serif", 12))
+
+ _stepsize = 10**(self.precision * -1)
+ self.setSingleStep(_stepsize)
+ self.setDecimals(self.precision)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+
+ _suggested_text = self.suggested_text
+ _added_text = ""
+
+ if self.show_units:
+ _added_text += " " + self.units
+ _suggested_text += self.units
+ if len(self.suffix) > 0:
+ _added_text += " " + self.suffix
+ _suggested_text += self.suffix
+
+ self.setSuffix(_added_text)
+
+ qrect = fm.boundingRect(_suggested_text)
+
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+ if self.pv_ctrl is not None:
+ self.setRange(int(self.pv_ctrl.lowerControlLimit),
+ int(self.pv_ctrl.upperControlLimit))
+
+
+ #if self.cafe.isConnected(self.handle):
+ # self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ #else:
+ # self.setButtonSymbols(QAbstractSpinBox.NoButtons)
+
+ def post_display_value(self, value):
+ '''set value from monitor'''
+ #print("value: : ", value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ self.setValue(value)
+ self.blockSignals(False)
+ else:
+ self.setValue(value)
+
+ def mousePressEvent(self, event):
+
+ _opt = QStyleOptionSpinBox()
+ self.initStyleOption(_opt)
+ _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxUp, self)
+ _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxDown, self)
+ self.previousValue = self.value()
+
+ if event.button() == Qt.LeftButton:
+ if _rect_up.contains(event.pos(), proper=False) or \
+ _rect_down.contains(event.pos(), proper=False):
+
+ if not self.cafe.isConnected(self.handle):
+ self.pv_message_in_a_box.setText(("Spinbox change value "
+ "events currently suspended\n"
+ "as channel {0} is disconnected.".format(self.pv_name)))
+ self.pv_message_in_a_box.exec()
+ return
+
+ QDoubleSpinBox.mousePressEvent(self, event)
+
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.lineEdit().cursorPositionAt(
+ local_event_position)
+ #print(local_event_position, " POS ", local_cursor_position)
+ self.lineEdit().setCursorPosition(local_cursor_position)
+
+
+ PVGateway.mousePressEvent(self, event)
+
+ #Clear Focus: only one step per mouse click.
+ #Not wanted for floats
+ #self.clearFocus()
+
+ def mouseReleaseEvent(self, event):
+ self.clearFocus()
+
+ def setValue(self, value):
+ #print("setValue called (B)", value, self.value())
+ self.currentValue = self.value()
+ QDoubleSpinBox.setValue(self, value)
+ #time.sleep(0.01)
+ #print("setValue called (A)", value, self.value())
+
+
+ def valuechange(self, fval):
+ #print("valuechange called, fval, value(), previous", fval, self.value(), self.previousValue)
+ status = self.cafe.set(self.handle, fval)
+
+
+ if status != self.cyca.ICAFE_NORMAL:
+
+ #print("previous current", self.previousValue, self.currentValue)
+
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'float')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("Spinbox set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+ else:
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'float')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ self.parent.statusbar.showMessage(
+ (self.widget_class + " " +
+ self.cafe.getStatusCodeAsString(status)))
+
+
+ def enterEvent(self, event):
+ self.setFocusPolicy(Qt.StrongFocus)
+ #print("eventtype", event.type())
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+
+ def leaveEvent(self, event):
+ #if self.value() != self.currentValue:
+ # self.setValue(self.currentValue)
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+ def keyPressEvent(self, event):
+ #print("key press event", event.type(), hex(event.key()))
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ QDoubleSpinBox.keyPressEvent(self, event)
+ self.clearFocus()
+ elif event.key() in (Qt.Key_Up, Qt.Key_Down):
+ QDoubleSpinBox.keyPressEvent(self, event)
+ else:
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ QDoubleSpinBox.keyPressEvent(self, event)
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+
+ #def keyReleaseEvent(self, event):
+ # print("key release")
+ # QDoubleSpinBox.keyReleaseEvent(self, event)
+
+
+ # The spin box should not gain focus by using the mouse wheel.
+ # This is accomplished by setting the focus policy to Qt.StrongFocus.
+ # The spin box should only accept wheel events if it already has the focus.
+ # This is accomplished by reimplementing QWidget.wheelEvent within a QSpinBox subclass:
+ def wheelEvent(self, event):
+ #print("wheelEvent", self.hasFocus())
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QDoubleSpinBox.wheelEvent(self, event)
+
+
+class reconnectQPushButton(QPushButton, QThread):
+ def __init__(self, parent=None):
+ super().__init__()
+ self.parent = parent
+ self.clicked.connect(self.onClicked)
+ self.isdirty = False
+ self._handles_to_reconnect = []
+ self.reconnectThread = None
+
+ #def __del__(self):
+ # print("D Called")
+ # self.reconnectThread.wait()
+
+ def onClicked(self, event):
+
+ self._handles_to_reconnect = []
+
+ for i in range(0, len(self.parent.pv_gateway)):
+ if self.parent.item(i, self.parent.no_columns-1).checkState() == Qt.Checked:
+ #print("handle", self.parent.pv_gateway[i].handle)
+ #self.parent.cafe.monitorStop(self.parent.pv_gateway[1].handle)
+ #self.parent.cafe.reconnect([self.parent.pv_gateway[1].handle])
+ self._handles_to_reconnect.append(self.parent.pv_gateway[i].handle)
+
+ #print("isConnected ", self.parent.cafe.isConnected(self.parent.pv_gateway[i].handle))
+
+ #self.reconnectThread = ReconnectThread(self.parent, self._handles_to_reconnect ) # QThread(self).create(self.reconnect)
+ #self.reconnectThread.start()
+ self.reconnect() #,self._handles_to_reconnect)
+ QApplication.processEvents()
+ #self.reconnectThread.wait()
+
+ def reconnect(self):
+ QApplication.processEvents()
+ #self.parent.cafe.printHandles()
+ #print(self._handles_to_reconnect)
+ self.isdirty = True
+ if len(self._handles_to_reconnect) > 0:
+ status=self.parent.cafe.reconnect(self._handles_to_reconnect)
+ self.isdirty = False
+ #Uncheck reconnected channels
+ for i in range(0, len(self.parent.pv_gateway)):
+ if self.parent.item(i, self.parent.no_columns-1).checkState() == Qt.Checked:
+ if self.parent.cafe.isConnected(self.parent.pv_gateway[i].handle):
+ self.parent.item(i, self.parent.no_columns-1).setCheckState(False)
+
+
+
+'''
+class ReconnectThread(QThread):
+
+ def __init__(self, parent, handles):
+ QThread.__init__(self)
+ self.parent=parent
+ self._handles = handles
+ print("Initialized")
+
+ def __del__(self):
+ self.wait()
+
+ def run(self):
+ print("reconnect")
+ self.isdirty = True
+ if len(self._handles) > 0:
+ #status=self.parent.cafe.reconnect(self._handles)
+ print("status")
+ self.isdirty = False
+ print("still running")
+'''
+
+class CAQTableWidget(QTableWidget):
+ '''Channel access enabled QTableWidget widget'''
+ #trigger_monitor_float = Signal(float, int, int)
+ #trigger_monitor_int = Signal(int, int, int)
+ #trigger_monitor_str = Signal(str, int, int)
+ #trigger_connect = Signal(int, str, int)
+
+ def hasNewData(self, _row, pv_data):
+
+ if self.pv_gateway[_row].pvd_previous is None:
+ return True
+
+ newDataFlag = False
+
+ if self.pv_gateway[_row].pvd_previous.ts[1] != pv_data.ts[1]:
+ newDataFlag = True
+ elif self.pv_gateway[_row].pvd_previous.ts[0] != pv_data.ts[0]:
+ newDataFlag = True
+ # Catch disconnect events(!!) and set newDataFlag only
+ elif self.pv_gateway[_row].pvd_previous.status != pv_data.status:
+ newDataFlag = True
+ return newDataFlag
+
+
+ def widget_update(self):
+
+ for _row, pvgate in enumerate(self.pv_gateway):
+ #for _row in range(0, len(self.pv_gateway)):
+ if not pvgate.notify_unison:
+ continue
+ _handle = pvgate.handle
+ _pvd = pvgate.cafe.getPVCache(_handle)
+
+ #Only if unison flag is set else return
+
+ #Does not cater for reconnections
+ #print(_row, self.pv_gateway[_row].pv_name, _pvd.status)
+ #if not self.hasNewData(_row, _pvd) and _pvd.status == \
+ # self.cyca.ICAFE_NORMAL:
+ #print(_row, " has no new data")
+ #continue
+
+ if _pvd.status in (self.cyca.ICAFE_CS_NEVER_CONN,
+ self.cyca.ICAFE_CA_OP_CONN_DOWN):
+ pvgate.pvd_previous = _pvd
+ continue
+
+ pvgate.pvd_previous = _pvd
+
+ #if timestamps the same - then skip
+ _value = _pvd.value[0]
+ _value = pvgate.format_display_value(_value)
+
+ qtwi = QTableWidgetItem(str(_value)+ " ")
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+
+ self.setItem(_row, self.no_columns-3,
+ QTableWidgetItem(qtwi))
+ self.item(_row, self.no_columns-3).setTextAlignment(Qt.AlignRight |
+ Qt.AlignVCenter)
+
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target - 1
+ _ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0:_ts_str_len - (
+ self.format_ts_nano-self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec :
+ _ts_str += "."
+ while _ts_str_len < _ilength_target :
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ qtwi = QTableWidgetItem( _ts_str)
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+ self.setItem(_row, self.no_columns-2, QTableWidgetItem(qtwi))
+ self.item(_row, self.no_columns-2).setTextAlignment(Qt.AlignCenter)
+
+ _prop = pvgate.qt_dynamic_property_get()
+
+ alarm_severity = _pvd.alarmSeverity
+
+ if _prop == pvgate.READBACK_ALARM:
+
+ if alarm_severity == pvgate.cyca.SEV_MAJOR:
+ _bgcolor = pvgate.settings.fgAlarmMajor
+ _fgcolor = "black"
+ elif alarm_severity == pvgate.cyca.SEV_MINOR:
+ _bgcolor = pvgate.settings.fgAlarmMinor
+ _fgcolor = "black"
+ elif alarm_severity == pvgate.cyca.SEV_INVALID:
+ _bgcolor = pvgate.settings.fgAlarmInvalid
+ _fgcolor = "#777777"
+ else:
+ _bgcolor = pvgate.settings.fgAlarmNoAlarm
+ #_bgcolor = pvgate.settings.bgReadbackAlarm
+ _fgcolor = "black"
+
+ #Colors for bg/fg reversed as is the old norm
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.no_columns-2).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.no_columns-3).setForeground(
+ QColor(_fgcolor))
+ self.item(_row, self.no_columns-2).setForeground(
+ QColor(_fgcolor))
+
+ elif _prop == pvgate.READBACK_STATIC:
+
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor(pvgate.settings.bgReadback))
+ self.item(_row, self.no_columns-2).setBackground(
+ QColor(pvgate.settings.bgReadback))
+
+ elif _prop == pvgate.DISCONNECTED:
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.no_columns-2).setBackground(QColor(
+ "#ffffff"))
+ self.item(_row, self.no_columns-3).setForeground(QColor(
+ "#777777"))
+ self.item(_row, self.no_columns-2).setForeground(QColor(
+ "#777777"))
+
+ else:
+ print (_prop, "widget_update unknown in element/row", _row,
+ _row+1)
+
+ QApplication.processEvents()
+
+ def __init__(self, parent=None, pv_list: list = ["PV_NAME_NOT_GIVEN"],
+ monitor_callback=None, pv_within_daq_group: bool = False,
+ color_mode = None, show_units: bool = True, prefix: str = "",
+ suffix: str = "", ts_res: str = "milli",
+ init_column: bool = False, init_list: list = [],
+ notify_freq_hz: int = 0, notify_unison: bool = True,
+ precision: int = 0, scale_factor: float = 1,
+ show_timestamp: bool = True, pv_list_show: list = None):
+
+ super().__init__()
+ self.columns_dict = {}
+ _column_dict_value = 0
+ self.columns_dict['PV'] = _column_dict_value
+ if init_column:
+ _column_dict_value += 1
+ self.columns_dict['Init'] = _column_dict_value
+ _column_dict_value += 1
+ self.columns_dict['Value'] = _column_dict_value
+ if show_timestamp:
+ _column_dict_value += 1
+ self.columns_dict['Timestamp'] = _column_dict_value
+ _column_dict_value += 1
+ self.columns_dict['Reconnect'] = _column_dict_value
+
+ self.setWindowModality(Qt.ApplicationModal)
+ self.no_columns = _column_dict_value + 1
+
+ self.init_column = init_column
+
+ self.init_list = init_list
+ if self.init_column and not self.init_list:
+ self.init_list = pv_list
+
+ self.icount = 0
+ self.notify_freq_hz = abs(notify_freq_hz)
+ self.notify_freq_hz_default = self.notify_freq_hz
+ self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
+ 1000 / self.notify_freq_hz
+
+ self.notify_unison = True if notify_unison and \
+ self.notify_freq_hz > 0 else False
+
+ self.precision = precision
+ self.scale_factor = scale_factor
+ self.show_timestamp = show_timestamp
+
+ self.format_ts_nano = 31 #max length of date
+ self.format_ts_micro = 28
+ self.format_ts_milli = 25
+ self.format_ts_deci = 23 #-8
+ self.format_ts_sec = 21
+ if "nano" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_nano
+ elif "micro" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_micro
+ elif "milli" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_milli
+ elif "deci" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_deci
+ elif "sec" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_sec
+ else:
+ self.format_ts_decimal_part = self.format_ts_milli
+
+ self.pv2item_dict = {}
+
+ self.pv_list = pv_list
+ self.pv_gateway = [None] * len(self.pv_list)
+
+ self.pv_list_show = pv_list_show
+ if self.pv_list_show is None:
+ self.pv_list_show = self.pv_list
+
+ _color_mode = [None] * len(self.pv_list)
+
+ if isinstance(color_mode, list):
+ for i in range (0, len(color_mode)):
+ _color_mode[i] = color_mode[i]
+
+ for i in range (0, len(self.pv_list)):
+
+ self.pv_gateway[i] = PVGateway().__init__(
+ parent, self.pv_list[i], monitor_callback,
+ pv_within_daq_group, _color_mode[i], show_units, prefix, suffix,
+ connect_triggers=False, notify_freq_hz=self.notify_freq_hz,
+ notify_unison=self.notify_unison, precision=self.precision)
+
+ self.pv_gateway[i].is_initialize_complete()
+ self.pv_gateway[i].trigger_connect.connect(
+ self.receive_connect_update)
+ self.pv_gateway[i].trigger_monitor_str.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_int.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_float.connect(
+ self.receive_monitor_update)
+
+ self.pv_gateway[i].widget_class = "QTableWidgetItem"
+
+ self.pv_gateway[i].qt_property_initial_values(
+ qt_object_name = self.pv_gateway[i].PV_READBACK,
+ tool_tip=False)
+ #cant do this - sender will be a qspinbox
+ #self.pv_gateway[i].post_display_value = self.post_display_value
+
+
+ #required for reconnect
+ self.cafe = self.pv_gateway[0].cafe
+ self.cyca = self.pv_gateway[0].cyca
+
+ self.timer = None
+ if self.notify_unison:
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.widget_update)
+ self.timer.singleShot(0, self.widget_update)
+ self.timer.start(self.notify_milliseconds)
+
+ self.configure_widget()
+
+ #Connect only deals with colours - only helps on reconnect
+ # In any case monitors take over
+ #Got to do this earlier or emit immediately after!!
+ for i in range(0, len(self.pv_gateway)):
+ if self.cafe.isConnected(self.pv_gateway[i].pv_name):
+ self.pv_gateway[i].trigger_connect.emit(
+ self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
+ self.pv_gateway[i].cyca.ICAFE_CS_CONN)
+
+
+ for i in range(0, len(self.pv_gateway)):
+ if not self.pv_gateway[i].pv_within_daq_group:
+ self.pv_gateway[i].monitor_start()
+
+ self.update_init_values()
+
+ self.configure_context_menu()
+
+
+ def configure_context_menu(self):
+ self.table_context_menu = QMenu()
+ self.table_context_menu.setObjectName("contextMenu")
+ self.table_context_menu.setWindowModality(Qt.NonModal)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.table_context_menu.addSection("---")
+
+ action1 = QAction("Configure Table PVs", self)
+ action1.triggered.connect(self.display_table_parameters)
+ self.table_context_menu.addAction(action1)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.table_context_menu.addSection("---")
+
+ QApplication.processEvents()
+
+
+ def restore_init_values(self):
+ _set_values_dict = self.get_init_values()
+ #print("set val dict", _set_values_dict))
+ #print("same as init vals", self.is_same_as_init_values())
+
+ _pvs_to_set, _values_to_set = zip(*_set_values_dict.items())
+ #zip returns tuples
+ #print(_pvs_to_set)
+ #print(_values_to_set)
+ _pvs_to_set = list(_pvs_to_set)
+ _values_to_set = list(_values_to_set)
+
+ status, status_list = self.cafe.setScalarList(_pvs_to_set,
+ _values_to_set)
+
+ if status != self.cyca.ICAFE_NORMAL:
+ _mess = ("The following device(s) reported an error " +
+ "in set operation:")
+ for i, status_value in enumerate(status_list):
+ if status_value != self.cyca.ICAFE_NORMAL:
+ _mess += ("\n" + _pvs_to_set[i] + " has status = " +
+ str(status_value) + " " +
+ self.cafe.getStatusCodeAsString(status_value) +
+ " " + self.cafe.getStatusInfo(status_value) )
+ qm = QMessageBox()
+ qm.setText(_mess)
+
+ qm.exec()
+ QApplication.processEvents()
+
+ self.init_value_button.setEnabled(True)
+
+
+ def is_same_as_init_values(self):
+ _init_values_dict = self.get_column_values(self.columns_dict['Init'])
+ _pvs, _init_values = zip(*_init_values_dict.items())
+ _current_values_dict = self.get_column_values(self.columns_dict['Value'])
+ _pvs, _current_values = zip(*_current_values_dict.items())
+ #zip returns tuples
+
+ if func_reduce(lambda i, j : i and j, map(
+ lambda m, k: m == k, _init_values, _current_values), True):
+ return True
+ else:
+ return False
+
+
+ def get_column_values(self, column_no):
+ _values_dict = {}
+ _start = 0
+ _end = len(self.pv_gateway)
+ _pvs = [None] * _end
+ _values_str = [None] * _end
+ _values = [None] * _end
+
+ for _row in range(_start, _end):
+ _values_str[_row] = self.item(_row, column_no).text()
+ _pvs[_row] = self.item(_row, 0).text()
+
+ _value_list = [float(_value_list) for _value_list in re.findall(
+ r'-?\d+\.?\d*', _values_str[_row])]
+
+ if not _value_list:
+ print("row", _row, "values", _values_str[_row], _pvs[_row])
+ _values[_row] = _values_str[_row] #Can be enum string
+ else:
+ _values[_row] = _value_list[0]
+
+
+ if _pvs[_row] in self.pv_list_show:
+ #_values_dict[_pvs[_row]] = _values[_row]
+ _values_dict[self.pv_gateway[_row].pv_name] = _values[_row]
+
+ #print("_pvs", _pvs)
+ #print("show", self.pv_list_show)
+ return _values_dict #_pvs_to_set, _values_to_set
+
+
+ def get_init_values(self):
+ return self.get_column_values(self.columns_dict['Init'])
+
+ def get_init_values_previous(self):
+ _set_values_dict = {}
+ _start = 0
+ _end = len(self.pv_gateway)
+ _pvs_to_set = [None] * _end
+ _values_to_set_str = [None] * _end
+ _values_to_set = [None] * _end
+ for _row in range(_start, _end):
+ _values_to_set_str[_row] = self.item(_row, self.columns_dict['Init']).text()
+ _pvs_to_set[_row] = self.item(_row, self.columns_dict['PV']).text()
+
+ _value_list = [float(_value_list) for _value_list in re.findall(
+ r'-?\d+\.?\d*', _values_to_set_str[_row])]
+
+ if not _value_list:
+ print("//row", _row, "values", _values_to_set_str[_row],
+ _pvs_to_set[_row])
+ _values_to_set[_row] = _values_to_set_str[_row] #Can be enum string
+ else:
+ _values_to_set[_row] = _value_list[0]
+
+
+ if _pvs_to_set[_row] in self.init_list:
+ #_set_values_dict[_pvs_to_set[_row]] = _values_to_set[_row]
+ _set_values_dict[self.pv_gateway[_row].pv_name] = _values_to_set[_row]
+ #print(_set_values_dict)
+ return _set_values_dict #_pvs_to_set, _values_to_set
+
+
+ def update_init_values(self):
+ _start = 0
+ _end=len(self.pv_gateway)
+ for _row in range(_start, _end):
+ _handle = self.pv_gateway[_row].handle
+ _value = self.pv_gateway[_row].cafe.getCache(_handle)
+
+ if _value is not None:
+ if self.scale_factor != 1:
+ _value = _value * self.scale_factor
+ _value = self.pv_gateway[_row].format_display_value(_value)
+ #print("value from update", _value)
+ qtwi = QTableWidgetItem(str(_value)+ " ")
+ _f = qtwi.font()
+ _f.setPointSize(8)
+ qtwi.setFont(_f)
+ self.setItem(_row, 1, qtwi)
+ self.item(_row, 1).setTextAlignment(
+ Qt.AlignRight | Qt.AlignVCenter)
+
+
+ def configure_widget(self):
+
+ _column_width_pvname = 210
+ _column_width_value = 110
+ _column_width_timestamp = 240
+ _column_width_checkbox = 25
+
+ self.setRowCount(len(self.pv_gateway)+1)
+ self.setColumnCount(self.no_columns)
+ self.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.resizeColumnsToContents()
+ self.resizeRowsToContents()
+ #self.horizontalHeader().setStretchLastSection(True);
+ self.setColumnWidth(self.columns_dict['PV'], _column_width_pvname)
+
+ self.setColumnWidth(self.columns_dict['Value'], _column_width_value)
+ if 'Init' in self.columns_dict.keys():
+ self.setColumnWidth(self.columns_dict['Init'], _column_width_value)
+ if 'Timestamp' in self.columns_dict.keys():
+ self.setColumnWidth(self.columns_dict['Timestamp'], _column_width_timestamp)
+ self.setColumnWidth(self.columns_dict['Reconnect'], _column_width_checkbox)
+
+ _pv_column = self.columns_dict['PV']
+ for i in range(0, len(self.pv_gateway)):
+ #self.setItem(i, _pv_column, QTableWidgetItem(self.pv_gateway[i].pv_name))
+ qtwt = QTableWidgetItem(self.pv_list_show[i])
+ f = qtwt.font()
+ f.setPointSize(8)
+ qtwt.setFont(f)
+ #qtwt.setTextAlignment(Qt.AlignLeft)
+ self.setItem(i, _pv_column, qtwt)
+ self.item(i, _pv_column).setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter)
+ for i_column in range(1, self.no_columns-1):
+ self.setItem(i, i_column, QTableWidgetItem(str("")))
+ self.item(i, i_column).setTextAlignment(Qt.AlignCenter |
+ Qt.AlignVCenter)
+ self.pv2item_dict[self.pv_gateway[i]] = i
+
+ cb_item = QTableWidgetItem()
+ cb_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
+ cb_item.setCheckState(Qt.Unchecked)
+ cb_item.setTextAlignment(Qt.AlignCenter)
+ cb_item.setToolTip(self.pv_gateway[i].pv_name)
+
+ self.setItem(i, self.no_columns-1, cb_item)
+ self.item(i, self.no_columns-1).setTextAlignment(Qt.AlignCenter |
+ Qt.AlignVCenter)
+
+ if self.init_column:
+ self.init_widget = QWidget()
+ _init_layout = QHBoxLayout(self.init_widget)
+ self.init_value_button = QPushButton()
+ self.init_value_button.setText("Update")
+ _f = self.init_value_button.font()
+ _f.setPointSize(8)
+ self.init_value_button.setFont(_f)
+ self.init_value_button.setFixedWidth(80)
+ self.init_value_button.clicked.connect(self.update_init_values)
+ self.init_value_button.setToolTip(
+ ("Stores initial, pre-measurement value. Update is also " +
+ "executed automatically before each measurement."))
+ _init_layout.addWidget(self.init_value_button)
+ _init_layout.setAlignment(Qt.AlignRight)
+ _init_layout.setContentsMargins(1,1,0,0) #Required
+ self.init_widget.setLayout(_init_layout)
+ self.setCellWidget(len(self.pv_gateway), 1, self.init_widget)
+
+ _restore_widget = QWidget()
+ _restore_layout = QHBoxLayout(_restore_widget)
+ self.restore_value_button = QPushButton()
+ #self.restore_value_button.setObjectName("Controller")
+ #self.restore_value_button.setProperty('actOnBeam', True)
+ self.restore_value_button.setStyleSheet(
+ "QPushButton{background-color: rgb(212, 219, 157);}")
+ self.restore_value_button.setText("Restore")
+ _f = self.restore_value_button.font()
+ _f.setPointSize(8)
+ self.restore_value_button.setFont(_f)
+ self.restore_value_button.setFixedWidth(80)
+ self.restore_value_button.clicked.connect(self.restore_init_values)
+ self.restore_value_button.setToolTip(
+ ("Restore devices to their pre-measurement values"))
+ _restore_layout.addWidget(self.restore_value_button)
+ _restore_layout.setAlignment(Qt.AlignRight)
+ _restore_layout.setContentsMargins(1,1,0,0) #Required
+ _restore_widget.setLayout(_restore_layout)
+ self.setCellWidget(len(self.pv_gateway), 2, _restore_widget)
+
+ #Do not display no for last row (Reconnect button)
+ _row_digit_last_cell = QTableWidgetItem(str(""))
+ self.setVerticalHeaderItem(len(self.pv_gateway), _row_digit_last_cell)
+ self.setItem(len(self.pv_gateway), 0, QTableWidgetItem(str("")))
+
+ _qwb = QWidget()
+
+ self.reconnect_button = reconnectQPushButton(self) #self required
+ #self.reconnect_button.setFont(self.font12);QFont("Sans Serif", 12)
+
+ f = self.reconnect_button.font()
+
+ if 'Timestamp' in self.columns_dict.keys():
+ f.setPointSize(8)
+ self.reconnect_button.setFixedWidth(100)
+ else:
+ f.setPointSize(6)
+ self.reconnect_button.setFixedWidth(58)
+
+ self.reconnect_button.setFont(f)
+
+
+ self.reconnect_button.setText("Reconnect")
+
+
+
+
+ _layout = QHBoxLayout(_qwb)
+ _layout.addWidget(self.reconnect_button)
+ _layout.setAlignment(Qt.AlignCenter)
+ _layout.setContentsMargins(0,0,0,0) #Required
+ _qwb.setLayout(_layout)
+
+ #_reconnect_button
+ self.setCellWidget(len(self.pv_gateway), self.no_columns-2, _qwb)
+
+ _qwc = QWidget()
+
+ self.cb_item_all = QCheckBox(self)
+ self.cb_item_all.setCheckState(Qt.Unchecked)
+ self.cb_item_all.stateChanged.connect(self.reconnectStateChanged)
+
+ _layout_cb = QHBoxLayout(_qwc)
+ _layout_cb.addWidget(self.cb_item_all)
+ _layout_cb.setAlignment(Qt.AlignLeft)
+ _layout_cb.setContentsMargins(3,2,0,1) #Required LTRB
+ _qwc.setLayout(_layout_cb)
+
+ self.setCellWidget(len(self.pv_gateway), self.no_columns-1, _qwc)
+
+
+ header_item = QTableWidgetItem("Process Variable")
+
+ '''
+ header_item.setText("Process Variable")
+ f= header_item.font()
+ f.setPixelSize(12)
+ header_item.setFont(f)
+ '''
+
+ self.setHorizontalHeaderItem(self.columns_dict['PV'], header_item)
+
+ if 'Init' in self.columns_dict.keys():
+ self.setHorizontalHeaderItem(self.columns_dict['Init'],
+ QTableWidgetItem("Initial Value"))
+
+ self.setHorizontalHeaderItem(self.columns_dict['Value'],
+ QTableWidgetItem("Value"))
+
+ if 'Timestamp' in self.columns_dict.keys():
+ self.setHorizontalHeaderItem(self.columns_dict['Timestamp'],
+ QTableWidgetItem("Timestamp"))
+ self.setHorizontalHeaderItem(self.columns_dict['Reconnect'],
+ QTableWidgetItem("R"))
+ self.setFocusPolicy(Qt.NoFocus)
+ self.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.setSelectionMode(QAbstractItemView.NoSelection)
+
+ self.verticalHeader().setDefaultAlignment(Qt.AlignRight)
+ self.verticalHeader().setFixedWidth(22)
+
+
+ #self.verticalHeader().setVisible(False)
+ #self.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch )
+
+ _fm_font = QFont("Sans Serif")
+ _fm_font.setPointSize(12)
+ fm = QFontMetricsF(_fm_font) #QFont("Sans Serif", 12))
+
+ _factor = 1
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ self.setFixedHeight(int(fm.lineSpacing() * _factor *
+ (len(self.pv_gateway)+3)))
+ _min_table_width = 620 if not self.init_column else 650
+ self.setMinimumWidth(_min_table_width)
+
+
+ for _row in range(0, len(self.pv_gateway)):
+ #print("name/row/columns", self.pv_gateway[_row].pv_name, _row, self.no_columns)
+ self.item(_row, _pv_column).setForeground(QColor("#000000"))
+ ##self.item(_row, _pv_column).setTextAlignment(Qt.AlignCenter)
+
+ for i_column in range(1, self.no_columns-2):
+ self.item(_row, i_column).setForeground(QColor("#000000"))
+ self.item(_row, i_column).setTextAlignment(Qt.AlignRight |
+ Qt.AlignVCenter)
+
+ self.item(_row, self.columns_dict['Value']).setBackground(QColor("#ffffff"))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setTextAlignment(Qt.AlignCenter)
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(QColor("#ffffff"))
+
+ @Slot(int)
+ def reconnectStateChanged(self, state):
+ if state == Qt.Unchecked:
+ for i in range(0, len(self.pv_gateway)):
+ self.item(i, self.columns_dict['Reconnect']).setCheckState(Qt.Unchecked)
+ else:
+ for i in range(0, len(self.pv_gateway)):
+ self.item(i, self.columns_dict['Reconnect']).setCheckState(Qt.Checked)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ _row = self.pv2item_dict[self.sender()]
+ '''
+ if _row in (3,):
+ print("receive mon update before post_display, value/row==", value, _row, self.pv_gateway[_row].pv_name)
+ print(self, _row, self.pv_gateway[_row].pv_name, ">>>>>>>from gateway>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ print("sender", self.sender())
+ '''
+
+ #print(self.pv2item_dict)
+ #print( value, status, alarm_severity)
+ #Timing on CAQTableWidget basis! Miss last events if many events
+ #First trigger should always happen
+ #if self.update_hz is not None:
+ # print(time.monotonic(), self.pv_gateway[_row].time_monotonic, (1/self.update_hz))
+ # if (time.monotonic() - self.pv_gateway[_row].time_monotonic) < (1/self.update_hz):
+ # return
+ # self.pv_gateway[_row].receive_monitor_update(value, status, alarm_severity)
+ self.pv_gateway[_row].time_monotonic = time.monotonic()
+ if self.scale_factor != 1:
+ value = value * self.scale_factor
+ _value = self.pv_gateway[_row].format_display_value(value)
+
+ #print("row no//", _row)
+ qtwi = QTableWidgetItem(str(_value) + " ")
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+ self.setItem(_row, self.columns_dict['Value'], qtwi)
+ self.item(_row, self.columns_dict['Value']).setTextAlignment(
+ Qt.AlignRight | Qt.AlignVCenter)
+
+ if 'Timestamp' in self.columns_dict.keys():
+
+ _handle = self.pv_gateway[_row].handle
+ #print("post_display HANDLE/val/row", _handle, _value, _row)
+ #_status = self.pv_gateway[_row].cafe.getStatus(_handle)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(_handle)
+
+
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target -1
+
+ ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0:_ts_str_len-(
+ self.format_ts_nano-self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+ #print(_ts_date)
+ #print(_ts_str)
+ #print("length of timestamp string ", _ts_str_len)
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec:
+ _ts_str += "."
+ while _ts_str_len < _ilength_target :
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ qtwi = QTableWidgetItem( _ts_str)
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+ self.setItem(_row, self.columns_dict['Timestamp'], qtwi)
+ self.item(_row, self.columns_dict['Timestamp']).setTextAlignment(Qt.AlignCenter)
+
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ if _prop == self.pv_gateway[_row].READBACK_ALARM:
+
+ if alarm_severity == self.pv_gateway[_row].cyca.SEV_MAJOR:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmMajor
+ _fgcolor = "black"
+ elif alarm_severity == self.pv_gateway[_row].cyca.SEV_MINOR:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmMinor
+ _fgcolor = "black"
+ elif alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmInvalid
+ _fgcolor = "#777777"
+ else:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmNoAlarm
+ #_bgcolor = self.pv_gateway[_row].settings.bgReadbackAlarm
+ _fgcolor = "black"
+
+ #Colors for bg/fg reversed as is the old norm
+ self.item(_row, self.columns_dict['Value']).setBackground(QColor(_bgcolor))
+ self.item(_row, self.columns_dict['Value']).setForeground(QColor(_fgcolor))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(QColor(_bgcolor))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(QColor(_fgcolor))
+
+
+ elif _prop == self.pv_gateway[_row].DISCONNECTED or \
+ alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Value']).setForeground(
+ QColor("#777777"))
+
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(
+ QColor("#777777"))
+
+
+ elif _prop == self.pv_gateway[_row].READBACK_STATIC:
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor(self.pv_gateway[_row].settings.bgReadback))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor(self.pv_gateway[_row].settings.bgReadback))
+ else:
+
+ print (_prop, self.pv_gateway[_row].DISCONNECTED, "(in monitor) unknown in element/row no.", _row, _row+1)
+ #TRZ SET PROPERTY
+
+ QApplication.processEvents(QEventLoop.AllEvents, 10)
+ #self.post_display_value(value)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ _row = self.pv2item_dict[self.sender()]
+
+ #print(self, self.pv_gateway[_row].pv_name, ">>>>>>>receive connect >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ #print("sender", self.sender())
+ #print("MY RECEIVE receive_connect_update for row = ", _row, " status=", status)
+ #print(handle, pv_name)
+ self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
+ post_display=False)
+ #print("after gateway connect update")
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ #self.post_display_value(status)
+ if _prop == self.pv_gateway[_row].DISCONNECTED:
+ #self.item(_row,0).setBackground(QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Value']).setBackground(QColor("#ffffff"))
+ #self.item(_row,0).setForeground(QColor("#777777"))
+ self.item(_row, self.columns_dict['Value']).setForeground(QColor("#777777"))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(QColor("#777777"))
+
+ QApplication.processEvents()
+
+ '''
+ def post_display_value(self, value):
+
+#IS CLEAN BEFORE EXIT
+ self.icount += 1;
+ print("recursion limit", sys.getrecursionlimit(), self.icount)
+ _row = self.pv2item_dict[self.sender()]
+ print("row no", _row)
+ _value = self.pv_gateway[_row].format_display_value(value)
+ print("row no//", _row)
+ self.setItem(_row, self.no_columns-3,
+ QTableWidgetItem(str(_value)+ " "))
+ self.item(_row, self.no_columns-3).setTextAlignment(Qt.AlignRight)
+
+ _handle = self.pv_gateway[_row].handle
+ #print("post_display HANDLE/val/row", _handle, _value, _row)
+ #_status = self.pv_gateway[_row].cafe.getStatus(_handle)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(_handle)
+
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target -1
+ _ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0:_ts_str_len - (self.format_ts_nano -
+ self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+ #print(_ts_date)
+ #print(_ts_str)
+ #print("length of timestamp string ", _ts_str_len)
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec :
+ _ts_str += "."
+ while _ts_str_len < _ilength_target :
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ self.setItem(_row, self.no_columns-2, QTableWidgetItem( _ts_str))
+ self.item(_row, self.no_columns-2).setTextAlignment(Qt.AlignCenter)
+
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ if _prop == self.pv_gateway[_row].READBACK_ALARM:
+ #self.item(_row,0).setBackground(QColor("#c8c8c8"))
+ self.item(_row, self.no_columns-3).setBackground(QColor("#c8c8c8"))
+ self.item(_row, self.no_columns-2).setBackground(QColor("#c8c8c8"))
+
+ elif _prop == self.pv_gateway[_row].READBACK_STATIC:
+ #self.item(_row,0).setBackground(QColor("#ffffe0"))
+ self.item(_row, self.no_columns-3).setBackground(QColor("#ffffe0"))
+ self.item(_row, self.no_columns-2).setBackground(QColor("#ffffe0"))
+
+ elif _prop == self.pv_gateway[_row].DISCONNECTED:
+ self.item(_row, self.no_columns-3).setBackground(QColor("#ffffff"))
+ self.item(_row, self.no_columns-2).setBackground(QColor("#ffffff"))
+ self.item(_row, self.no_columns-3).setForeground(QColor("#777777"))
+ self.item(_row, self.no_columns-2).setForeground(QColor("#777777"))
+
+ else:
+ print (_prop, "unknown in element/row ==>", _row, _row+1)
+ #TRZ SET PROPERTY
+
+ QApplication.processEvents()
+ #self.setStyleSheet("QTableWidget::item {margin-right: 5 }");
+
+ #self.setStyleSheet("QHeaderView::section:horizontal {margin-right: 2; border: 1px solid}");
+ '''
+ def table_precision_user_changed(self, new_value):
+ self.pvgateway_precision = new_value
+
+ for pvgate in self.pv_gateway:
+ if pvgate.pv_ctrl is not None:
+ self.pvgateway_precision = min(pvgate.pv_ctrl.precision,
+ new_value)
+
+ pvgate.precision_user = self.pvgateway_precision
+ pvgate.precision = self.pvgateway_precision
+
+ _pvd = self.cafe.getPVCache(pvgate.handle)
+
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ pvgate.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+
+
+ def table_precision_ioc_reset(self):
+ '''
+ _max_current_precision_value = -1
+ for i, pvgate in enumerate(self.pv_gateway):
+ if pvgate.pv_ctrl is not None:
+
+ _max_current_precision_value = max(
+ pvgate.precision_user, _max_current_precision_value)
+
+ if _max_current_precision_value == -1:
+ _max_current_precision_value = self.max_precision_value
+ '''
+ if self.max_precision_value == self.table_precision_user_wgt.value():
+ self.table_precision_user_changed(self.max_precision_value)
+ else:
+ self.table_precision_user_wgt.setValue(self.max_precision_value)
+
+ def table_refresh_rate_changed(self, new_idx):
+
+ _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
+ _notify_milliseconds = 0 if _notify_freq_hz == 0 else \
+ 1000 / _notify_freq_hz
+
+ self.notify_freq_hz = _notify_freq_hz
+
+ if _notify_milliseconds == 0:
+ for pvgate in self.pv_gateway:
+ pvgate.notify_unison = False
+ pvgate.notify_milliseconds = _notify_milliseconds
+ pvgate.notify_freq_hz = self.notify_freq_hz
+ pvgate.monitor_stop()
+ time.sleep(0.01)
+ for pvgate in self.pv_gateway:
+ pvgate.monitor_start()
+
+ else:
+ for pvgate in self.pv_gateway:
+ if not pvgate.notify_unison:
+ pvgate.monitor_stop()
+
+ for pvgate in self.pv_gateway:
+ pvgate.notify_milliseconds = _notify_milliseconds
+ pvgate.notify_freq_hz = self.notify_freq_hz
+
+ if not pvgate.notify_unison:
+ pvgate.notify_unison = True
+ pvgate.monitor_start()
+ else:
+
+ self.cafe.updateMonitorPolicyDeltaMS(
+ pvgate.handle, pvgate.monitor_id,
+ pvgate.notify_milliseconds)
+
+ #for pvgate in self.pv_gateway:
+ # pvgate.monitor_start()
+ # print("pvgate / mon started", pvgate)
+
+ if self.timer is not None:
+ self.timer.stop()
+ else:
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.widget_update)
+ self.timer.singleShot(0, self.widget_update)
+
+ if _notify_milliseconds > 0:
+ self.timer.start(_notify_milliseconds)
+
+ def table_ts_resolution_changed(self, new_idx):
+
+ for i, (key, ts_res) in enumerate(self.ts_combox_idx_dict.items()):
+ if i == new_idx:
+ self.format_ts_decimal_part = ts_res
+ break;
+
+ for pvgate in self.pv_gateway:
+ _pvd = self.cafe.getPVCache(pvgate.handle)
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ pvgate.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+ elif isinstance(_pvd.value[0], int):
+ pvgate.trigger_monitor_int.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+ else:
+ pvgate.trigger_monitor_str.emit(
+ str(_pvd.value[0]), _pvd.status,
+ _pvd.alarmSeverity)
+
+
+ def display_table_parameters(self):
+ display_wgt = QDialog(self)
+ display_wgt.setWindowTitle("PV Parameters")
+ layout = QVBoxLayout()
+ common_label_width = 120
+ common_wgt_width = 160
+ common_hbox_width = common_label_width + common_wgt_width + 20
+
+ self.initial_value = 0
+ self.max_precision_value = 0
+ for i, pvgate in enumerate(self.pv_gateway):
+ if pvgate.pv_ctrl is not None:
+ if pvgate.pv_ctrl.precision > 0:
+ self.max_precision_value = max(self.max_precision_value,
+ pvgate.pv_ctrl.precision)
+ self.initial_value = max(self.initial_value,
+ pvgate.precision)
+
+ if self.max_precision_value > 0:
+ #precision user
+ _hbox_wgt = QWidget()
+ _hbox = QHBoxLayout()
+ precision_user_label = QLabel("Precision (user):")
+ self.table_precision_user_wgt = QSpinBox(self)
+ self.table_precision_user_wgt.setFocusPolicy(Qt.NoFocus)
+ self.table_precision_user_wgt.setValue(self.initial_value)
+ self.table_precision_user_wgt.setMaximum(self.max_precision_value)
+ self.table_precision_user_wgt.valueChanged.connect(
+ self.table_precision_user_changed)
+ precision_user_label.setAlignment(Qt.AlignLeft)
+ self.table_precision_user_wgt.setAlignment(Qt.AlignLeft)
+ _hbox.addWidget(precision_user_label)
+ _hbox.addWidget(self.table_precision_user_wgt)
+ _hbox.setAlignment(Qt.AlignLeft)
+ _hbox_wgt.setLayout(_hbox)
+
+ precision_user_label.setFixedWidth(common_label_width)
+ self.table_precision_user_wgt.setFixedWidth(40)
+ _hbox_wgt.setFixedWidth(common_hbox_width)
+
+ #precision ioc
+ _hbox2_wgt = QWidget()
+ _hbox2 = QHBoxLayout()
+ precision_ioc_label = QLabel("Precision (ioc): ")
+ precision_ioc = QPushButton(self)
+ precision_ioc.setText("Reset")
+ precision_ioc.clicked.connect(self.table_precision_ioc_reset)
+ precision_ioc_label.setAlignment(Qt.AlignLeft)
+
+
+ _hbox2.addWidget(precision_ioc_label)
+ _hbox2.addWidget(precision_ioc)
+ _hbox2.setAlignment(Qt.AlignLeft)
+
+ _hbox2_wgt.setLayout(_hbox2)
+
+ precision_ioc_label.setFixedWidth(common_label_width)
+ precision_ioc.setFixedWidth(50)
+
+ _hbox2_wgt.setFixedWidth(common_hbox_width)
+
+
+ layout.addWidget(_hbox_wgt)
+ layout.addWidget(_hbox2_wgt)
+
+
+ if 'Timestamp' in self.columns_dict.keys():
+ #time-stamp
+ _hbox4_wgt = QWidget()
+ _hbox4 = QHBoxLayout()
+ ts_label = QLabel("Timestamp: ")
+
+ self.ts_combox_idx_dict = {'second (s)':self.format_ts_sec,
+ 'decisecond (ds)':self.format_ts_deci,
+ 'millisecond (ms)':self.format_ts_milli,
+ 'microsecond (\u03bcs)':self.format_ts_micro,
+ 'nanosecond (ns)':self.format_ts_nano}
+
+ ts_resolution = QComboBox(self)
+ for (key, ts_res) in (self.ts_combox_idx_dict.items()):
+ ts_resolution.addItem(key)
+
+
+ _current_idx = 0
+
+ for i, (key, ts_res) in enumerate(self.ts_combox_idx_dict.items()):
+ if ts_res == self.format_ts_decimal_part:
+ _current_idx = i
+ break
+
+ ts_resolution.setCurrentIndex(_current_idx)
+ ts_resolution.currentIndexChanged.connect(
+ self.table_ts_resolution_changed)
+
+ _hbox4.addWidget(ts_label)
+ _hbox4.addWidget(ts_resolution)
+ _hbox4_wgt.setLayout(_hbox4)
+
+ ts_label.setFixedWidth(common_label_width)
+ ts_resolution.setFixedWidth(common_wgt_width)
+ _hbox4_wgt.setFixedWidth(common_hbox_width)
+
+ layout.addWidget(_hbox4_wgt)
+
+ #precision refresh rate
+ _hbox3_wgt = QWidget()
+ _hbox3 = QHBoxLayout()
+ refresh_freq_label = QLabel("Refresh rate: ")
+ #_default_refresh_val = 0 if self.notify_freq_hz <= 0 else \
+ # self.notify_freq_hz
+ _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
+ self.notify_freq_hz_default
+
+ self.refresh_freq_combox_idx_dict = {0:0, 1:10, 2:5, 3:2, 4:1, 5:0.5,
+ 6:_default_refresh_val}
+ refresh_freq = QComboBox(self)
+ refresh_freq.addItem('direct')
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[1]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[2]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[3]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[4]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[5]))
+
+ _default_text = 'default (direct)' if _default_refresh_val == 0 else \
+ 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
+
+ refresh_freq.addItem(_default_text)
+
+ for key, value in self.refresh_freq_combox_idx_dict.items():
+ if value == self.notify_freq_hz:
+ refresh_freq.setCurrentIndex(key)
+ break
+
+
+ refresh_freq.currentIndexChanged.connect(
+ self.table_refresh_rate_changed)
+
+
+ _hbox3.addWidget(refresh_freq_label)
+ _hbox3.addWidget(refresh_freq)
+ _hbox3_wgt.setLayout(_hbox3)
+
+ refresh_freq_label.setFixedWidth(common_label_width)
+ refresh_freq.setFixedWidth(common_wgt_width)
+ _hbox3_wgt.setFixedWidth(common_hbox_width)
+
+ layout.addWidget(_hbox3_wgt)
+
+ layout.setAlignment(Qt.AlignLeft)
+ layout.setContentsMargins(10, 0, 0, 0)
+ layout.setSpacing(0)
+
+ display_wgt.setMinimumWidth(340)
+ display_wgt.setLayout(layout)
+
+ display_wgt.exec()
+
+
+ def mousePressEvent(self, event):
+ #print(event.pos())
+ row = self.indexAt(event.pos()).row()
+ #print("current item", row)
+ if row < len(self.pv_list) and row > -1:
+ self.pv_gateway[row].mousePressEvent(event)
+ #elif row == -1:
+ # self.display_table_parameters()
+ else:
+ button = event.button()
+ #print("button", button, Qt.RightButton)
+ if button == Qt.RightButton:
+ self.table_context_menu.exec(QCursor.pos())
+ self.clearFocus()
+
+
+
+ #remove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearSelection()
+ self.clearFocus()
+ del event
+
+
+
+class QMessageWidget(QListWidget):
+ """Log message window."""
+ def __init__(self, parent=None):
+ super(QMessageWidget, self).__init__(parent)
+ self.myItem = None
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+
+
+ def leaveEvent(self, event):
+ if self.myItem:
+ self.clearSelection()
+ self.clearFocus()
+ del event
+
+ def mousePressEvent(self, event):
+ item = self.itemAt(event.x(), event.y())
+ if item:
+ self.myItem = item
+ self.setCurrentItem(self.myItem)
+
+ def keyPressEvent(self, event):
+ if event.matches(QKeySequence.Copy):
+ nitem = event.count()
+ if nitem:
+ if self.myItem is not None:
+ _str = self.myItem.text()
+ #beg = mystr.find("file = ")
+ #end = mystr.find("'", beg+6)
+ #newstr = "\""+mystr[beg+6:end]+"\""
+ QApplication.clipboard().setText(_str)
+
+
+
+
+
+class QResultsWidget:
+ """Results table"""
+ def __init__(self, summary_dict=None, table_dict=None):
+
+ self.summary_dict = summary_dict
+ self.table_dict = table_dict
+ self._group_box = None
+
+ def group_box(self, title=""):
+ self._group_box = QGroupBox(title)
+ self._group_box.setObjectName("OUTERLEFT")
+ _vbox = QVBoxLayout()
+ _qspace = QFrame()
+ _qspace.setFixedHeight(10)
+ _vbox.addWidget(_qspace)
+
+ _font = QFont("Sans Serif", 10)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, (label, text) in enumerate(self.summary_dict.items()):
+ if len(str(label)) > len(longest_str_item1):
+ longest_str_item1 = str(label)
+ if len(str(text)) > len(longest_str_item2):
+ longest_str_item2 = str(text)
+
+ fm = QFontMetricsF(_font)
+
+ _factor = 1.15
+
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ qrect1 = fm.boundingRect(longest_str_item1)
+ qrect2 = fm.boundingRect(longest_str_item2)
+ _width_scaling_factor = 1.5
+ _width_scaling_factor_le = 1.15
+ _widget_height = 25
+ for i, (label, text) in enumerate(self.summary_dict.items()):
+ #print(label, text)
+ qlabel = QLabel(label)
+ qle = QLineEdit(text)
+ qlabel.setFont(_font)
+ qlabel.setStyleSheet(("QLabel{color:black;" +
+ "margin:0px; padding:2px;}"))
+ qlabel.setFixedWidth(qrect1.width() * _width_scaling_factor)
+ qlabel.setFixedHeight(_widget_height)
+
+ qle.setFocusPolicy(Qt.NoFocus)
+ qle.setFont(_font)
+ qle.setStyleSheet(("QLineEdit{color:blue;" +
+ "background-color: lightgray;" +
+ "qproperty-readOnly: true;" +
+ "margin:0px; padding:2px;}"))
+ qle.setFixedWidth(qrect2.width() * _width_scaling_factor_le)
+ qle.setFixedHeight(_widget_height)
+ qle.setAlignment(Qt.AlignRight)
+
+ _hbox_widget = QWidget()
+ _hbox = QHBoxLayout()
+ _hbox.addWidget(qlabel)
+ _hbox.addWidget(qle)
+ _hbox_widget.setLayout(_hbox)
+ _hbox.setAlignment(Qt.AlignCenter)
+ _hbox.setContentsMargins(0, 2, 0, 0)
+ _vbox.addWidget(_hbox_widget)
+
+ _vbox.setContentsMargins(0, 0, 0, 0)
+ _vbox.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+
+ _vbox2_widget = QWidget()
+ _vbox2 = QVBoxLayout()
+ _vbox2.setContentsMargins(0, 20, 0, 40)
+ table = QTableWidget(len(self.table_dict)-1, 2)
+ table.verticalHeader().setVisible(False)
+ table.setFocusPolicy(Qt.NoFocus)
+ #table.setFont(_font)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, (label, text) in enumerate(self.table_dict.items()):
+ item1 = QTableWidgetItem(str(label))
+ item2 = QTableWidgetItem(str(text))
+ item1.setTextAlignment(Qt.AlignCenter)
+ item2.setTextAlignment(Qt.AlignCenter)
+ item1.setForeground(QColor("black"))
+ item2.setForeground(QColor("black"))
+ if i%2 == 0:
+ item1.setBackground(QColor("lightgray"))
+ item2.setBackground(QColor("lightgray"))
+
+ if len(str(label)) > len(longest_str_item1):
+ longest_str_item1 = str(label)
+ if len(str(text)) > len(longest_str_item2):
+ longest_str_item2 = str(text)
+
+ if i == 0:
+ #item1.setFont(_font)
+ #item2.setFont(_font)
+ table.setHorizontalHeaderItem(0, item1)
+ table.setHorizontalHeaderItem(1, item2)
+ else:
+ table.setItem(i-1, 0, item1)
+ table.setItem(i-1, 1, item2)
+
+
+ fm = QFontMetricsF(_font)
+
+ _factor = 1.2
+
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ qrect = fm.boundingRect(longest_str_item1 + longest_str_item2 )
+
+ _width_scaling_factor = 1.04
+ table.resizeColumnsToContents()
+ table.resizeRowsToContents()
+ #print(fm.lineSpacing())
+ table.setFixedHeight((fm.lineSpacing() * _factor * len(self.table_dict))
+ + fm.lineSpacing()*2)
+
+ #table.setColumnWidth(0, fm.boundingRect(longest_str_item1).width() * _width_scaling_factor)
+ #table.setColumnWidth(1, fm.boundingRect(longest_str_item2).width() * _width_scaling_factor)
+ table.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+ #table.setFixedWidth(220)
+ _vbox2.addWidget(table)
+ _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+ _vbox2_widget.setLayout(_vbox2)
+
+ _vbox.addWidget(_vbox2_widget)
+
+ self._group_box.setLayout(_vbox)
+ self._group_box.setContentsMargins(20, 20, 20, 20)
+ self._group_box.setAlignment(Qt.AlignTop)
+ self._group_box.setFixedHeight(table.height() + (
+ _widget_height*len(self.summary_dict))) #_vbox2_widget.height() -50)
+ self._group_box.setFixedWidth(table.width() + 20)
+ return self._group_box
+
+
+class QResultsTableWidget():
+ """Results table"""
+ def __init__(self, column_headings=None):
+
+ self.column_headings = column_headings
+ self._group_box = None
+
+ def group_box(self, title="Table of Results"):
+ self._group_box = QGroupBox(title)
+ self._group_box.setObjectName("OUTER")
+
+ _font = QFont("Sans Serif", 10)
+
+ _vbox2_widget = QWidget()
+ _vbox2 = QVBoxLayout()
+ _vbox2.setContentsMargins(0, 20, 0, 40)
+ table = QTableWidget(1, len(self.column_headings))
+ table.verticalHeader().setVisible(True)
+ table.setFocusPolicy(Qt.NoFocus)
+ table.setFont(_font)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, heading in enumerate(self.column_headings):
+ _item = QTableWidgetItem(str(heading))
+ table.setHorizontalHeaderItem(i, _item)
+
+
+ table.resizeColumnsToContents()
+ table.resizeRowsToContents()
+ #print(fm.lineSpacing())
+ table.setFixedHeight(400)
+
+ _vbox2.addWidget(table)
+ _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+ _vbox2_widget.setLayout(_vbox2)
+
+
+ self._group_box.setLayout(_vbox2)
+ self._group_box.setContentsMargins(20, 20, 20, 20)
+ self._group_box.setAlignment(Qt.AlignTop)
+
+ self._group_box.setFixedWidth(table.width() + 20)
+ return self._group_box
+
+
+class QHDFDockWidget(QDockWidget):
+
+ def __init__(self, title=None, parent=None):
+ super().__init__(title, parent)
+ self.parent = parent
+ self.is_docked = True
+ self.geometry_from_qsettings = self.parent.getGeometry()
+ self.topLevelChanged.connect(self._top_level_changed)
+ self.setVisible(False)
+ self.setFloating(False)
+ self.geometry_from_qsettings = self.parent.geometry()
+ print( "START GEOEMTRY ",self.geometry_from_qsettings)
+
+
+ def closeEvent(self, event: QCloseEvent):
+ ######################print("Super ClosingEvent....")
+ super().closeEvent(event)
+ print("Super ClosedEvent....")
+ print("float/1", self.isFloating())
+ print("visible/1", self.isVisible())
+ #print("isDocked/1", self.is_docked)
+ print("before", self.parent.geometry(), self.geometry_from_qsettings)
+ #self.parent.setGeometry(self.geometry_from_qsettings)
+ #self.setGeometry(self.geometry_from_qsettings)
+ #QApplication.processEvents()
+ print("after", self.parent.geometry())
+
+
+ def changeEvent(self, event):
+ print("event Type", event.type())
+ print("Sender", self.sender())
+ #This implies that one of restore/quit button of the widget was clicked
+ #if self.senderSignalIndex() == 34:
+ # self.close()
+ print("before//", self.parent.geometry(), self.geometry_from_qsettings)
+ #self.parent.setGeometry(self.geometry_from_qsettings)
+ #Generic
+ #if "QAbstractButton" in str(self.sender()):
+ # self.geometry_from_qsettings = self.parent.geometry()
+ print("after", self.parent.geometry())
+
+ def _top_level_changed(self, is_floating):
+ #Need MUTEX
+ print("is_floating", is_floating)
+ #self.setVisible(False)
+ #self.setFloating(True)
+ #ResetGeometry
+ #self.parent.setGeometry(self.geometry_from_qsettings)
+ #QApplication.processEvents()
+
+class QNoDockWidget(QDockWidget):
+
+ def __init__(self, title=None, parent=None):
+ super().__init__(title, parent)
+ self.parent = parent
+ self.is_docked = True
+ self.geometry_from_qsettings = self.parent.getGeometry()
+ self.topLevelChanged.connect(self._top_level_changed)
+ self.setVisible(False)
+ self.setFloating(True)
+
+ def changeEvent(self, event):
+ #print("event Type", event.type())
+ #print("Sender", self.sender())
+ #This implies that one of restore/quit button of the widget was clicked
+ #if self.senderSignalIndex() == 34:
+ # self.close()
+ #Generic
+ if "QAbstractButton" in str(self.sender()):
+ self.geometry_from_qsettings = self.parent.geometry()
+
+
+ #def closeEvent(self, event: QCloseEvent):
+ ######################print("Super ClosingEvent....")
+ #super().closeEvent(event)
+ #print("Super ClosedEvent....")
+ #print("float/1", self.isFloating())
+ #print("visible/1", self.isVisible())
+ #print("isDocked/1", self.is_docked)
+
+
+
+ #This is for the quit button
+ #if not self.isVisible():
+ # self.topLevelChanged.emit(False)
+ # print("emitting..")
+ # self.topLevelChanged.emit(False)
+
+
+ def _top_level_changed(self, is_floating):
+ #Need MUTEX
+
+ self.setVisible(False)
+ self.setFloating(True)
+ #ResetGeometry
+ self.parent.setGeometry(self.geometry_from_qsettings)
+ QApplication.processEvents()
+
+
+
+class CAQStripChart(PlotWidget):
+ '''Channel access enabled pyqtgraph.PlotWidget'''
+
+ def daq_start(self):
+ self.blockSignals(False)
+
+ def daq_pause(self):
+ self.blockSignals(True)
+
+ def daq_stop(self):
+ self.blockSignals(True)
+
+ def __init__(self, parent=None, pv_list: list = ['PV_NAME_NOT_GIVEN'],
+ monitor_callback=None, pv_within_daq_group: bool = False,
+ color_mode = None, show_units: bool = False, prefix: str = "",
+ suffix: str = "", notify_freq_hz: int = 0, title: str = "",
+ ylabel: str = "", force_ts_align = True, text_label = [],
+ pen_color_idx = 0):
+ super().__init__()
+
+ self.no_channels = len(pv_list)
+ self.pen_color_idx = pen_color_idx
+ self.text_label = text_label
+ self.found = False
+ self.time_zero = [0] * self.no_channels
+ self.time_delta = [0] * self.no_channels
+ self.pv_list = pv_list
+ self.pv2item_dict = {}
+ self.pv_gateway = [None] * self.no_channels
+
+ self.pvd_previous_list = [None] * self.no_channels
+ self.val_previous = [None] * self.no_channels
+
+ self.curve = [None] * self.no_channels
+
+ for i in range (0, len(self.pv_list)):
+ self.pv_gateway[i] = PVGateway().__init__(
+ parent, pv_list[i], monitor_callback, pv_within_daq_group,
+ color_mode, show_units, prefix, suffix,
+ #connect_callback=self.py_connect_callback,
+ connect_triggers=False, notify_freq_hz=notify_freq_hz,
+ monitor_dbr_time = True)
+
+ self.pv_gateway[i].is_initialize_complete()
+
+
+ self.pvd_previous_list[i] = self.pv_gateway[i].pvd
+
+ self.pv_gateway[i].trigger_connect.connect(
+ self.receive_connect_update)
+
+ self.pv_gateway[i].trigger_monitor_str.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_int.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_float.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor.connect(
+ self.receive_monitor_dbr_time)
+
+ self.pv_gateway[i].widget_class = "PlotWidget"
+
+ self.pv2item_dict[self.pv_gateway[i]] = i
+
+ self.cafe = self.pv_gateway[0].cafe
+ self.cyca = self.pv_gateway[0].cyca
+ for i in range(0, len(self.pv_gateway)):
+ if self.cafe.isConnected(self.pv_gateway[i].pv_name):
+ self.pv_gateway[i].trigger_connect.emit(
+ self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
+ self.pv_gateway[i].cyca.ICAFE_CS_CONN)
+
+
+ for i in range(0, len(self.pv_gateway)):
+ if not self.pv_gateway[i].pv_within_daq_group:
+ self.pv_gateway[i].monitor_start()
+
+ sampleinterval = 0.2
+ timewindow = 1800.0
+
+ self.ts_delta_max = 0.6
+
+ # Data stuff
+ self._interval = int(sampleinterval*1000)
+ self._bufsize = 9000 #int(timewindow/0.33)
+ self._bufsize2 = 9000 # int(timewindow/1.33)
+ self.databuffer = [None] * self.no_channels
+ self.timebuffer = [None] * self.no_channels
+ self.x = [None] * self.no_channels
+ self.y = [None] * self.no_channels
+ self.x_shifted = [None] * self.no_channels
+
+ self.idx = [0] * self.no_channels
+
+ for i in range(0, self.no_channels):
+ bsize = self._bufsize if i == 0 else self._bufsize2
+ self.databuffer[i] = collections.deque([None]*bsize, bsize)
+ self.timebuffer[i] = collections.deque([0]*bsize, bsize)
+ self.x[i] = np.zeros(bsize, dtype=np.float)
+ self.y[i] = np.zeros(bsize, dtype=np.float)
+
+ _long_size=20
+ #self.data_series_buffer = collections.deque([0]*_long_size, _long_size)
+ #self.time_series_buffer = collections.deque([0]*_long_size, _long_size)
+
+ #self.data_series = [] * self.no_channels
+ #self.time_series = [] * self.no_channels
+
+ self.iflag_series = 0
+
+ #self.x = np.linspace(-timewindow, 0.0, self._bufsize)
+ #self.x_series = np.zeros(_long_size, dtype=np.float)
+ #self.y_series = np.zeros(_long_size, dtype=np.float)
+ if title is not None:
+ self.setTitle(str(title)) #self.pv_gateway[0].pv_name)
+ self.showGrid(x=True, y=True)
+ #self.setLabel('left', ylabel, self.pv_gateway[0].units)
+ self.setLabel('bottom', 'time', 'min')
+ self.setBackground((60, 60, 60)) #247, 236, 249))
+ #self.setLimits(yMin=-0.11)
+ #self.setLimits(yMin=-1, yMax=1)
+ ax = pg.AxisItem('left')
+ ax.enableAutoSIPrefix(enable=False)
+ ax.setLabel(ylabel, self.pv_gateway[0].units)
+
+ ay = pg.AxisItem('bottom')
+ ay.enableAutoSIPrefix(enable=False)
+ ay.setLabel('time', 'min')
+
+ if 'BPM' in text_label:
+ ax.setTickSpacing(0.2, 0.1)
+ #ax.setRange(-1, 1)
+ #ax.setParentItem(self.graphicsItem())
+ axitems = {}
+ axitems['left'] = ax
+ axitems['bottom'] = ay
+
+ self.setAxisItems(axitems)
+
+ pg.setConfigOption('leftButtonPan', False)
+
+ self.plotItem.setMouseEnabled(y=False) # Only allow zoom in X-axis
+ self.plotItem.setMouseEnabled(x=False) # Only allow zoom in Y-axis
+
+ #(125, 249, 255)
+ if self.pen_color_idx == 0:
+ pen_list = [ (255, 155, 0), (255,255,0), (0, 180, 255) ]
+ elif self.pen_color_idx == 1:
+ pen_list = [ (125, 249, 255), (255,255,0), (0, 180, 255) ]
+ else:
+ pen_list = [ (0, 180, 255), (125, 249, 255), (255,255,0) ]
+
+ for i in range(0, len(self.pv_gateway)):
+ self.curve[i] = self.plot(self.x[0], self.y[0], pen=pen_list[i]) # (0, 253, 235))
+ #self.curve[1] = self.plot(self.x[1], self.y[1], pen=(255,255,0))
+ #offset=(1.0, 1.0),
+ l=pg.LegendItem() #horSpacing=20, verSpacing=0, labelTextColor=(205, 205, 205),
+ #labelTextSize='6px', colCount=1)
+
+ l.setParentItem(self.graphicsItem())
+ l.anchor((0,0), (0.08, 0.0))
+ #l.setLabelTextColor((205, 205, 205))
+ #l.setLabelTextSize(9) does not exists(!)
+ #l.setOffset(-60)
+ for curv, label in zip(self.curve, self.text_label):
+ l.addItem(curv, label)
+
+ #for curv, pv in zip(self.curve, self.pv_gateway):
+ # l.addItem(curv, self.textpv.pv_name)
+
+ #self.daq_stop()
+ #print(self._bufsize)
+ #print(len(self.x), len(self.y))
+ QApplication.processEvents()
+
+ @Slot(object, int)
+ def receive_monitor_dbr_time(self, pvdata, alarm_severity):
+ _row = self.pv2item_dict[self.sender()]
+ #print("row, value from pvdata==>", _row, pvdata.value[0], self.pv_gateway[_row].pv_name)
+
+ ts_now = pvdata.ts[0] + pvdata.ts[1] * 10**(-9)
+ ts_previous = (self.pvd_previous_list[_row].ts[0] +
+ self.pvd_previous_list[_row].ts[1] * 10**(-9))
+ ts_delta = ts_now - ts_previous
+
+ if pvdata.ts[0] < self.pvd_previous_list[_row].ts[0]:
+ print("Funny ts value", self.pv_gateway[_row].pv_name)
+ pvdata.show()
+ return
+
+ if (pvdata.ts[0] == self.pvd_previous_list[_row].ts[0]) and (
+ pvdata.ts[1] == self.pvd_previous_list[_row].ts[1]):
+ #print(self.pv_gateway[_row].pv_name)
+ #pvdata.show()
+ #self.pvd_previous_list[_row].show()
+ return
+
+ value = pvdata.value[0]
+ #discard first callbacks
+ #if ts_delta > 2.0:
+ # self.pvd_previous_list[_row] = _pvd
+ # return;
+ self.pvd_previous_list[_row] = pvdata
+ self.val_previous[_row] = value
+ #self.pvd_previous_list[_row].ts[0] = _pvd.ts[0]
+ #self.pvd_previous_list[_row].ts[1] = _pvd.ts[1]
+
+ self.databuffer[_row].append(value)
+ self.timebuffer[_row].append(self.time_delta[_row])
+
+ highest_ts = self.timebuffer[0][0] \
+ if self.timebuffer[0][0] is not None else 0
+ for i in range(1, len(self.timebuffer)):
+ if self.timebuffer[i][0] is None:
+ continue
+ elif self.timebuffer[i][0] > highest_ts:
+ highest_ts = self.timebuffer[i][0]
+
+ if self.timebuffer[_row][0] is not None:
+ for i, val in enumerate(self.timebuffer[_row]):
+ if val > highest_ts:
+ self.idx[_row] = i - 1
+ break
+
+ self.y[_row][:] = self.databuffer[_row]
+ self.x[_row][:] = self.timebuffer[_row]
+
+ idx = self.idx[_row]
+ self.x_shifted[_row] = list(map(lambda m : (m - self.time_delta[_row]), self.x[_row][idx:]))
+
+ #print("idx", self.idx)
+
+ #if 'AVG' in self.pv_gateway[_row].pv_name:
+ # for row in range(0, len(self.curve)):
+ # self.curve[row].setData(self.x_shifted[row], self.y[row][idx:])
+
+ self.curve[_row].setData(self.x_shifted[_row], self.y[_row][idx:])
+ self.time_delta[_row] = ((
+ pvdata.ts[0] + pvdata.ts[1]*10**(-9)) - self.time_zero[0])/60
+
+ #QApplication.processEvents()
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ #self.pv_gateway.receive_monitor_update(value, status, alarm_severity)
+ _row = self.pv2item_dict[self.sender()]
+ #if _row == 1:
+ # return
+ print("row, value===>", _row, value, self.pv_gateway[_row].pv_name)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(
+ self.pv_gateway[_row].handle)
+
+ #print("value", _pvd.value[0], self.pvd_previous_list[_row].value[0])
+
+ _pvd2 = self.pv_gateway[_row].pvd
+
+ print ("val", value, _pvd2.value[0], _pvd.value[0], self.pvd_previous_list[_row].value[0])
+
+ ts_now = _pvd.ts[0] + _pvd.ts[1] * 10**(-9)
+
+ ts_previous = (self.pvd_previous_list[_row].ts[0] +
+ self.pvd_previous_list[_row].ts[1] * 10**(-9))
+ ts_delta = ts_now - ts_previous
+
+ if value == self.val_previous[_row]:
+ #if (_pvd.ts[0] == self.pvd_previous_list[_row].ts[0]) and (
+ # _pvd.ts[1] == self.pvd_previous_list[_row].ts[1]):
+ print(self.pv_gateway[_row].pv_name)
+ _pvd.show()
+ #self.pvd_previous_list[_row].show()
+ return
+
+
+ #discard first callbacks
+ #if ts_delta > 2.0:
+ # self.pvd_previous_list[_row] = _pvd
+ # return;
+ self.pvd_previous_list[_row] = _pvd2
+ self.val_previous[_row] = value
+ #self.pvd_previous_list[_row].ts[0] = _pvd.ts[0]
+ #self.pvd_previous_list[_row].ts[1] = _pvd.ts[1]
+
+ self.databuffer[_row].append(value)
+ self.timebuffer[_row].append(self.time_delta[_row])
+
+ highest_ts = self.timebuffer[0][0] \
+ if self.timebuffer[0][0] is not None else 0
+ for i in range(1, len(self.timebuffer)):
+ if self.timebuffer[i][0] is None:
+ continue
+ elif self.timebuffer[i][0] > highest_ts:
+ highest_ts = self.timebuffer[i][0]
+
+
+ if self.timebuffer[_row][0] is not None:
+ for i, val in enumerate(self.timebuffer[_row]):
+ if val > highest_ts:
+ self.idx[_row] = i - 1
+ break
+
+
+ '''
+ for i in range(1, self.timebuffer):
+ if self.timebuffer[i][0] is not None:
+ a = self.timebuffer[0][0]
+ for i, val in enumerate(self.timebuffer[_row]):
+ if val > a:
+ idx = i - 1
+ break
+ '''
+
+ self.y[_row][:] = self.databuffer[_row]
+ self.x[_row][:] = self.timebuffer[_row]
+
+
+ #self.y[_row][:] = self.databuffer[_row]
+ #self.x[_row][:] = self.timebuffer[_row]
+
+ '''
+ #print(ts_delta, value, self.pvd_previous.value[0])
+ #if (ts_delta < self.ts_delta_max) and (value < self.pvd_previous.value[0]) :
+ if (value < self.pvd_previous.value[0]) :
+ self.data_series_buffer.append(value)
+ self.time_series_buffer.append(ts_now - self.time_zero ) #self.time_delta)
+ self.y_series[:] = self.data_series_buffer
+ self.x_series[:] = self.time_series_buffer
+ #print(self.x_series, self.y_series)
+ #elif ts_delta < 1.0:
+ if len(self.data_series_buffer) > 15:
+ #x_series = np.array(self.time_series, dtype=np.float)
+ #y_series = np.array(self.data_series, dtype=np.float)
+ _x=self.x_series.reshape((-1, 1))
+
+ model = LinearRegression()
+ model.fit(_x, self.y_series)
+ r_sq = model.score(_x, self.y_series)
+ ###JCprint('coefficient of determination:', r_sq, "slope", model.coef_ , "lifetime:", self.y_series[0]/model.coef_ / 3600)
+ #print('intercept:', model.intercept_)
+ #print('slope:', model.coef_)
+ #print('max value', y_series[0], y_series[1])
+ if r_sq > 0.995:
+ _I = self.y_series[0]
+ ###JCprint("lifetime:", _I/model.coef_ / 3600)
+
+
+ y_pred = model.predict(_x)
+ #print("len, y_pred, _x", len(y_pred), len(self.y_series), len(_x))
+ #print('predicted response:', y_pred, sep='\n')
+ m_sq_error = mean_squared_error(self.y_series, y_pred)
+ #print('Mean squared error: {0:.9f}'.format(
+ # mean_squared_error(y_series, y_pred)))
+ #print('Coefficient of determination: {0:.9f}'.format(
+ # r2_score(y_series, y_pred)))
+
+
+
+ self.trigger_series_sequence.emit(self.x_series, self.y_series)
+ #print("emit")
+ self.data_series = []
+ self.time_series = []
+ #print(len(self.x_series), len(self.y_series))
+ else:
+ self.data_series = []
+ self.time_series = []
+
+ '''
+
+
+ #dt = (self.x[-1] - self.x[-2])
+ #print("dt", dt)
+ #Lowet IPCT before trigger is set to t=0
+ idx = self.idx[_row]
+ self.x_shifted[_row] = list(map(lambda m : (m - self.time_delta[_row]), self.x[_row][idx:]))
+
+ ##self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+
+ #print("row len len ", _row, self.time_delta[0], self.time_delta[1])
+
+
+ self.curve[_row].setData(self.x_shifted[_row], self.y[_row][idx:])
+
+ self.time_delta[_row] = (
+ (_pvd.ts[0] + _pvd.ts[1]*10**(-9)) - self.time_zero[0])
+
+
+ '''
+ LOOK_BACK = -800
+ if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name:
+ LOOK_BACK = -250
+
+ if value > self.y[-2]:
+ if not self.found:
+ #print(x_shifted[-240:], self.y[-240:])
+ #self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+ max_index = self.y[LOOK_BACK:].argmax()
+
+ if max_index == 0:
+ return
+ print("max index=", max_index)
+
+ #print(x_shifted[-600+max_index:], self.x[-600+max_index:])
+ #print(self.y[-600+max_index:-2])
+ self.found = True
+ #print("Are Signals blocked??", self.signalsBlocked())
+ self.trigger_decay_sequence.emit(np.array(
+ x_shifted[LOOK_BACK+max_index+9:-2]), self.y[LOOK_BACK+max_index+9:-2])
+ else:
+ self.found = False
+ '''
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ print("pv_name==>", pv_name)
+
+ _row = self.pv2item_dict[self.sender()]
+ self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
+ post_display=False)
+
+ #self.pv_gateway.receive_connect_update(handle, pv_name, status)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(self.pv_gateway[_row].handle)
+ if self.time_zero[_row] == 0:
+ self.time_zero[_row] = _pvd.ts[0] + _pvd.ts[1]*10**(-9)
+
+ self.pvd_previous = _pvd
+
+
+ #renove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+
+class CAQPCTChart(PlotWidget):
+ '''Channel access enabled pyqtgraph.PlotWidget'''
+ #trigger_monitor_float = Signal(float, int, int)
+ #trigger_monitor_int = Signal(int, int, int)
+ #trigger_monitor_str = Signal(str, int, int)
+
+ #trigger_connect = Signal(int, str, int)
+
+ trigger_decay_sequence = Signal(np.ndarray, np.ndarray)
+ trigger_series_sequence = Signal(np.ndarray, np.ndarray)
+ #def py_connect_callback(self, handle, pvname, status):
+ # self.trigger_connect.emit(int(handle), str(pvname), int(status))
+ # print("py connect callback", handle, pvname, status)
+
+ def daq_start(self):
+ self.blockSignals(False)
+
+
+ def daq_pause(self):
+ self.blockSignals(True)
+
+ def daq_stop(self):
+ self.blockSignals(True)
+
+
+ def __init__(self, parent=None, pv_list: list = ['PV_NAME_NOT_GIVEN'],
+ monitor_callback=None, pv_within_daq_group: bool = False,
+ color_mode = None, show_units: bool = False, prefix: str = "",
+ suffix: str = "", notify_freq_hz: int = 0):
+ super().__init__()
+
+ self.found = False
+ self.time_zero = 0
+ self.time_delta = 0
+ self.pv_list = pv_list
+ self.pv2item_dict = {}
+ self.pv_gateway = [None] * len(self.pv_list)
+ self.pvd_previous = None
+
+ for i in range (0, len(self.pv_list)):
+ self.pv_gateway[i] = PVGateway().__init__(
+ parent, pv_list[i], monitor_callback, pv_within_daq_group,
+ color_mode, show_units, prefix, suffix,
+ #connect_callback=self.py_connect_callback,
+ connect_triggers=False, notify_freq_hz=notify_freq_hz )
+
+ self.pv_gateway[i].is_initialize_complete()
+
+ self.pv_gateway[i].trigger_connect.connect(
+ self.receive_connect_update)
+
+ self.pv_gateway[i].trigger_monitor_str.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_int.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_float.connect(
+ self.receive_monitor_update)
+
+ self.pv_gateway[i].widget_class = "PlotWidget"
+
+ self.pv2item_dict[self.pv_gateway[i]] = i
+
+ self.cafe = self.pv_gateway[0].cafe
+ self.cyca = self.pv_gateway[0].cyca
+ for i in range(0, len(self.pv_gateway)):
+ if self.cafe.isConnected(self.pv_gateway[i].pv_name):
+ self.pv_gateway[i].trigger_connect.emit(
+ self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
+ self.pv_gateway[i].cyca.ICAFE_CS_CONN)
+
+ for i in range(0, len(self.pv_gateway)):
+ if not self.pv_gateway[i].pv_within_daq_group:
+ self.pv_gateway[i].monitor_start()
+
+ sampleinterval=0.333
+ timewindow=1800.0
+
+ self.ts_delta_max = 0.6
+
+ # Data stuff
+ self._interval = int(sampleinterval*1000)
+ self._bufsize = int(timewindow/sampleinterval)
+ self.databuffer = collections.deque([None]*self._bufsize, self._bufsize)
+ self.timebuffer = collections.deque([0]*self._bufsize, self._bufsize)
+
+ _long_size=20
+ self.data_series_buffer = collections.deque([0]*_long_size, _long_size)
+ self.time_series_buffer = collections.deque([0]*_long_size, _long_size)
+
+ self.data_series = []
+ self.time_series = []
+
+ self.iflag_series = 0
+
+ #self.x = np.linspace(-timewindow, 0.0, self._bufsize)
+ self.x = np.zeros(self._bufsize, dtype=np.float)
+ self.y = np.zeros(self._bufsize, dtype=np.float)
+
+ self.x_series = np.zeros(_long_size, dtype=np.float)
+ self.y_series = np.zeros(_long_size, dtype=np.float)
+
+ self.setTitle("PCT(t)") #self.pv_gateway[0].pv_name)
+ self.showGrid(x=True, y=True)
+ self.setLabel('left', 'I', 'mA')
+ self.setLabel('bottom', 'time', 's')
+ self.setBackground((60, 60, 60)) #247, 236, 249))
+ self.setLimits(yMin=-0.11)
+
+ self.plotItem.setMouseEnabled(y=False) # Only allow zoom in X-axis
+ self.plotItem.setMouseEnabled(x=True) # Only allow zoom in Y-axis
+
+ self.curve = self.plot(self.x, self.y, pen=(125, 249, 255)) # (0, 253, 235))
+ #self.curve2 = self.plot(self.x, self.y, pen=(255,255,0))
+
+ l=pg.LegendItem(offset=(0., 0.5))
+ l.setParentItem(self.graphicsItem())
+ l.setLabelTextColor((125, 249, 255))
+ #l.setOffset(-60)
+ l.addItem(self.curve, str(self.pv_gateway[0].pv_name))
+ '''
+ l2=self.addLegend()
+ l2.setLabelTextColor('g')
+ l2.setOffset(10)
+ l2.addItem(self.curve2, str(self.pv_gateway[0].pv_name))
+ '''
+ self.daq_stop()
+ print(self._bufsize)
+ print(len(self.x), len(self.y))
+
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ #self.pv_gateway.receive_monitor_update(value, status, alarm_severity)
+ _row = self.pv2item_dict[self.sender()]
+ #print("value===>", value, self.pv_gateway[_row].pv_name)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(
+ self.pv_gateway[_row].handle)
+
+ ts_now = _pvd.ts[0] + _pvd.ts[1] * 10**(-9)
+ ts_previous = self.pvd_previous.ts[0] + self.pvd_previous.ts[1] * 10**(-9)
+ ts_delta = ts_now - ts_previous
+
+ if (_pvd.ts[0] == self.pvd_previous.ts[0]) and (
+ _pvd.ts[1] == self.pvd_previous.ts[1]):
+ #_pvd.show()
+ return
+
+
+ #discard first callbacks
+ if ts_delta > 2.0:
+ self.pvd_previous = _pvd
+ return;
+
+ self.databuffer.append(value)
+ self.y[:] = self.databuffer
+ self.timebuffer.append(self.time_delta)
+ self.x[:] = self.timebuffer
+
+ #print(ts_delta, value, self.pvd_previous.value[0])
+ #if (ts_delta < self.ts_delta_max) and (value < self.pvd_previous.value[0]) :
+ if (value < self.pvd_previous.value[0]) :
+ self.data_series_buffer.append(value)
+ self.time_series_buffer.append(ts_now - self.time_zero ) #self.time_delta)
+ self.y_series[:] = self.data_series_buffer
+ self.x_series[:] = self.time_series_buffer
+ #print(self.x_series, self.y_series)
+ #elif ts_delta < 1.0:
+ if len(self.data_series_buffer) > 15:
+ #x_series = np.array(self.time_series, dtype=np.float)
+ #y_series = np.array(self.data_series, dtype=np.float)
+ _x=self.x_series.reshape((-1, 1))
+
+ model = LinearRegression()
+ model.fit(_x, self.y_series)
+ r_sq = model.score(_x, self.y_series)
+ ###JCprint('coefficient of determination:', r_sq, "slope", model.coef_ , "lifetime:", self.y_series[0]/model.coef_ / 3600)
+ #print('intercept:', model.intercept_)
+ #print('slope:', model.coef_)
+ #print('max value', y_series[0], y_series[1])
+ if r_sq > 0.995:
+ _I = self.y_series[0]
+ ###JCprint("lifetime:", _I/model.coef_ / 3600)
+
+
+ y_pred = model.predict(_x)
+ #print("len, y_pred, _x", len(y_pred), len(self.y_series), len(_x))
+ #print('predicted response:', y_pred, sep='\n')
+ m_sq_error = mean_squared_error(self.y_series, y_pred)
+ #print('Mean squared error: {0:.9f}'.format(
+ # mean_squared_error(y_series, y_pred)))
+ #print('Coefficient of determination: {0:.9f}'.format(
+ # r2_score(y_series, y_pred)))
+
+
+
+ self.trigger_series_sequence.emit(self.x_series, self.y_series)
+ #print("emit")
+ self.data_series = []
+ self.time_series = []
+ #print(len(self.x_series), len(self.y_series))
+ else:
+ self.data_series = []
+ self.time_series = []
+
+
+ self.pvd_previous = _pvd
+
+ #dt = (self.x[-1] - self.x[-2])
+ #print("dt", dt)
+ #Lowet IPCT before trigger is set to t=0
+ x_shifted= list(map(lambda m : (m - self.time_delta), self.x))
+
+ ##self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+ self.curve.setData(x_shifted, self.y)
+
+ self.time_delta = (
+ _pvd.ts[0] + _pvd.ts[1]*10**(-9)) - self.time_zero
+ #x_shifted2= list(map(lambda m : m -self.time_delta-1 , self.x))
+ #self.curve2.setData(x_shifted2, self.y)
+ #QApplication.processEvents()
+ #print(type(x_shifted), type(self.y), type([1.1]), type(1.1))
+
+ LOOK_BACK = -800
+ if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name:
+ LOOK_BACK = -250
+
+ if value > self.y[-2]:
+ if not self.found:
+ #print(x_shifted[-240:], self.y[-240:])
+ #self.y = np.where(self.y != self.y, 0, self.y) #test for nan
+ max_index = self.y[LOOK_BACK:].argmax()
+
+ if max_index == 0:
+ return
+ print("max index=", max_index)
+
+ #print(x_shifted[-600+max_index:], self.x[-600+max_index:])
+ #print(self.y[-600+max_index:-2])
+ self.found = True
+ #print("Are Signals blocked??", self.signalsBlocked())
+ self.trigger_decay_sequence.emit(np.array(
+ x_shifted[LOOK_BACK+max_index+9:-2]), self.y[LOOK_BACK+max_index+9:-2])
+ else:
+ self.found = False
+
+
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ print("pv_name==>", pv_name)
+
+ _row = self.pv2item_dict[self.sender()]
+ self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
+ post_display=False)
+
+ #self.pv_gateway.receive_connect_update(handle, pv_name, status)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(self.pv_gateway[_row].handle)
+ if self.time_zero == 0:
+ self.time_zero = _pvd.ts[0] + _pvd.ts[1]*10**(-9)
+ #print(self.time_zero)
+ self.pvd_previous = _pvd
+
+
+ #renove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event