diff --git a/__pycache__/pvgateway.cpython-37.pyc b/__pycache__/pvgateway.cpython-37.pyc new file mode 100644 index 0000000..af6ecc4 Binary files /dev/null and b/__pycache__/pvgateway.cpython-37.pyc differ diff --git a/__pycache__/pvwidgets.cpython-37.pyc b/__pycache__/pvwidgets.cpython-37.pyc new file mode 100644 index 0000000..f2890e7 Binary files /dev/null and b/__pycache__/pvwidgets.cpython-37.pyc differ diff --git a/pvgateway.py b/pvgateway.py index 688b49f..2d6d64d 100644 --- a/pvgateway.py +++ b/pvgateway.py @@ -1,242 +1,259 @@ -"""The module provides data and metadata of a process variable through PyCafe.""" +""" +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 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 (QEvent, QMutex, QPoint, QProcess, QSettings, Qt, QUrl, + Signal) 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, +from qtpy.QtGui import QCursor, QDesktopServices, QFont +from qtpy.QtWidgets import (QAction, QApplication, QComboBox, QDialog, + QHBoxLayout, QLabel, 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 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. + 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. + 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_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_connect = Signal(int, str, int) - trigger_daq = Signal(object, 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' - + READBACK_STATIC = 'static' + #Properties, dynamic - DISCONNECTED = 'disconnected' + 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' + DAQ_PAUSED = 'paused' - #ObjectName, defined by CAQ + #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", + + _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, + 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, + 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 "": + 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 = True if notify_unison and \ - self.notify_freq_hz > 0 else False + self.notify_unison = bool(notify_unison) and bool(self.notify_freq_hz) self.parent = parent - self.pv_name = pv_name - + 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_ALARM, self.READBACK_STATIC): - self.color_mode = color_mode - - self.color_mode_requested = self.color_mode + self.color_mode = color_mode - if monitor_callback is not None: + self.color_mode_requested = self.color_mode + + if monitor_callback is not None: self.monitor_callback = monitor_callback else: - self.monitor_callback = None + self.monitor_callback = None self.pv_within_daq_group = pv_within_daq_group - - self.show_units = show_units - self.prefix = prefix + + 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 + 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 = Rjson(self.parent.appname) - + #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.msg_release_value = None - self.monitor_id = 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.has_precision_user = bool(precision) self.precision_pv = 3 - self.precision = (self.precision_user if self.has_precision_user else + 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.pv_info = None self.record_type = None - if self.parent.showMessage is not None: - self.showMessage = self.parent.showMessage - else: - self.showMessage = 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.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.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.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.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.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(".") + #_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) + 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_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) @@ -246,7 +263,7 @@ class PVGateway(QWidget): 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) @@ -266,46 +283,37 @@ class PVGateway(QWidget): 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) + _font.setPixelSize(12) action5.setFont(_font) - if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"): + if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"): self.context_menu.addSection("") - #return action6 and 5 code here eventually + #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.setWindowModality(Qt.NonModal) 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.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 - 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( @@ -315,13 +323,11 @@ class PVGateway(QWidget): _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() - + + #wgts = self.cafe.getWidgets(self.handle) + self.trigger_connect.emit( - int(self.handle), str(self.pv_name), + int(self.handle), str(self.pv_name), int(_channel_info.cafeConnectionState)) #In case user is misinformed if not _handle_within_group_flag: @@ -330,61 +336,54 @@ class PVGateway(QWidget): 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, + #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)) - + 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.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) - + + 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: + + if self.pvd is None: self.pvd = self.cafe.getPVCache(self.handle) - - if self.pv_ctrl is None: + + 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") + 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)): @@ -392,85 +391,79 @@ class PVGateway(QWidget): _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 = " " - + 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 + _max_control_abs = 0 _enum_list = self.pv_ctrl.enumStrings - if len(_enum_list) > 0: + 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 + _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 + _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) + "." - - #print("precision", self.precision, self.pv_name) self.precision = min(9, self.precision) #safety net - for i in range (0, self.precision): + for i in range(0, self.precision): _suggested_text_from_value += "0" - - if len(_current_value) > len(_suggested_text_from_value): + + 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 + 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) - - + + 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) + _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) @@ -478,18 +471,17 @@ class PVGateway(QWidget): else: if qsettings.value(_var_text) is not None: self.suggested_text = qsettings.value(_var_text) - if qsettings.value(_ctrl_abs) is not None: + if qsettings.value(_ctrl_abs) is not None: self.max_control_abs_str = qsettings.value(_ctrl_abs) - qsettings.endGroup() qsettings.endGroup() - qsettings.endGroup() + qsettings.endGroup() def is_initialize_complete(self): - icount = 0; - while not self.initialize_complete : + icount = 0 + while not self.initialize_complete: time.sleep(0.01) self.initialize_meta_data() icount += 1 @@ -497,28 +489,26 @@ class PVGateway(QWidget): return False return True - def cleanup(self, close_pv = 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: + _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() - + self.widget.deleteLater() + def format_display_value(self, value): - + if value is None: - print(self, self.pv_name, ">>>>>>>>>>>>format_display_value is None>>>>>>") + print(self, self.pv_name, ">>>>format_display_value is None") #return if isinstance(value, str): @@ -526,75 +516,59 @@ class PVGateway(QWidget): 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) - + _value_form = ("{:< .%sf}" % self.precision) _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 "": + _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): + 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("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)) - + Checks for existence of widget. Waits up to a maximun of 100 ms. + ''' 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): + 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 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) @@ -602,55 +576,45 @@ class PVGateway(QWidget): 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) + 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) - #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 == "": + if not self.msg_label: _value = self.cafe.getCache(handle, dt='native') #Another reconnection in progress!!! - - if _value == None: + + if _value is 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.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: + + 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: + #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. @@ -663,319 +627,214 @@ class PVGateway(QWidget): 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,): + elif daq_state in (self.cyca.ICAFE_DAQ_PAUSED,): if _current_qt_dynamic_property != self.DAQ_PAUSED: - self.qt_property_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() - + 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") - + 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]) + _value = self.cafe.getStringFromEnum(self.handle, + daq_pvd.value[0]) else: - _value = daq_pvd.value[0] + _value = daq_pvd.value[0] if daq_pvd.status == self.cyca.ICAFE_NORMAL: - if self.msg_label == "": - self.post_display_value(_value) - + 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) + return #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 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: + 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() - + 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() - + 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() + 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() - #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) + self.mutex_post_display.lock() + _current_qt_dynamic_property = self.qt_dynamic_property_get() 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) + + 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) + 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" + 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.qt_property_alarm_sev_no_alarm() + + else: + if _current_qt_dynamic_property != self.DISCONNECTED: + self.qt_property_disconnect() + + self.mutex_post_display.unlock() - 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) - + pvd = pvdata if not hasattr(self, 'cafe'): - print ("py_monitor_callback: name/handle self cafe is NONE =>>>>>>>>>>>> ", - pv_name, handle) + 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 + if pvd.status == self.cyca.ICAFE_CS_NEVER_CONN: + print("initialize again") + self.initialize() - #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() + 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.''' - #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) + self.handle, dbr=self.cyca.CY_DBR_TIME) #start with gateway supplied monitor callback handler - elif self.monitor_callback is None: + 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, + self.handle, cb=self.py_monitor_callback, + dbr=self.cyca.CY_DBR_TIME, notify_milliseconds=self.notify_milliseconds) - else: + else: self.monitor_id = self.cafe.monitorStart( - self.handle, cb=self.monitor_callback, + 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_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) + + if self.monitor_id in _monID_all: 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 - - + #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) + self.desc = self.cafe.getDescription(self.handle) return - elif self.desc is not None: + elif self.desc is not None: return - else: - self.cafe.supplementHandle(self.handle) + 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: + + if self.desc is not None: return - + ###Back-up solution _found = str(self.pv_name).find(".") if _found != -1: @@ -1007,14 +866,12 @@ class PVGateway(QWidget): 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) + 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 = "" @@ -1023,7 +880,7 @@ class PVGateway(QWidget): #verify user input if self.show_units is True and self.suffix is not None: if self.suffix == self.units: - self.show_units = False + self.show_units = False def _qt_readback_color_mode(self): @@ -1031,281 +888,232 @@ class PVGateway(QWidget): 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): + 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)) - - + 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): - + 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]) + 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: 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: + 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: + + 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(): + 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: - #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''' + 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(): + for _property in self._qt_property_selected.keys(): if _property == property_state: self.setProperty(_property, True) - self._qt_property_selected[_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): + def qt_property_disconnect(self): '''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): + def qt_property_reconnect(self): '''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: + else: self.color_mode = self.NOT_ACT_ON_BEAM #self.setStatusTip("PV setting does not influence beam") - #self._qt_property_selected = + #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)) + 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_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)) + 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_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)) + 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); + event = QEvent(QEvent.StyleChange) + QApplication.sendEvent(self, event) self.update() self.updateGeometry() else: - #self.style().unpolish(self) self.style().polish(self) - QApplication.processEvents() + QApplication.processEvents() def pv_status_text_header(self, source="Channel Access"): _source = source _source_separator = "----------------------------------------" - _text = """ + _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 += """ + _text += """ On press, sends value: {0}
- """.format(self.msg_press_value, "DarkOrchid") + """.format(self.msg_press_value, "DarkOrchid") if self.msg_release_value is not None: - _text += """ + _text += """ On release, sends value: {0}
- """.format(self.msg_release_value, "DarkOrchid") - + """.format(self.msg_release_value, "DarkOrchid") + if self.pv_within_daq_group: - if self.qt_object_name in (self.PV_DAQ_BS,): + if self.qt_object_name in self.PV_DAQ_BS: _ds_color = "Navy Blue" else: _ds_color = "Black" else: - _ds_color = "Black" + _ds_color = "Black" - _text += """ + _text += """ {0}
Data source: {1}
{0}
- PV: {2} + PV: {2} """.format(_source_separator, _source, self.pv_name, "DarkOrchid", - _ds_color) + _ds_color) if self.desc is None: self.set_desc() if self.desc == "": - _text += """

- """ + _text += """

+ """ return _text _text += """ -
- Description: {6} +
+ Description: {0}

- """.format(self.widget_class, self.qt_object_name, \ - self.color_mode, _source_separator, _source, \ - self.pv_name, self.desc, "DarkOrchid" - ) + """.format(self.desc, "DarkOrchid") + return _text + def pv_status_text_enum(self): - + _val_enum = None _value = self.pvd.value[0] if isinstance(_value, str): @@ -1313,45 +1121,43 @@ class PVGateway(QWidget): elif _value is not None: _val_enum = self.cafe.getStringFromEnum(self.handle, _value) - _color = "Blue" + _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, + 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): + _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" + _color = "White" - _text = """ + _text = """

Value: {1} [{2}]
- """.format(_color, _value , _val_enum - ) + """.format(_color, _value, _val_enum) - return _text + 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_str = "[ " + for i in range(0, _end_range): _value = self.pvd.value[i] if _value is None: _value = '0' @@ -1360,58 +1166,57 @@ class PVGateway(QWidget): elif isinstance(_value, int): _value_str += str(_value) else: - if self.pv_ctrl is not None: - _value_form = ("{:<.%sf}" % self.pv_ctrl.precision) + 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: + 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) + _value_str += str(_value) else: if self.pv_ctrl is not None: - _value_form = ("{:<.%sf}" % self.pv_ctrl.precision) + _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" + _color = "Blue" - #To catch DAQ case + #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, + 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): + _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 = """ + _text = """

- Value: {1} {2}
+ Value: {1} {2}
""".format(_color, _value_str, self.units) return _text - + def pv_status_text_timestamp(self): _status_not_ok_color = "IndianRed" @@ -1419,66 +1224,62 @@ class PVGateway(QWidget): _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, + 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" + _ts_color = "White" _color = "White" - elif self.qt_object_name in (self.PV_DAQ_CA): + 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 + 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 =""" - """ + _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" + _color = "White" - if self.qt_object_name in (self.PV_DAQ_BS): - if self.qt_dynamic_property_get() in (self.DAQ_STOPPED, + 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, + _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" @@ -1491,17 +1292,16 @@ class PVGateway(QWidget): elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR: _color = "Red" elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID: - _color = "White" - - + _color = "White" + _text += """
- Alarm status: {1}
+ Alarm status: {1}
Alarm severity: {2} - """.format(_color, self.pvd.alarmStatusAsString, + """.format(_color, self.pvd.alarmStatusAsString, self.pvd.alarmSeverityAsString) - - return _text - + + return _text + def pv_access(self): _accessIs = "" if self.pv_info is None: @@ -1510,88 +1310,85 @@ class PVGateway(QWidget): _accessIs += "Read" if self.pv_info.accessWrite: _accessIs += "Write" - return _accessIs + return _accessIs def pv_status_text_enum_metadata(self): - _text = """

- ENUM strings: {2}

+ _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) + """.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.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) + _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}
+ _text += """ + Nelem: {1}
""".format("MediumBlue", self.pvd.nelem) - _text += """ - Precision (PV): {1}
+ _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(), + """.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, ): + + 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.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 =""" - """ - + _text = """ + """ + #No all record types have alarms - #className is not supported at psi since introduction of the linux ca gateway + #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 @@ -1600,42 +1397,44 @@ class PVGateway(QWidget): 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: + + if self.cafe.hasAlarmStatusSeverity(self.handle): _text = """

- Lower/Upper alarm limit:    {1}  /  {4}
- Lower/Upper warning limit: {2}  /  {3} + Lower/Upper alarm limit:    + {1}  /  {4}
+ Lower/Upper warning limit: + {2}  /  {3}

- """.format("MediumBlue", + """.format("MediumBlue", self.pvc.lowerAlarmLimit, self.pvc.lowerWarningLimit, - self.pvc.upperWarningLimit, self.pvc.upperAlarmLimit) + 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 \ + _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} + return _text + _text = """

+ Lower/Upper control limit: + {3}  /  {4}
+ Lower/Upper display limit: + {1}  /  {2}

- """.format("MediumBlue", + """.format("MediumBlue", self.pvc.lowerDisplayLimit, self.pvc.upperDisplayLimit, - self.pvc.lowerControlLimit, self.pvc.upperControlLimit) + 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)" @@ -1643,78 +1442,79 @@ class PVGateway(QWidget): 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: + 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: + 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" ) + """.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)) + """.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()) - 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 + #"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)): #, QUrl.TolerantMode) - if showMessage is not None: - self.showMessage(MsgSeverity.ERROR, __pymodule__, _line(), - "Failed to open URL {0}".format(urlIs)) + 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.settings.urlDatabuffer + urlIs = self.url_databuffer urlIs = urlIs + self.pv_name - - if not QDesktopServices.openUrl(QUrl(urlIs)): #, QUrl.TolerantMode) - if showMessage is not None: - self.showMessage(MsgSeverity.ERROR, __pymodule__, _line(), - "Failed to open URL {0}".format(urlIs)) + + 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]]]" + configStr = ("-config = [[[true,\"" + self.pv_name + + "\",\"Channel\",1,1]]]") commandStr = "/sf/op/bin/strip_chart" argStr = ["-nlaf", "-start", configStr, "&"] QProcess.startDetached(commandStr, argStr) @@ -1723,29 +1523,25 @@ class PVGateway(QWidget): 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 = display_wgt.geometry() # + _parentRect = self.context_menu.geometry() + _rect.moveTo(display_wgt.mapToGlobal( - QPoint(_parentRect.x() + _parentRect.width() - _rect.width(), - _parentRect.y()))) - + 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.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: + self.cyca.CY_DBR_FLOAT, + self.cyca.CY_DBR_DOUBLE) and precision_flag: #precision user _hbox_wgt = QWidget() _hbox = QHBoxLayout() @@ -1757,31 +1553,31 @@ class PVGateway(QWidget): _max = self.pv_ctrl.precision else: _max = 6 - self.precision_user_wgt.setMaximum(_max) + self.precision_user_wgt.setMaximum(_max) self.precision_user_wgt.valueChanged.connect( - self.precision_user_changed) + 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) + 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) - + 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) + precision_ioc.setFixedWidth(20) _hbox2_wgt.setFixedWidth(145) layout.addWidget(_hbox_wgt) @@ -1790,89 +1586,76 @@ class PVGateway(QWidget): #precision refresh rate _hbox3_wgt = QWidget() _hbox3 = QHBoxLayout() - refresh_freq_label = QLabel("Refresh rate: ") + 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} + 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])) + self.refresh_freq_combox_idx_dict[1])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[2])) + self.refresh_freq_combox_idx_dict[2])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[3])) + self.refresh_freq_combox_idx_dict[3])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[4])) + self.refresh_freq_combox_idx_dict[4])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[5])) - + 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: + 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) + 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.setMinimumWidth(340) + display_wgt.setLayout(layout) + display_wgt.exec() QApplication.processEvents() def precision_ioc_reset(self): - if self.pv_ctrl is not None: + 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 + self.precision = new_value + + _pvd = self.cafe.getPVCache(self.handle) - _pvd = self.cafe.getPVCache(self.handle) - if _pvd.value[0] is not None: - if isinstance(_pvd.value[0], float): + 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) - ''' + _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] + _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 @@ -1881,28 +1664,27 @@ class PVGateway(QWidget): self.notify_unison = False self.monitor_stop() self.monitor_start() - + else: - self.cafe.updateMonitorPolicyDeltaMS(self.handle, - self.monitor_id, - self.notify_milliseconds) + 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.. + #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()) + + 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() + + def mouseReleaseEvent(self, event): + event.ignore() diff --git a/pvgateway.py- b/pvgateway.py- new file mode 100644 index 0000000..2bd8d74 --- /dev/null +++ b/pvgateway.py- @@ -0,0 +1,1924 @@ +"""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 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 pyqtacc.bdbase.readjson import ReadJSON +#from pyqtacc.bdbase.enumkind import DAQState + +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(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.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 = 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 'showMessage' in dir(self.parent): + 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 'deg' in _value_str: + # print(_value_str, len(_value_str)) + + 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 22993_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.url_archiver + 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.url_databuffer + 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/pvgateway.py-- b/pvgateway.py-- new file mode 100644 index 0000000..2d6d64d --- /dev/null +++ b/pvgateway.py-- @@ -0,0 +1,1690 @@ +""" +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() + diff --git a/pvwidgets.py b/pvwidgets.py index 29f50a9..3e05f6d 100644 --- a/pvwidgets.py +++ b/pvwidgets.py @@ -2,50 +2,83 @@ __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.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, QCheckBox, QComboBox, QDialog, - QDockWidget, QDoubleSpinBox, QFrame, QGroupBox, - QHeaderView, QHBoxLayout, QLabel, QLineEdit, + 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) - + 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) @@ -53,15 +86,14 @@ class CAQLineEdit(QLineEdit, PVGateway): 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, + 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) + notify_freq_hz: int = 0, precision: int = 0): - super().__init__(parent, pv_name, monitor_callback, + 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 ) + notify_freq_hz=notify_freq_hz, precision=precision) self.is_initialize_complete() self.configure_widget() @@ -69,20 +101,11 @@ class CAQLineEdit(QLineEdit, PVGateway): 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) @@ -97,20 +120,20 @@ class CAQLineEdit(QLineEdit, PVGateway): 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)) + + 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) + self.qt_property_initial_values(qt_object_name=self.PV_DAQ_CA) else: - self.qt_property_initial_values(qt_object_name = self.PV_READBACK) + self.qt_property_initial_values(qt_object_name=self.PV_READBACK) #renove highlighting which persists after mouse leaves def mouseMoveEvent(self, event): @@ -123,42 +146,42 @@ class CAQLineEdit(QLineEdit, PVGateway): class CAQLabel(QLabel, PVGateway): '''Channel access enabled QLabel widget''' - trigger_monitor_float = Signal(float, int, int) + 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_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() - + + 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. - ''' + '''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) + + @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) @@ -166,38 +189,27 @@ class CAQLabel(QLabel, PVGateway): @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) + fm = QFontMetricsF(QFont("Sans Serif", 10)) + qrect = fm.boundingRect(self.suggested_text) _width_scaling_factor = 1.15 - self.setFixedHeight((fm.lineSpacing()*1.8)) + 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) + self.qt_property_initial_values(qt_object_name=self.PV_DAQ_CA) else: - self.qt_property_initial_values(qt_object_name = self.PV_READBACK) + self.qt_property_initial_values(qt_object_name=self.PV_READBACK) #For use with CAQMenu class QLineEditExtended(QLineEdit): @@ -206,418 +218,386 @@ class QLineEditExtended(QLineEdit): self.parent = parent def mousePressEvent(self, event): - button = event.button() - if button == Qt.RightButton: + button = event.button() + if button == Qt.RightButton: self.parent.showContextMenu() - elif button == Qt.LeftButton: + 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_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, + 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.is_initialize_complete() + self.configure_widget() - #After 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. - ''' + '''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.setItemData(i, Qt.AlignCenter, Qt.TextAlignmentRole) + + fm = QFontMetricsF(QFont("Sans Serif", 10)) + qrect = fm.boundingRect(self.suggested_text) - ''' - 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.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, + self.setCurrentIndex(self.cafe.getEnumFromString(self.handle, value)) - - elif isinstance(value, int): + + elif isinstance(value, int): self.setCurrentIndex(value) #Should not happen - elif isinstance(value, float): - self.setCurrentIndex(int(value)) + elif isinstance(value, float): + self.setCurrentIndex(int(value)) if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"): self.blockSignals(False) - - #self.previousIndex = self.currentIndex() - return + + #self.previousIndex = self.currentIndex() + return else: print(("ERROR: overloaded post_display_value: 'setCurrentIndex' " - "method does not exist!")) + "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() + "CAQMenu set operation reports error:\n{0}".format( + self.cafe.getStatusCodeAsString(status))) + self.pv_message_in_a_box.exec() + def mousePressEvent(self, event): - def mousePressEvent(self, event): - - button = event.button() - if button == Qt.RightButton: + button = event.button() + if button == Qt.RightButton: PVGateway.mousePressEvent(self, event) elif self.pv_info is not None: - if self.pv_info.accessWrite == 0: + if self.pv_info.accessWrite == 0: event.ignore() return - else: - QComboBox.mousePressEvent(self, event) - + 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: + 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: + 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}") - + 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: + #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) - + 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_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_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, + 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, + 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, + 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) - + 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.suggested_text = self.suggested_text.rjust(_suggested_text_length, + "^") + + self.configure_widget() - 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. - ''' + '''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) - + 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.setFixedHeight((fm.lineSpacing()*2.0)) self.setFixedWidth((qrect.width() * _width_scaling_factor)) - - self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER) + + 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"): + 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) - ''' + self.qt_style_polish() 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): + 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: + 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_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_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.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_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_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.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.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_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_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) - + 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.currentText = "" self.returnPressed.connect(self.valuechange) - self.configure_widget() + 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. - ''' + '''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): + + def configure_widget(self): self.setFocusPolicy(Qt.StrongFocus) - - fm = QFontMetricsF(QFont("Sans Serif", 12)) - qrect = fm.boundingRect(self.suggested_text) - + + fm = QFontMetricsF(QFont("Sans Serif", 12)) + qrect = fm.boundingRect(self.suggested_text) + _width_scaling_factor = 1.15 - self.setFixedHeight((fm.lineSpacing()*1.8)) + 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) + + 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 + strText = valStr.format(round(val, self.precision)) print(strText, " precision ", self.precision) self.setText(strText) else: @@ -626,15 +606,8 @@ class CAQTextEntry(QLineEdit, PVGateway): 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: @@ -643,328 +616,73 @@ class CAQTextEntry(QLineEdit, PVGateway): 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.qt_style_polish() if self.text() != self.currentText: - QLineEdit.setText(self, self.currentText) - + QLineEdit.setText(self, self.currentText) + self.setCursorPosition(100) self.clearFocus() self.setFocusPolicy(Qt.NoFocus) - del event + del event - #def mouseMoveEvent(self, event): - # print("mouseMoveEvent", event.type()) - - def mousePressEvent(self, event): + def mousePressEvent(self, event): if event.button() == Qt.RightButton: - PVGateway.mousePressEvent(self, event) + 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_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() + 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. - ''' - #print(" py_connect_callback::::::::::::::::::::::::::::::::::::::::::::::::::::::::::..: START ") + ''' + Callback function to be invoked on change of pv connection + status. + ''' 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 @@ -972,132 +690,106 @@ class CAQDoubleSpinBox(QDoubleSpinBox, PVGateway): 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().setEnabled(True) 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) + self.lineEdit().setAlignment(Qt.AlignLeft) + self.lineEdit().setFont(QFont("Sans Serif", 16)) fm = QFontMetricsF(QFont("Sans Serif", 12)) - _suggested_text = self.suggested_text + _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.15 - - self.setFixedHeight((fm.lineSpacing()*1.8)) + _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) + 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), + 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) + '''Convert value to index''' if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"): self.blockSignals(True) - - self.setValue(value) + self.setValue(int(round(value))) self.blockSignals(False) else: - self.setValue(value) + 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, + 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) + 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, " 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()) + def setValue(self, intVal): + QSpinBox.setValue(self, intVal) 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) - + def value_change(self, intVal): + 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, 'float') - + _value = self.cafe.getCache(self.handle, 'int') + if _value is not None: - self.setValue(_value) - + 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)))) @@ -1107,19 +799,238 @@ class CAQDoubleSpinBox(QDoubleSpinBox, PVGateway): if self.previousValue is not None: self.setValue(self.previousValue) else: - _value = self.cafe.getCache(self.handle, 'float') - + _value = self.cafe.getCache(self.handle, 'int') + if _value is not None: - self.setValue(_value) - - self.parent.statusbar.showMessage( - (self.widget_class + " " + + 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) - #print("eventtype", event.type()) if self.pv_info is not None: if self.pv_info.accessWrite == 0: self.setProperty("readOnly", True) @@ -1127,44 +1038,35 @@ class CAQDoubleSpinBox(QDoubleSpinBox, PVGateway): 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.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): + + 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): + elif event.key() in (Qt.Key_Up, Qt.Key_Down): QDoubleSpinBox.keyPressEvent(self, event) - else: + 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: + # 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: @@ -1176,88 +1078,60 @@ class reconnectQPushButton(QPushButton, QThread): super().__init__() self.parent = parent self.clicked.connect(self.onClicked) - self.isdirty = False - self._handles_to_reconnect = [] + 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 = [] + + 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) + 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() - #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 + 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) + 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 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_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_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 = 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]: + 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: @@ -1265,44 +1139,86 @@ class CAQTableWidget(QTableWidget): 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 + _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 + 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 + + #if timestamps the same - then skip _value = _pvd.value[0] - _value = pvgate.format_display_value(_value) + _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 | + 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 @@ -1313,19 +1229,18 @@ class CAQTableWidget(QTableWidget): _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) - + _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 : + 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 + _ilength_target = _ilength_target -1 - qtwi = QTableWidgetItem( _ts_str) + qtwi = QTableWidgetItem(_ts_str) f = qtwi.font() f.setPointSize(8) qtwi.setFont(f) @@ -1333,194 +1248,187 @@ class CAQTableWidget(QTableWidget): 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() - + _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 + _bgcolor = pvgate.fg_alarm_major _fgcolor = "black" elif alarm_severity == pvgate.cyca.SEV_MINOR: - _bgcolor = pvgate.settings.fgAlarmMinor + _bgcolor = pvgate.fg_alarm_minor _fgcolor = "black" elif alarm_severity == pvgate.cyca.SEV_INVALID: - _bgcolor = pvgate.settings.fgAlarmInvalid + _bgcolor = pvgate.fg_alarm_invalid _fgcolor = "#777777" else: - _bgcolor = pvgate.settings.fgAlarmNoAlarm - #_bgcolor = pvgate.settings.bgReadbackAlarm + _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)) + QColor(_bgcolor)) self.item(_row, self.no_columns-2).setBackground( - QColor(_bgcolor)) + QColor(_bgcolor)) self.item(_row, self.no_columns-3).setForeground( - QColor(_fgcolor)) + QColor(_fgcolor)) self.item(_row, self.no_columns-2).setForeground( - QColor(_fgcolor)) + QColor(_fgcolor)) elif _prop == pvgate.READBACK_STATIC: - + self.item(_row, self.no_columns-3).setBackground( - QColor(pvgate.settings.bgReadback)) + QColor(pvgate.bg_readback)) self.item(_row, self.no_columns-2).setBackground( - QColor(pvgate.settings.bgReadback)) - + 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")) + 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() + 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 = "", + 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, + 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 + _column_dict_value += 1 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 + _column_dict_value += 1 self.columns_dict['Timestamp'] = _column_dict_value - _column_dict_value += 1 + _column_dict_value += 1 self.columns_dict['Reconnect'] = _column_dict_value - self.setWindowModality(Qt.ApplicationModal) + self.setWindowModality(Qt.ApplicationModal) self.no_columns = _column_dict_value + 1 - - self.init_column = init_column - + + 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 = 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.notify_unison = bool(notify_unison) and bool(self.notify_freq_hz) - self.precision = precision + 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_micro = 28 self.format_ts_milli = 25 - self.format_ts_deci = 23 #-8 + 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 + self.format_ts_decimal_part = self.format_ts_milli elif "deci" in ts_res.lower(): - self.format_ts_decimal_part = self.format_ts_deci + self.format_ts_decimal_part = self.format_ts_deci elif "sec" in ts_res.lower(): - self.format_ts_decimal_part = self.format_ts_sec + 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_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 + 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(color_mode)): + _color_mode[i] = color_mode[i] - for i in range (0, len(self.pv_list)): + for i in range(0, len(self.pv_list)): - self.pv_gateway[i] = PVGateway().__init__( - parent, self.pv_list[i], monitor_callback, + 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].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.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].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 - - + 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 = QTimer() self.timer.timeout.connect(self.widget_update) self.timer.singleShot(0, self.widget_update) self.timer.start(self.notify_milliseconds) - - self.configure_widget() - + + self.configure_widget() + #Connect only deals with colours - only helps on reconnect - # In any case monitors take over + # In any case monitors take over #Got to do this earlier or emit immediately after!! - for i in range(0, len(self.pv_gateway)): + 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) - - + 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() @@ -1528,70 +1436,78 @@ class CAQTableWidget(QTableWidget): 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"): + + 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"): + if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"): self.table_context_menu.addSection("---") - + QApplication.processEvents() - def restore_init_values(self): + def restore_init_values(self, pv_list: list = []): _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) + 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, + 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:") + _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) + " " + + str(status_value) + " " + self.cafe.getStatusCodeAsString(status_value) + - " " + self.cafe.getStatusInfo(status_value) ) + " " + self.cafe.getStatusInfo(status_value)) qm = QMessageBox() qm.setText(_mess) - + qm.exec() QApplication.processEvents() - - self.init_value_button.setEnabled(True) - + + 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']) + _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 + + 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): + def get_column_values(self, column_no): _values_dict = {} - _start = 0 + _start = 0 _end = len(self.pv_gateway) _pvs = [None] * _end _values_str = [None] * _end @@ -1600,86 +1516,83 @@ class CAQTableWidget(QTableWidget): 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])] - + r'-?\d+\.?\d*', _values_str[_row])] + if not _value_list: - print("row", _row, "values", _values_str[_row], _pvs[_row]) + 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 - - + + 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 + _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() + _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])] - + r'-?\d+\.?\d*', _values_to_set_str[_row])] + if not _value_list: - print("//row", _row, "values", _values_to_set_str[_row], + 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] = _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[_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 + _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) + _start = 0 + _end = len(self.pv_gateway) + for _row in range(_start, _end): - _handle = self.pv_gateway[_row].handle + _handle = self.pv_gateway[_row].handle _value = self.pv_gateway[_row].cafe.getCache(_handle) - + if _value is not None: - if self.scale_factor != 1: + if self.scale_factor != 1: _value = _value * self.scale_factor - _value = self.pv_gateway[_row].format_display_value(_value) - #print("value from update", _value) + _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) - - + 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 - + _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) @@ -1687,44 +1600,46 @@ class CAQTableWidget(QTableWidget): 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) + 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) + qtwt.setFont(f) + self.setItem(i, _pv_column, qtwt) - self.item(i, _pv_column).setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter) + 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.AlignCenter | - Qt.AlignVCenter) + 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.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) - + 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() + _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) @@ -1732,21 +1647,19 @@ class CAQTableWidget(QTableWidget): 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.")) + ("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 + _init_layout.setContentsMargins(1, 1, 0, 0) #Required self.init_widget.setLayout(_init_layout) - self.setCellWidget(len(self.pv_gateway), 1, self.init_widget) - + 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) + _restore_layout = QHBoxLayout(_restore_widget) + self.restore_value_button = QPushButton() self.restore_value_button.setStyleSheet( - "QPushButton{background-color: rgb(212, 219, 157);}") + "QPushButton{background-color: rgb(212, 219, 157);}") self.restore_value_button.setText("Restore") _f = self.restore_value_button.font() _f.setPointSize(8) @@ -1757,20 +1670,19 @@ class CAQTableWidget(QTableWidget): ("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_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), 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.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(): @@ -1780,109 +1692,90 @@ class CAQTableWidget(QTableWidget): f.setPointSize(6) self.reconnect_button.setFixedWidth(58) - self.reconnect_button.setFont(f) - + 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) + _layout.setContentsMargins(0, 0, 0, 0) #Required #_reconnect_button - self.setCellWidget(len(self.pv_gateway), self.no_columns-2, _qwb) - - _qwc = QWidget() + self.setCellWidget(len(self.pv_gateway), self.no_columns-2, _qwb) - self.cb_item_all = QCheckBox(self) - self.cb_item_all.setCheckState(Qt.Unchecked) + 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") - _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) - + self.setCellWidget(len(self.pv_gateway), self.no_columns-1, + self.cb_item_all) 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'], + self.setHorizontalHeaderItem(self.columns_dict['Init'], QTableWidgetItem("Initial Value")) - - self.setHorizontalHeaderItem(self.columns_dict['Value'], + + self.setHorizontalHeaderItem(self.columns_dict['Value'], QTableWidgetItem("Value")) - - if 'Timestamp' in self.columns_dict.keys(): - self.setHorizontalHeaderItem(self.columns_dict['Timestamp'], + + if 'Timestamp' in self.columns_dict.keys(): + self.setHorizontalHeaderItem(self.columns_dict['Timestamp'], QTableWidgetItem("Timestamp")) - self.setHorizontalHeaderItem(self.columns_dict['Reconnect'], + 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 ) + self.verticalHeader().setFixedWidth(22) _fm_font = QFont("Sans Serif") _fm_font.setPointSize(12) - fm = QFontMetricsF(_fm_font) #QFont("Sans Serif", 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))) + 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) - + 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")) + 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")) - + 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) + 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) + 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) @@ -1890,79 +1783,56 @@ class CAQTableWidget(QTableWidget): 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: + 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) + " ") + _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) + 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: + while _ts_str_len < _ilength_target: _ts_date += "0" - _ilength_target = _ilength_target -1 + _ilength_target = _ilength_target -1 - ts_str_len = len(_ts_date) - _ts_str = _ts_date[0:_ts_str_len-( + ##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 + _ts_str_len = len(_ts_str) - qtwi = QTableWidgetItem( _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) + qtwi.setFont(f) self.setItem(_row, self.columns_dict['Timestamp'], qtwi) - self.item(_row, self.columns_dict['Timestamp']).setTextAlignment(Qt.AlignCenter) + self.item(_row, self.columns_dict['Timestamp']).setTextAlignment( + Qt.AlignCenter) + + _prop = self.pv_gateway[_row].qt_dynamic_property_get() - _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" @@ -1972,19 +1842,22 @@ class CAQTableWidget(QTableWidget): elif alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID: _bgcolor = self.pv_gateway[_row].settings.fgAlarmInvalid _fgcolor = "#777777" - else: + 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)) + 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)) - - + 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( @@ -1996,227 +1869,137 @@ class CAQTableWidget(QTableWidget): self.item(_row, self.columns_dict['Timestamp']).setBackground( QColor("#ffffff")) self.item(_row, self.columns_dict['Timestamp']).setForeground( - QColor("#777777")) + 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)) + 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].settings.bgReadback)) + 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) - #TRZ SET PROPERTY - QApplication.processEvents(QEventLoop.AllEvents, 10) - #self.post_display_value(value) - - + 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()] - - #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, + + 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) + + #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")) + 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() + self.item(_row, self.columns_dict['Timestamp']).setBackground( + QColor("#ffffff")) + self.item(_row, self.columns_dict['Timestamp']).setForeground( + QColor("#777777")) - ''' - 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) + QApplication.processEvents() - _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, + 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) - + 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): + if isinstance(_pvd.value[0], float): pvgate.trigger_monitor_float.emit( - _pvd.value[0], _pvd.status, _pvd.alarmSeverity) + _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_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: + + 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() + for pvgate in self.pv_gateway: + pvgate.monitor_start() - else: + 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: - + pvgate.monitor_start() + else: + self.cafe.updateMonitorPolicyDeltaMS( - pvgate.handle, pvgate.monitor_id, + 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() + + if self.timer is not None: + self.timer.stop() else: - self.timer = QTimer() + 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) - + + 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()): + + for i, ts_res in enumerate(self.ts_combox_idx_dict.values()): if i == new_idx: self.format_ts_decimal_part = ts_res - break; + break - for pvgate in self.pv_gateway: - _pvd = self.cafe.getPVCache(pvgate.handle) + 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) + _pvd.value[0], _pvd.status, _pvd.alarmSeverity) elif isinstance(_pvd.value[0], int): - pvgate.trigger_monitor_int.emit( + 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) + str(_pvd.value[0]), _pvd.status, _pvd.alarmSeverity) def display_table_parameters(self): @@ -2233,11 +2016,11 @@ class CAQTableWidget(QTableWidget): 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.pv_ctrl.precision) + self.initial_value = max(self.initial_value, pvgate.precision) - - if self.max_precision_value > 0: + + if self.max_precision_value > 0: #precision user _hbox_wgt = QWidget() _hbox = QHBoxLayout() @@ -2245,9 +2028,9 @@ class CAQTableWidget(QTableWidget): 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.setMaximum(self.max_precision_value) self.table_precision_user_wgt.valueChanged.connect( - self.table_precision_user_changed) + self.table_precision_user_changed) precision_user_label.setAlignment(Qt.AlignLeft) self.table_precision_user_wgt.setAlignment(Qt.AlignLeft) _hbox.addWidget(precision_user_label) @@ -2256,18 +2039,17 @@ class CAQTableWidget(QTableWidget): _hbox_wgt.setLayout(_hbox) precision_user_label.setFixedWidth(common_label_width) - self.table_precision_user_wgt.setFixedWidth(40) + 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 = 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) @@ -2276,40 +2058,38 @@ class CAQTableWidget(QTableWidget): _hbox2_wgt.setLayout(_hbox2) precision_ioc_label.setFixedWidth(common_label_width) - precision_ioc.setFixedWidth(50) + precision_ioc.setFixedWidth(50) + + _hbox2_wgt.setFixedWidth(common_hbox_width) - _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} + 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()): + for key, ts_res in self.ts_combox_idx_dict.items(): ts_resolution.addItem(key) + _current_idx = 0 - _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.setCurrentIndex(_current_idx) ts_resolution.currentIndexChanged.connect( self.table_ts_resolution_changed) @@ -2318,98 +2098,89 @@ class CAQTableWidget(QTableWidget): _hbox4_wgt.setLayout(_hbox4) ts_label.setFixedWidth(common_label_width) - ts_resolution.setFixedWidth(common_wgt_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: ") + 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} + 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])) + self.refresh_freq_combox_idx_dict[1])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[2])) + self.refresh_freq_combox_idx_dict[2])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[3])) + self.refresh_freq_combox_idx_dict[3])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[4])) + self.refresh_freq_combox_idx_dict[4])) refresh_freq.addItem('{0} Hz'.format( - self.refresh_freq_combox_idx_dict[5])) - + 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: + 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) + 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.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: + + 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): - #event.ignore() pass def leaveEvent(self, event): self.clearSelection() - self.clearFocus() + self.clearFocus() del event - - + class QMessageWidget(QListWidget): """Log message window.""" @@ -2418,18 +2189,16 @@ class QMessageWidget(QListWidget): 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: + if item: self.myItem = item self.setCurrentItem(self.myItem) @@ -2439,19 +2208,14 @@ class QMessageWidget(QListWidget): 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 @@ -2468,22 +2232,22 @@ class QResultsWidget: 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) - + 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) + + 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 @@ -2492,16 +2256,16 @@ class QResultsWidget: qlabel = QLabel(label) qle = QLineEdit(text) qlabel.setFont(_font) - qlabel.setStyleSheet(("QLabel{color:black;" + - "margin:0px; padding:2px;}")) + 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;" + + 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) @@ -2511,25 +2275,25 @@ class QResultsWidget: _hbox = QHBoxLayout() _hbox.addWidget(qlabel) _hbox.addWidget(qle) - _hbox_widget.setLayout(_hbox) + _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 = 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)) @@ -2538,214 +2302,157 @@ class QResultsWidget: item1.setForeground(QColor("black")) item2.setForeground(QColor("black")) if i%2 == 0: - item1.setBackground(QColor("lightgray")) - item2.setBackground(QColor("lightgray")) + 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) + longest_str_item2 = str(text) if i == 0: #item1.setFont(_font) #item2.setFont(_font) table.setHorizontalHeaderItem(0, item1) table.setHorizontalHeaderItem(1, item2) - else: + else: table.setItem(i-1, 0, item1) - table.setItem(i-1, 1, item2) + table.setItem(i-1, 1, item2) + fm = QFontMetricsF(_font) - 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 ) - + + 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) + + 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) - + + _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.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 = 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) - + 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): + + 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.application_geometry + 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()) + 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): - #Need MUTEX - print("is_floating", is_floating) - #self.setVisible(False) - #self.setFloating(True) - #ResetGeometry - #self.parent.setGeometry(self.geometry_from_qsettings) - #QApplication.processEvents() - + pass + + class QNoDockWidget(QDockWidget): - - def __init__(self, title=None, parent=None): + + 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.geometry_from_qsettings = self.parent.application_geometry + self.topLevelChanged.connect(self._top_level_changed) + self.setVisible(False) self.setFloating(True) - #ResetGeometry + + 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): + + 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) @@ -2753,33 +2460,32 @@ class CAQStripChart(PlotWidget): self.found = False self.time_zero = [0] * self.no_channels self.time_delta = [0] * self.no_channels - self.pv_list = pv_list + 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, + 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) + 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.receive_monitor_update) self.pv_gateway[i].trigger_monitor_int.connect( self.receive_monitor_update) self.pv_gateway[i].trigger_monitor_float.connect( @@ -2787,40 +2493,38 @@ class CAQStripChart(PlotWidget): self.pv_gateway[i].trigger_monitor.connect( self.receive_monitor_dbr_time) - self.pv_gateway[i].widget_class = "PlotWidget" - + 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)): + 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 + + 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 = [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) @@ -2828,9 +2532,9 @@ class CAQStripChart(PlotWidget): self.x[i] = np.zeros(bsize, dtype=np.float) self.y[i] = np.zeros(bsize, dtype=np.float) - _long_size=20 + ##_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.time_series_buffer = collections.deque([0]*_long_size, _long_size) #self.data_series = [] * self.no_channels #self.time_series = [] * self.no_channels @@ -2841,104 +2545,98 @@ class CAQStripChart(PlotWidget): #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.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.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) ] - + 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]) # (0, 253, 235)) - #self.curve[1] = self.plot(self.x[1], self.y[1], pen=(255,255,0)) - - l=pg.LegendItem(offset=(0., 0.5), colCount=1) + 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)) - #l.setOffset(-60) + for curv, pv in zip(self.curve, self.pv_gateway): - l.addItem(curv, pv.pv_name) - - #self.daq_stop() - #print(self._bufsize) - #print(len(self.x), len(self.y)) + l.addItem(curv, pv.pv_name) + QApplication.processEvents() - - @Slot(object, int) + + @Slot(object, int) def receive_monitor_dbr_time(self, pvdata, alarm_severity): + + #Check on 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 - + 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] + + value = pvdata.value[0] #discard first callbacks #if ts_delta > 2.0: - # self.pvd_previous_list[_row] = _pvd + # 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[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)): + 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 + 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.x_shifted[_row] = list( + map(lambda m: (m - self.time_delta[_row]), self.x[_row][idx:])) - #print("idx", self.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() - + 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()] - #if _row == 1: - # return - print("row, value===>", _row, value, self.pv_gateway[_row].pv_name) + + #print("row, value===>", _row, value, self.pv_gateway[_row].pv_name) _pvd = self.pv_gateway[_row].cafe.getPVCache( self.pv_gateway[_row].handle) @@ -2946,106 +2644,109 @@ class CAQStripChart(PlotWidget): _pvd2 = self.pv_gateway[_row].pvd - print ("val", value, _pvd2.value[0], _pvd.value[0], self.pvd_previous_list[_row].value[0]) + 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 - + 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 + # 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[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)): + 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 + 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 - ''' - 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 (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.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: + 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) + ###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("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))) + # r2_score(y_series, y_pred))) - - self.trigger_series_sequence.emit(self.x_series, self.y_series) + + self.trigger_series_sequence.emit(self.x_series, + self.y_series) #print("emit") self.data_series = [] self.time_series = [] @@ -3053,73 +2754,75 @@ class CAQStripChart(PlotWidget): else: self.data_series = [] self.time_series = [] - - ''' - - #dt = (self.x[-1] - self.x[-2]) + ''' + + + #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.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] - - + _pvd.ts[0] + _pvd.ts[1]*10**(-9)) - self.time_zero[0] + + ''' - LOOK_BACK = -800 + LOOK_BACK = -800 if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name: LOOK_BACK = -250 - if value > self.y[-2]: + 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]) + + #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]) + 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, + 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) + _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): @@ -3129,69 +2832,67 @@ class CAQStripChart(PlotWidget): class CAQPCTChart(PlotWidget): '''Channel access enabled pyqtgraph.PlotWidget''' - #trigger_monitor_float = Signal(float, int, int) + #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_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)) + # 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 = "", + 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.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, + 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 ) + 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.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.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)): + 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), @@ -3200,21 +2901,21 @@ class CAQPCTChart(PlotWidget): 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 + + 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 + + _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.time_series_buffer = collections.deque([0]*_long_size, _long_size) self.data_series = [] self.time_series = [] @@ -3224,45 +2925,45 @@ class CAQPCTChart(PlotWidget): #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.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.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.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 = 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)) + + l.addItem(self.curve, str(self.pv_gateway[0].pv_name)) ''' - l2=self.addLegend() + l2=self.addLegend() l2.setLabelTextColor('g') l2.setOffset(10) l2.addItem(self.curve2, str(self.pv_gateway[0].pv_name)) - ''' - self.daq_stop() + ''' + 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) @@ -3270,19 +2971,18 @@ class CAQPCTChart(PlotWidget): 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 - + 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.pvd_previous = _pvd + return self.databuffer.append(value) self.y[:] = self.databuffer @@ -3290,43 +2990,48 @@ class CAQPCTChart(PlotWidget): 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]) : + #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.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: + 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)) - + _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) + ###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] + #_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)) + + ####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) + ##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))) + # r2_score(y_series, y_pred))) - - self.trigger_series_sequence.emit(self.x_series, self.y_series) + self.trigger_series_sequence.emit(self.x_series, + self.y_series) #print("emit") self.data_series = [] self.time_series = [] @@ -3334,62 +3039,64 @@ class CAQPCTChart(PlotWidget): else: self.data_series = [] self.time_series = [] - - + + self.pvd_previous = _pvd - #dt = (self.x[-1] - self.x[-2]) + #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)) + 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 + _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 + + LOOK_BACK = -800 if 'ARIDI-PCT2:CURRENT' in self.pv_gateway[_row].pv_name: LOOK_BACK = -250 - if value > self.y[-2]: + 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]) + + #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]) + 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, + 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) + _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) @@ -3398,7 +3105,6 @@ class CAQPCTChart(PlotWidget): #renove highlighting which persists after mouse leaves def mouseMoveEvent(self, event): - #event.ignore() pass def leaveEvent(self, event): diff --git a/pvwidgets.py- b/pvwidgets.py- new file mode 100644 index 0000000..f44f251 --- /dev/null +++ b/pvwidgets.py- @@ -0,0 +1,3508 @@ +''' 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 (QBrush, 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, + 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 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) + + 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(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, precision=precision) + + 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", 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(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, 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): + + #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", 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)) + #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) + + #Uncheck global reconnect check box + self.parent.cb_item_all.setCheckState(Qt.Unchecked) + + +''' +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 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) + + #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.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 = 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, pv_list:list = []): + _set_values_dict = self.get_init_values() + #print("pvwidgets.py set val dict", _set_values_dict) + #print("same as init vals", self.is_same_as_init_values()) + + + if not pv_list: + _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) + else: + _pvs_to_set = pv_list + _values_to_set = [] + for pv in pv_list: + _values_to_set.append(_set_values_dict[pv]) + + #print(_pvs_to_set, _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 = 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)): + #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.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.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) + + + 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") + + + ''' + _qwc = QWidget() + + _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, self.cb_item_all) + + + 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].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) + #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.application_geometry + 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()) + self.parent.setGeometry(self.geometry_from_qsettings) + 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.application_geometry #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 __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): + 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().__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', '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]) # (0, 253, 235)) + #self.curve[1] = self.plot(self.x[1], self.y[1], pen=(255,255,0)) + + l=pg.LegendItem(offset=(0., 0.5), colCount=1) + l.setParentItem(self.graphicsItem()) + + l.setLabelTextColor((255, 255, 255)) + #l.setOffset(-60) + for curv, pv in zip(self.curve, self.pv_gateway): + l.addItem(curv, pv.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) + + 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().__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 diff --git a/pvwidgets.py-- b/pvwidgets.py-- new file mode 100644 index 0000000..3e05f6d --- /dev/null +++ b/pvwidgets.py-- @@ -0,0 +1,3112 @@ +''' 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