Пример #1
0
class TyphosAlarmPolygon(TyphosAlarm, PyDMDrawingPolygon):
    QtCore.Q_ENUMS(_KindLevel)
    kindLevel = TyphosAlarm.kindLevel
    numberOfPoints = PyDMDrawingPolygon.numberOfPoints
Пример #2
0
class TyphosAlarm(TyphosObject, PyDMDrawing, _KindLevel, _AlarmLevel):
    """
    Class that holds logic and routines common to all Typhos Alarm widgets.

    Overall, these classes exist to summarize alarm states from Ophyd Devices
    and change the colors on indicator widgets appropriately.

    We will consider a subset of the signals that is of KindLevel and above and
    summarize state based on the "worst" alarm we see as defined by AlarmLevel.
    """
    QtCore.Q_ENUMS(_KindLevel)
    QtCore.Q_ENUMS(_AlarmLevel)

    KindLevel = KindLevel
    AlarmLevel = AlarmLevel

    alarm_changed = QtCore.Signal(_AlarmLevel)

    def __init__(self, *args, **kwargs):
        self._kind_level = KindLevel.HINTED
        super().__init__(*args, **kwargs)
        # Default drawing properties, can override if needed
        self.penWidth = 2
        self.penColor = QtGui.QColor('black')
        self.penStyle = Qt.SolidLine
        self.reset_alarm_state()
        self.alarm_changed.connect(self.set_alarm_color)

    @QtCore.Property(_KindLevel)
    def kindLevel(self):
        """
        Determines which signals to include in the alarm summary.

        If this is "hinted", only include hinted signals.
        If this is "normal", include normal and hinted signals.
        If this is "config", include everything except for omitted signals
        If this is "omitted", include all signals
        """
        return self._kind_level

    @kindLevel.setter
    def kindLevel(self, kind_level):
        # We must update the alarm config to add/remove PVs as appropriate.
        self._kind_level = kind_level
        self.update_alarm_config()

    @QtCore.Property(str)
    def channel(self):
        """
        The channel address to use for this widget.

        If this is a happi:// channel, we'll create the device and
        add it to this widget.

        If this is a ca:// channel, we'll connect to the PV and include its
        alarm information in the evaluation of this widget.

        There is an assumption that you'll either be using this via one of the
        channel options or by using "add_device" one or more times. There may
        be some strange behavior if you try to set up this widget using both
        approaches at the same time.
        """
        if self._channel:
            return str(self._channel)
        return None

    @channel.setter
    def channel(self, value):
        if self._channel != value:
            # Remove old connection
            if self._channels:
                for channel in self._channels:
                    if hasattr(channel, 'disconnect'):
                        channel.disconnect()
                    if channel in self.signal_info:
                        del self.signal_info[channel]
                self._channels.clear()
            # Load new channel
            self._channel = str(value)
            if 'happi://' in self._channel:
                channel = HappiChannel(
                    address=self._channel,
                    tx_slot=self._tx,
                )
            else:
                channel = PyDMChannel(
                    address=self._channel,
                    connection_slot=partial(self.update_connection,
                                            addr=self._channel),
                    severity_slot=partial(self.update_severity,
                                          addr=self._channel),
                )
                self.signal_info[self._channel] = SignalInfo(
                    address=self._channel,
                    channel=channel,
                    signal_name='',
                    connected=False,
                    severity=AlarmLevel.INVALID,
                )
            self._channels = [channel]
            # Connect the channel to the HappiPlugin
            if hasattr(channel, 'connect'):
                channel.connect()

    def _tx(self, value):
        """Receive information from happi channel"""
        self.add_device(value['obj'])

    def reset_alarm_state(self):
        self.signal_info = {}
        self.device_info = defaultdict(list)
        self.alarm_summary = AlarmLevel.DISCONNECTED
        self.set_alarm_color(AlarmLevel.DISCONNECTED)

    def channels(self):
        """
        Let pydm know about our pydm channels.
        """
        ch = list(self._channels)
        for info in self.signal_info.values():
            ch.append(info.channel)
        return ch

    def add_device(self, device):
        """
        Initialize our alarm handling when adding a device.
        """
        super().add_device(device)
        self.setup_alarm_config(device)

    def clear_all_alarm_configs(self):
        """
        Reset this widget down to the "no alarm handling" state.
        """
        for ch in (info.channel for info in self.signal_info.values()):
            ch.disconnect()
        self.reset_alarm_state()

    def setup_alarm_config(self, device):
        """
        Add a device to the alarm summary.

        This will pick PVs based on the device kind and the configured kind
        level, configuring the PyDMChannels to update our alarm state and
        color when we get updates from our PVs.
        """
        sigs = get_all_signals_from_device(
            device, filter_by=KIND_FILTERS[self._kind_level])
        channel_addrs = [channel_from_signal(sig) for sig in sigs]
        for sig in sigs:
            if not isinstance(sig, EpicsSignalBase):
                register_signal(sig)
        channels = [
            PyDMChannel(
                address=addr,
                connection_slot=partial(self.update_connection, addr=addr),
                severity_slot=partial(self.update_severity, addr=addr),
            ) for addr in channel_addrs
        ]

        for ch, sig in zip(channels, sigs):
            info = SignalInfo(
                address=ch.address,
                channel=ch,
                signal_name=sig.dotted_name,
                connected=False,
                severity=AlarmLevel.INVALID,
            )
            self.signal_info[ch.address] = info
            self.device_info[device.name].append(info)
            ch.connect()

        all_channels = self.channels()
        if all_channels:
            logger.debug(
                f'Finished setup of alarm config for device {device.name} on '
                f'alarm widget with channel {all_channels[0]}.')
        else:
            logger.warning(
                f'Tried to set up alarm config for device {device.name}, but '
                'did not configure any channels! Check your kindLevel!')

    def update_alarm_config(self):
        """
        Clean up the existing alarm config and create a new one.

        This must be called when settings like KindLevel are changed so we can
        re-evaluate them.
        """
        self.clear_all_alarm_configs()
        for dev in self.devices:
            self.setup_alarm_config(dev)

    def update_connection(self, connected, addr):
        """Slot that will be called when a PV connects or disconnects."""
        self.signal_info[addr].connected = connected
        self.update_current_alarm()

    def update_severity(self, severity, addr):
        """Slot that will be called when a PV's alarm severity changes."""
        self.signal_info[addr].severity = severity
        self.update_current_alarm()

    def update_current_alarm(self):
        """
        Check what the current worst available alarm state is.

        If the alarm state is different than the last time we checked,
        emit the "alarm_changed" signal. This signal is configured at
        init to change the color of this widget.
        """
        if not self.signal_info:
            new_alarm = AlarmLevel.INVALID
        else:
            new_alarm = max(info.alarm for info in self.signal_info.values())
        if new_alarm != self.alarm_summary:
            try:
                self.alarm_changed.emit(new_alarm)
            except RuntimeError:
                # Widget was destroyed and not properly cleaned up
                logger.debug('Dangling reference to alarm widget!')
                return
            else:
                logger.debug(
                    f'Updated alarm from {self.alarm_summary} to {new_alarm} '
                    f'on alarm widget with channel {self.channels()[0]}')

        self.alarm_summary = new_alarm

    def set_alarm_color(self, alarm_level):
        """
        Change the alarm color to the shade defined by the current alarm level.
        """
        self.setStyleSheet(indicator_stylesheet(self.__class__, alarm_level))

    def eventFilter(self, obj, event):
        """
        Extra handling for showing the user which alarms are alarming.

        We'll show this information on mouseover if anything is disconnected or
        in an alarm state, unless the user middle-clicks, which will have the
        default PyDM behavior of showing all the channels and copying them to
        clipboard.
        """
        # super() doesn't work here, some strange pyqt thing
        default_pydm_event = PyDMPrimitiveWidget.eventFilter(self, obj, event)
        if default_pydm_event:
            return True
        if event.type() == QtCore.QEvent.Enter:
            alarming = self.show_alarm_tooltip(event)
            return alarming
        return False

    def show_alarm_tooltip(self, event):
        """
        Show a tooltip that reveals which channels are alarmed or disconnected.
        """
        tooltip_lines = []

        # Start with the channel field, just show the status.
        if self.channel in self.signal_info:
            info = self.signal_info[self.channel]
            tooltip_lines.append(f'Channel {info.describe()}')

        # Handle each device
        for name, device_info_list in self.device_info.items():
            # At least show the device name
            tooltip_lines.append(f'Device {name}')
            has_alarm = False
            for info in device_info_list:
                if info.alarm != AlarmLevel.NO_ALARM:
                    if not has_alarm:
                        has_alarm = True
                        tooltip_lines.append('-' * 2 * len(tooltip_lines[-1]))
                    tooltip_lines.append(info.describe())

        if tooltip_lines:
            tooltip = os.linesep.join(tooltip_lines)
            QtWidgets.QToolTip.showText(
                self.mapToGlobal(QtCore.QPoint(event.x() + 10, event.y())),
                tooltip,
                self,
            )

        # Return True if we showed something
        return bool(tooltip_lines)
Пример #3
0
class TyphosAlarmEllipse(TyphosAlarm, PyDMDrawingEllipse):
    QtCore.Q_ENUMS(_KindLevel)
    kindLevel = TyphosAlarm.kindLevel
Пример #4
0
class TyphosAlarmTriangle(TyphosAlarm, PyDMDrawingTriangle):
    QtCore.Q_ENUMS(_KindLevel)
    kindLevel = TyphosAlarm.kindLevel
Пример #5
0
class TyphosAlarmCircle(TyphosAlarm, PyDMDrawingCircle):
    QtCore.Q_ENUMS(_KindLevel)
    kindLevel = TyphosAlarm.kindLevel
Пример #6
0
class PyDMDateTimeEdit(QtWidgets.QDateTimeEdit, PyDMWritableWidget, TimeBase):
    QtCore.Q_ENUMS(TimeBase)
    returnPressed = QtCore.Signal()
    """
    A QDateTimeEdit with support for setting the text via a PyDM Channel, or
    through the PyDM Rules system.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """
    def __init__(self, parent=None, init_channel=None):
        self._block_past_date = True
        self._relative = True
        self._time_base = TimeBase.Milliseconds

        QtWidgets.QDateTimeEdit.__init__(self, parent=parent)
        PyDMWritableWidget.__init__(self, init_channel=init_channel)
        self.setDisplayFormat("yyyy/MM/dd hh:mm:ss.zzz")
        self.setDateTime(QtCore.QDateTime.currentDateTime())
        self.setCalendarPopup(True)
        self.returnPressed.connect(self.send_value)

    @QtCore.Property(TimeBase)
    def timeBase(self):
        """Whether to use milliseconds or seconds as time base for the widget"""
        return self._time_base

    @timeBase.setter
    def timeBase(self, base):
        if self._time_base != base:
            self._time_base = base

    @QtCore.Property(bool)
    def relative(self):
        """
        Whether the value in milliseconds is relative to current date or if it
        is milliseconds since epoch.
        """
        return self._relative

    @relative.setter
    def relative(self, checked):
        if self._relative != checked:
            self._relative = checked

    @QtCore.Property(bool)
    def blockPastDate(self):
        """Error out if user tries to set value to a date older than current."""
        return self._block_past_date

    @blockPastDate.setter
    def blockPastDate(self, block):
        if block != self._block_past_date:
            self._block_past_date = block

    def keyPressEvent(self, key_event):
        ret = super(PyDMDateTimeEdit, self).keyPressEvent(key_event)
        if key_event.key() in [QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter]:
            self.returnPressed.emit()
        return ret

    def send_value(self):
        val = self.dateTime()
        now = QtCore.QDateTime.currentDateTime()
        if self._block_past_date and val < now:
            logger.error('Selected date cannot be lower than current date.')
            return

        if self.relative:
            new_value = now.msecsTo(val)
        else:
            new_value = val.currentMSecsSinceEpoch()

        if self.timeBase == TimeBase.Seconds:
            new_value /= 1000.0
        self.send_value_signal.emit(new_value)

    def value_changed(self, new_val):
        super(PyDMDateTimeEdit, self).value_changed(new_val)

        if self.timeBase == TimeBase.Seconds:
            new_val *= 1000

        val = QtCore.QDateTime.currentDateTime()
        if self._relative:
            val = val.addMSecs(new_val)
        else:
            val.setMSecsSinceEpoch(new_val)
        self.setDateTime(val)
Пример #7
0
class PyDMDateTimeLabel(QtWidgets.QLabel, PyDMWidget, TimeBase):
    QtCore.Q_ENUMS(TimeBase)
    """
    A QLabel with support for setting the text via a PyDM Channel, or
    through the PyDM Rules system.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    init_channel : str, optional
        The channel to be used by the widget.
    """
    def __init__(self, parent=None, init_channel=None):
        QtWidgets.QLabel.__init__(self, parent=parent)
        PyDMWidget.__init__(self, init_channel=init_channel)

        self._block_past_date = True
        self._relative = True
        self._time_base = TimeBase.Milliseconds
        self._text_format = "yyyy/MM/dd hh:mm:ss.zzz"
        self.setText("")

    @QtCore.Property(str)
    def textFormat(self):
        """The format to use when displaying the date/time values."""
        return self._text_format

    @textFormat.setter
    def textFormat(self, text_format):
        if self._text_format != text_format:
            self._text_format = text_format
            self.value_changed(self.value)

    @QtCore.Property(TimeBase)
    def timeBase(self):
        """Whether to use milliseconds or seconds as time base for the widget"""
        return self._time_base

    @timeBase.setter
    def timeBase(self, base):
        if self._time_base != base:
            self._time_base = base

    @QtCore.Property(bool)
    def relative(self):
        """
        Whether the value in milliseconds is relative to current date or if it
        is milliseconds since epoch.
        """
        return self._relative

    @relative.setter
    def relative(self, checked):
        if self._relative != checked:
            self._relative = checked

    def value_changed(self, new_val):
        super(PyDMDateTimeLabel, self).value_changed(new_val)

        if self.timeBase == TimeBase.Seconds:
            new_val *= 1000

        val = QtCore.QDateTime.currentDateTime()
        if self._relative:
            val = val.addMSecs(new_val)
        else:
            val.setMSecsSinceEpoch(new_val)
        self.setText(val.toString(self.textFormat))