commit bb2ae79f293133bc62c8f2cbc6e04091a76ac3f9
Author: Jan Chrin
Date: Tue Nov 2 14:32:16 2021 +0100
Initial commit
diff --git a/pvgateway.py b/pvgateway.py
new file mode 100644
index 0000000..688b49f
--- /dev/null
+++ b/pvgateway.py
@@ -0,0 +1,1908 @@
+"""The module provides data and metadata of a process variable through PyCafe."""
+__author__ = 'Jan T. M. Chrin'
+
+import copy
+import inspect
+import sys
+import time
+
+from datetime import datetime
+from distutils.version import LooseVersion
+
+from qtpy.QtCore import (QEvent, QObject, QMutex, QPoint, QProcess, QRect,
+ QSettings, Qt, QUrl, Signal, Slot)
+from qtpy.QtCore import __version__ as QT_VERSION_STR
+from qtpy.QtGui import (QColor, QCursor, QDesktopServices, QFont, QPainter,
+ QPalette)
+from qtpy.QtWidgets import (QAction, QApplication, QComboBox, QDialog,
+ QHBoxLayout, QLabel, QLineEdit, QMenu, QMessageBox,
+ QPushButton, QSpinBox, QVBoxLayout, QWidget)
+
+from bdbase.readjson import Rjson
+from bdbase.enumkind import DAQState
+
+def __LINE__():
+ return inspect.currentframe().f_back_f_lineno
+
+
+class PVGateway(QWidget):
+ """Retrieves pv metadata through PyCafe.
+
+ The PVGateway class when subclassed by Qt widgets enables their connectivity
+ to channel access.
+
+ Attributes:
+ monid: (int) Monitor id
+ units : (str) Units associated with the pv
+
+ trigger_monitor_ is the signal triggered by updates arising from
+ monitored pvs.
+ trigger_connect is the signal triggered from changes in pv connection status.
+ widget_handle_dict is a dictionary mapping widgets to their pv handle.
+ A pv handle may be associated to more than one widget.
+ """
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int) #pvdata, status
+
+
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+ #Properties, user supplied
+ ACT_ON_BEAM = 'actOnBeam'
+ NOT_ACT_ON_BEAM = 'notActOnBeam'
+ READBACK_ALARM = 'alarm'
+ READBACK_STATIC = 'static'
+
+ #Properties, dynamic
+ DISCONNECTED = 'disconnected'
+ ALARM_SEV_MINOR = 'alarmSevMinor'
+ ALARM_SEV_MAJOR = 'alarmSevMajor'
+ ALARM_SEV_INVALID = 'alarmSevInvalid'
+ ALARM_SEV_NO_ALARM = READBACK_ALARM
+ DAQ_STOPPED = 'stopped'
+ DAQ_PAUSED = 'paused'
+
+ #ObjectName, defined by CAQ
+ PV_CONTROLLER = "Controller"
+ PV_READBACK = "Readback"
+ PV_DAQ_BS = "BSRead"
+ PV_DAQ_CA = "CARead"
+
+ _DAQ_CAFE_SG_NAME = "gBS2CA"
+
+ _alarm_severity_record_types = ["ai", "ao", "calc", "calcout", "dfanout",
+ "longin", "longout", "pid", "sel",
+ "steppermotor", "sub"]
+
+ #parent is Gui
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ connect_callback=None, msg_label: str = "",
+ connect_triggers: bool = True, notify_freq_hz: int = 0,
+ notify_unison: bool = False, precision: int = 0,
+ monitor_dbr_time: bool = False):
+
+ #super(PVGateway, self).__init__() # do NOT use parent
+ #It turned out a widget was created with the main window as a parent, but incorrectly placed.
+ #Parent must not be QMainWindow. This interferes with the toolbar!! 16 Aug. 2020
+ super().__init__()
+
+ if parent is None:
+ return
+
+ if pv_name is "":
+ return
+
+ self.connect_callback = connect_callback
+ self.notify_freq_hz = abs(notify_freq_hz)
+ self.notify_freq_hz_default = self.notify_freq_hz
+
+ self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
+ 1000 / self.notify_freq_hz
+
+ self.notify_unison = True if notify_unison and \
+ self.notify_freq_hz > 0 else False
+
+ self.parent = parent
+ self.pv_name = pv_name
+
+ self.color_mode = None
+
+ if color_mode is not None:
+ if color_mode in (self.ACT_ON_BEAM,
+ self.NOT_ACT_ON_BEAM,
+ self.READBACK_ALARM,
+ self.READBACK_STATIC):
+ self.color_mode = color_mode
+
+ self.color_mode_requested = self.color_mode
+
+ if monitor_callback is not None:
+ self.monitor_callback = monitor_callback
+ else:
+ self.monitor_callback = None
+
+ self.pv_within_daq_group = pv_within_daq_group
+
+ self.show_units = show_units
+ self.prefix = prefix
+ self.suffix = suffix
+
+ self.cafe = self.parent.cafe
+ self.cyca = self.parent.cyca
+ if self.parent.settings is not None:
+ self.settings = self.parent.settings
+ else:
+ self.settings = Rjson(self.parent.appname)
+
+ self.daq_group_name = self._DAQ_CAFE_SG_NAME
+ self.desc = None
+ self.handle = None
+ self.initialize_complete = False
+ self.initialize_again = False
+
+ self.msg_label = msg_label
+ self.msg_press_value = None
+ self.msg_release_value = None
+
+ self.monitor_id = None
+ self.monitor_dbr_time = monitor_dbr_time
+
+ self.mutex_post_display = QMutex()
+
+
+ self.precision_user = precision
+ self.has_precision_user = True if precision > 0 else False
+ self.precision_pv = 3
+
+ self.precision = (self.precision_user if self.has_precision_user else
+ self.precision_pv)
+
+ self.pvd = None
+ self.pv_ctrl = None
+ self.pv_info = None
+ self.record_type = None
+
+ if self.parent.showMessage is not None:
+ self.showMessage = self.parent.showMessage
+ else:
+ self.showMessage = None
+
+ self.qt_object_name = None
+
+ self.qt_property_controller = {
+ self.DISCONNECTED : False,
+ self.ACT_ON_BEAM : False, self.NOT_ACT_ON_BEAM : False
+ }
+
+ self.qt_property_readback = {
+ self.DISCONNECTED : False,
+ self.READBACK_ALARM : False, self.READBACK_STATIC : False,
+ self.ALARM_SEV_MINOR : False, self.ALARM_SEV_MAJOR : False,
+ self.ALARM_SEV_INVALID : False
+ }
+
+ self.qt_property_daq_bs = {
+ self.DISCONNECTED : False,
+ self.READBACK_ALARM : False, self.READBACK_STATIC : False,
+ self.ALARM_SEV_MINOR : False, self.ALARM_SEV_MAJOR : False,
+ self.ALARM_SEV_INVALID : False,
+ self.DAQ_STOPPED : False,
+ self.DAQ_PAUSED : False
+ }
+
+ self.qt_property_daq_ca = {
+ self.DISCONNECTED : False,
+ self.READBACK_ALARM : False, self.READBACK_STATIC : False,
+ self.ALARM_SEV_MINOR : False, self.ALARM_SEV_MAJOR : False,
+ self.ALARM_SEV_INVALID : False,
+ self.DAQ_STOPPED : False,
+ self.DAQ_PAUSED : False
+ }
+
+ self.qt_object_to_property = {
+ self.PV_CONTROLLER : self.qt_property_controller,
+ self.PV_READBACK : self.qt_property_readback,
+ self.PV_DAQ_BS : self.qt_property_daq_bs,
+ self.PV_DAQ_CA : self.qt_property_daq_ca
+ }
+
+ self._qt_property_selected = {}
+
+ self.status_tip = None
+ self.suggested_text = ""
+ self.time_monotonic = time.monotonic()
+ self.pvd_previous = None
+ self.timeout = 0.2
+ self.units = ""
+
+ self.widget = self
+
+ _widget_name_part = str(self.widget.__class__).split("\'")[1].split(".")
+ _widget_class_part = _widget_name_part[1].split(".")
+ self.widget_class = _widget_name_part[len(_widget_name_part)-1]
+
+ if pv_within_daq_group:
+ self.trigger_daq_int.connect(self.receive_daq_update)
+ self.trigger_daq.connect(self.receive_daq_update)
+ self.trigger_daq_str.connect(self.receive_daq_update)
+
+ elif connect_triggers:
+ self.trigger_monitor.connect(self.receive_monitor_dbr_time)
+ self.trigger_monitor_str.connect(self.receive_monitor_update)
+ self.trigger_monitor_int.connect(self.receive_monitor_update)
+ self.trigger_monitor_float.connect(self.receive_monitor_update)
+ self.trigger_connect.connect(self.receive_connect_update)
+
+ self.context_menu = QMenu()
+ self.context_menu.setObjectName("contextMenu")
+ self.context_menu.setWindowModality(Qt.NonModal) #ApplicationModal
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.0"):
+ self.context_menu.addSection("PV: {0}".format(self.pv_name))
+
+ action1 = QAction("Text Info", self)
+ action1.triggered.connect(self.pv_status_text)
+
+ action2 = QAction("Lookup in Archiver", self)
+ action2.triggered.connect(self.lookup_archiver)
+
+ action3 = QAction("Lookup in Databuffer", self)
+ action3.triggered.connect(self.lookup_databuffer)
+
+ action4 = QAction("Strip Chart (PShell)", self)
+ action4.triggered.connect(self.strip_chart)
+
+ action6 = QAction("Configure Display Parameters", self)
+ action6.triggered.connect(self.display_parameters)
+
+ self.context_menu.addAction(action1)
+ self.context_menu.addAction(action2)
+ self.context_menu.addAction(action3)
+ self.context_menu.addAction(action4)
+
+ action5 = QAction("Reconnect: {0}".format(self.pv_name), self)
+ action5.triggered.connect(self.reconnect_channel)
+ _font = QFont()
+ _font.setPixelSize(12)
+ action5.setFont(_font)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.context_menu.addSection("")
+
+ #return action6 and 5 code here eventually
+ self.context_menu.addAction(action6)
+ self.context_menu.addAction(action5)
+
+ self.pv_message_in_a_box = QMessageBox()
+ self.pv_message_in_a_box.setObjectName("pvinfo")
+
+ #Qt.NonModal often causes harmless QXcbConnection: XCB error: 3 (BadWindow), sequence:
+ #but only if the window is closed too quickly(!)
+ #Qt.ApplicationModal not used as it blocks input to all windows
+ self.pv_message_in_a_box.setWindowModality(Qt.NonModal) #Qt.ApplicationModal
+ self.pv_message_in_a_box.setIcon(QMessageBox.Information)
+ self.pv_message_in_a_box.setStandardButtons(QMessageBox.Close) # Shows QMessageBox.Close shows Close
+ self.pv_message_in_a_box.setDefaultButton(QMessageBox.Close) #Show OK
+
+ self.initialize()
+ '''
+ #temporary code position
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.precision > 0:
+ self.context_menu.addAction(action6)
+
+ self.context_menu.addAction(action5)
+ '''
+ return self
+
+
+ def initialize(self):
+ '''Initialze class attributes and connect to ca if required.'''
+
+ _handle_within_group_flag = False
+ if self.pv_within_daq_group:
+ self.handle = self.cafe.getHandleFromPVWithinGroup(
+ self.pv_name, self.daq_group_name)
+ if self.handle > 0:
+ self.cafe.addWidget(self.handle, self.widget)
+ _handle_within_group_flag = True
+ #Callback already invoked to emit signal here!!
+ _channel_info = self.cafe.getChannelInfo(self.handle)
+ print(self.pv_name, self.handle)
+ w = self.cafe.getWidgets(self.handle)
+ print("widget list", w)
+ #_channel_info.show()
+
+ self.trigger_connect.emit(
+ int(self.handle), str(self.pv_name),
+ int(_channel_info.cafeConnectionState))
+ #In case user is misinformed
+ if not _handle_within_group_flag:
+ self.handle = self.cafe.getHandleFromPV(self.pv_name)
+ if self.connect_callback is None:
+ self.connect_callback = self.py_connect_callback
+
+ if self.handle > 0:
+
+ #The second time round, widget is gateway rather than parent, Why is that?
+ self.cafe.setPyConnectCallbackFn(self.handle,
+ self.connect_callback)
+
+ self.cafe.addWidget(self.handle, self.widget)
+
+ _channel_info = self.cafe.getChannelInfo(self.handle)
+ self.trigger_connect.emit(
+ self.handle, self.pv_name,
+ int(_channel_info.cafeConnectionState))
+
+
+ #print("====OLD===============", self.handle, self.widget, self.parent)
+ else:
+
+ self.cafe.openPrepare()
+ self.handle = self.cafe.open(self.pv_name,
+ self.connect_callback)
+ self.cafe.addWidget(self.handle, self.widget)
+ self.cafe.openNowAndWait(self.timeout, self.handle)
+ #self.cafe.openNow()
+ #_channel_info = self.cafe.getChannelInfo(self.handle)
+ #self.trigger_connect.emit(int(self.handle), str(self.pv_name), int(_channel_info.cafeConnectionState))
+ #print("====NEW============ ==", self.handle, self.widget)
+
+ self.initialize_meta_data()
+
+ self.pv_message_in_a_box.setWindowTitle(self.pv_name)
+
+
+ def initialize_meta_data(self):
+
+ _current_value = ""
+
+ if self.cafe.isConnected(self.handle) and \
+ self.cafe.initCallbackComplete(self.handle):
+
+ if self.pvd is None:
+ self.pvd = self.cafe.getPVCache(self.handle)
+
+ if self.pv_ctrl is None:
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+ self.set_precision_and_units()
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.pv_name)
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ _rtype = self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+ #print ("record_type", self.record_type)
+
+
+ _current_value = self.cafe.getCache(self.handle)
+ if isinstance(_current_value, (int, float)):
+ #space for positive numbers
+ _value_form = ("{:<+.%sf}" % self.precision)
+ _current_value = _value_form.format(
+ round(_current_value, self.precision))
+ #if self.desc is None:
+ # self.set_desc()
+
+ #Reset
+ self.initialize_complete = True
+
+ #verify user input
+ if self.show_units is True:
+ if self.suffix == self.units and self.units != "":
+ self.show_units = False
+
+ self.suggested_text = self.prefix
+ if len(self.prefix) > 0:
+ self.suggested_text += " "
+
+ _suggested_text_from_value = " "
+
+ _max_control_abs = 0
+
+ if self.pv_ctrl is not None:
+ _lower_control_abs = abs(int(self.pv_ctrl.lowerControlLimit))
+ _upper_control_abs = abs(int(self.pv_ctrl.upperControlLimit))
+ _max_control_abs = max(_lower_control_abs, _upper_control_abs)
+ if _max_control_abs is None:
+ _max_control_abs = 0
+
+ _enum_list = self.pv_ctrl.enumStrings
+
+ if len(_enum_list) > 0:
+ _enum_list_member_max_length = 0
+ _enum_list_member_max_index = 0
+
+ for i in range(0, len(_enum_list)):
+ if len(_enum_list[i]) > _enum_list_member_max_length:
+ _enum_list_member_max_length = len(_enum_list[i])
+ _enum_list_member_max_index = i
+ _suggested_text_from_value += \
+ _enum_list[_enum_list_member_max_index] + "." #Add extra space
+ else:
+ if self.pv_ctrl.lowerControlLimit < 0:
+ _suggested_text_from_value += "-"
+ _suggested_text_from_value += str(_max_control_abs) + "."
+
+
+ #print("precision", self.precision, self.pv_name)
+ self.precision = min(9, self.precision) #safety net
+ for i in range (0, self.precision):
+ _suggested_text_from_value += "0"
+
+ if len(_current_value) > len(_suggested_text_from_value):
+ _suggested_text_from_value = _current_value
+
+ self.suggested_text += _suggested_text_from_value
+
+ if self.show_units:
+ self.suggested_text += " " + self.units
+ self.suggested_text += self.suffix
+
+ _suggested_text_length = len(self.suggested_text)
+ self.suggested_text = self.suggested_text.center(
+ _suggested_text_length+2)
+
+ self.max_control_abs_str = str(_max_control_abs)
+
+
+ _max_control_abs_length = len(self.max_control_abs_str)
+ _offset = 9
+ self.max_control_abs_str = self.max_control_abs_str.center(
+ _max_control_abs_length + _offset)
+
+
+ qsettings = QSettings()
+ qsettings.beginGroup("Widget")
+ qsettings.beginGroup(self.pv_name)
+ qsettings.beginGroup(self.widget_class)
+ #_var_base = "Widget/" + self.pv_name + "/" + self.widget_class + "/"
+ _var_text = "suggested_text"
+ _ctrl_abs = "max_control_abs_str"
+
+ if self.cafe.isConnected(self.handle) and \
+ self.cafe.initCallbackComplete(self.handle):
+ qsettings.setValue(_var_text, self.suggested_text)
+ qsettings.setValue(_ctrl_abs, self.max_control_abs_str)
+ else:
+ if qsettings.value(_var_text) is not None:
+ self.suggested_text = qsettings.value(_var_text)
+ if qsettings.value(_ctrl_abs) is not None:
+ self.max_control_abs_str = qsettings.value(_ctrl_abs)
+
+
+ qsettings.endGroup()
+ qsettings.endGroup()
+ qsettings.endGroup()
+
+
+ def is_initialize_complete(self):
+ icount = 0;
+ while not self.initialize_complete :
+ time.sleep(0.01)
+ self.initialize_meta_data()
+ icount += 1
+ if icount > 50:
+ return False
+ return True
+
+ def cleanup(self, close_pv = True):
+ '''Clean up the widget.'''
+ ##Check for monitors
+ ##QWidget::close() is basically a combination of QWidget::closeEvent(), QWidget::hide(), and QObject::deleteLater() (if Qt::WA_DeleteOnClose is set).
+
+ #Make sure mon id is valid
+ if self.handle > 0:
+ _monID_list = self.cafe.getMonitorIDs(self.handle)
+ if self.monitor_id in _monID_list:
+ self.cafe.monitorStop(self.handle, self.monitor_id)
+ #self.cafe.monitorStop(self.handle, self.monitor_id)
+ #Do not close of there are other monitors
+ if self.cafe.getNoMonitors(self.handle) > 0:
+ if close_pv is True:
+ self.cafe.close(self.pv_name)
+ self.widget.deleteLater()
+
+
+ def format_display_value(self, value):
+
+ if value is None:
+ print(self, self.pv_name, ">>>>>>>>>>>>format_display_value is None>>>>>>")
+ #return
+
+ if isinstance(value, str):
+ _value_str = value
+ elif isinstance(value, int):
+ _value_str = str(value)
+ else:
+ _value_form = ("{:< .%sf}" % self.precision) #space for positive numbers
+ #print("v/prec", value, self.precision, flush=True)
+
+ _rounded_value = round(value, self.precision)
+ #print(_rounded_value, flush=True)
+ _value_str = _value_form.format(_rounded_value)
+ #print(_value_str, flush=True)
+
+ if self.show_units:
+ _value_str += " " + self.units + " "
+ if self.suffix is not "":
+ _value_str += " " + self.suffix + " "
+
+ if self.prefix is not "":
+ _space = ""
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.lowerDisplayLimit < 0:
+ _space = " "
+ _value_str = self.prefix + _space + _value_str
+
+ return _value_str
+
+ def post_display_value(self, value):
+
+ #self.mutex_post_display.lock()
+
+ _value_str = self.format_display_value(value)
+
+ if "setText" in dir(self):
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ self.setText(_value_str)
+ self.blockSignals(False)
+ else:
+ #print("value =", _value_str, flush=True)
+ self.setText(_value_str)
+
+ else:
+ print("setText method does not exist for this widget class:\n", self.widget.__class__)
+ print("sender was: ", self.sender())
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ Checks for existence of widget. Waits up to a maximun of 100 ms.
+ '''
+ #print(" py_connect_callback:: START ")
+ #print(" py_connect_callback:: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", pvname)
+ #print(handle, pvname, status, self.cafe.getStatusCodeAsString(status))
+
+ pv_name = pvname
+ _widget = None
+ #_widgetList =self.cafe.getWidgets(handle)
+ #for i in range(0, len(_widgetList)):
+ #_widget = _widgetList[i]
+ #_widget.trigger_connect.emit(int(handle), str(pv_name), int(status))
+ #print (i, "widget at connect>>>>>>>>>>>>>>>>>>>>>>", _widget.__class__)
+ self.trigger_connect.emit(int(handle), str(pv_name), int(status))
+ #print(" py_connect_callback:: END ")
+
+
+ def receive_connect_update(self, handle, pv_name, status, post_display=True):
+ '''Triggered by connect signal. For Widget to overload.'''
+
+ #print(" receive _connect_callback:: START ")
+ #print ("RRReceive_connect triggered for widget", self, "with status", status)
+ #print ("RRReceive_connect triggered for widget", handle, pv_name, status)
+ _alarm_severity = None
+ if status == self.cyca.ICAFE_CS_CONN:
+ self.initialize_connect = True
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ _rtype = self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ #print(self.pv_name)
+ #print("record type====>", self.record_type)
+ else:
+ self.record_type = self.pv_info.className
+
+ self.set_precision_and_units(reconnectFlag=True)
+ #THis will connect to a new channel
+ #if self.desc is None:
+ # self.set_desc()
+ #print("msg_lab", self.msg_label, "/", len(self.msg_label))
+
+ if self.msg_label == "":
+ _value = self.cafe.getCache(handle, dt='native')
+ #Another reconnection in progress!!!
+
+ if _value == None:
+ return
+ else:
+ _value = self.msg_label
+ #print("_value", _value, "/", len(_value))
+
+ #print("_value", _value, "/")
+ if post_display:
+ self.post_display_value(_value)
+ self.qt_property_reconnect()
+
+ else:
+ self.qt_property_disconnect()
+
+
+ if status == self.cyca.ICAFE_CS_CLOSED:
+ self.initialize_again = True
+
+ elif self.initialize_again:
+ #monitos_id informs whether or not widget has a monitor
+ #CAQMessageButton for instance does not have a monitor
+
+ if not self.pv_within_daq_group and self.monitor_id is not None:
+ self.monitor_start()
+ #print("RESTART MONITOR FOR THIS WIDGET", flush=True)
+
+ self.initialize_again = False
+
+ #print(" receive _connect_callback:: END ")
+
+ return
+
+
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ ''' DAQ mode is widget specific.
+ DAQ may be in BS mode, but channels within DAQ stream that
+ are not BS enabled will be flagged as CA Mode, i.e., CARead
+ '''
+
+ _current_qt_dynamic_property = self.qt_dynamic_property_get()
+
+ alarm_severity = daq_pvd.alarmSeverity
+ self.pvd = daq_pvd
+
+ #print("BEFORE mode, object_name, daqState", daq_mode, self.qt_object_name, daq_state)
+
+ if daq_mode != self.qt_object_name:
+ self.qt_object_name = daq_mode
+ self.setObjectName(self.qt_object_name)
+ self.qt_style_polish()
+
+ #print("AFTER mode, object_name, daqState", daq_mode, self.qt_object_name, daq_state)
+
+ if daq_state in (self.cyca.ICAFE_DAQ_STOPPED,):
+ #if daq_state in (DAQState.CA_STOP, DAQState.BS_STOP, DAQState.CA_PAUSE,
+ # DAQState.BS_PAUSE):
+ if _current_qt_dynamic_property != self.DAQ_STOPPED:
+ self.qt_property_daq_stopped()
+
+ elif daq_state in (self.cyca.ICAFE_DAQ_PAUSED,):
+ if _current_qt_dynamic_property != self.DAQ_PAUSED:
+ self.qt_property_daq_paused()
+
+ elif daq_state in (self.cyca.ICAFE_DAQ_RUN,):
+ #if _current_qt_dynamic_property != self.READBACK_ALARM:
+ # self.qt_property_alarm_sev_no_alarm()
+ #print ("before", daq_mode, _current_qt_dynamic_property)
+
+ if daq_mode == self.PV_DAQ_BS and \
+ _current_qt_dynamic_property != self.READBACK_STATIC:
+ self.qt_property_static()
+
+ elif daq_mode == self.PV_DAQ_CA:
+ #if _current_qt_dynamic_property not in (self.READBACK_STATIC,
+ # self.READBACK_ALARM,):
+ if self.color_mode != self.color_mode_requested:
+ self.color_mode == self.color_mode_requested
+ #print("new colore mode")
+
+ if self.cafe.isEnum(self.handle) and \
+ isinstance(daq_pvd.value[0], int):
+ _value = self.cafe.getStringFromEnum(self.handle,
+ daq_pvd.value[0])
+ else:
+ _value = daq_pvd.value[0]
+
+ if daq_pvd.status == self.cyca.ICAFE_NORMAL:
+ if self.msg_label == "":
+ self.post_display_value(_value)
+
+ if daq_mode == self.PV_DAQ_BS:
+ return
+
+ #Fro DAQ when channel connects after application start-up
+ #if _current_qt_dynamic_property == self.DISCONNECTED:
+ # self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+ #Check if color settings are correct
+ ##if _current_qt_dynamic_property == self.READBACK_STATIC and \
+ if alarm_severity > self.cyca.SEV_NO_ALARM:
+ self.color_mode = self.READBACK_ALARM
+ self.color_mode_requested = self.READBACK_ALARM
+
+ if self.color_mode == self.READBACK_ALARM:
+ if alarm_severity == self.cyca.SEV_MINOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
+ self.qt_property_alarm_sev_minor()
+
+ elif alarm_severity == self.cyca.SEV_MAJOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
+ self.qt_property_alarm_sev_major()
+
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ elif alarm_severity == self.cyca.SEV_NO_ALARM:
+ if _current_qt_dynamic_property != self.ALARM_SEV_NO_ALARM:
+ self.qt_property_alarm_sev_no_alarm()
+
+ elif _current_qt_dynamic_property != self.READBACK_STATIC:
+ self.qt_property_static()
+
+ #print ("after", daq_mode, self.qt_dynamic_property_get() )
+ else:
+ if _current_qt_dynamic_property != self.DISCONNECTED:
+ self.qt_property_disconnect()
+
+
+ def receive_monitor_dbr_time(self, pvdata, alarm_severity):
+ print("in gateway", self.pv_name)
+ #pvdata.show()
+
+ def receive_monitor_update(self, value, status, alarm_severity):
+ '''Triggered by monitor signal. For Widget to overload.'''
+
+ self.mutex_post_display.lock()
+ _current_qt_dynamic_property = self.qt_dynamic_property_get()
+ #print(self.pv_name, value, status, alarm_severity)
+ #if isinstance(value, (int, float)):
+ # if value < 100:
+ # print("CURRENT PROPERY VALUE", self.pv_name, _current_qt_dynamic_property, value, status, alarm_severity )
+ #print ("sender //2//", self.sender(), value)
+
+ #print("receive monitor update/1", self.pv_name, self.qt_object_name, self._qt_property_selected)
+
+ if status == self.cyca.ICAFE_NORMAL:
+ '''
+ if isinstance(value, (int, float)):
+
+ if value < -20:
+ alarm_severity = self.cyca.SEV_INVALID
+ elif value < -1:
+ alarm_severity = self.cyca.SEV_MAJOR
+ elif value < 5:
+ alarm_severity = self.cyca.SEV_MINOR
+ else:
+ alarm_severity = self.cyca.SEV_NO_ALARM
+ '''
+
+ if self.msg_label == "":
+ self.post_display_value(value)
+
+ #For DAQ when channel connects after application start-up
+ if _current_qt_dynamic_property == self.DISCONNECTED:
+ self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+ #Check if color settings are correct
+ elif _current_qt_dynamic_property == self.READBACK_STATIC:
+ if alarm_severity > self.cyca.SEV_NO_ALARM and \
+ alarm_severity < self.cyca.SEV_INVALID:
+ self.color_mode = self.READBACK_ALARM
+ self.status_tip = "Widget color mode is dynamic, pv with alarm limits"
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ if self.color_mode == self.READBACK_ALARM:
+
+ if alarm_severity == self.cyca.SEV_MINOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MINOR:
+ self.qt_property_alarm_sev_minor()
+
+ elif alarm_severity == self.cyca.SEV_MAJOR:
+ if _current_qt_dynamic_property != self.ALARM_SEV_MAJOR:
+ self.qt_property_alarm_sev_major()
+
+ elif alarm_severity == self.cyca.SEV_INVALID:
+ if _current_qt_dynamic_property != self.ALARM_SEV_INVALID:
+ self.qt_property_alarm_sev_invalid()
+
+ elif alarm_severity == self.cyca.SEV_NO_ALARM:
+ if _current_qt_dynamic_property != self.ALARM_SEV_NO_ALARM:
+ self.qt_property_alarm_sev_no_alarm()
+
+ else:
+ if _current_qt_dynamic_property != self.DISCONNECTED:
+ self.qt_property_disconnect()
+
+ self.mutex_post_display.unlock()
+
+ #print("receive monitor update/2", self.pv_name, self.qt_object_name, self._qt_property_selected)
+
+ def py_monitor_callback(self, handle, pvname, pvdata):
+
+
+ '''Callback function to be invoked on change of pv value.
+ cafe.getCache and cafe.set operations permitted within callback.
+ '''
+ '''
+ if "PULSEID" in pvname:
+ pass
+ else:
+ print ("py_monitor_callback: name/handle ",pvname, handle )
+ '''
+ pv_name = pvname
+ pvd = pvdata
+ #print("===================================")
+ #print("pvname/handle in mon callback ", pv_name, handle)
+
+
+ if not hasattr(self, 'cafe'):
+ print ("py_monitor_callback: name/handle self cafe is NONE =>>>>>>>>>>>> ",
+ pv_name, handle)
+ return
+ #pv_name = self.cafe.getPVNameFromHandle(self.handle)
+ #pvd = self.cafe.getPVCache(self.handle)
+
+ self.pvd = pvd
+ '''
+ if pvname != pv_name:
+ print ("py_monitor_callback: name/handle/monid =>>>>>>>>>>>> ",
+ pv_name, handle, self.cafe.getMonitorIDInCallback(handle))
+ print ("PV NAME NOT THE SAME **** WIDGET in monitor callback ", self)
+ '''
+ #_pvc = self.cafe.getCtrlCache(handle)
+ #print("_pvc.nelem",_pvc.nelem)
+ #_pvc.show()
+ #print(_pvc.nelem)
+
+ #_pvd = self.cafe.getPVCache(handle)
+ #pvd.showMax(4000) #set no of elemets to 1 in pvctrlCache!
+ #print(pvd.nelem)
+
+ '''
+ _info = self.cafe.getChannelInfo(handle)
+ _info.show()
+ '''
+
+
+
+ #_widgetList =self.cafe.getWidgets(handle)
+
+ _widget = None
+
+ #print ("END monid =>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ", self.cafe.getMonitorIDInCallback(handle))
+ #print(self, self.pv_name)
+ #print(_widgetList)
+ '''
+ self.mutex.lock()
+
+ for _widget, _handle in self.widget_handle_dict.items():
+ if _handle == handle:
+ '''
+ for i in range(0, 1): #len(_widgetList)):
+ #_widget = _widgetList[i]
+ if pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
+ print("initialize again")
+ self.initialize()
+
+ elif pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _alarm_severity = self.cyca.ICAFE_CA_OP_CONN_DOWN
+ #print("COMPARE ALARM SEVERITIES ", _alarm_severity, pvd.alarmSeverity)
+ else:
+ _alarm_severity = pvd.alarmSeverity
+
+
+
+ if self.monitor_dbr_time:
+ self.trigger_monitor.emit(pvd, _alarm_severity)
+
+ elif isinstance(pvd.value[0], str):
+ self.trigger_monitor_str.emit((pvd.value[0]), pvd.status, _alarm_severity) #, _widget)
+ #print("emitted str value", pvd.value[0])
+ elif isinstance(pvd.value[0], int):
+ self.trigger_monitor_int.emit((pvd.value[0]), pvd.status, _alarm_severity)
+
+ else:
+ #print(dir(self.receivers(self, self.trigger_monitor_float)))
+ self.trigger_monitor_float.emit(float(pvd.value[0]), pvd.status, _alarm_severity)
+ #print("emitted float value", pvd.value[0])
+ pass
+
+
+
+
+ #if _widget is None:
+ # print("NO WIDGET FOR THIS PV!!!! pv = ", pv_name)
+ #self.mutex.unlock()
+
+
+
+ def monitor_start(self):
+ '''Initiate monitor on pv.'''
+ #print(self, self.pv_name, "Initiate monitor on pv:", self.monitor_callback, self.py_monitor_callback)
+ if self.handle > 0:
+ #Is monitor in waiting - now deleted with monitor_stop
+ if self.notify_unison:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, dbr=self.cyca.CY_DBR_TIME)
+ #start with gateway supplied monitor callback handler
+ elif self.monitor_callback is None:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, cb=self.py_monitor_callback,
+ dbr=self.cyca.CY_DBR_TIME,
+ notify_milliseconds=self.notify_milliseconds)
+ else:
+ self.monitor_id = self.cafe.monitorStart(
+ self.handle, cb=self.monitor_callback,
+ dbr=self.cyca.CY_DBR_TIME,
+ notify_milliseconds=self.notify_milliseconds)
+
+ def monitor_stop(self):
+ #print("monitor_stopped")
+ if self.handle > 0:
+ _monID_list = self.cafe.getMonitorIDs(self.handle)
+ _monID_inwaiting_list = self.cafe.getMonitorIDsInWaiting(self.handle)
+ _monID_all = _monID_list + _monID_inwaiting_list
+
+ if self.monitor_id in _monID_all:
+ #print("stopping in monitor_stop for handle", self.monitor_id)
+ self.cafe.monitorStop(self.handle, self.monitor_id)
+ #print("stopped in monitor_stop for handle", self.monitor_id)
+ #Is monitor in waiting?
+ #remove monitors in waiting
+
+
+
+ def reconnect_channel(self):
+ self.cafe.reconnect([self.handle]) #list
+
+ def set_desc(self):
+ '''Set description of pv from pv.DESC'''
+
+ if self.cafe.hasDescription(self.handle):
+ self.desc = self.cafe.getDescription(self.handle)
+ return
+ elif self.desc is not None:
+ return
+ else:
+ self.cafe.supplementHandle(self.handle)
+ if self.cafe.hasDescription(self.handle):
+ self.desc = self.cafe.getDescription(self.handle)
+
+ if self.desc is not None:
+ return
+
+ ###Back-up solution
+ _found = str(self.pv_name).find(".")
+ if _found != -1:
+ _pv_desc = str(self.pv_name)[0:_found] +".DESC"
+ else:
+ _pv_desc = self.pv_name +".DESC"
+ _handle_desc = self.cafe.getHandleFromPVName(_pv_desc)
+
+ _handle_desc_already_open = False
+
+ if _handle_desc == 0:
+ self.cafe.openPrepare()
+ _handle_desc = self.cafe.open(_pv_desc)
+ self.cafe.openNowAndWait(self.timeout, _handle_desc)
+ time.sleep(0.001)
+ else:
+ _handle_desc_already_open = True
+
+ if self.cafe.isConnected(_handle_desc):
+ self.desc = self.cafe.getCache(_handle_desc, 'str')
+ if self.desc is None:
+ self.desc = self.cafe.get(_handle_desc, 'str')
+ else:
+ self.desc = None
+
+ if not _handle_desc_already_open:
+ self.cafe.close(_handle_desc)
+
+ def set_precision_and_units(self, reconnectFlag: bool = False):
+ '''Set the pv precision and units.'''
+ if self.pv_ctrl is None or reconnectFlag is True:
+ self.pv_ctrl = self.cafe.getCtrlCache(self.handle)
+
+ if self.pv_ctrl is not None:
+ if not self.has_precision_user:
+ self.precision = self.pv_ctrl.precision
+ if self.pv_ctrl.units is not None:
+ #print(self.pv_ctrl.units)
+ #print(type(self.pv_ctrl.units))
+ self.units = str(self.pv_ctrl.units)
+ else:
+ self.units = ""
+
+ if reconnectFlag is True:
+ #verify user input
+ if self.show_units is True and self.suffix is not None:
+ if self.suffix == self.units:
+ self.show_units = False
+
+
+ def _qt_readback_color_mode(self):
+ '''Color mode is determined from CAFE and depends on whether the pv:
+ has alarm limits (self.color_mode = 'readbackAlarm')
+ or is without alarm limits (self.color_mode = 'readbackStatic')
+ '''
+
+
+ #Already set by user
+ if self.color_mode is self.READBACK_ALARM:
+ return
+
+
+ if self.cafe.isConnected(self.handle):
+ pvd = self.cafe.getPVCache(self.handle)
+ if pvd.alarmSeverity in (self.cyca.SEV_MINOR, self.cyca.SEV_MAJOR) or \
+ self.cafe.hasAlarmStatusSeverity(self.handle):
+ self.color_mode = self.READBACK_ALARM
+ self.status_tip = "Widget color mode is dynamic, pv with alarm limits"
+ #print(self.pv_name, "has alarm svev", self.cafe.hasAlarmStatusSeverity(self.handle))
+
+
+ else:
+ self.color_mode = self.READBACK_STATIC
+ self.status_tip = "Widget color mode is static, pv without alarm limits"
+
+
+ def qt_property_initial_values(self, qt_object_name: str = None, tool_tip: bool = True):
+
+ '''Set Qt property values.'''
+ self.qt_object_name = qt_object_name
+ if tool_tip:
+ self.setToolTip(self.pv_name)
+ self.setObjectName(self.qt_object_name)
+ if self.qt_object_name in self.qt_object_to_property.keys():
+ self._qt_property_selected = copy.deepcopy(self.qt_object_to_property[self.qt_object_name])
+ else:
+ print ("qt_property_initial_values: Object not found in dictionary")
+
+ #print("qt_property_initial_values", self.qt_object_name, self._qt_property_selected)
+
+
+ if self.cafe.isConnected(self.handle):
+
+ if self.qt_object_name == self.PV_READBACK:
+ self._qt_readback_color_mode()
+ #self.setStatusTip(self.status_tip)
+
+ elif self.qt_object_name == self.PV_CONTROLLER:
+ if self.color_mode == self.ACT_ON_BEAM:
+ #self.setStatusTip("PV setting acts directly on beam")
+ pass
+ else:
+ self.color_mode = self.NOT_ACT_ON_BEAM
+ #self.setStatusTip("PV setting does not influence beam")
+
+ elif self.qt_object_name == self.PV_DAQ_CA:
+ self._qt_readback_color_mode()
+
+ elif self.qt_object_name == self.PV_DAQ_BS:
+ self.color_mode = self.READBACK_STATIC
+
+ #print("qt_property_initial_values//", self.pv_name, self.qt_object_name, self._qt_property_selected)
+ self._qt_dynamic_property_set(self.color_mode)
+ #print("qt_property_initial_values///", self.pv_name, self.qt_object_name, self._qt_property_selected)
+
+ else:
+ self.qt_property_disconnect()
+
+ #print("qt_property_initial_values", self.pv_name, self.qt_object_name, self.color_mode)
+
+ '''
+ meta_obj = self.metaObject()
+ count = meta_obj.propertyCount()
+ for i in range(0, count):
+ meta_prop = meta_obj.property(i)
+ name = meta_prop.name()
+ print(i, name, self.property(name))
+ '''
+
+ def qt_dynamic_property_get(self, property_state : str = None):
+ '''Retrieves the requested property value'''
+ '''else that which is currently true'''
+
+ for _property, _value in self._qt_property_selected.items(): #states.items():
+ if property_state is not None:
+ if _property == property_state:
+ return _value
+ elif _value:
+ #print(self, _property, "SELECTED")
+ return _property
+
+ def _qt_dynamic_property_set(self, property_state : str = None):
+ '''Set the Input property to true, and the remainder to False'''
+ '''If None is given then all dynamic properties are set to False'''
+
+ #print("qt_property_set/", property_state, self.pv_name, self.qt_object_name, self.color_mode)
+ #if property_state in self.qt_property_states.keys():
+ for _property, _value in self._qt_property_selected.items(): #states.items():
+ if _property == property_state:
+ self.setProperty(_property, True)
+ self._qt_property_selected[_property] = True
+ else:
+ self.setProperty(_property, False)
+ self._qt_property_selected[_property] = False
+
+ #print("qt_property_set//", self.pv_name, self.qt_object_name, self.color_mode)
+
+ #l = self.dynamicPropertyNames()
+ #for i in range (0, len(l)):
+ # print(i, l[i])
+ #return self._qt_property_selected
+
+ def qt_property_disconnect(self, redraw=False):
+ '''Set Qt disconnect property value.'''
+
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.DISCONNECTED)
+
+ '''
+ if not self.initialize_complete:
+ self.setStatusTip("PV={0} was never connected".format(self.pv_name))
+ else:
+ self.setStatusTip("PV={0} is presently disconnected".format(self.pv_name))
+ '''
+ #print("qt_property_disconnect", self.pv_name, self.qt_object_name, self.color_mode)
+ self.qt_style_polish()
+
+ return #self._qt_property_selected
+
+
+ def qt_property_reconnect(self, redraw=False):
+ '''Set Qt connected property value.'''
+ #self.setObjectName("PyCafe")
+ #self.setToolTip(self.pv_name)
+ #l = self.dynamicPropertyNames()
+ #for i in range (0, len(l)-1):
+ # print(i, l[i])
+ #self.setProperty(str(l[i],'utf-8'), False)
+
+ if self.qt_object_name == self.PV_READBACK:
+ self._qt_readback_color_mode()
+ #self.setStatusTip(self.status_tip)
+
+
+ elif self.qt_object_name == self.PV_CONTROLLER:
+ if self.color_mode == self.ACT_ON_BEAM:
+ #self.setStatusTip("PV setting acts directly on beam")
+ pass
+ else:
+ self.color_mode = self.NOT_ACT_ON_BEAM
+ #self.setStatusTip("PV setting does not influence beam")
+
+
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.color_mode)
+
+ #print("qt_property_reconnect", self.pv_name, self.qt_object_name, self.color_mode)
+
+ #l = self.dynamicPropertyNames()
+ #for i in range (0, len(l)):
+ # print(i, l[i])
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_major(self, redraw=False):
+ '''Set Qt MAJOR property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.ALARM_SEV_MAJOR)
+ self.setStatusTip("{0} reports value in MAJOR alarm state!".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_minor(self, redraw=False):
+ '''Set Qt MINOR property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.ALARM_SEV_MINOR)
+ self.setStatusTip("{0} reports value in MINOR alarm state!".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_no_alarm(self, redraw=False):
+ '''Set Qt READBACK_ALARM property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.READBACK_ALARM)
+ self.setStatusTip("{0} reports value in normal state".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_alarm_sev_invalid(self, redraw=False):
+ '''Set Qt INVALID property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.ALARM_SEV_INVALID)
+ self.setStatusTip("PV={0} reports an INVALID value!".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_static(self, redraw=False):
+ '''Set Qt STATIC property value.'''
+ self._qt_dynamic_property_set(self.READBACK_STATIC)
+ self.setStatusTip("PV={0} does not have an alarm state".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_property_daq_stopped(self, redraw=False):
+ '''Set Qt STOPPED property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.DAQ_STOPPED)
+ self.setStatusTip("PV={0} reports DAQ has stopped".format(self.pv_name))
+ self.qt_style_polish()
+
+
+ def qt_property_daq_paused(self, redraw=False):
+ '''Set Qt STOPPED property value.'''
+ #self._qt_property_selected =
+ self._qt_dynamic_property_set(self.DAQ_PAUSED)
+ self.setStatusTip("PV={0} reports DAQ has paused".format(self.pv_name))
+ self.qt_style_polish()
+
+ def qt_style_polish(self, redraw=False):
+ if redraw:
+ self.style().unpolish(self)
+ self.style().polish(self)
+ event=QEvent(QEvent.StyleChange)
+ QApplication.sendEvent(self, event);
+ self.update()
+ self.updateGeometry()
+ else:
+ #self.style().unpolish(self)
+ self.style().polish(self)
+ QApplication.processEvents()
+
+ def pv_status_text_header(self, source="Channel Access"):
+ _source = source
+ _source_separator = "----------------------------------------"
+ _text = """
+
+ Widget: {0} ({1}, {2})
+
+ """.format(self.widget_class, self.qt_object_name, self.color_mode)
+
+ if self.msg_press_value is not None:
+ _text += """
+ On press, sends value: {0}
+ """.format(self.msg_press_value, "DarkOrchid")
+
+ if self.msg_release_value is not None:
+ _text += """
+ On release, sends value: {0}
+ """.format(self.msg_release_value, "DarkOrchid")
+
+ if self.pv_within_daq_group:
+ if self.qt_object_name in (self.PV_DAQ_BS,):
+ _ds_color = "Navy Blue"
+ else:
+ _ds_color = "Black"
+ else:
+ _ds_color = "Black"
+
+ _text += """
+ {0}
+ Data source: {1}
+ {0}
+ PV: {2}
+ """.format(_source_separator, _source, self.pv_name, "DarkOrchid",
+ _ds_color)
+
+ if self.desc is None:
+ self.set_desc()
+
+ if self.desc == "":
+ _text += """
+ """
+ return _text
+
+ _text += """
+
+ Description: {6}
+
+ """.format(self.widget_class, self.qt_object_name, \
+ self.color_mode, _source_separator, _source, \
+ self.pv_name, self.desc, "DarkOrchid"
+ )
+ return _text
+
+ def pv_status_text_enum(self):
+
+ _val_enum = None
+ _value = self.pvd.value[0]
+ if isinstance(_value, str):
+ _val_enum = self.cafe.getEnumFromString(self.handle, _value)
+ elif _value is not None:
+ _val_enum = self.cafe.getStringFromEnum(self.handle, _value)
+
+ _color = "Blue"
+
+ #To catch case where channel is called by user
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ _text = """
+
+ Value: {1} [{2}]
+ """.format(_color, _value , _val_enum
+ )
+
+ return _text
+
+ def pv_status_text_data(self):
+
+ _value_str = ""
+ _first_end = 9
+ _end_range = min(self.pvd.nelem, _first_end)
+ if _end_range > 1:
+ _value_str = "[ "
+ for i in range (0, _end_range):
+ _value = self.pvd.value[i]
+ if _value is None:
+ _value = '0'
+ if isinstance(_value, str):
+ _value_str += _value
+ elif isinstance(_value, int):
+ _value_str += str(_value)
+ else:
+ if self.pv_ctrl is not None:
+ _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
+ _value_str += _value_form.format(
+ round(_value, self.pv_ctrl.precision))
+ if i < (_end_range-1):
+ _value_str += " "
+
+ if self.pvd.nelem > _first_end:
+ _value_str += " ... "
+ _value = self.pvd.value[self.pvd.nelem-1]
+ if isinstance(_value, str):
+ _value_str += _value
+ elif isinstance(_value, int):
+ _value_str += str(value)
+ else:
+ if self.pv_ctrl is not None:
+ _value_form = ("{:<.%sf}" % self.pv_ctrl.precision)
+ _value_str += _value_form.format(
+ round(_value, self.pv_ctrl.precision))
+ _value_str += " "
+ if _end_range > 1:
+ _value_str += "]"
+
+ _color = "Blue"
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+
+
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ _text = """
+
+ Value: {1} {2}
+ """.format(_color, _value_str, self.units)
+
+ return _text
+
+
+ def pv_status_text_timestamp(self):
+ _status_not_ok_color = "IndianRed"
+ _status_ok_color = "DimGray"
+ _ts_color = "Blue"
+ _color = _status_ok_color
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _ts_color = "White"
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _ts_color = "White"
+ _color = "White"
+
+ elif not self.cafe.isConnected(self.handle):
+ _ts_color = "White"
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _ts_color = "White"
+
+
+ if self.pvd.status != self.cyca.ICAFE_NORMAL:
+ _color = _status_not_ok_color
+ _text = """
+ Timestamp: {2}
+ Status: {3}
{4}
+ """.format( _ts_color, _color, self.pvd.tsDateAsString, \
+ self.pvd.statusAsString, \
+ self.cafe.getStatusInfo(self.pvd.status))
+
+ return _text
+
+
+ def pv_status_text_alarm(self):
+ _text ="""
+ """
+ _color = "DimGray"
+
+
+ #To catch DAQ case
+ if self.pv_within_daq_group:
+
+
+ if self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
+ _color = "Yellow"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
+ _color = "Red"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
+ _color = "White"
+
+ if self.qt_object_name in (self.PV_DAQ_BS):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DAQ_PAUSED,
+ self.DISCONNECTED):
+ _color = "White"
+ elif self.qt_object_name in (self.PV_DAQ_CA):
+ if self.qt_dynamic_property_get() in (self.DAQ_STOPPED,
+ self.DISCONNECTED):
+ _color = "White"
+
+
+ elif not self.cafe.isConnected(self.handle):
+ _color = "White"
+
+ elif self.pvd.status == self.cyca.ICAFE_CA_OP_CONN_DOWN:
+ _color = "White"
+
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MINOR:
+ _color = "Yellow"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_MAJOR:
+ _color = "Red"
+ elif self.pvd.alarmSeverity == self.cyca.SEV_INVALID:
+ _color = "White"
+
+
+ _text += """
+ Alarm status: {1}
+ Alarm severity: {2}
+ """.format(_color, self.pvd.alarmStatusAsString,
+ self.pvd.alarmSeverityAsString)
+
+ return _text
+
+ def pv_access(self):
+ _accessIs = ""
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info.accessRead:
+ _accessIs += "Read"
+ if self.pv_info.accessWrite:
+ _accessIs += "Write"
+ return _accessIs
+
+ def pv_status_text_enum_metadata(self):
+ _text = """
+ ENUM strings: {2}
+ Data type (native): {3}
+ Record type: {4}
+ RW Access: {5}
+ IOC: {6}
+ """.format( "MediumBlue", "DarkOrchid", self.pvc.enumStrings,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+ def pv_status_text_metadata(self):
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ if self.record_type in ["stringin", "stringout"]:
+ _text = """
+ Data type (native): {3}
+ Record type: {4}
+ RW Access: {5}
+ IOC: {6}
+ """.format("MediumBlue", self.pvd.nelem, self.pvc.precision,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+ _text = """
+ """
+ if self.pvd.nelem > 1:
+ _text += """
+ Nelem: {1}
+ """.format("MediumBlue", self.pvd.nelem)
+
+ _text += """
+ Precision (PV): {1}
+ Data type (native): {2}
+ Record type: {3}
+ RW Access: {4}
+ IOC: {5}
+ """.format("MediumBlue", self.pvc.precision,
+ self.pv_info.dataTypeAsString,
+ self.record_type, self.pv_access(),
+ self.pv_info.hostName)
+ return _text
+
+
+ def pv_status_text_alarm_limits(self, ):
+
+ if self.pv_info is None:
+ self.pv_info = self.cafe.getChannelInfo(self.handle)
+ if self.pv_info is not None and self.record_type is None:
+ if "Not Supported" in self.pv_info.className:
+ _rtype = self.cafe.get(self.pv_name.split(".")[0] + ".RTYP")
+ self.record_type = _rtype if _rtype is not None else self.pv_info.className
+ self.cafe.close(self.pv_name.split(".")[0] + ".RTYP")
+ else:
+ self.record_type = self.pv_info.className
+
+ _text ="""
+ """
+
+ #No all record types have alarms
+ #className is not supported at psi since introduction of the linux ca gateway
+ #Not Supported by Gateway
+
+ #self.pv_info.show()
+ #self.pv_ctrl.show()
+ #print(self.record_type)
+ #print(self._alarm_severity_record_types)
+
+ if "Not Supported" in str(self.record_type):
+ pass
+ elif self.record_type not in self._alarm_severity_record_types:
+ return _text
+
+ if self.pvc.lowerAlarmLimit == 0 and self.pvc.upperAlarmLimit == 0 and \
+ self.pvc.lowerWarningLimit == 0 and self.pvc.upperWarningLimit == 0:
+
+ return _text
+
+ if self.cafe.hasAlarmStatusSeverity(self.handle): # or "Not Supported" in self.pv_info.className:
+ _text = """
+ Lower/Upper alarm limit: {1} / {4}
+ Lower/Upper warning limit: {2} / {3}
+
+ """.format("MediumBlue",
+ self.pvc.lowerAlarmLimit, self.pvc.lowerWarningLimit,
+ self.pvc.upperWarningLimit, self.pvc.upperAlarmLimit)
+ return _text
+
+ def pv_status_text_display_limits(self):
+ _text ="""
+ """
+ if self.pvc.lowerDisplayLimit == 0 and self.pvc.upperDisplayLimit == 0 and \
+ self.pvc.lowerControlLimit == 0 and self.pvc.upperControlLimit == 0:
+ return _text
+ _text = """
+ Lower/Upper control limit: {3} / {4}
+ Lower/Upper display limit: {1} / {2}
+
+ """.format("MediumBlue",
+ self.pvc.lowerDisplayLimit, self.pvc.upperDisplayLimit,
+ self.pvc.lowerControlLimit, self.pvc.upperControlLimit)
+ return _text
+
+
+
+
+ def pv_status_text(self):
+ '''pv metadata to accompany widget's dialog box.'''
+ QApplication.processEvents()
+ _source = "Channel Access"
+
+ if self.pv_within_daq_group:
+ if self.qt_object_name == self.PV_DAQ_BS:
+ _source = "DAQ (Beam Synchronous)"
+ #self.pvd written to in receive_daq_update
+ elif self.qt_object_name == self.PV_DAQ_CA:
+ _source = "DAQ (Channel Access)"
+ self.pvd = self.cafe.getPVCache(self.handle)
+ if self.pvd.pulseID > 0:
+ _source += "
Pulse ID: {0}".format(self.pvd.pulseID)
+ else:
+ self.pvd = self.cafe.getPVCache(self.handle)
+
+ ##For testing...
+ ##self.pvd.status = self.cyca.ICAFE_CA_OP_CONN_DOWN
+ ##self.pvd.statusAsString = 'ICAFE_CA_OP_CONN_DOWN'
+ #i, pvd, pvc = self.cafe.getChannelDataStore(self.handle)
+ self.pvc = self.cafe.getCtrlCache(self.handle)
+ #self.pvc.show()
+
+ _text_data ="""
+ """
+
+ if self.pvd.status == self.cyca.ECAFE_INVALID_HANDLE:
+ _text_data = """ Status: {1}
{2}
+ """.format("Blue", "Channel closed while DAQ in STOP state.",
+ "PV info requires DAQ to be in RUN/PAUSED state" )
+
+
+ elif self.pvd.status == self.cyca.ICAFE_CS_NEVER_CONN:
+ _text_data = """ Status: {1}
{2}
+ """.format("Red", self.pvd.statusAsString, self.cafe.getStatusInfo(self.pvd.status))
+
+ elif self.pvc.noEnumStrings > 0:
+ _text_data = self.pv_status_text_enum() + \
+ self.pv_status_text_timestamp() + \
+ self.pv_status_text_alarm() + \
+ self.pv_status_text_enum_metadata()
+
+ else:
+ _text_data = self.pv_status_text_data()+ \
+ self.pv_status_text_timestamp() + \
+ self.pv_status_text_alarm() + \
+ self.pv_status_text_metadata() + \
+ self.pv_status_text_alarm_limits() + \
+ self.pv_status_text_display_limits()
+
+ self.pv_message_in_a_box.setText(
+ self.pv_status_text_header(source=_source) + _text_data
+ )
+ QApplication.processEvents()
+ self.pv_message_in_a_box.exec()
+
+
+ def lookup_archiver(self):
+ '''Plot pvdata from archiver.'''
+ #"https://ui-data-api.psi.ch/prepare?channel = sf-archiverappliance/"
+ urlIs = self.settings.urlArchiver
+ urlIs = urlIs + self.pv_name
+
+ if not QDesktopServices.openUrl(QUrl(urlIs)): #, QUrl.TolerantMode)
+ if showMessage is not None:
+ self.showMessage(MsgSeverity.ERROR, __pymodule__, _line(),
+ "Failed to open URL {0}".format(urlIs))
+
+ def lookup_databuffer(self):
+ '''Plot beam synchronous pvdata from databuffer.'''
+ #""https://ui-data-api.psi.ch/prepare?channel = sf-databuffer/"
+ urlIs = self.settings.urlDatabuffer
+ urlIs = urlIs + self.pv_name
+
+ if not QDesktopServices.openUrl(QUrl(urlIs)): #, QUrl.TolerantMode)
+ if showMessage is not None:
+ self.showMessage(MsgSeverity.ERROR, __pymodule__, _line(),
+ "Failed to open URL {0}".format(urlIs))
+ QApplication.processEvents()
+
+ def strip_chart(self):
+ '''PShell strip chart.'''
+ configStr = "-config = [[[true,\"" + self.pv_name + "\",\"Channel\",1,1]]]"
+ commandStr = "/sf/op/bin/strip_chart"
+ argStr = ["-nlaf", "-start", configStr, "&"]
+ QProcess.startDetached(commandStr, argStr)
+
+
+ def display_parameters(self):
+ display_wgt = QDialog(self)
+
+ _rect = display_wgt.geometry() #get current geometry of help window
+ _parentRect = self.context_menu.geometry() # QRect(100, 1000, 640, 480) #get current geometry of this window
+ #print(_rect, _parentRect)
+ _rect.moveTo(display_wgt.mapToGlobal(
+ QPoint(_parentRect.x() + _parentRect.width() - _rect.width(),
+ _parentRect.y())))
+
+ display_wgt.setGeometry(_rect)
+
+ #This has no effect
+ #display_wgt.setWindowModality(Qt.WindowModal) #Qt.ApplicationModal Qt.ApplicationModal
+ display_wgt.setWindowTitle(self.pv_name)
+ layout = QVBoxLayout()
+ #print("sender==================>", self.sender(), self)
+ #self.the_gw = self
+ #print("getNativeDataType", self.cafe.getDataTypeNative(self.handle))
+
+ precision_flag = True
+ if self.pv_ctrl is not None:
+ if self.pv_ctrl.precision <= 0:
+ precision_flag = False
+ if self.cafe.getDataTypeNative(self.handle) in (
+ self.cyca.CY_DBR_FLOAT, self.cyca.CY_DBR_DOUBLE) and precision_flag:
+ #precision user
+ _hbox_wgt = QWidget()
+ _hbox = QHBoxLayout()
+ precision_user_label = QLabel("Precision (user):")
+ self.precision_user_wgt = QSpinBox(self)
+ self.precision_user_wgt.setFocusPolicy(Qt.NoFocus)
+ self.precision_user_wgt.setValue(int(self.precision))
+ if self.pv_ctrl is not None:
+ _max = self.pv_ctrl.precision
+ else:
+ _max = 6
+ self.precision_user_wgt.setMaximum(_max)
+ self.precision_user_wgt.valueChanged.connect(
+ self.precision_user_changed)
+ _hbox.addWidget(precision_user_label)
+ _hbox.addWidget(self.precision_user_wgt)
+ _hbox_wgt.setLayout(_hbox)
+
+ precision_user_label.setFixedWidth(110)
+ self.precision_user_wgt.setFixedWidth(35)
+ _hbox_wgt.setFixedWidth(160)
+
+ #precision ioc
+ _hbox2_wgt = QWidget()
+ _hbox2 = QHBoxLayout()
+ precision_ioc_label = QLabel("Precision (ioc): ")
+ precision_ioc = QPushButton(self)
+ precision_ioc.setText(" {} ".format(_max))
+ precision_ioc.clicked.connect(self.precision_ioc_reset)
+
+ _hbox2.addWidget(precision_ioc_label)
+ _hbox2.addWidget(precision_ioc)
+ _hbox2_wgt.setLayout(_hbox2)
+
+ precision_ioc_label.setFixedWidth(110)
+ precision_ioc.setFixedWidth(20)
+ _hbox2_wgt.setFixedWidth(145)
+
+ layout.addWidget(_hbox_wgt)
+ layout.addWidget(_hbox2_wgt)
+
+ #precision refresh rate
+ _hbox3_wgt = QWidget()
+ _hbox3 = QHBoxLayout()
+ refresh_freq_label = QLabel("Refresh rate: ")
+ _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
+ self.notify_freq_hz_default
+
+ self.refresh_freq_combox_idx_dict = {0:0, 1:10, 2:5, 3:2, 4:1, 5:0.5,
+ 6:_default_refresh_val}
+ refresh_freq = QComboBox(self)
+ refresh_freq.addItem('direct')
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[1]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[2]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[3]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[4]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[5]))
+
+ _default_text = 'default (direct)' if _default_refresh_val == 0 else \
+ 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
+
+ refresh_freq.addItem(_default_text)
+
+
+ for key, value in self.refresh_freq_combox_idx_dict.items():
+ if value == self.notify_freq_hz:
+ refresh_freq.setCurrentIndex(key)
+ break
+ refresh_freq.currentIndexChanged.connect(self.refresh_rate_changed)
+
+
+ _hbox3.addWidget(refresh_freq_label)
+ _hbox3.addWidget(refresh_freq)
+ _hbox3_wgt.setLayout(_hbox3)
+
+ refresh_freq_label.setFixedWidth(110)
+ refresh_freq.setFixedWidth(115)
+ _hbox3_wgt.setFixedWidth(235)
+
+ layout.addWidget(_hbox3_wgt)
+
+ layout.setAlignment(Qt.AlignLeft)
+ layout.setContentsMargins(10, 0, 0, 0)
+ layout.setSpacing(0)
+
+ display_wgt.setMinimumWidth(340)
+ display_wgt.setLayout(layout)
+
+ display_wgt.exec()
+ QApplication.processEvents()
+
+ def precision_ioc_reset(self):
+ if self.pv_ctrl is not None:
+ self.precision_user = self.pv_ctrl.precision
+ self.precision = self.pv_ctrl.precision
+ if self.precision is not None:
+ self.precision_user_wgt.setValue(self.precision)
+ #self.precision_user_changed(self.precision)
+ #_value = self.cafe.getCache(self.handle)
+ #self.trigger_monitor_float.emit(_value, 1, 0)
+
+ def precision_user_changed(self, new_value):
+ self.precision_user = new_value
+ self.precision = new_value
+
+ _pvd = self.cafe.getPVCache(self.handle)
+
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ self.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+
+ '''
+ _value = self.cafe.getCache(self.handle)
+ #print("widget", self.widget, self.widget.sender())
+ if _value is not None:
+ #self.post_display_value(_value)
+ self.trigger_monitor_float.emit(_value, 1, 0)
+ '''
+
+ def refresh_rate_changed(self, new_idx):
+ _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
+ self.notify_milliseconds = 0 if _notify_freq_hz == 0 else \
+ 1000 / _notify_freq_hz
+ self.notify_freq_hz = _notify_freq_hz
+
+ if self.notify_unison:
+ self.notify_unison = False
+ self.monitor_stop()
+ self.monitor_start()
+
+ else:
+ self.cafe.updateMonitorPolicyDeltaMS(self.handle,
+ self.monitor_id,
+ self.notify_milliseconds)
+
+ #https://doc.qt.io/qt-5.9/qtwidgets-mainwindows-menus-example.html
+ #Since Qt5 this has to be implemented in order to avoid the Select All dialogue button appearing..
+ def contextMenuEvent(self, event):
+ return
+
+ def showContextMenu(self):
+ self.context_menu.exec(QCursor.pos())
+
+ def mousePressEvent(self, event):
+ '''Action on mouse press event.'''
+ button = event.button()
+ if button == Qt.RightButton:
+ #contextMenu.exec(event.globalPos())
+ self.context_menu.exec(QCursor.pos())
+ self.clearFocus()
+
+ def mouseReleaseEvent(self, event):
+ event.ignore()
+
diff --git a/pvwidgets.py b/pvwidgets.py
new file mode 100644
index 0000000..29f50a9
--- /dev/null
+++ b/pvwidgets.py
@@ -0,0 +1,3406 @@
+''' Module with channel access enabled QtWidgets.'''
+__author__ = 'Jan T. M. Chrin'
+
+import re
+import sys
+import time
+
+import collections
+import numpy as np
+from sklearn.linear_model import LinearRegression
+from sklearn.metrics import mean_squared_error, r2_score
+from distutils.version import LooseVersion
+from functools import reduce as func_reduce
+
+from qtpy.QtCore import (qVersion, QEvent, QEventLoop, QObject, QPoint, QSize,
+ Qt, QThread, QTimer, Signal, Slot)
+from qtpy.QtGui import (QCloseEvent, QColor, QCursor, QFont, QFontMetricsF,
+ QIcon, QKeySequence)
+from qtpy.QtCore import __version__ as QT_VERSION_STR
+from qtpy.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QAction,
+ QApplication, QCheckBox, QComboBox, QDialog,
+ QDockWidget, QDoubleSpinBox, QFrame, QGroupBox,
+ QHeaderView, QHBoxLayout, QLabel, QLineEdit,
+ QListWidget, QMenu, QMessageBox, QPushButton,
+ QSpinBox, QStyle, QStyleOptionSpinBox, QTableWidget,
+ QTableWidgetItem, QVBoxLayout, QWidget)
+
+import pyqtgraph as pg
+from pyqtgraph import PlotWidget
+
+from caqtwidgets.pvgateway import PVGateway
+
+
+class AppQLineEdit(QLineEdit):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+
+
+class CAQLineEdit(QLineEdit, PVGateway):
+ '''Channel access enabled QLineEdit widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ notify_freq_hz: int = 0):
+ #super(CAQLineEdit, self).__init__(parent)
+
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback,
+ notify_freq_hz=notify_freq_hz )
+
+ self.is_initialize_complete()
+ self.configure_widget()
+
+ if not self.pv_within_daq_group:
+ self.monitor_start()
+
+ '''
+ print("fixed width==========>", self.width())
+ print ("size", self.size()) # (100, 30)
+ print ("sizeHint", self.sizeHint()) # (190, 37)
+ print ("min Size", self.minimumSize()) #(0, 0)
+ print ("min SizeHint", self.minimumSizeHint()) # (26, 37)
+ print ("sizePolicy", self.sizePolicy()) #Object
+ '''
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(object, str, int)
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.NoFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+ _width_scaling_factor = 1.15
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ if self.pv_within_daq_group:
+ self.qt_property_initial_values(qt_object_name = self.PV_DAQ_CA)
+ else:
+ self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+ #renove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearFocus()
+ del event
+
+class CAQLabel(QLabel, PVGateway):
+ '''Channel access enabled QLabel widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+
+ trigger_connect = Signal(int, str, int)
+
+ trigger_daq = Signal(object, str, int)
+ trigger_daq_int = Signal(object, str, int)
+ trigger_daq_str = Signal(object, str, int)
+
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = "",
+ notify_freq_hz: int = 0):
+ #super(CAQLabel, self).__init__(parent)
+
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback,
+ notify_freq_hz= notify_freq_hz)
+
+ self.is_initialize_complete()
+
+ self.configure_widget()
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+ @Slot(object, str, int)
+ def receive_daq_update(self, daq_pvd, daq_mode, daq_state):
+ PVGateway.receive_daq_update(self, daq_pvd, daq_mode, daq_state)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ #print(self, self.pv_name, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ # if (time.monotonic() - self.time_monotonic) < 0.9945:
+ # return
+ #self.time_monotonic = time.monotonic()
+ #self.lock.acquire()
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+ #self.lock.release()
+ #QApplication.processEvents()
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.NoFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth((qrect.width() * _width_scaling_factor))
+ #self.setFixedWidth(140)
+
+ if self.pv_within_daq_group:
+ self.qt_property_initial_values(qt_object_name = self.PV_DAQ_CA)
+ else:
+ self.qt_property_initial_values(qt_object_name = self.PV_READBACK)
+
+#For use with CAQMenu
+class QLineEditExtended(QLineEdit):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.parent = parent
+
+ def mousePressEvent(self, event):
+ button = event.button()
+ if button == Qt.RightButton:
+ self.parent.showContextMenu()
+ elif button == Qt.LeftButton:
+ self.parent.mousePressEvent(event)
+
+class CAQMenu(QComboBox, PVGateway):
+ '''Channel access enabled QMenu widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, prefix: str = "", suffix: str = ""):
+ #super(CAQMenu, self)
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete()
+
+ self.configure_widget()
+
+ #After configure:widget
+ self.currentIndexChanged.connect(self.value_change)
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+
+ def configure_widget(self):
+
+ self.previousIndex = None
+
+ self.setFocusPolicy(Qt.NoFocus)
+ self.setEditable(True)
+ self.setLineEdit(QLineEditExtended(self))
+ self.lineEdit().setReadOnly(True)
+ self.lineEdit().setAlignment(Qt.AlignCenter)
+
+ #self.lineEdit().setMouseTracking(True)
+ #self.setAttribute(Qt.WA_MouseNoMask)
+ #self.lineEdit().setAttribute(Qt.WA_NoMousePropagation)
+ #self.lineEdit().setAttribute(Qt.WA_WindowPropagation)
+
+ enumStringList = self.cafe.getEnumStrings(self.handle)
+
+ self.addItems(enumStringList)
+ for i in range(0, self.count()):
+ self.setItemData(i, Qt.AlignCenter, Qt.TextAlignmentRole);
+
+ '''
+ self.ensurePolished()
+ print(dir(self.style().property("font")))
+ f=self.style().property("font")
+ self.style().unpolish(self);
+ self.style().polish(self);
+ self.update()
+ '''
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.1
+
+ self.setFixedHeight(fm.lineSpacing()*1.8)
+ self.setFixedWidth((qrect.width()+40) * _width_scaling_factor)
+
+ self.qt_property_initial_values(qt_object_name=self.PV_CONTROLLER)
+
+ def post_display_value(self, value):
+ '''Convert value to index'''
+ if "setCurrentIndex" in dir(self):
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if isinstance(value, str):
+ self.setCurrentIndex(self.cafe.getEnumFromString(self.handle,
+ value))
+
+ elif isinstance(value, int):
+ self.setCurrentIndex(value)
+ #Should not happen
+ elif isinstance(value, float):
+ self.setCurrentIndex(int(value))
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+ #self.previousIndex = self.currentIndex()
+ return
+ else:
+ print(("ERROR: overloaded post_display_value: 'setCurrentIndex' "
+ "method does not exist!"))
+
+
+ def value_change(self, indx):
+
+ status = self.cafe.set(self.handle, indx)
+
+ if status != self.cyca.ICAFE_NORMAL:
+ #self.showSetErrorMsg(status)
+
+ value = self.cafe.getCache(self.handle, 'int')
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if value is not None:
+ self.setCurrentIndex(value)
+ else:
+ if self.previousIndex is not None:
+ self.setCurrentIndex(self.previousIndex)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("CAQMenu set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+
+ def mousePressEvent(self, event):
+
+ button = event.button()
+ if button == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+
+ elif self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ event.ignore()
+ return
+ else:
+ QComboBox.mousePressEvent(self, event)
+
+ self.previousIndex = self.currentIndex()
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ for i in range(0, self.count()):
+ self.setItemIcon(i, QIcon(":/forbidden.png"))
+ self.setStyleSheet(("QComboBox {background: transparent}" +
+ "QComboBox::drop-down {image: url(:/forbidden.png)}"))
+
+ def leaveEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ for i in range(0, self.count()):
+ self.setItemIcon(i, QIcon())
+ self.setStyleSheet("QComboBox::drop-down {background: transparent}")
+
+
+ #The widget should not gain focus by using the mouse wheel.
+ #This is accomplished by setting the focus policy to Qt.StrongFocus.
+ #The widget should only accept wheel events if it already has the focus.
+ #This is accomplished by reimplementing QWidget.wheelEvent within a QSpinBox subclass:
+ def wheelEvent(self, event):
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QComboBox.wheelEvent(self, event)
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ '''Triggered by monitor signal'''
+
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+
+class CAQMessageButton(QPushButton, PVGateway):
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ notify_freq_hz: int = 0,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, msg_label: str = "",
+ msg_press_value = None, msg_release_value =None,
+ start_monitor=False):
+ super().__init__(parent=parent, pv_name=pv_name,
+ monitor_callback=monitor_callback,
+ notify_freq_hz=\
+ notify_freq_hz,
+ pv_within_daq_group=pv_within_daq_group,
+ color_mode=color_mode, show_units=show_units,
+ msg_label=msg_label,
+ connect_callback=self.py_connect_callback)
+
+
+ self.msg_press_value = msg_press_value
+ self.msg_release_value = msg_release_value
+
+ if self.msg_press_value is not None:
+ self.pressed.connect(self.act_on_pressed)
+ if self.msg_release_value is not None:
+ self.released.connect(self.act_on_released)
+
+ self.msg_label = msg_label
+ self.suggested_text = self.msg_label
+ _suggested_text_length = len(self.suggested_text)+3
+ self.suggested_text = self.suggested_text.rjust(
+ _suggested_text_length,"^")
+
+ self.configure_widget()
+
+ self.msg_press_status = self.cyca.ICAFE_NORMAL
+ self.msg_release_status = self.cyca.ICAFE_NORMAL
+ self.msg_report_status = "PV={0}\n".format(self.pv_name)
+ self.msg_has_error = False
+
+ if not self.pv_within_daq_group and start_monitor:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ #print(self, self.pv_name, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.StrongFocus)
+ #self.setAutoDefault(True)
+ self.setCheckable(True) #Recognizes press and release states
+ #self.setChecked(True)
+ #self.setFlat(True)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.0
+
+ self.setText(self.msg_label)
+ self.setFixedHeight((fm.lineSpacing()*2.0))
+ self.setFixedWidth((qrect.width() * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+
+ def leaveEvent(self, event):
+ #if self.pv_info.accessWrite == 0:
+ if self.property("readOnly"):
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+ '''
+ def enterEvent(self, event):
+ if self.pv_info.accessWrite == 0:
+ self.setEnabled(False)
+
+ def leaveEvent(self, event):
+ if not self.isEnabled():
+ self.setEnabled(True)
+ '''
+
+ def mouseReleaseEvent(self, event):
+ #print("LOCAL mouseRelease = = > This Event is required so that clicked is activated!!!!!!")
+ if self.msg_release_value is not None:
+ time.sleep(0.1)
+ QPushButton.mouseReleaseEvent(self, event)
+
+ def mousePressEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 1:
+ QPushButton.mousePressEvent(self, event)
+ if event.button() == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+
+ def act_on_pressed(self):
+ #print("caQPushButton press ValueChanged--> ")
+ if self.msg_press_value is not None:
+ self.msg_press_status = self.cafe.set(self.handle, self.msg_press_value)
+ if self.msg_press_status != self.cyca.ICAFE_NORMAL:
+ self.msg_report_status += ("Error in set operation (at press button):\n{0}\n"
+ .format(self.cafe.getStatusCodeAsString(self.msg_press_status)))
+ self.msg_has_error = True
+ qm = QMessageBox()
+ qm.setText(self.msg_report_status)
+ qm.exec()
+ QApplication.processEvents()
+
+ def act_on_released(self):
+ #print("caQPushButton release ValueChanged--> ")
+ if self.msg_release_value is not None:
+ self.msg_release_status = self.cafe.set(self.handle, self.msg_release_value)
+ if self.msg_release_status != self.cyca.ICAFE_NORMAL:
+ self.msg_report_status += ("Error in set operation (at release button):\n{0}\n"
+ .format(self.cafe.getStatusCodeAsString(self.msg_release_status)))
+ self.msg_has_error = True
+
+ if self.msg_has_error:
+ self.msg_has_error = False
+ self.pv_message_in_a_box.setText(self.msg_report_status)
+ self.pv_message_in_a_box.exec()
+ self.msg_report_status = "PV={0}\n".format(self.pv_name)
+ qm = QMessageBox()
+ qm.setText(self.msg_report_status)
+ qm.exec()
+ QApplication.processEvents()
+
+class CAQTextEntry(QLineEdit, PVGateway):
+ '''Channel access enabled QTextEntry widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete() #waits a fraction of a second
+
+ self.currentText =""
+ self.returnPressed.connect(self.valuechange)
+ self.configure_widget()
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ #print(self, self.pv_name, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+ qrect = fm.boundingRect(self.suggested_text)
+
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()+10) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+ def valuechange(self):
+ print(self.handle, self.text())
+ status = self.cafe.set(self.handle, self.text())
+ if status != self.cyca.ICAFE_NORMAL:
+ #elf.showSetErrorMsg(status)
+ if self.cafe.getNoMonitors(self.handle) == 0:
+ val = self.cafe.get(self.handle, 'native')
+ else:
+ val = self.cafe.getCache(self.handle, 'native')
+ #print("val", val)
+ if val is not None:
+ if isinstance(val, str):
+ strText = val
+ else:
+ valStr = ("{: .%sf}" % self.precision)
+ strText = valStr.format(round(val, self.precision)) ### + " " + self.units
+ print(strText, " precision ", self.precision)
+ self.setText(strText)
+ else:
+ #Do this for TextInfo cache
+ if self.cafe.getNoMonitors(self.handle) == 0:
+ val = self.cafe.get(self.handle, 'native')
+
+ def setText(self, value):
+
+ QLineEdit.setText(self, value)
+
+ #QLineEdit.setFixedWidth(self, QLineEdit.width(self)+10)
+ self.currentText = self.text()
+
+ #status = self.cafe.set(self.handle, value)
+ #if status ! = self.cyca.ICAFE_NORMAL:
+ #self.showSetErrorMsg(status)
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ def leaveEvent(self, event):
+
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ if self.text() != self.currentText:
+ QLineEdit.setText(self, self.currentText)
+
+ self.setCursorPosition(100)
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+ #def mouseMoveEvent(self, event):
+ # print("mouseMoveEvent", event.type())
+
+ def mousePressEvent(self, event):
+ if event.button() == Qt.RightButton:
+ PVGateway.mousePressEvent(self, event)
+ self.clearFocus()
+ return
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.cursorPositionAt(local_event_position)
+ self.setCursorPosition(local_cursor_position)
+
+
+
+
+
+class CAQSpinBox(QSpinBox, PVGateway):
+ '''Channel access enabled QTextEntry widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units = False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent, pv_name, monitor_callback,
+ pv_within_daq_group, color_mode, show_units, prefix,
+ suffix, connect_callback=self.py_connect_callback)
+ #super().__init__(parent=parent, pv_name=pv_name, monitor_callback=monitor_callback,
+ # pv_within_daq_group= pv_within_daq_group, color_mode=color_mode, show_units=show_units, prefix=prefix,
+ # suffix=suffix, connect_callback=self.py_connect_callback)
+
+
+ self.is_initialize_complete()
+
+ self.valueChanged.connect(self.value_change)
+ self.configure_widget()
+ if not self.pv_within_daq_group:
+ self.monitor_start()
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ #print(" py_connect_callback::::::::::::::::::::::::::::::::::::::::::::::::::::::::::..: START ")
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+ #print(" py_connect_callback:: END ")
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+
+ def configure_widget(self):
+ self.previousValue = None
+ self.currentValue = None
+ self.setFocusPolicy(Qt.StrongFocus)
+ self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ self.setAccelerated(False)
+ self.setLineEdit(QLineEditExtended(self))
+ self.lineEdit().setEnabled(True)
+ self.lineEdit().setReadOnly(False)
+ self.lineEdit().setAlignment(Qt.AlignLeft)
+ self.lineEdit().setFont(QFont("Sans Serif", 16))
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+
+ _suggested_text = self.max_control_abs_str
+ _added_text = ""
+
+ if self.show_units:
+ _added_text += " " + self.units
+ _suggested_text += self.units
+ if len(self.suffix) > 0:
+ _added_text += " " + self.suffix
+ _suggested_text += self.suffix
+
+ self.setSuffix(_added_text)
+
+ qrect = fm.boundingRect(_suggested_text)
+ _width_scaling_factor = 1.0
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+ if self.pv_ctrl is not None:
+ self.setRange(int(self.pv_ctrl.lowerControlLimit),
+ int(self.pv_ctrl.upperControlLimit))
+
+
+ def post_display_value(self, value):
+ '''Convert value to index'''
+ #print ("MON VALUE IN QSPINBOX ", value, " //////////// ", int(value))
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ self.setValue(int(round(value)))
+ self.blockSignals(False)
+ else:
+ self.setValue(int(round(value)))
+
+
+ def mousePressEvent(self, event):
+ _opt = QStyleOptionSpinBox()
+ self.initStyleOption(_opt)
+ _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxUp, self)
+ _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxDown, self)
+
+ #print(_rect_up.x(), _rect_down.x(), event.x(), event.y(), event.localPos(), event.pos())
+
+ self.previousValue = self.value()
+
+ if event.button() == Qt.LeftButton:
+ if _rect_up.contains(event.pos(), proper=True) or \
+ _rect_down.contains(event.pos(), proper=True):
+
+ if not self.cafe.isConnected(self.handle):
+ self.pv_message_in_a_box.setText(("Spinbox change value "
+ "events currently suspended\n"
+ "as channel {0} is disconnected.".format(self.pv_name)))
+ self.pv_message_in_a_box.exec()
+
+ return
+
+ QSpinBox.mousePressEvent(self, event)
+ #Clear Focus: only one step per mouse click.
+ self.clearFocus()
+
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.lineEdit().cursorPositionAt(local_event_position)
+
+ #print(local_event_position, " QSPINBOX VALUE POS ", local_cursor_position)
+ self.lineEdit().setCursorPosition(local_cursor_position)
+
+
+ PVGateway.mousePressEvent(self, event)
+
+ #Clear Focus: only one step per mouse click.
+ #self.clearFocus()
+
+ def setValue(self, intVal):
+ #print( QSpinBox.value(self), intVal)
+ #print( "setValue called//1//", intVal)
+ QSpinBox.setValue(self, intVal)
+ self.currentValue = self.value()
+ #print( "setValue called//2//", intVal)
+
+
+ def value_change(self, intVal):
+ #print("valuechange called", intVal, QSpinBox.value(self))
+ status = self.cafe.set(self.handle, intVal)
+
+ if status != self.cyca.ICAFE_NORMAL:
+
+ #print("previous current", self.previousValue, self.currentValue)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'int')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("Spinbox set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+ else:
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'int')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ self.parent.statusbar.showMessage(
+ (self.widget_class + " " +
+ self.cafe.getStatusCodeAsString(status)))
+
+
+
+ def enterEvent(self, event):
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+ def leaveEvent(self, event):
+ #if self.value() != self.currentValue:
+ # self.setValue(self.currentValue)
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+
+ def keyPressEvent(self, event):
+ #print("key press event", event.type(), hex(event.key()))
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ QSpinBox.keyPressEvent(self, event)
+ self.clearFocus()
+ elif event.key() in (Qt.Key_Up, Qt.Key_Down):
+ QSpinBox.keyPressEvent(self, event)
+ else:
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ QSpinBox.keyPressEvent(self, event)
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+ # The spin box should not gain focus by using the mouse wheel.
+ # This is accomplished by setting the focus policy to Qt.StrongFocus.
+ # The spin box should only accept wheel events if it already has the focus.
+ # This is accomplished by reimplementing QWidget.wheelEvent within a QSpinBox subclass:
+ def wheelEvent(self, event):
+ #print("wheelEvent", self.hasFocus())
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QSpinBox.wheelEvent(self, event)
+
+ # def enterEvent(self,event):
+ # print("EnterEvent set focusPolicy to Strong")
+ # self.setFocusPolicy(Qt.StrongFocus)
+
+
+
+class CAQDoubleSpinBox(QDoubleSpinBox, PVGateway):
+ '''Channel access enabled QDoubleSpinBox widget'''
+ trigger_monitor_float = Signal(float, int, int)
+ trigger_monitor_int = Signal(int, int, int)
+ trigger_monitor_str = Signal(str, int, int)
+ trigger_monitor = Signal(object, int)
+ trigger_connect = Signal(int, str, int)
+
+ def __init__(self, parent=None, pv_name: str = "", monitor_callback=None,
+ pv_within_daq_group: bool = False, color_mode = None,
+ show_units: bool = False, prefix: str = "", suffix: str = ""):
+ super().__init__(parent=parent, pv_name=pv_name,
+ monitor_callback=monitor_callback,
+ pv_within_daq_group= pv_within_daq_group,
+ color_mode=color_mode, show_units=show_units,
+ prefix=prefix, suffix=suffix,
+ connect_callback=self.py_connect_callback)
+
+ self.is_initialize_complete()
+ self.valueChanged.connect(self.valuechange)
+ self.configure_widget()
+
+ if self.pv_within_daq_group is False:
+ self.monitor_start()
+
+
+ def py_connect_callback(self, handle, pvname, status):
+ '''Callback function to be invoked on change of pv connection status.
+ '''
+ #print(" py_connect_callback::::::::::::::::::::::::::::::::::::::::::::::::::::::::::..: START ")
+ self.trigger_connect.emit(int(handle), str(pvname), int(status))
+ #print(" py_connect_callback:: END ")
+
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+ PVGateway.receive_monitor_update(self, value, status, alarm_severity)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ PVGateway.receive_connect_update(self, handle, pv_name, status)
+ #self.configure_widget()
+ #self.setSingleStep(0.1)
+
+
+ def configure_widget(self):
+ self.previousValue = None
+ self.currentValue = None
+ self.setFocusPolicy(Qt.StrongFocus)
+ self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ self.setAccelerated(False)
+ self.setLineEdit(QLineEditExtended(self))
+ #self.lineEdit().setObjectName("Controller")
+ #self.lineEdit().setProperty("notActOnBeam", True)
+ #self.lineEdit().setEnabled(False)
+ self.lineEdit().setReadOnly(False)
+ self.lineEdit().setAlignment(Qt.AlignRight)
+ self.lineEdit().setFont(QFont("Sans Serif", 12))
+
+ _stepsize = 10**(self.precision * -1)
+ self.setSingleStep(_stepsize)
+ self.setDecimals(self.precision)
+
+ fm = QFontMetricsF(QFont("Sans Serif", 12))
+
+ _suggested_text = self.suggested_text
+ _added_text = ""
+
+ if self.show_units:
+ _added_text += " " + self.units
+ _suggested_text += self.units
+ if len(self.suffix) > 0:
+ _added_text += " " + self.suffix
+ _suggested_text += self.suffix
+
+ self.setSuffix(_added_text)
+
+ qrect = fm.boundingRect(_suggested_text)
+
+ _width_scaling_factor = 1.15
+
+ self.setFixedHeight((fm.lineSpacing()*1.8))
+ self.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+
+ self.qt_property_initial_values(qt_object_name = self.PV_CONTROLLER)
+
+ if self.pv_ctrl is not None:
+ self.setRange(int(self.pv_ctrl.lowerControlLimit),
+ int(self.pv_ctrl.upperControlLimit))
+
+
+ #if self.cafe.isConnected(self.handle):
+ # self.setButtonSymbols(QAbstractSpinBox.UpDownArrows)
+ #else:
+ # self.setButtonSymbols(QAbstractSpinBox.NoButtons)
+
+ def post_display_value(self, value):
+ '''set value from monitor'''
+ #print("value: : ", value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+
+ self.setValue(value)
+ self.blockSignals(False)
+ else:
+ self.setValue(value)
+
+ def mousePressEvent(self, event):
+
+ _opt = QStyleOptionSpinBox()
+ self.initStyleOption(_opt)
+ _rect_up = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxUp, self)
+ _rect_down = self.style().subControlRect(QStyle.CC_SpinBox, _opt,
+ QStyle.SC_SpinBoxDown, self)
+ self.previousValue = self.value()
+
+ if event.button() == Qt.LeftButton:
+ if _rect_up.contains(event.pos(), proper=False) or \
+ _rect_down.contains(event.pos(), proper=False):
+
+ if not self.cafe.isConnected(self.handle):
+ self.pv_message_in_a_box.setText(("Spinbox change value "
+ "events currently suspended\n"
+ "as channel {0} is disconnected.".format(self.pv_name)))
+ self.pv_message_in_a_box.exec()
+ return
+
+ QDoubleSpinBox.mousePressEvent(self, event)
+
+ local_event_position = QPoint(event.x(), event.y())
+ local_cursor_position = self.lineEdit().cursorPositionAt(
+ local_event_position)
+ #print(local_event_position, " POS ", local_cursor_position)
+ self.lineEdit().setCursorPosition(local_cursor_position)
+
+
+ PVGateway.mousePressEvent(self, event)
+
+ #Clear Focus: only one step per mouse click.
+ #Not wanted for floats
+ #self.clearFocus()
+
+ def mouseReleaseEvent(self, event):
+ self.clearFocus()
+
+ def setValue(self, value):
+ #print("setValue called (B)", value, self.value())
+ self.currentValue = self.value()
+ QDoubleSpinBox.setValue(self, value)
+ #time.sleep(0.01)
+ #print("setValue called (A)", value, self.value())
+
+
+ def valuechange(self, fval):
+ #print("valuechange called, fval, value(), previous", fval, self.value(), self.previousValue)
+ status = self.cafe.set(self.handle, fval)
+
+
+ if status != self.cyca.ICAFE_NORMAL:
+
+ #print("previous current", self.previousValue, self.currentValue)
+
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'float')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+ self.pv_message_in_a_box.setText(
+ ("Spinbox set operation reports error:\n{0}"
+ .format(self.cafe.getStatusCodeAsString(status))))
+ self.pv_message_in_a_box.exec()
+
+ else:
+ if self.previousValue is not None:
+ self.setValue(self.previousValue)
+ else:
+ _value = self.cafe.getCache(self.handle, 'float')
+
+ if _value is not None:
+ self.setValue(_value)
+
+ self.parent.statusbar.showMessage(
+ (self.widget_class + " " +
+ self.cafe.getStatusCodeAsString(status)))
+
+
+ def enterEvent(self, event):
+ self.setFocusPolicy(Qt.StrongFocus)
+ #print("eventtype", event.type())
+ if self.pv_info is not None:
+ if self.pv_info.accessWrite == 0:
+ self.setProperty("readOnly", True)
+ self.qt_style_polish()
+ self.setReadOnly(True)
+
+ def leaveEvent(self, event):
+ #if self.value() != self.currentValue:
+ # self.setValue(self.currentValue)
+ if self.isReadOnly():
+ self.setReadOnly(False)
+ self.setProperty(self.qt_dynamic_property_get(), True)
+ self.qt_style_polish()
+
+ self.clearFocus()
+ self.setFocusPolicy(Qt.NoFocus)
+ del event
+
+ def keyPressEvent(self, event):
+ #print("key press event", event.type(), hex(event.key()))
+ if event.key() in (Qt.Key_Return, Qt.Key_Enter):
+ QDoubleSpinBox.keyPressEvent(self, event)
+ self.clearFocus()
+ elif event.key() in (Qt.Key_Up, Qt.Key_Down):
+ QDoubleSpinBox.keyPressEvent(self, event)
+ else:
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(True)
+ QDoubleSpinBox.keyPressEvent(self, event)
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.blockSignals(False)
+
+
+
+ #def keyReleaseEvent(self, event):
+ # print("key release")
+ # QDoubleSpinBox.keyReleaseEvent(self, event)
+
+
+ # The spin box should not gain focus by using the mouse wheel.
+ # This is accomplished by setting the focus policy to Qt.StrongFocus.
+ # The spin box should only accept wheel events if it already has the focus.
+ # This is accomplished by reimplementing QWidget.wheelEvent within a QSpinBox subclass:
+ def wheelEvent(self, event):
+ #print("wheelEvent", self.hasFocus())
+ if self.hasFocus() is False:
+ event.ignore()
+ else:
+ QDoubleSpinBox.wheelEvent(self, event)
+
+
+class reconnectQPushButton(QPushButton, QThread):
+ def __init__(self, parent=None):
+ super().__init__()
+ self.parent = parent
+ self.clicked.connect(self.onClicked)
+ self.isdirty = False
+ self._handles_to_reconnect = []
+ self.reconnectThread = None
+
+ #def __del__(self):
+ # print("D Called")
+ # self.reconnectThread.wait()
+
+ def onClicked(self, event):
+
+ self._handles_to_reconnect = []
+
+ for i in range(0, len(self.parent.pv_gateway)):
+ if self.parent.item(i, self.parent.no_columns-1).checkState() == Qt.Checked:
+ #print("handle", self.parent.pv_gateway[i].handle)
+ #self.parent.cafe.monitorStop(self.parent.pv_gateway[1].handle)
+ #self.parent.cafe.reconnect([self.parent.pv_gateway[1].handle])
+ self._handles_to_reconnect.append(self.parent.pv_gateway[i].handle)
+
+ #print("isConnected ", self.parent.cafe.isConnected(self.parent.pv_gateway[i].handle))
+
+ #self.reconnectThread = ReconnectThread(self.parent, self._handles_to_reconnect ) # QThread(self).create(self.reconnect)
+ #self.reconnectThread.start()
+ self.reconnect() #,self._handles_to_reconnect)
+ QApplication.processEvents()
+ #self.reconnectThread.wait()
+
+ def reconnect(self):
+ QApplication.processEvents()
+ #self.parent.cafe.printHandles()
+ #print(self._handles_to_reconnect)
+ self.isdirty = True
+ if len(self._handles_to_reconnect) > 0:
+ status=self.parent.cafe.reconnect(self._handles_to_reconnect)
+ self.isdirty = False
+ #Uncheck reconnected channels
+ for i in range(0, len(self.parent.pv_gateway)):
+ if self.parent.item(i, self.parent.no_columns-1).checkState() == Qt.Checked:
+ if self.parent.cafe.isConnected(self.parent.pv_gateway[i].handle):
+ self.parent.item(i, self.parent.no_columns-1).setCheckState(False)
+
+
+
+'''
+class ReconnectThread(QThread):
+
+ def __init__(self, parent, handles):
+ QThread.__init__(self)
+ self.parent=parent
+ self._handles = handles
+ print("Initialized")
+
+ def __del__(self):
+ self.wait()
+
+ def run(self):
+ print("reconnect")
+ self.isdirty = True
+ if len(self._handles) > 0:
+ #status=self.parent.cafe.reconnect(self._handles)
+ print("status")
+ self.isdirty = False
+ print("still running")
+'''
+
+class CAQTableWidget(QTableWidget):
+ '''Channel access enabled QTableWidget widget'''
+ #trigger_monitor_float = Signal(float, int, int)
+ #trigger_monitor_int = Signal(int, int, int)
+ #trigger_monitor_str = Signal(str, int, int)
+ #trigger_connect = Signal(int, str, int)
+
+ def hasNewData(self, _row, pv_data):
+
+ if self.pv_gateway[_row].pvd_previous is None:
+ return True
+
+ newDataFlag = False
+
+ if self.pv_gateway[_row].pvd_previous.ts[1] != pv_data.ts[1]:
+ newDataFlag = True
+ elif self.pv_gateway[_row].pvd_previous.ts[0] != pv_data.ts[0]:
+ newDataFlag = True
+ # Catch disconnect events(!!) and set newDataFlag only
+ elif self.pv_gateway[_row].pvd_previous.status != pv_data.status:
+ newDataFlag = True
+ return newDataFlag
+
+
+ def widget_update(self):
+
+ for _row, pvgate in enumerate(self.pv_gateway):
+ #for _row in range(0, len(self.pv_gateway)):
+ if not pvgate.notify_unison:
+ continue
+ _handle = pvgate.handle
+ _pvd = pvgate.cafe.getPVCache(_handle)
+
+ #Only if unison flag is set else return
+
+ #Does not cater for reconnections
+ #print(_row, self.pv_gateway[_row].pv_name, _pvd.status)
+ #if not self.hasNewData(_row, _pvd) and _pvd.status == \
+ # self.cyca.ICAFE_NORMAL:
+ #print(_row, " has no new data")
+ #continue
+
+ if _pvd.status in (self.cyca.ICAFE_CS_NEVER_CONN,
+ self.cyca.ICAFE_CA_OP_CONN_DOWN):
+ pvgate.pvd_previous = _pvd
+ continue
+
+ pvgate.pvd_previous = _pvd
+
+ #if timestamps the same - then skip
+ _value = _pvd.value[0]
+ _value = pvgate.format_display_value(_value)
+
+ qtwi = QTableWidgetItem(str(_value)+ " ")
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+
+ self.setItem(_row, self.no_columns-3,
+ QTableWidgetItem(qtwi))
+ self.item(_row, self.no_columns-3).setTextAlignment(Qt.AlignRight |
+ Qt.AlignVCenter)
+
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target - 1
+ _ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0:_ts_str_len - (
+ self.format_ts_nano-self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec :
+ _ts_str += "."
+ while _ts_str_len < _ilength_target :
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ qtwi = QTableWidgetItem( _ts_str)
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+ self.setItem(_row, self.no_columns-2, QTableWidgetItem(qtwi))
+ self.item(_row, self.no_columns-2).setTextAlignment(Qt.AlignCenter)
+
+ _prop = pvgate.qt_dynamic_property_get()
+
+ alarm_severity = _pvd.alarmSeverity
+
+ if _prop == pvgate.READBACK_ALARM:
+
+ if alarm_severity == pvgate.cyca.SEV_MAJOR:
+ _bgcolor = pvgate.settings.fgAlarmMajor
+ _fgcolor = "black"
+ elif alarm_severity == pvgate.cyca.SEV_MINOR:
+ _bgcolor = pvgate.settings.fgAlarmMinor
+ _fgcolor = "black"
+ elif alarm_severity == pvgate.cyca.SEV_INVALID:
+ _bgcolor = pvgate.settings.fgAlarmInvalid
+ _fgcolor = "#777777"
+ else:
+ _bgcolor = pvgate.settings.fgAlarmNoAlarm
+ #_bgcolor = pvgate.settings.bgReadbackAlarm
+ _fgcolor = "black"
+
+ #Colors for bg/fg reversed as is the old norm
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.no_columns-2).setBackground(
+ QColor(_bgcolor))
+ self.item(_row, self.no_columns-3).setForeground(
+ QColor(_fgcolor))
+ self.item(_row, self.no_columns-2).setForeground(
+ QColor(_fgcolor))
+
+ elif _prop == pvgate.READBACK_STATIC:
+
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor(pvgate.settings.bgReadback))
+ self.item(_row, self.no_columns-2).setBackground(
+ QColor(pvgate.settings.bgReadback))
+
+ elif _prop == pvgate.DISCONNECTED:
+ self.item(_row, self.no_columns-3).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.no_columns-2).setBackground(QColor(
+ "#ffffff"))
+ self.item(_row, self.no_columns-3).setForeground(QColor(
+ "#777777"))
+ self.item(_row, self.no_columns-2).setForeground(QColor(
+ "#777777"))
+
+ else:
+ print (_prop, "widget_update unknown in element/row", _row,
+ _row+1)
+
+ QApplication.processEvents()
+
+ def __init__(self, parent=None, pv_list: list = ["PV_NAME_NOT_GIVEN"],
+ monitor_callback=None, pv_within_daq_group: bool = False,
+ color_mode = None, show_units: bool = True, prefix: str = "",
+ suffix: str = "", ts_res: str = "milli",
+ init_column: bool = False, init_list: list = [],
+ notify_freq_hz: int = 0, notify_unison: bool = True,
+ precision: int = 0, scale_factor: float = 1,
+ show_timestamp: bool = True, pv_list_show: list = None):
+
+ super().__init__()
+ self.columns_dict = {}
+ _column_dict_value = 0
+ self.columns_dict['PV'] = _column_dict_value
+ if init_column:
+ _column_dict_value += 1
+ self.columns_dict['Init'] = _column_dict_value
+ _column_dict_value += 1
+ self.columns_dict['Value'] = _column_dict_value
+ if show_timestamp:
+ _column_dict_value += 1
+ self.columns_dict['Timestamp'] = _column_dict_value
+ _column_dict_value += 1
+ self.columns_dict['Reconnect'] = _column_dict_value
+
+ self.setWindowModality(Qt.ApplicationModal)
+ self.no_columns = _column_dict_value + 1
+
+ self.init_column = init_column
+
+ self.init_list = init_list
+ if self.init_column and not self.init_list:
+ self.init_list = pv_list
+
+ self.icount = 0
+ self.notify_freq_hz = abs(notify_freq_hz)
+ self.notify_freq_hz_default = self.notify_freq_hz
+ self.notify_milliseconds = 0 if self.notify_freq_hz == 0 else \
+ 1000 / self.notify_freq_hz
+
+ self.notify_unison = True if notify_unison and \
+ self.notify_freq_hz > 0 else False
+
+ self.precision = precision
+ self.scale_factor = scale_factor
+ self.show_timestamp = show_timestamp
+
+ self.format_ts_nano = 31 #max length of date
+ self.format_ts_micro = 28
+ self.format_ts_milli = 25
+ self.format_ts_deci = 23 #-8
+ self.format_ts_sec = 21
+ if "nano" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_nano
+ elif "micro" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_micro
+ elif "milli" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_milli
+ elif "deci" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_deci
+ elif "sec" in ts_res.lower():
+ self.format_ts_decimal_part = self.format_ts_sec
+ else:
+ self.format_ts_decimal_part = self.format_ts_milli
+
+ self.pv2item_dict = {}
+
+ self.pv_list = pv_list
+ self.pv_gateway = [None] * len(self.pv_list)
+
+ self.pv_list_show = pv_list_show
+ if self.pv_list_show is None:
+ self.pv_list_show = self.pv_list
+
+ _color_mode = [None] * len(self.pv_list)
+
+ if isinstance(color_mode, list):
+ for i in range (0, len(color_mode)):
+ _color_mode[i] = color_mode[i]
+
+ for i in range (0, len(self.pv_list)):
+
+ self.pv_gateway[i] = PVGateway().__init__(
+ parent, self.pv_list[i], monitor_callback,
+ pv_within_daq_group, _color_mode[i], show_units, prefix, suffix,
+ connect_triggers=False, notify_freq_hz=self.notify_freq_hz,
+ notify_unison=self.notify_unison, precision=self.precision)
+
+ self.pv_gateway[i].is_initialize_complete()
+ self.pv_gateway[i].trigger_connect.connect(
+ self.receive_connect_update)
+ self.pv_gateway[i].trigger_monitor_str.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_int.connect(
+ self.receive_monitor_update)
+ self.pv_gateway[i].trigger_monitor_float.connect(
+ self.receive_monitor_update)
+
+ self.pv_gateway[i].widget_class = "QTableWidgetItem"
+
+ self.pv_gateway[i].qt_property_initial_values(
+ qt_object_name = self.pv_gateway[i].PV_READBACK,
+ tool_tip=False)
+ #cant do this - sender will be a qspinbox
+ #self.pv_gateway[i].post_display_value = self.post_display_value
+
+
+ #required for reconnect
+ self.cafe = self.pv_gateway[0].cafe
+ self.cyca = self.pv_gateway[0].cyca
+
+ self.timer = None
+ if self.notify_unison:
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.widget_update)
+ self.timer.singleShot(0, self.widget_update)
+ self.timer.start(self.notify_milliseconds)
+
+ self.configure_widget()
+
+ #Connect only deals with colours - only helps on reconnect
+ # In any case monitors take over
+ #Got to do this earlier or emit immediately after!!
+ for i in range(0, len(self.pv_gateway)):
+ if self.cafe.isConnected(self.pv_gateway[i].pv_name):
+ self.pv_gateway[i].trigger_connect.emit(
+ self.pv_gateway[i].handle, str(self.pv_gateway[i].pv_name),
+ self.pv_gateway[i].cyca.ICAFE_CS_CONN)
+
+
+ for i in range(0, len(self.pv_gateway)):
+ if not self.pv_gateway[i].pv_within_daq_group:
+ self.pv_gateway[i].monitor_start()
+
+ self.update_init_values()
+
+ self.configure_context_menu()
+
+
+ def configure_context_menu(self):
+ self.table_context_menu = QMenu()
+ self.table_context_menu.setObjectName("contextMenu")
+ self.table_context_menu.setWindowModality(Qt.NonModal)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.table_context_menu.addSection("---")
+
+ action1 = QAction("Configure Table PVs", self)
+ action1.triggered.connect(self.display_table_parameters)
+ self.table_context_menu.addAction(action1)
+
+ if LooseVersion(QT_VERSION_STR) >= LooseVersion("5.3"):
+ self.table_context_menu.addSection("---")
+
+ QApplication.processEvents()
+
+
+ def restore_init_values(self):
+ _set_values_dict = self.get_init_values()
+ #print("set val dict", _set_values_dict))
+ #print("same as init vals", self.is_same_as_init_values())
+
+ _pvs_to_set, _values_to_set = zip(*_set_values_dict.items())
+ #zip returns tuples
+ #print(_pvs_to_set)
+ #print(_values_to_set)
+ _pvs_to_set = list(_pvs_to_set)
+ _values_to_set = list(_values_to_set)
+
+ status, status_list = self.cafe.setScalarList(_pvs_to_set,
+ _values_to_set)
+
+ if status != self.cyca.ICAFE_NORMAL:
+ _mess = ("The following device(s) reported an error " +
+ "in set operation:")
+ for i, status_value in enumerate(status_list):
+ if status_value != self.cyca.ICAFE_NORMAL:
+ _mess += ("\n" + _pvs_to_set[i] + " has status = " +
+ str(status_value) + " " +
+ self.cafe.getStatusCodeAsString(status_value) +
+ " " + self.cafe.getStatusInfo(status_value) )
+ qm = QMessageBox()
+ qm.setText(_mess)
+
+ qm.exec()
+ QApplication.processEvents()
+
+ self.init_value_button.setEnabled(True)
+
+
+ def is_same_as_init_values(self):
+ _init_values_dict = self.get_column_values(self.columns_dict['Init'])
+ _pvs, _init_values = zip(*_init_values_dict.items())
+ _current_values_dict = self.get_column_values(self.columns_dict['Value'])
+ _pvs, _current_values = zip(*_current_values_dict.items())
+ #zip returns tuples
+
+ if func_reduce(lambda i, j : i and j, map(
+ lambda m, k: m == k, _init_values, _current_values), True):
+ return True
+ else:
+ return False
+
+
+ def get_column_values(self, column_no):
+ _values_dict = {}
+ _start = 0
+ _end = len(self.pv_gateway)
+ _pvs = [None] * _end
+ _values_str = [None] * _end
+ _values = [None] * _end
+
+ for _row in range(_start, _end):
+ _values_str[_row] = self.item(_row, column_no).text()
+ _pvs[_row] = self.item(_row, 0).text()
+
+ _value_list = [float(_value_list) for _value_list in re.findall(
+ r'-?\d+\.?\d*', _values_str[_row])]
+
+ if not _value_list:
+ print("row", _row, "values", _values_str[_row], _pvs[_row])
+ _values[_row] = _values_str[_row] #Can be enum string
+ else:
+ _values[_row] = _value_list[0]
+
+
+ if _pvs[_row] in self.pv_list_show:
+ #_values_dict[_pvs[_row]] = _values[_row]
+ _values_dict[self.pv_gateway[_row].pv_name] = _values[_row]
+
+ #print("_pvs", _pvs)
+ #print("show", self.pv_list_show)
+ return _values_dict #_pvs_to_set, _values_to_set
+
+
+ def get_init_values(self):
+ return self.get_column_values(self.columns_dict['Init'])
+
+ def get_init_values_previous(self):
+ _set_values_dict = {}
+ _start = 0
+ _end = len(self.pv_gateway)
+ _pvs_to_set = [None] * _end
+ _values_to_set_str = [None] * _end
+ _values_to_set = [None] * _end
+ for _row in range(_start, _end):
+ _values_to_set_str[_row] = self.item(_row, self.columns_dict['Init']).text()
+ _pvs_to_set[_row] = self.item(_row, self.columns_dict['PV']).text()
+
+ _value_list = [float(_value_list) for _value_list in re.findall(
+ r'-?\d+\.?\d*', _values_to_set_str[_row])]
+
+ if not _value_list:
+ print("//row", _row, "values", _values_to_set_str[_row],
+ _pvs_to_set[_row])
+ _values_to_set[_row] = _values_to_set_str[_row] #Can be enum string
+ else:
+ _values_to_set[_row] = _value_list[0]
+
+
+ if _pvs_to_set[_row] in self.init_list:
+ #_set_values_dict[_pvs_to_set[_row]] = _values_to_set[_row]
+ _set_values_dict[self.pv_gateway[_row].pv_name] = _values_to_set[_row]
+ #print(_set_values_dict)
+ return _set_values_dict #_pvs_to_set, _values_to_set
+
+
+ def update_init_values(self):
+ _start = 0
+ _end=len(self.pv_gateway)
+ for _row in range(_start, _end):
+ _handle = self.pv_gateway[_row].handle
+ _value = self.pv_gateway[_row].cafe.getCache(_handle)
+
+ if _value is not None:
+ if self.scale_factor != 1:
+ _value = _value * self.scale_factor
+ _value = self.pv_gateway[_row].format_display_value(_value)
+ #print("value from update", _value)
+ qtwi = QTableWidgetItem(str(_value)+ " ")
+ _f = qtwi.font()
+ _f.setPointSize(8)
+ qtwi.setFont(_f)
+ self.setItem(_row, 1, qtwi)
+ self.item(_row, 1).setTextAlignment(
+ Qt.AlignRight | Qt.AlignVCenter)
+
+
+ def configure_widget(self):
+
+ _column_width_pvname = 210
+ _column_width_value = 110
+ _column_width_timestamp = 240
+ _column_width_checkbox = 25
+
+ self.setRowCount(len(self.pv_gateway)+1)
+ self.setColumnCount(self.no_columns)
+ self.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.resizeColumnsToContents()
+ self.resizeRowsToContents()
+ #self.horizontalHeader().setStretchLastSection(True);
+ self.setColumnWidth(self.columns_dict['PV'], _column_width_pvname)
+
+ self.setColumnWidth(self.columns_dict['Value'], _column_width_value)
+ if 'Init' in self.columns_dict.keys():
+ self.setColumnWidth(self.columns_dict['Init'], _column_width_value)
+ if 'Timestamp' in self.columns_dict.keys():
+ self.setColumnWidth(self.columns_dict['Timestamp'], _column_width_timestamp)
+ self.setColumnWidth(self.columns_dict['Reconnect'], _column_width_checkbox)
+
+ _pv_column = self.columns_dict['PV']
+ for i in range(0, len(self.pv_gateway)):
+ #self.setItem(i, _pv_column, QTableWidgetItem(self.pv_gateway[i].pv_name))
+ qtwt = QTableWidgetItem(self.pv_list_show[i])
+ f = qtwt.font()
+ f.setPointSize(8)
+ qtwt.setFont(f)
+ #qtwt.setTextAlignment(Qt.AlignLeft)
+ self.setItem(i, _pv_column, qtwt)
+ self.item(i, _pv_column).setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter)
+ for i_column in range(1, self.no_columns-1):
+ self.setItem(i, i_column, QTableWidgetItem(str("")))
+ self.item(i, i_column).setTextAlignment(Qt.AlignCenter |
+ Qt.AlignVCenter)
+ self.pv2item_dict[self.pv_gateway[i]] = i
+
+ cb_item = QTableWidgetItem()
+ cb_item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
+ cb_item.setCheckState(Qt.Unchecked)
+ cb_item.setTextAlignment(Qt.AlignCenter)
+ cb_item.setToolTip(self.pv_gateway[i].pv_name)
+
+ self.setItem(i, self.no_columns-1, cb_item)
+ self.item(i, self.no_columns-1).setTextAlignment(Qt.AlignCenter |
+ Qt.AlignVCenter)
+
+ if self.init_column:
+ self.init_widget = QWidget()
+ _init_layout = QHBoxLayout(self.init_widget)
+ self.init_value_button = QPushButton()
+ self.init_value_button.setText("Update")
+ _f = self.init_value_button.font()
+ _f.setPointSize(8)
+ self.init_value_button.setFont(_f)
+ self.init_value_button.setFixedWidth(80)
+ self.init_value_button.clicked.connect(self.update_init_values)
+ self.init_value_button.setToolTip(
+ ("Stores initial, pre-measurement value. Update is also " +
+ "executed automatically before each measurement."))
+ _init_layout.addWidget(self.init_value_button)
+ _init_layout.setAlignment(Qt.AlignRight)
+ _init_layout.setContentsMargins(1,1,0,0) #Required
+ self.init_widget.setLayout(_init_layout)
+ self.setCellWidget(len(self.pv_gateway), 1, self.init_widget)
+
+ _restore_widget = QWidget()
+ _restore_layout = QHBoxLayout(_restore_widget)
+ self.restore_value_button = QPushButton()
+ #self.restore_value_button.setObjectName("Controller")
+ #self.restore_value_button.setProperty('actOnBeam', True)
+ self.restore_value_button.setStyleSheet(
+ "QPushButton{background-color: rgb(212, 219, 157);}")
+ self.restore_value_button.setText("Restore")
+ _f = self.restore_value_button.font()
+ _f.setPointSize(8)
+ self.restore_value_button.setFont(_f)
+ self.restore_value_button.setFixedWidth(80)
+ self.restore_value_button.clicked.connect(self.restore_init_values)
+ self.restore_value_button.setToolTip(
+ ("Restore devices to their pre-measurement values"))
+ _restore_layout.addWidget(self.restore_value_button)
+ _restore_layout.setAlignment(Qt.AlignRight)
+ _restore_layout.setContentsMargins(1,1,0,0) #Required
+ _restore_widget.setLayout(_restore_layout)
+ self.setCellWidget(len(self.pv_gateway), 2, _restore_widget)
+
+ #Do not display no for last row (Reconnect button)
+ _row_digit_last_cell = QTableWidgetItem(str(""))
+ self.setVerticalHeaderItem(len(self.pv_gateway), _row_digit_last_cell)
+ self.setItem(len(self.pv_gateway), 0, QTableWidgetItem(str("")))
+
+ _qwb = QWidget()
+
+ self.reconnect_button = reconnectQPushButton(self) #self required
+ #self.reconnect_button.setFont(self.font12);QFont("Sans Serif", 12)
+
+ f = self.reconnect_button.font()
+
+ if 'Timestamp' in self.columns_dict.keys():
+ f.setPointSize(8)
+ self.reconnect_button.setFixedWidth(100)
+ else:
+ f.setPointSize(6)
+ self.reconnect_button.setFixedWidth(58)
+
+ self.reconnect_button.setFont(f)
+
+
+ self.reconnect_button.setText("Reconnect")
+
+
+
+
+ _layout = QHBoxLayout(_qwb)
+ _layout.addWidget(self.reconnect_button)
+ _layout.setAlignment(Qt.AlignCenter)
+ _layout.setContentsMargins(0,0,0,0) #Required
+ _qwb.setLayout(_layout)
+
+ #_reconnect_button
+ self.setCellWidget(len(self.pv_gateway), self.no_columns-2, _qwb)
+
+ _qwc = QWidget()
+
+ self.cb_item_all = QCheckBox(self)
+ self.cb_item_all.setCheckState(Qt.Unchecked)
+ self.cb_item_all.stateChanged.connect(self.reconnectStateChanged)
+
+ _layout_cb = QHBoxLayout(_qwc)
+ _layout_cb.addWidget(self.cb_item_all)
+ _layout_cb.setAlignment(Qt.AlignLeft)
+ _layout_cb.setContentsMargins(3,2,0,1) #Required LTRB
+ _qwc.setLayout(_layout_cb)
+
+ self.setCellWidget(len(self.pv_gateway), self.no_columns-1, _qwc)
+
+
+ header_item = QTableWidgetItem("Process Variable")
+
+ '''
+ header_item.setText("Process Variable")
+ f= header_item.font()
+ f.setPixelSize(12)
+ header_item.setFont(f)
+ '''
+
+ self.setHorizontalHeaderItem(self.columns_dict['PV'], header_item)
+
+ if 'Init' in self.columns_dict.keys():
+ self.setHorizontalHeaderItem(self.columns_dict['Init'],
+ QTableWidgetItem("Initial Value"))
+
+ self.setHorizontalHeaderItem(self.columns_dict['Value'],
+ QTableWidgetItem("Value"))
+
+ if 'Timestamp' in self.columns_dict.keys():
+ self.setHorizontalHeaderItem(self.columns_dict['Timestamp'],
+ QTableWidgetItem("Timestamp"))
+ self.setHorizontalHeaderItem(self.columns_dict['Reconnect'],
+ QTableWidgetItem("R"))
+ self.setFocusPolicy(Qt.NoFocus)
+ self.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.setSelectionMode(QAbstractItemView.NoSelection)
+
+ self.verticalHeader().setDefaultAlignment(Qt.AlignRight)
+ self.verticalHeader().setFixedWidth(22)
+
+
+ #self.verticalHeader().setVisible(False)
+ #self.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch )
+
+ _fm_font = QFont("Sans Serif")
+ _fm_font.setPointSize(12)
+ fm = QFontMetricsF(_fm_font) #QFont("Sans Serif", 12))
+
+ _factor = 1
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ self.setFixedHeight(int(fm.lineSpacing() * _factor *
+ (len(self.pv_gateway)+3)))
+ _min_table_width = 620 if not self.init_column else 650
+ self.setMinimumWidth(_min_table_width)
+
+
+ for _row in range(0, len(self.pv_gateway)):
+ #print("name/row/columns", self.pv_gateway[_row].pv_name, _row, self.no_columns)
+ self.item(_row, _pv_column).setForeground(QColor("#000000"))
+ ##self.item(_row, _pv_column).setTextAlignment(Qt.AlignCenter)
+
+ for i_column in range(1, self.no_columns-2):
+ self.item(_row, i_column).setForeground(QColor("#000000"))
+ self.item(_row, i_column).setTextAlignment(Qt.AlignRight |
+ Qt.AlignVCenter)
+
+ self.item(_row, self.columns_dict['Value']).setBackground(QColor("#ffffff"))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setTextAlignment(Qt.AlignCenter)
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(QColor("#ffffff"))
+
+ @Slot(int)
+ def reconnectStateChanged(self, state):
+ if state == Qt.Unchecked:
+ for i in range(0, len(self.pv_gateway)):
+ self.item(i, self.columns_dict['Reconnect']).setCheckState(Qt.Unchecked)
+ else:
+ for i in range(0, len(self.pv_gateway)):
+ self.item(i, self.columns_dict['Reconnect']).setCheckState(Qt.Checked)
+
+ @Slot(str, int, int)
+ @Slot(int, int, int)
+ @Slot(float, int, int)
+ def receive_monitor_update(self, value, status, alarm_severity):
+
+ _row = self.pv2item_dict[self.sender()]
+ '''
+ if _row in (3,):
+ print("receive mon update before post_display, value/row==", value, _row, self.pv_gateway[_row].pv_name)
+ print(self, _row, self.pv_gateway[_row].pv_name, ">>>>>>>from gateway>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ print("sender", self.sender())
+ '''
+
+ #print(self.pv2item_dict)
+ #print( value, status, alarm_severity)
+ #Timing on CAQTableWidget basis! Miss last events if many events
+ #First trigger should always happen
+ #if self.update_hz is not None:
+ # print(time.monotonic(), self.pv_gateway[_row].time_monotonic, (1/self.update_hz))
+ # if (time.monotonic() - self.pv_gateway[_row].time_monotonic) < (1/self.update_hz):
+ # return
+ # self.pv_gateway[_row].receive_monitor_update(value, status, alarm_severity)
+ self.pv_gateway[_row].time_monotonic = time.monotonic()
+ if self.scale_factor != 1:
+ value = value * self.scale_factor
+ _value = self.pv_gateway[_row].format_display_value(value)
+
+ #print("row no//", _row)
+ qtwi = QTableWidgetItem(str(_value) + " ")
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+ self.setItem(_row, self.columns_dict['Value'], qtwi)
+ self.item(_row, self.columns_dict['Value']).setTextAlignment(
+ Qt.AlignRight | Qt.AlignVCenter)
+
+ if 'Timestamp' in self.columns_dict.keys():
+
+ _handle = self.pv_gateway[_row].handle
+ #print("post_display HANDLE/val/row", _handle, _value, _row)
+ #_status = self.pv_gateway[_row].cafe.getStatus(_handle)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(_handle)
+
+
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target -1
+
+ ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0:_ts_str_len-(
+ self.format_ts_nano-self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+ #print(_ts_date)
+ #print(_ts_str)
+ #print("length of timestamp string ", _ts_str_len)
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec:
+ _ts_str += "."
+ while _ts_str_len < _ilength_target :
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ qtwi = QTableWidgetItem( _ts_str)
+ f = qtwi.font()
+ f.setPointSize(8)
+ qtwi.setFont(f)
+
+ self.setItem(_row, self.columns_dict['Timestamp'], qtwi)
+ self.item(_row, self.columns_dict['Timestamp']).setTextAlignment(Qt.AlignCenter)
+
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ if _prop == self.pv_gateway[_row].READBACK_ALARM:
+
+ if alarm_severity == self.pv_gateway[_row].cyca.SEV_MAJOR:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmMajor
+ _fgcolor = "black"
+ elif alarm_severity == self.pv_gateway[_row].cyca.SEV_MINOR:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmMinor
+ _fgcolor = "black"
+ elif alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmInvalid
+ _fgcolor = "#777777"
+ else:
+ _bgcolor = self.pv_gateway[_row].settings.fgAlarmNoAlarm
+ #_bgcolor = self.pv_gateway[_row].settings.bgReadbackAlarm
+ _fgcolor = "black"
+
+ #Colors for bg/fg reversed as is the old norm
+ self.item(_row, self.columns_dict['Value']).setBackground(QColor(_bgcolor))
+ self.item(_row, self.columns_dict['Value']).setForeground(QColor(_fgcolor))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(QColor(_bgcolor))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(QColor(_fgcolor))
+
+
+ elif _prop == self.pv_gateway[_row].DISCONNECTED or \
+ alarm_severity == self.pv_gateway[_row].cyca.SEV_INVALID:
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Value']).setForeground(
+ QColor("#777777"))
+
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(
+ QColor("#777777"))
+
+
+ elif _prop == self.pv_gateway[_row].READBACK_STATIC:
+ self.item(_row, self.columns_dict['Value']).setBackground(
+ QColor(self.pv_gateway[_row].settings.bgReadback))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(
+ QColor(self.pv_gateway[_row].settings.bgReadback))
+ else:
+
+ print (_prop, self.pv_gateway[_row].DISCONNECTED, "(in monitor) unknown in element/row no.", _row, _row+1)
+ #TRZ SET PROPERTY
+
+ QApplication.processEvents(QEventLoop.AllEvents, 10)
+ #self.post_display_value(value)
+
+
+ @Slot(int, str, int)
+ def receive_connect_update(self, handle: int, pv_name: str, status: int):
+ '''Triggered by connect signal'''
+ _row = self.pv2item_dict[self.sender()]
+
+ #print(self, self.pv_gateway[_row].pv_name, ">>>>>>>receive connect >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
+ #print("sender", self.sender())
+ #print("MY RECEIVE receive_connect_update for row = ", _row, " status=", status)
+ #print(handle, pv_name)
+ self.pv_gateway[_row].receive_connect_update(handle, pv_name, status,
+ post_display=False)
+ #print("after gateway connect update")
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ #self.post_display_value(status)
+ if _prop == self.pv_gateway[_row].DISCONNECTED:
+ #self.item(_row,0).setBackground(QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Value']).setBackground(QColor("#ffffff"))
+ #self.item(_row,0).setForeground(QColor("#777777"))
+ self.item(_row, self.columns_dict['Value']).setForeground(QColor("#777777"))
+ if 'Timestamp' in self.columns_dict.keys():
+ self.item(_row, self.columns_dict['Timestamp']).setBackground(QColor("#ffffff"))
+ self.item(_row, self.columns_dict['Timestamp']).setForeground(QColor("#777777"))
+
+ QApplication.processEvents()
+
+ '''
+ def post_display_value(self, value):
+
+#IS CLEAN BEFORE EXIT
+ self.icount += 1;
+ print("recursion limit", sys.getrecursionlimit(), self.icount)
+ _row = self.pv2item_dict[self.sender()]
+ print("row no", _row)
+ _value = self.pv_gateway[_row].format_display_value(value)
+ print("row no//", _row)
+ self.setItem(_row, self.no_columns-3,
+ QTableWidgetItem(str(_value)+ " "))
+ self.item(_row, self.no_columns-3).setTextAlignment(Qt.AlignRight)
+
+ _handle = self.pv_gateway[_row].handle
+ #print("post_display HANDLE/val/row", _handle, _value, _row)
+ #_status = self.pv_gateway[_row].cafe.getStatus(_handle)
+ _pvd = self.pv_gateway[_row].cafe.getPVCache(_handle)
+
+ _ts_date = _pvd.tsDateAsString
+ _ts_str_len = len(_ts_date)
+ _ilength_target = self.format_ts_nano
+
+ while _ts_str_len < _ilength_target:
+ _ts_date += "0"
+ _ilength_target = _ilength_target -1
+ _ts_str_len = len(_ts_date)
+ _ts_str = _ts_date[0:_ts_str_len - (self.format_ts_nano -
+ self.format_ts_decimal_part)]
+ _ts_str_len = len(_ts_str)
+ #print(_ts_date)
+ #print(_ts_str)
+ #print("length of timestamp string ", _ts_str_len)
+ _ilength_target = self.format_ts_decimal_part
+ if self.format_ts_decimal_part == self.format_ts_deci:
+ if _ts_str_len == self.format_ts_sec :
+ _ts_str += "."
+ while _ts_str_len < _ilength_target :
+ _ts_str += "0"
+ _ilength_target = _ilength_target -1
+
+ self.setItem(_row, self.no_columns-2, QTableWidgetItem( _ts_str))
+ self.item(_row, self.no_columns-2).setTextAlignment(Qt.AlignCenter)
+
+ _prop = self.pv_gateway[_row].qt_dynamic_property_get()
+
+ if _prop == self.pv_gateway[_row].READBACK_ALARM:
+ #self.item(_row,0).setBackground(QColor("#c8c8c8"))
+ self.item(_row, self.no_columns-3).setBackground(QColor("#c8c8c8"))
+ self.item(_row, self.no_columns-2).setBackground(QColor("#c8c8c8"))
+
+ elif _prop == self.pv_gateway[_row].READBACK_STATIC:
+ #self.item(_row,0).setBackground(QColor("#ffffe0"))
+ self.item(_row, self.no_columns-3).setBackground(QColor("#ffffe0"))
+ self.item(_row, self.no_columns-2).setBackground(QColor("#ffffe0"))
+
+ elif _prop == self.pv_gateway[_row].DISCONNECTED:
+ self.item(_row, self.no_columns-3).setBackground(QColor("#ffffff"))
+ self.item(_row, self.no_columns-2).setBackground(QColor("#ffffff"))
+ self.item(_row, self.no_columns-3).setForeground(QColor("#777777"))
+ self.item(_row, self.no_columns-2).setForeground(QColor("#777777"))
+
+ else:
+ print (_prop, "unknown in element/row ==>", _row, _row+1)
+ #TRZ SET PROPERTY
+
+ QApplication.processEvents()
+ #self.setStyleSheet("QTableWidget::item {margin-right: 5 }");
+
+ #self.setStyleSheet("QHeaderView::section:horizontal {margin-right: 2; border: 1px solid}");
+ '''
+ def table_precision_user_changed(self, new_value):
+ self.pvgateway_precision = new_value
+
+ for pvgate in self.pv_gateway:
+ if pvgate.pv_ctrl is not None:
+ self.pvgateway_precision = min(pvgate.pv_ctrl.precision,
+ new_value)
+
+ pvgate.precision_user = self.pvgateway_precision
+ pvgate.precision = self.pvgateway_precision
+
+ _pvd = self.cafe.getPVCache(pvgate.handle)
+
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ pvgate.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+
+
+ def table_precision_ioc_reset(self):
+ '''
+ _max_current_precision_value = -1
+ for i, pvgate in enumerate(self.pv_gateway):
+ if pvgate.pv_ctrl is not None:
+
+ _max_current_precision_value = max(
+ pvgate.precision_user, _max_current_precision_value)
+
+ if _max_current_precision_value == -1:
+ _max_current_precision_value = self.max_precision_value
+ '''
+ if self.max_precision_value == self.table_precision_user_wgt.value():
+ self.table_precision_user_changed(self.max_precision_value)
+ else:
+ self.table_precision_user_wgt.setValue(self.max_precision_value)
+
+ def table_refresh_rate_changed(self, new_idx):
+
+ _notify_freq_hz = self.refresh_freq_combox_idx_dict[new_idx]
+ _notify_milliseconds = 0 if _notify_freq_hz == 0 else \
+ 1000 / _notify_freq_hz
+
+ self.notify_freq_hz = _notify_freq_hz
+
+ if _notify_milliseconds == 0:
+ for pvgate in self.pv_gateway:
+ pvgate.notify_unison = False
+ pvgate.notify_milliseconds = _notify_milliseconds
+ pvgate.notify_freq_hz = self.notify_freq_hz
+ pvgate.monitor_stop()
+ time.sleep(0.01)
+ for pvgate in self.pv_gateway:
+ pvgate.monitor_start()
+
+ else:
+ for pvgate in self.pv_gateway:
+ if not pvgate.notify_unison:
+ pvgate.monitor_stop()
+
+ for pvgate in self.pv_gateway:
+ pvgate.notify_milliseconds = _notify_milliseconds
+ pvgate.notify_freq_hz = self.notify_freq_hz
+
+ if not pvgate.notify_unison:
+ pvgate.notify_unison = True
+ pvgate.monitor_start()
+ else:
+
+ self.cafe.updateMonitorPolicyDeltaMS(
+ pvgate.handle, pvgate.monitor_id,
+ pvgate.notify_milliseconds)
+
+ #for pvgate in self.pv_gateway:
+ # pvgate.monitor_start()
+ # print("pvgate / mon started", pvgate)
+
+ if self.timer is not None:
+ self.timer.stop()
+ else:
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.widget_update)
+ self.timer.singleShot(0, self.widget_update)
+
+ if _notify_milliseconds > 0:
+ self.timer.start(_notify_milliseconds)
+
+ def table_ts_resolution_changed(self, new_idx):
+
+ for i, (key, ts_res) in enumerate(self.ts_combox_idx_dict.items()):
+ if i == new_idx:
+ self.format_ts_decimal_part = ts_res
+ break;
+
+ for pvgate in self.pv_gateway:
+ _pvd = self.cafe.getPVCache(pvgate.handle)
+ if _pvd.value[0] is not None:
+ if isinstance(_pvd.value[0], float):
+ pvgate.trigger_monitor_float.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+ elif isinstance(_pvd.value[0], int):
+ pvgate.trigger_monitor_int.emit(
+ _pvd.value[0], _pvd.status, _pvd.alarmSeverity)
+ else:
+ pvgate.trigger_monitor_str.emit(
+ str(_pvd.value[0]), _pvd.status,
+ _pvd.alarmSeverity)
+
+
+ def display_table_parameters(self):
+ display_wgt = QDialog(self)
+ display_wgt.setWindowTitle("PV Parameters")
+ layout = QVBoxLayout()
+ common_label_width = 120
+ common_wgt_width = 160
+ common_hbox_width = common_label_width + common_wgt_width + 20
+
+ self.initial_value = 0
+ self.max_precision_value = 0
+ for i, pvgate in enumerate(self.pv_gateway):
+ if pvgate.pv_ctrl is not None:
+ if pvgate.pv_ctrl.precision > 0:
+ self.max_precision_value = max(self.max_precision_value,
+ pvgate.pv_ctrl.precision)
+ self.initial_value = max(self.initial_value,
+ pvgate.precision)
+
+ if self.max_precision_value > 0:
+ #precision user
+ _hbox_wgt = QWidget()
+ _hbox = QHBoxLayout()
+ precision_user_label = QLabel("Precision (user):")
+ self.table_precision_user_wgt = QSpinBox(self)
+ self.table_precision_user_wgt.setFocusPolicy(Qt.NoFocus)
+ self.table_precision_user_wgt.setValue(self.initial_value)
+ self.table_precision_user_wgt.setMaximum(self.max_precision_value)
+ self.table_precision_user_wgt.valueChanged.connect(
+ self.table_precision_user_changed)
+ precision_user_label.setAlignment(Qt.AlignLeft)
+ self.table_precision_user_wgt.setAlignment(Qt.AlignLeft)
+ _hbox.addWidget(precision_user_label)
+ _hbox.addWidget(self.table_precision_user_wgt)
+ _hbox.setAlignment(Qt.AlignLeft)
+ _hbox_wgt.setLayout(_hbox)
+
+ precision_user_label.setFixedWidth(common_label_width)
+ self.table_precision_user_wgt.setFixedWidth(40)
+ _hbox_wgt.setFixedWidth(common_hbox_width)
+
+ #precision ioc
+ _hbox2_wgt = QWidget()
+ _hbox2 = QHBoxLayout()
+ precision_ioc_label = QLabel("Precision (ioc): ")
+ precision_ioc = QPushButton(self)
+ precision_ioc.setText("Reset")
+ precision_ioc.clicked.connect(self.table_precision_ioc_reset)
+ precision_ioc_label.setAlignment(Qt.AlignLeft)
+
+
+ _hbox2.addWidget(precision_ioc_label)
+ _hbox2.addWidget(precision_ioc)
+ _hbox2.setAlignment(Qt.AlignLeft)
+
+ _hbox2_wgt.setLayout(_hbox2)
+
+ precision_ioc_label.setFixedWidth(common_label_width)
+ precision_ioc.setFixedWidth(50)
+
+ _hbox2_wgt.setFixedWidth(common_hbox_width)
+
+
+ layout.addWidget(_hbox_wgt)
+ layout.addWidget(_hbox2_wgt)
+
+
+ if 'Timestamp' in self.columns_dict.keys():
+ #time-stamp
+ _hbox4_wgt = QWidget()
+ _hbox4 = QHBoxLayout()
+ ts_label = QLabel("Timestamp: ")
+
+ self.ts_combox_idx_dict = {'second (s)':self.format_ts_sec,
+ 'decisecond (ds)':self.format_ts_deci,
+ 'millisecond (ms)':self.format_ts_milli,
+ 'microsecond (\u03bcs)':self.format_ts_micro,
+ 'nanosecond (ns)':self.format_ts_nano}
+
+ ts_resolution = QComboBox(self)
+ for (key, ts_res) in (self.ts_combox_idx_dict.items()):
+ ts_resolution.addItem(key)
+
+
+ _current_idx = 0
+
+ for i, (key, ts_res) in enumerate(self.ts_combox_idx_dict.items()):
+ if ts_res == self.format_ts_decimal_part:
+ _current_idx = i
+ break
+
+ ts_resolution.setCurrentIndex(_current_idx)
+ ts_resolution.currentIndexChanged.connect(
+ self.table_ts_resolution_changed)
+
+ _hbox4.addWidget(ts_label)
+ _hbox4.addWidget(ts_resolution)
+ _hbox4_wgt.setLayout(_hbox4)
+
+ ts_label.setFixedWidth(common_label_width)
+ ts_resolution.setFixedWidth(common_wgt_width)
+ _hbox4_wgt.setFixedWidth(common_hbox_width)
+
+ layout.addWidget(_hbox4_wgt)
+
+ #precision refresh rate
+ _hbox3_wgt = QWidget()
+ _hbox3 = QHBoxLayout()
+ refresh_freq_label = QLabel("Refresh rate: ")
+ #_default_refresh_val = 0 if self.notify_freq_hz <= 0 else \
+ # self.notify_freq_hz
+ _default_refresh_val = 0 if self.notify_freq_hz_default <= 0 else \
+ self.notify_freq_hz_default
+
+ self.refresh_freq_combox_idx_dict = {0:0, 1:10, 2:5, 3:2, 4:1, 5:0.5,
+ 6:_default_refresh_val}
+ refresh_freq = QComboBox(self)
+ refresh_freq.addItem('direct')
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[1]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[2]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[3]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[4]))
+ refresh_freq.addItem('{0} Hz'.format(
+ self.refresh_freq_combox_idx_dict[5]))
+
+ _default_text = 'default (direct)' if _default_refresh_val == 0 else \
+ 'default ({0} Hz)'.format(self.refresh_freq_combox_idx_dict[6])
+
+ refresh_freq.addItem(_default_text)
+
+ for key, value in self.refresh_freq_combox_idx_dict.items():
+ if value == self.notify_freq_hz:
+ refresh_freq.setCurrentIndex(key)
+ break
+
+
+ refresh_freq.currentIndexChanged.connect(
+ self.table_refresh_rate_changed)
+
+
+ _hbox3.addWidget(refresh_freq_label)
+ _hbox3.addWidget(refresh_freq)
+ _hbox3_wgt.setLayout(_hbox3)
+
+ refresh_freq_label.setFixedWidth(common_label_width)
+ refresh_freq.setFixedWidth(common_wgt_width)
+ _hbox3_wgt.setFixedWidth(common_hbox_width)
+
+ layout.addWidget(_hbox3_wgt)
+
+ layout.setAlignment(Qt.AlignLeft)
+ layout.setContentsMargins(10, 0, 0, 0)
+ layout.setSpacing(0)
+
+ display_wgt.setMinimumWidth(340)
+ display_wgt.setLayout(layout)
+
+ display_wgt.exec()
+
+
+ def mousePressEvent(self, event):
+ #print(event.pos())
+ row = self.indexAt(event.pos()).row()
+ #print("current item", row)
+ if row < len(self.pv_list) and row > -1:
+ self.pv_gateway[row].mousePressEvent(event)
+ #elif row == -1:
+ # self.display_table_parameters()
+ else:
+ button = event.button()
+ #print("button", button, Qt.RightButton)
+ if button == Qt.RightButton:
+ self.table_context_menu.exec(QCursor.pos())
+ self.clearFocus()
+
+
+
+ #remove highlighting which persists after mouse leaves
+ def mouseMoveEvent(self, event):
+ #event.ignore()
+ pass
+
+ def leaveEvent(self, event):
+ self.clearSelection()
+ self.clearFocus()
+ del event
+
+
+
+class QMessageWidget(QListWidget):
+ """Log message window."""
+ def __init__(self, parent=None):
+ super(QMessageWidget, self).__init__(parent)
+ self.myItem = None
+ self.setSelectionMode(QAbstractItemView.ExtendedSelection)
+ self.setFocusPolicy(Qt.StrongFocus)
+
+
+
+ def leaveEvent(self, event):
+ if self.myItem:
+ self.clearSelection()
+ self.clearFocus()
+ del event
+
+ def mousePressEvent(self, event):
+ item = self.itemAt(event.x(), event.y())
+ if item:
+ self.myItem = item
+ self.setCurrentItem(self.myItem)
+
+ def keyPressEvent(self, event):
+ if event.matches(QKeySequence.Copy):
+ nitem = event.count()
+ if nitem:
+ if self.myItem is not None:
+ _str = self.myItem.text()
+ #beg = mystr.find("file = ")
+ #end = mystr.find("'", beg+6)
+ #newstr = "\""+mystr[beg+6:end]+"\""
+ QApplication.clipboard().setText(_str)
+
+
+
+
+
+class QResultsWidget:
+ """Results table"""
+ def __init__(self, summary_dict=None, table_dict=None):
+
+ self.summary_dict = summary_dict
+ self.table_dict = table_dict
+ self._group_box = None
+
+ def group_box(self, title=""):
+ self._group_box = QGroupBox(title)
+ self._group_box.setObjectName("OUTERLEFT")
+ _vbox = QVBoxLayout()
+ _qspace = QFrame()
+ _qspace.setFixedHeight(10)
+ _vbox.addWidget(_qspace)
+
+ _font = QFont("Sans Serif", 10)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, (label, text) in enumerate(self.summary_dict.items()):
+ if len(str(label)) > len(longest_str_item1):
+ longest_str_item1 = str(label)
+ if len(str(text)) > len(longest_str_item2):
+ longest_str_item2 = str(text)
+
+ fm = QFontMetricsF(_font)
+
+ _factor = 1.15
+
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ qrect1 = fm.boundingRect(longest_str_item1)
+ qrect2 = fm.boundingRect(longest_str_item2)
+ _width_scaling_factor = 1.5
+ _width_scaling_factor_le = 1.15
+ _widget_height = 25
+ for i, (label, text) in enumerate(self.summary_dict.items()):
+ #print(label, text)
+ qlabel = QLabel(label)
+ qle = QLineEdit(text)
+ qlabel.setFont(_font)
+ qlabel.setStyleSheet(("QLabel{color:black;" +
+ "margin:0px; padding:2px;}"))
+ qlabel.setFixedWidth(qrect1.width() * _width_scaling_factor)
+ qlabel.setFixedHeight(_widget_height)
+
+ qle.setFocusPolicy(Qt.NoFocus)
+ qle.setFont(_font)
+ qle.setStyleSheet(("QLineEdit{color:blue;" +
+ "background-color: lightgray;" +
+ "qproperty-readOnly: true;" +
+ "margin:0px; padding:2px;}"))
+ qle.setFixedWidth(qrect2.width() * _width_scaling_factor_le)
+ qle.setFixedHeight(_widget_height)
+ qle.setAlignment(Qt.AlignRight)
+
+ _hbox_widget = QWidget()
+ _hbox = QHBoxLayout()
+ _hbox.addWidget(qlabel)
+ _hbox.addWidget(qle)
+ _hbox_widget.setLayout(_hbox)
+ _hbox.setAlignment(Qt.AlignCenter)
+ _hbox.setContentsMargins(0, 2, 0, 0)
+ _vbox.addWidget(_hbox_widget)
+
+ _vbox.setContentsMargins(0, 0, 0, 0)
+ _vbox.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+
+ _vbox2_widget = QWidget()
+ _vbox2 = QVBoxLayout()
+ _vbox2.setContentsMargins(0, 20, 0, 40)
+ table = QTableWidget(len(self.table_dict)-1, 2)
+ table.verticalHeader().setVisible(False)
+ table.setFocusPolicy(Qt.NoFocus)
+ #table.setFont(_font)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, (label, text) in enumerate(self.table_dict.items()):
+ item1 = QTableWidgetItem(str(label))
+ item2 = QTableWidgetItem(str(text))
+ item1.setTextAlignment(Qt.AlignCenter)
+ item2.setTextAlignment(Qt.AlignCenter)
+ item1.setForeground(QColor("black"))
+ item2.setForeground(QColor("black"))
+ if i%2 == 0:
+ item1.setBackground(QColor("lightgray"))
+ item2.setBackground(QColor("lightgray"))
+
+ if len(str(label)) > len(longest_str_item1):
+ longest_str_item1 = str(label)
+ if len(str(text)) > len(longest_str_item2):
+ longest_str_item2 = str(text)
+
+ if i == 0:
+ #item1.setFont(_font)
+ #item2.setFont(_font)
+ table.setHorizontalHeaderItem(0, item1)
+ table.setHorizontalHeaderItem(1, item2)
+ else:
+ table.setItem(i-1, 0, item1)
+ table.setItem(i-1, 1, item2)
+
+
+ fm = QFontMetricsF(_font)
+
+ _factor = 1.2
+
+ if LooseVersion(QT_VERSION_STR) < LooseVersion("5.0"):
+ _factor = 1.18
+
+ qrect = fm.boundingRect(longest_str_item1 + longest_str_item2 )
+
+ _width_scaling_factor = 1.04
+ table.resizeColumnsToContents()
+ table.resizeRowsToContents()
+ #print(fm.lineSpacing())
+ table.setFixedHeight((fm.lineSpacing() * _factor * len(self.table_dict))
+ + fm.lineSpacing()*2)
+
+ #table.setColumnWidth(0, fm.boundingRect(longest_str_item1).width() * _width_scaling_factor)
+ #table.setColumnWidth(1, fm.boundingRect(longest_str_item2).width() * _width_scaling_factor)
+ table.setFixedWidth(((qrect.width()) * _width_scaling_factor))
+ #table.setFixedWidth(220)
+ _vbox2.addWidget(table)
+ _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+ _vbox2_widget.setLayout(_vbox2)
+
+ _vbox.addWidget(_vbox2_widget)
+
+ self._group_box.setLayout(_vbox)
+ self._group_box.setContentsMargins(20, 20, 20, 20)
+ self._group_box.setAlignment(Qt.AlignTop)
+ self._group_box.setFixedHeight(table.height() + (
+ _widget_height*len(self.summary_dict))) #_vbox2_widget.height() -50)
+ self._group_box.setFixedWidth(table.width() + 20)
+ return self._group_box
+
+
+class QResultsTableWidget():
+ """Results table"""
+ def __init__(self, column_headings=None):
+
+ self.column_headings = column_headings
+ self._group_box = None
+
+ def group_box(self, title="Table of Results"):
+ self._group_box = QGroupBox(title)
+ self._group_box.setObjectName("OUTER")
+
+ _font = QFont("Sans Serif", 10)
+
+ _vbox2_widget = QWidget()
+ _vbox2 = QVBoxLayout()
+ _vbox2.setContentsMargins(0, 20, 0, 40)
+ table = QTableWidget(1, len(self.column_headings))
+ table.verticalHeader().setVisible(True)
+ table.setFocusPolicy(Qt.NoFocus)
+ table.setFont(_font)
+
+ longest_str_item1 = ""
+ longest_str_item2 = ""
+
+ for i, heading in enumerate(self.column_headings):
+ _item = QTableWidgetItem(str(heading))
+ table.setHorizontalHeaderItem(i, _item)
+
+
+ table.resizeColumnsToContents()
+ table.resizeRowsToContents()
+ #print(fm.lineSpacing())
+ table.setFixedHeight(400)
+
+ _vbox2.addWidget(table)
+ _vbox2.setAlignment(Qt.AlignCenter|Qt.AlignTop)
+ _vbox2_widget.setLayout(_vbox2)
+
+
+ self._group_box.setLayout(_vbox2)
+ self._group_box.setContentsMargins(20, 20, 20, 20)
+ self._group_box.setAlignment(Qt.AlignTop)
+
+ self._group_box.setFixedWidth(table.width() + 20)
+ return self._group_box
+
+
+class QHDFDockWidget(QDockWidget):
+
+ def __init__(self, title=None, parent=None):
+ super().__init__(title, parent)
+ self.parent = parent
+ self.is_docked = True
+ self.geometry_from_qsettings = self.parent.getGeometry()
+ self.topLevelChanged.connect(self._top_level_changed)
+ self.setVisible(False)
+ self.setFloating(False)
+ self.geometry_from_qsettings = self.parent.geometry()
+ print( "START GEOEMTRY ",self.geometry_from_qsettings)
+
+
+ def closeEvent(self, event: QCloseEvent):
+ ######################print("Super ClosingEvent....")
+ super().closeEvent(event)
+ print("Super ClosedEvent....")
+ print("float/1", self.isFloating())
+ print("visible/1", self.isVisible())
+ #print("isDocked/1", self.is_docked)
+ print("before", self.parent.geometry(), self.geometry_from_qsettings)
+ #self.parent.setGeometry(self.geometry_from_qsettings)
+ #self.setGeometry(self.geometry_from_qsettings)
+ #QApplication.processEvents()
+ print("after", self.parent.geometry())
+
+
+ def changeEvent(self, event):
+ print("event Type", event.type())
+ print("Sender", self.sender())
+ #This implies that one of restore/quit button of the widget was clicked
+ #if self.senderSignalIndex() == 34:
+ # self.close()
+ print("before//", self.parent.geometry(), self.geometry_from_qsettings)
+ #self.parent.setGeometry(self.geometry_from_qsettings)
+ #Generic
+ #if "QAbstractButton" in str(self.sender()):
+ # self.geometry_from_qsettings = self.parent.geometry()
+ print("after", self.parent.geometry())
+
+ def _top_level_changed(self, is_floating):
+ #Need MUTEX
+ print("is_floating", is_floating)
+ #self.setVisible(False)
+ #self.setFloating(True)
+ #ResetGeometry
+ #self.parent.setGeometry(self.geometry_from_qsettings)
+ #QApplication.processEvents()
+
+class QNoDockWidget(QDockWidget):
+
+ def __init__(self, title=None, parent=None):
+ super().__init__(title, parent)
+ self.parent = parent
+ self.is_docked = True
+ self.geometry_from_qsettings = self.parent.getGeometry()
+ self.topLevelChanged.connect(self._top_level_changed)
+ self.setVisible(False)
+ self.setFloating(True)
+
+ def changeEvent(self, event):
+ #print("event Type", event.type())
+ #print("Sender", self.sender())
+ #This implies that one of restore/quit button of the widget was clicked
+ #if self.senderSignalIndex() == 34:
+ # self.close()
+ #Generic
+ if "QAbstractButton" in str(self.sender()):
+ self.geometry_from_qsettings = self.parent.geometry()
+
+
+ #def closeEvent(self, event: QCloseEvent):
+ ######################print("Super ClosingEvent....")
+ #super().closeEvent(event)
+ #print("Super ClosedEvent....")
+ #print("float/1", self.isFloating())
+ #print("visible/1", self.isVisible())
+ #print("isDocked/1", self.is_docked)
+
+
+
+ #This is for the quit button
+ #if not self.isVisible():
+ # self.topLevelChanged.emit(False)
+ # print("emitting..")
+ # self.topLevelChanged.emit(False)
+
+
+ def _top_level_changed(self, is_floating):
+ #Need MUTEX
+
+ self.setVisible(False)
+ self.setFloating(True)
+ #ResetGeometry
+ self.parent.setGeometry(self.geometry_from_qsettings)
+ QApplication.processEvents()
+
+
+
+class CAQStripChart(PlotWidget):
+ '''Channel access enabled pyqtgraph.PlotWidget'''
+
+ def __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