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