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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

qOj+ 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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