Source code for bcdaqwidgets.bcdaqwidgets

#!/usr/bin/env python

'''
BcdaQWidgets: PyEpics-aware PyQt widgets for the APS

Copyright (c) 2009-2017, UChicago Argonne, LLC.
See LICENSE file for details.

The bcdaqwidgets [#]_ module provides a set of PyQt4
widgets that are EPICS-aware.  These include:

=============================  ================================================================
widget                         description
=============================  ================================================================
:class:`BcdaQLabel`            EPICS-aware QLabel widget
:class:`BcdaQLineEdit`         EPICS-aware QLineEdit widget
:class:`BcdaQPushButton`       EPICS-aware QPushButton widget
:class:`BcdaQMomentaryButton`  sends a value when pressed or released, label does not change
:class:`BcdaQToggleButton`     toggles boolean PV when pressed
:class:`BcdaQLabel_RBV`        makes motor RBV field background green when motor is moving
=============================  ================================================================

.. [#] BCDA: Beam line Controls and Data Acquisition group 
       of the Advanced Photon Source, Argonne National Laboratory,
       http://www.aps.anl.gov/bcda

.. note:: bcdaqwidgets must be imported AFTER importing PyQt4
'''


import os
import sys
from PyQt4 import QtCore, QtGui
pyqtSignal = QtCore.pyqtSignal
import epics


[docs]def typesafe_enum(*sequential, **named): ''' typesafe enum EXAMPLE:: >>> Numbers = typesafe_enum('ZERO', 'ONE', 'TWO', four='IV') >>> Numbers.ZERO 0 >>> Numbers.ONE 1 >>> Numbers.four IV :see: http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-typesafe_enum-in-python ''' enums = dict(zip(sequential, range(len(sequential))), **named) return type('TypesafeEnum', (), enums)
AllowedStates = typesafe_enum('DISCONNECTED', 'CONNECTED',) CLUT = { # clut: Color LookUp Table AllowedStates.DISCONNECTED: "#ffffff", # white AllowedStates.CONNECTED: "#e0e0e0", # a bit darker than default #f0f0f0 } SeverityColor = typesafe_enum('NO_ALARM', 'MINOR', 'MAJOR', 'CALC_INVALID') SeverityColor.NO_ALARM = "green" # green SeverityColor.MINOR = "#ff0000" # dark orange since yellow looks bad against gray SeverityColor.MAJOR = "red" # red SeverityColor.CALC_INVALID = "pink" # pink BACKGROUND_DEFAULT = '#efefef' BACKGROUND_DONE_MOVING = BACKGROUND_DEFAULT BACKGROUND_MOVING = 'lightgreen' DMOV_COLOR_TABLE = {1: BACKGROUND_DONE_MOVING, 0: BACKGROUND_MOVING} BLANKS = ' '*4
[docs]class StyleSheet(object): ''' manage style sheet settings for a Qt widget Example:: widget = QtGui.QLabel('example label') sty = bcdaqwidgets.StyleSheet(widget) sty.updateStyleSheet({ 'font': 'bold', 'color': 'white', 'background-color': 'dodgerblue', 'qproperty-alignment': 'AlignCenter', }) ''' def __init__(self, widget, sty={}): ''' :param obj widget: the Qt widget on which to apply the style sheet :param dict sty: starting dictionary of style sheet settings ''' self.widget = widget widgetclass = str(type(widget)).strip('>').split('.')[-1].strip("'") self.widgetclass = widgetclass self.style_cache = dict(sty)
[docs] def clearCache(self): '''clear the internal cache''' self.style_cache = {}
[docs] def updateStyleSheet(self, sty={}): '''change specified styles and apply all to widget''' self._updateCache(sty) if self.widget is not None: self.widget.setStyleSheet(str(self))
def _updateCache(self, sty={}): '''update internal cache with specified styles''' for key, value in sty.items(): self.style_cache[key] = value def __str__(self): '''returns a CSS text with the cache settings''' s = self.widgetclass + ' {\n' for key, value in sorted(self.style_cache.items()): s += ' %s: %s;\n' % (key, value) s += '}' return s
[docs]class BcdaQSignalDef(QtCore.QObject): ''' Define the signals used to communicate between the PyEpics thread and the PyQt4 (main Qt4 GUI) thread. ''' newFgColor = pyqtSignal() newBgColor = pyqtSignal() newText = pyqtSignal(str) dmov = pyqtSignal(int)
[docs]class BcdaQWidgetSuper(object): '''superclass for EPICS-aware widgets''' css = {} def __init__(self, pvname=None, useAlarmState=False): self.style_dict = {} self.pv = None # PyEpics PV object self.ca_callback = None self.ca_connect_callback = None self.state = AllowedStates.DISCONNECTED self.labelSignal = BcdaQSignalDef() self.clut = dict(CLUT) self.useAlarmState = useAlarmState self.severity_color_list = [SeverityColor.NO_ALARM, SeverityColor.MINOR, SeverityColor.MAJOR, SeverityColor.CALC_INVALID] # for internal use persisting the various styleSheet settings self._style_sheet = StyleSheet(self) self.updateStyleSheet(self.css)
[docs] def ca_connect(self, pvname, ca_callback=None, ca_connect_callback=None): ''' Connect this widget with the EPICS pvname :param str pvname: EPICS Process Variable name :param obj ca_callback: EPICS CA callback handler method :param obj ca_connect_callback: EPICS CA connection state callback handler method ''' if self.pv is not None: self.ca_disconnect() if len(pvname) > 0: self.ca_callback = ca_callback self.ca_connect_callback = ca_connect_callback self.pv = epics.PV(pvname, callback=self.onPVChange, connection_callback=self.onPVConnect) self.state = AllowedStates.CONNECTED self.setToolTip(pvname)
[docs] def ca_disconnect(self): '''disconnect from this EPICS PV, if connected''' if self.pv is not None: self.pv.remove_callback() pvname = self.pv.pvname self.pv.disconnect() self.pv = None self.ca_callback = None self.ca_connect_callback = None self.state = AllowedStates.DISCONNECTED self.SetText(BLANKS) self.SetBackgroundColor() self.setToolTip(pvname + ' not connected')
[docs] def onPVConnect(self, pvname='', **kw): '''respond to a PyEpics CA connection event''' conn = kw['conn'] self.state = { # adjust the state False: AllowedStates.DISCONNECTED, True: AllowedStates.CONNECTED, }[conn] self.labelSignal.newBgColor.emit() # threadsafe update of the widget if self.ca_connect_callback is not None: # caller wants to be notified of this camonitor event self.ca_connect_callback(**kw)
[docs] def onPVChange(self, pvname=None, char_value=None, **kw): '''respond to a PyEpics camonitor() event''' self.labelSignal.newText.emit(char_value) # threadsafe update of the widget if self.ca_callback is not None: # caller wants to be notified of this camonitor event self.ca_callback(pvname=pvname, char_value=char_value, **kw)
[docs] def SetText(self, text, *args, **kw): '''set the text of the widget (threadsafe update)''' self.setText(text) # if desired, color the text based on the alarm severity if self.useAlarmState and self.pv is not None: self.pv.get_ctrlvars() if self.pv.severity is not None: if self.pv.severity < 0 or self.pv.severity >= len(self.severity_color_list): print self.pv.severity print self.severity_color_list pass color = self.severity_color_list[self.pv.severity] self.updateStyleSheet({'color': color})
[docs] def updateStyleSheet(self, changes_dict): '''update the widget's stylesheet''' self._style_sheet.updateStyleSheet(changes_dict)
[docs]class BcdaQLabel(QtGui.QLabel, BcdaQWidgetSuper): ''' Provide the value of an EPICS PV on a PyQt4.QtGui.QLabel USAGE:: import bcdaqwidgets ... widget = bcdaqwidgets.BcdaQLabel() widget.ca_connect("example:m1.RBV") :param str pvname: epics process variable name for this widget :param bool useAlarmState: change the text color based on pv severity :param str bgColorPv: update widget's background color based on this pv's value ''' css = { 'background-color': 'bisque', 'border': '1px solid gray', 'font': 'bold', } def __init__(self, pvname=None, useAlarmState=False, bgColorPv=None): ''':param str text: initial Label text (really, we can ignore this)''' QtGui.QLabel.__init__(self, BLANKS) BcdaQWidgetSuper.__init__(self, useAlarmState=useAlarmState) # define the signals we'll use in the camonitor handler to update the GUI self.labelSignal = BcdaQSignalDef() self.labelSignal.newBgColor.connect(self.SetBackgroundColor) self.labelSignal.newText.connect(self.SetText) self.updateStyleSheet(self.css) self.clut = dict(CLUT) self.pv = None self.ca_callback = None self.ca_connect_callback = None self.state = AllowedStates.DISCONNECTED self.SetBackgroundColor() self.setAlignment(QtCore.Qt.AlignHCenter) if pvname is not None and isinstance(pvname, str): self.ca_connect(pvname) if bgColorPv is not None: self.bgColorObj = epics.PV(pvname=bgColorPv, callback=self.onBgColorObjChanged) self.bgColor_clut = {'not connected': 'white', '0': '#88ff88', '1': 'transparent'} self.bgColor = None self.bgColorSignal = BcdaQSignalDef() self.bgColorSignal.newBgColor.connect(self.SetBackgroundColorExtra)
[docs] def onBgColorObjChanged(self, *args, **kw): '''epics pv callback when bgColor PV changes''' if not self.bgColorObj.connected: # white and displayed text is ' ' self.bgColor = self.bgColor_clut['not connected'] else: value = str(self.bgColorObj.get()) if value in self.bgColor_clut: self.bgColor = self.bgColor_clut[value] # trigger the background color to change self.bgColorSignal.newBgColor.emit()
[docs] def SetBackgroundColorExtra(self, *args, **kw): '''changes the background color of the widget''' if self.bgColor is not None: self.updateStyleSheet({'background-color': self.bgColor}) self.bgColor = None
[docs] def SetBackgroundColor(self, *args, **kw): '''set the background color of the widget via its stylesheet''' color = self.clut[self.state] self.updateStyleSheet({'background-color': color})
[docs]class BcdaQLineEdit(QtGui.QLineEdit, BcdaQWidgetSuper): ''' Provide the value of an EPICS PV on a PyQt4.QtGui.QLineEdit USAGE:: import bcdaqwidgets ... widget = bcdaqwidgets.BcdaQLineEdit() widget.ca_connect("example:m1.VAL") ''' css = { 'background-color': 'bisque', 'border': '3px inset gray', } def __init__(self, pvname=None, useAlarmState=False): ''':param str text: initial Label text (really, we can ignore this)''' QtGui.QLineEdit.__init__(self, BLANKS) BcdaQWidgetSuper.__init__(self) # define the signals we'll use in the camonitor handler to update the GUI self.labelSignal.newBgColor.connect(self.SetBackgroundColor) self.labelSignal.newText.connect(self.SetText) self.clut = dict(CLUT) self.clut[AllowedStates.CONNECTED] = "bisque" self.updateStyleSheet(self.css) self.SetBackgroundColor() self.setAlignment(QtCore.Qt.AlignHCenter) self.returnPressed.connect(self.onReturnPressed) if pvname is not None and isinstance(pvname, str): self.ca_connect(pvname)
[docs] def onReturnPressed(self): '''send the widget's text to the EPICS PV''' if self.pv is not None and len(self.text()) > 0: self.pv.put(str(self.text()))
[docs] def SetBackgroundColor(self, *args, **kw): '''set the background color of the QLineEdit() via its QPalette''' color = self.clut[self.state] self.updateStyleSheet({'background-color': color})
[docs]class BcdaQPushButton(QtGui.QPushButton, BcdaQWidgetSuper): ''' Provide a QtGui.QPushButton connected to an EPICS PV It is necessary to also call the SetPressedValue() and/or SetReleasedValue() method to define the value to be sent to the EPICS PV with the corresponding push button event. If left unconfigured, no action will be taken. USAGE:: import bcdaqwidgets ... widget = bcdaqwidgets.BcdaQPushButton() widget.ca_connect("example:bo0") widget.SetReleasedValue(1) ''' css = {'font': 'bold',} def __init__(self, label='', pvname=None, pressed_value=None, released_value=None): ''':param str text: initial Label text (really, we can ignore this)''' QtGui.QPushButton.__init__(self, label) BcdaQWidgetSuper.__init__(self) self.labelSignal = BcdaQSignalDef() self.labelSignal.newBgColor.connect(self.SetBackgroundColor) self.labelSignal.newText.connect(self.SetText) self.clut = dict(CLUT) self.updateStyleSheet(self.css) self.pv = None self.ca_callback = None self.ca_connect_callback = None self.state = AllowedStates.DISCONNECTED self.setCheckable(True) self.SetBackgroundColor() self.clicked[bool].connect(self.onPressed) self.released.connect(self.onReleased) self.pressed_value = pressed_value self.released_value = released_value if pvname is not None and isinstance(pvname, str): self.ca_connect(pvname)
[docs] def onPressed(self, **kw): '''button was pressed, send preset value to EPICS''' if self.pv is not None and self.pressed_value is not None: self.pv.put(self.pressed_value, wait=True)
[docs] def onReleased(self, **kw): '''button was released, send preset value to EPICS''' if self.pv is not None and self.released_value is not None: self.pv.put(self.released_value, wait=True)
[docs] def SetPressedValue(self, value): '''specify the value to be sent to the EPICS PV when the button is pressed''' self.pressed_value = value
[docs] def SetReleasedValue(self, value): '''specify the value to be sent to the EPICS PV when the button is released''' self.released_value = value
[docs] def SetBackgroundColor(self, *args, **kw): '''set the background color of the QPushButton() via its stylesheet''' color = self.clut[self.state] self.updateStyleSheet({'background-color': color})
[docs]class BcdaQMomentaryButton(BcdaQPushButton): ''' Send a value when released, label does not change if PV changes. It only acts on mouse pressed signal. This is a special case of a BcdaQPushButton where the text on the button does not respond to changes of the value of the attached EPICS PV. It is a good choice to use, for example, for a motor STOP button. USAGE:: import bcdaqwidgets ... widget = bcdaqwidgets.BcdaQMomentaryButton('Stop') widget.ca_connect("example:m1.STOP") widget.SetPressedValue(1) ''' # disable these methods def SetText(self, *args, **kw): pass def onReleased(self, **kw): pass
[docs] def onPressed(self, **kw): '''button was pressed, send preset value to EPICS''' if self.pv is not None and self.pressed_value is not None: self.pv.put(self.pressed_value) self.setCheckable(False)
[docs]class BcdaQToggleButton(BcdaQPushButton): ''' Toggles boolean PV when pressed This is a special case of a BcdaQPushButton where the text on the button changes with the value of the attached EPICS PV. In this case, the displayed value is the name of the next state of the EPICS PV when the button is pressed. It is a good choice to use, for example, for an ON/OFF button. USAGE:: import bcdaqwidgets ... widget = bcdaqwidgets.BcdaQToggleButton() widget.ca_connect("example:room_light") widget.SetReleasedValue(1) ''' def __init__(self, pvname=None): BcdaQPushButton.__init__(self, pvname=pvname) self.value_names = {1: 'change to 0', 0: 'change to 1'} self.setToolTip('tell EPICS PV to do this') if pvname is not None and isinstance(pvname, str): self.ca_connect(pvname)
[docs] def ca_connect(self, pvname, ca_callback=None, ca_connect_callback=None): ''' ''' BcdaQPushButton.ca_connect(self, pvname, ca_callback=None, ca_connect_callback=None) labels = self.pv.enum_strs if labels is not None and len(labels) == 2: # describe what will happen when the button is pressed self.value_names = list(reversed(labels))
[docs] def onPressed(self): '''button was pressed, toggle the EPICS value as a boolean''' if self.pv is not None: self.pv.put(not self.pv.get(), wait=True)
# disable these methods def onReleased(self, **kw): pass def SetPressedValue(self, value): pass def SetReleasedValue(self, value): pass
[docs] def SetText(self, *args, **kw): '''set the text of the widget (threadsafe update) from the EPICS PV''' self.setText(self.value_names[self.pv.get()])
[docs]class BcdaQLabel_RBV(BcdaQLabel): ''' makes motor readback field (BcdaQLabel) background green when motor is moving EXAMPLE:: pvname = 'ioc:m1' w = bcdaqwidgets.BcdaQLabel_RBV() layout.addWidget(w) w.ca_connect(pvname+'.RBV') ''' def __init__(self, *args, **kw): BcdaQLabel.__init__(self, *args, **kw) self.sty = StyleSheet(self) self.signal = BcdaQSignalDef() self.dmov = None
[docs] def ca_connect(self, rbv_pv): ''' ''' super(BcdaQLabel_RBV, self).ca_connect(rbv_pv) dmov_pv = rbv_pv.split('.')[0] + '.DMOV' self.dmov = epics.PV(dmov_pv, callback=self.dmov_callback) self.signal.dmov.connect(self.setBackgroundColor)
[docs] def dmov_callback(self, *args, **kw): '''called in PyEpics thread''' self.signal.dmov.emit(kw['value'])
[docs] def setBackgroundColor(self, value): '''called in GUI thread''' color = DMOV_COLOR_TABLE[value] self.sty.updateStyleSheet({'background-color': color})
RBV_BcdaQLabel = BcdaQLabel_RBV # legacy name