Beispiel #1
0
    def set_channels(self, new_channels):
        if not new_channels:
            self.setEnabled(False)
        else:
            self.setEnabled(True)

        # Check which channel can be removed
        address2pop = list()
        for address in self._address2channel.keys():
            if address not in new_channels:
                address2pop.append(address)
            else:
                new_channels.remove(address)

        # Remove channels
        for address in address2pop:
            self._address2channel[address].disconnect()
            self._address2channel.pop(address)
            self._address2conn.pop(address)

        # Add new channels
        for address in new_channels:
            self._address2conn[address] = False
            channel = PyDMChannel(address=address,
                                  connection_slot=self.connection_changed)
            channel.connect()
            self._address2channel[address] = channel

        self._channels = list(self._address2channel.values())

        self._update_state()
Beispiel #2
0
    def _setup_channels(self):
        if not self._prefix or not self._segment:
            return

        for entry, pv_format in UndulatorWidget.CHANNELS.items():
            pv = pv_format.format(prefix=self.prefix, segment=self.segment)
            conn_cb = functools.partial(self.conn_cb, entry)
            val_cb = functools.partial(self.value_cb, entry)
            ch = PyDMChannel(pv, value_slot=val_cb, connection_slot=conn_cb)
            self._channels[entry] = ch
            self._values[entry] = None
            self._connections[entry] = False

        for _, ch in self._channels.items():
            ch.connect()
Beispiel #3
0
    def set_channels2values(self, new_channels2values):
        """Set channels2values."""
        self._address2values = _dcopy(new_channels2values)

        if not new_channels2values:
            self.setEnabled(False)
        else:
            self.setEnabled(True)

        # Check which channel can be removed
        address2pop = list()
        for address in self._address2channel.keys():
            if address not in new_channels2values.keys():
                address2pop.append(address)

        # Remove channels
        for address in address2pop:
            self._address2channel[address].disconnect()
            self._address2channel.pop(address)
            self._address2status.pop(address)
            self._address2conn.pop(address)
            self._address2currvals.pop(address)

        # Add new channels
        for address, value in new_channels2values.items():
            if address not in self._address2channel.keys():
                self._address2conn[address] = False
                self._address2status[address] = 'UNDEF'
                self._address2currvals[address] = 'UNDEF'
                channel = PyDMChannel(address=address,
                                      connection_slot=self.connection_changed,
                                      value_slot=self.value_changed)
                channel.connect()
                self._address2channel[address] = channel
            self._address2values[address] = value

        self._channels = list(self._address2channel.values())

        # redo comparisions
        for ad, des in self._address2values.items():
            self._address2status[ad] = self._check_status(
                ad, des, self._address2currvals[ad])

        self._update_statuses()
Beispiel #4
0
class AlarmTree(Display):
    def __init__(self, parent=None, macros=None, **kwargs):
        super().__init__(parent=parent, macros=macros, ui_filename=TREE_32_UI)

        self.isSystem = macros.get("D", "Sys") == "Sys"
        self.isWarn = macros.get("T", "Warn") == "Warn"

        # Warning Groups
        self.ch_mod_warn_report = PyDMChannel(
            address="ca://" + macros["P"] + ":ModWarnGroup-Mon",
            value_slot=self.get_mod_warn_report,
        )

        self.ch_sys_warn_report = PyDMChannel(
            address="ca://" + macros["P"] + ":SysWarnGroup-Mon",
            value_slot=self.get_sys_warn_report,
        )

        # Error Groups
        self.ch_mod_error_report = PyDMChannel(
            address="ca://" + macros["P"] + ":ModErrGroup-Mon",
            value_slot=self.get_mod_error_report,
        )

        self.ch_sys_error_report = PyDMChannel(
            address="ca://" + macros["P"] + ":SysErrGroup-Mon",
            value_slot=self.get_sys_error_report,
        )

        if self.isSystem:
            if self.isWarn:
                self.ch_sys_warn_report.connect()
            else:
                self.ch_sys_error_report.connect()
        else:
            if self.isWarn:
                self.ch_mod_warn_report.connect()
            else:
                self.ch_mod_error_report.connect()

    # Warning
    def get_mod_warn_report(self, value):
        self.label.setText("\n".join(get_report(value, "Module")))

    def get_sys_warn_report(self, value):
        self.label.setText("\n".join(get_report(value, "System")))

    # Error
    def get_mod_error_report(self, value):
        self.label.setText("\n".join(get_report(value, "Module")))

    def get_sys_error_report(self, value):
        self.label.setText("\n".join(get_report(value, "System")))
Beispiel #5
0
    def _setup_channels(self):
        if not self._prefix:
            return

        pvs = {
            'first': 'ca://{prefix}PE:UND:FirstSegment_RBV',
            'last': 'ca://{prefix}PE:UND:LastSegment_RBV'
        }

        for entry, pv_format in pvs.items():
            pv = pv_format.format(prefix=self.prefix)
            conn_cb = functools.partial(self.conn_cb, entry)
            val_cb = functools.partial(self.value_cb, entry == 'first')
            ch = PyDMChannel(pv, value_slot=val_cb, connection_slot=conn_cb)
            self._channels[entry] = ch
            self._connections[entry] = False

        for _, ch in self._channels.items():
            ch.connect()
Beispiel #6
0
class OpenCloseStateMixin(object):
    """
    The OpenCloseStateMixin class adds two channels (Open and Close State) and
    a `state` property based on a combination of the two channels to the
    widget.

    The state property can be used at stylesheet in the following manner:

    .. code-block:: css

        *[state="Close"] {
            background-color: blue;
        }

    Parameters
    ----------
    open_suffix : str
        The suffix to be used along with the channelPrefix from PCDSSymbolBase
        to compose the open state channel address.
    close_suffix : str
        The suffix to be used along with the channelPrefix from PCDSSymbolBase
        to compose the close state channel address.
    """
    def __init__(self, open_suffix, close_suffix, **kwargs):
        self._open_suffix = open_suffix
        self._close_suffix = close_suffix

        self._state_open = False
        self._state_close = False

        self._open_connected = False
        self._close_connected = False

        self.state_open_channel = None
        self.state_close_channel = None
        super(OpenCloseStateMixin, self).__init__(**kwargs)

    @Property(str, designable=False)
    def state(self):
        """
        Property used to query the state of the widget.

        Returns
        -------
        str
            The return string will either be `Open`, `Close` or `INVALID` when
            it was not possible to determine the state.
        """
        if self._state_open == self._state_close:
            return "INVALID"
        if self._state_open:
            return "Open"
        else:
            return "Close"

    def create_channels(self):
        """
        This method invokes `create_channels` from the super classes and adds
        the `state_open_channel` and `state_close_channel` to the widget along
        with a reset for the state and interlock_connected variables.
        """
        super(OpenCloseStateMixin, self).create_channels()
        if not self._open_suffix or not self._close_suffix:
            return

        self._open_connected = False
        self._close_connected = False
        self._state_open = False
        self._state_close = False
        self._state = "INVALID"

        self.state_open_channel = PyDMChannel(
            address="{}{}".format(self._channels_prefix, self._open_suffix),
            connection_slot=partial(self.state_connection_changed, "OPEN"),
            value_slot=partial(self.state_value_changed, "OPEN"))
        self.state_open_channel.connect()

        self.state_close_channel = PyDMChannel(
            address="{}{}".format(self._channels_prefix, self._close_suffix),
            connection_slot=partial(self.state_connection_changed, "CLOSE"),
            value_slot=partial(self.state_value_changed, "CLOSE"))
        self.state_close_channel.connect()

    def status_tooltip(self):
        """
        This method adds the contribution of the open close state mixin into
        the general status tooltip.

        Returns
        -------
        str
        """
        status = super(OpenCloseStateMixin, self).status_tooltip()
        if status:
            status += os.linesep
        status += "State: {}".format(self.state)
        return status

    def state_connection_changed(self, which, conn):
        """
        Callback invoked when the connection status changes for one of the
        channels in this mixin.

        Parameters
        ----------
        which : str
            String defining which channel is sending the information. It must
            be either "OPEN" or "CLOSE".
        conn : bool
            True if connected, False otherwise.
        """
        if which == "OPEN":
            self._open_connected = conn
        else:
            self._close_connected = conn

    def state_value_changed(self, which, value):
        """
        Callback invoked when the value changes for one of the channels in this
        mixin.

        Parameters
        ----------
        which : str
            String defining which channel is sending the information. It must
            be either "OPEN" or "CLOSE".
        value : int
            The value from the channel which will be either 0 or 1 with 1
            meaning that a certain state is active.
        """
        if which == "OPEN":
            self._state_open = value
        else:
            self._state_close = value

        self.update_stylesheet()
        self.update_status_tooltip()
Beispiel #7
0
class StateMixin(object):
    """
    The StateMixin class adds the state channel and `state` property to the
    widget.

    The state property can be used at stylesheet in the following manner:

    .. code-block:: css

        *[state="Vented"] {
            border: 5px solid blue;
        }

    Parameters
    ----------
    state_suffix : str
        The suffix to be used along with the channelPrefix from PCDSSymbolBase
        to compose the state channel address.
    """
    def __init__(self, state_suffix, **kwargs):
        self._state_suffix = state_suffix
        self._state = ""
        self._state_value = None
        self._state_enum = []
        self._state_connected = False
        self.state_channel = None
        super(StateMixin, self).__init__(**kwargs)

    @Property(str, designable=False)
    def state(self):
        """
        Property used to query the state of the widget.

        Returns
        -------
        str
        """
        return self._state

    def create_channels(self):
        """
        This method invokes `create_channels` from the super classes and adds
        the `state_channel` to the widget along with a reset for the
        state and state_connected variables.
        """
        super(StateMixin, self).create_channels()
        if not self._state_suffix:
            return

        self._state_connected = False
        self._state = ""

        self.state_channel = PyDMChannel(
            address="{}{}".format(self._channels_prefix, self._state_suffix),
            connection_slot=self.state_connection_changed,
            value_slot=self.state_value_changed,
            enum_strings_slot=self.state_enum_changed)
        self.state_channel.connect()

    def status_tooltip(self):
        """
        This method adds the contribution of the state mixin into the general
        status tooltip.

        Returns
        -------
        str
        """
        status = super(StateMixin, self).status_tooltip()
        if status:
            status += os.linesep
        status += "State: {}".format(self.state)
        return status

    def state_connection_changed(self, conn):
        """
        Callback invoked when the connection status changes for the State
        Channel.

        Parameters
        ----------
        conn : bool
            True if connected, False otherwise.
        """
        self._state_connected = conn

    def state_enum_changed(self, items):
        """
        Callback invoked when the enumeration strings change for the State
        Channel.
        This callback triggers the update of the state message and also a
        repaint of the widget with the new stylesheet guidelines for the
        current state value.

        Parameters
        ----------
        items : tuple
            The string items
        """
        if items is None:
            return
        self._state_enum = items
        self._update_state_msg()

    def state_value_changed(self, value):
        """
        Callback invoked when the value change for the State Channel.
        This callback triggers the update of the state message and also a
        repaint of the widget with the new stylesheet guidelines for the
        current state value.

        Parameters
        ----------
        value : int
        """
        if value is None:
            return
        self._state_value = value
        self._update_state_msg()

    def _update_state_msg(self):
        """
        Internal method that updates the state property and triggers an update
        on the stylesheet and tooltip.
        """
        if self._state_value is None:
            return
        if len(self._state_enum) > 0:
            try:
                self._state = self._state_enum[self._state_value]
            except IndexError:
                self._state = ""
        else:
            self._state = str(self._state_value)
        self.update_stylesheet()
        self.update_status_tooltip()
Beispiel #8
0
class InterlockMixin(object):
    """
    The InterlockMixin class adds the interlock channel and `interlocked`
    property to the widget.

    The interlocked property can be used at stylesheet in the following manner:


    .. code-block:: css

        *[interlocked="true"] {
            background-color: red;
        }

    Parameters
    ----------
    interlock_suffix : str
        The suffix to be used along with the channelPrefix from PCDSSymbolBase
        to compose the interlock channel address.
    """
    def __init__(self, interlock_suffix, **kwargs):
        self._interlock_suffix = interlock_suffix
        self._interlocked = False
        self._interlock_connected = False
        self.interlock_channel = None
        super(InterlockMixin, self).__init__(**kwargs)

    @Property(bool, designable=False)
    def interlocked(self):
        """
        Property used to query interlock state.

        Returns
        -------
        bool
        """
        return self._interlocked

    def create_channels(self):
        """
        This method invokes `create_channels` from the super classes and adds
        the `interlock_channel` to the widget along with a reset for the
        interlocked and interlock_connected variables.
        """
        super(InterlockMixin, self).create_channels()
        if not self._interlock_suffix:
            return

        self._interlocked = True
        self._interlock_connected = False

        self.interlock_channel = PyDMChannel(
            address="{}{}".format(self._channels_prefix,
                                  self._interlock_suffix),
            connection_slot=self.interlock_connection_changed,
            value_slot=self.interlock_value_changed)
        self.interlock_channel.connect()

    def status_tooltip(self):
        """
        This method adds the contribution of the interlock mixin into the
        general status tooltip.

        Returns
        -------
        str
        """
        status = super(InterlockMixin, self).status_tooltip()
        if status:
            status += os.linesep
        status += "Interlocked: {}".format(self.interlocked)
        return status

    def interlock_connection_changed(self, conn):
        """
        Callback invoked when the connection status changes for the Interlock
        Channel.

        Parameters
        ----------
        conn : bool
            True if connected, False otherwise.
        """
        self._interlock_connected = conn

    def interlock_value_changed(self, value):
        """
        Callback invoked when the value changes for the Interlock Channel.

        Parameters
        ----------
        value : int
            The value from the channel will be either 0 or 1 with 0 meaning
            that the widget is interlocked.
        """
        self._interlocked = value == 0
        self.controls_frame.setEnabled(not self._interlocked)
        self.update_stylesheet()
        self.update_status_tooltip()
Beispiel #9
0
class MCADisplay(Display):
    def __init__(self, parent=None, args=None, macros=None):
        super(MCADisplay, self).__init__(parent=parent,
                                         args=args,
                                         macros=macros)
        # Debug Logger
        self.logger = logging.getLogger('mca_logger')
        self.separator = "\n" + ("-" * 20) + "\n"
        self.epics = ''

        self.macro_dict = macros
        self.display_state = 'FILE'

        self.num_ROI = 9
        self.ROI = []
        self.start = []
        self.end = []
        self.counts = []
        self.lines = []
        self.set_ROI_widgets()
        cli_args = self.parse_args(args)

        self.energy, self.element = build_dic(cli_args)

        self.waveform.plotItem.scene().sigMouseMoved.connect(self.mouse_moved)
        self.waveform.setXLabels(["Energy (eV)"])
        self.waveform.setYLabels(["Count"])

        # Add Channels
        self.waveform.addChannel(None, None, name="Full", color="white")
        color_list = ["red", "green", "blue"]
        for wave in range(self.num_ROI):
            name = f"ROI{wave+1}"
            color = color_list[wave % len(color_list)]
            self.waveform.addChannel(None,
                                     None,
                                     name=name,
                                     color=color,
                                     lineWidth=2)

        for wave in range(18):
            name = f"Line{wave+1:02d}"
            self.waveform.addChannel(None,
                                     None,
                                     name=name,
                                     color="white",
                                     lineWidth=2,
                                     lineStyle=Qt.DashLine)

        self.curve = self.waveform._curves[0]
        self.croi = self.waveform._curves[1:10]
        self.line = self.waveform._curves[10:28]

        if (self.macro_dict is not None) and ("FIT" in self.macro_dict):
            if (self.macro_dict["FIT"].lower() == "cauchy"):
                self.fitc = "Cauchy"
            else:
                self.fitc = "Gaussian"
        else:
            self.fitc = "Gaussian"

        self.connect_data()

        self.dataSourceTabWidget.currentChanged.connect(self.change_tab_source)

        self.openFile.clicked.connect(self.open_file)
        self.previousMCA.clicked.connect(self.previous_mca)
        self.nextMCA.clicked.connect(self.next_mca)
        self.fullView.clicked.connect(self.full_view)

        self.previousMCA.setEnabled(False)
        self.nextMCA.setEnabled(False)

        self.record = []
        self.record_i = 0

    def set_ROI_widgets(self):
        """
        Appends all ROI related fields to their related lists in Display for reference
        """
        for i in range(1, self.num_ROI + 1):
            self.ROI.append(self.findChild(QtWidgets.QCheckBox, f"ROI{i}"))
            self.start.append(self.findChild(QtWidgets.QLineEdit, f"start{i}"))
            self.end.append(self.findChild(QtWidgets.QLineEdit, f"end{i}"))
            self.counts.append(
                self.findChild(QtWidgets.QLineEdit, f"counts{i}"))
            self.lines.append(self.findChild(QtWidgets.QLineEdit, f"lines{i}"))
        return

    def ui_filename(self):
        return 'SSRL_MCA.ui'

    def ui_filepath(self):
        return path.join(path.dirname(path.realpath(__file__)),
                         self.ui_filename())

    def mouse_moved(self, point):
        """
        Tracks the mouse movement in the view
        """
        if (not self.waveform.sceneBoundingRect().contains(point)):
            return

        point_v = self.waveform.getViewBox().mapSceneToView(point)

        emin = int(point_v.x()) - 200
        emax = int(point_v.x()) + 200
        line_e = [
            ei for ei in self.energy if ((ei[0] > emin) and (ei[0] < emax))
        ]
        line_p = sorted(line_e, key=itemgetter(2))

        l_text = ""

        for ip in range(min(6, len(line_p))):
            if (ip > 0):
                l_text = l_text + ", "

            l_text = l_text + line_p[ip][1] + "-" + line_p[ip][2] + ": " +     \
                str(int(line_p[ip][0]))

        self.mouse_e.setText(str(int(point_v.x())))
        self.mouse_c.setText(str(int(point_v.y())))
        self.mouse_p.setText(l_text)

    def parse_args(self, args):
        """
        Argument parser for the option to read a file from command line
        """
        parser = argparse.ArgumentParser()
        parser.add_argument('--f',
                            dest='filename',
                            help='Input filename as string to be opened.')
        parsed_args, _unknown_args = parser.parse_known_args(args)
        return parsed_args

    def full_view(self, *args, **kwargs):
        self.waveform.resetAutoRangeX()
        self.waveform.resetAutoRangeY()

    def change_tab_source(self):
        """
        Called when tab responsible for indicating current data source is changed
        """
        # If we are in the right state already, do nothing
        if (self.dataSourceTabWidget.currentWidget() == self.dataTab and self.display_state == "DATA") or \
           (self.dataSourceTabWidget.currentWidget() == self.fileTab and self.display_state == "FILE"):
            return
        # Switching to live data
        if (self.dataSourceTabWidget.currentWidget() == self.dataTab):
            self.show_exposure()
            self.connect_data()
        # Switching to reading from file
        elif (self.dataSourceTabWidget.currentWidget() == self.fileTab):
            file_message = QtWidgets.QMessageBox.question(
                self, 'Switching to File', 'Close live data connection?',
                QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes)
            if file_message == QtWidgets.QMessageBox.No:
                self.dataSourceTabWidget.setCurrentWidget(self.dataTab)
                return
            else:
                self.display_state = "FILE"
                self.disconnect_data()
            self.show_mca()
        return

    def show_exposure(self):
        """
        Modifies UI to reflect features for live data processing
        """
        self.recordNum_l.hide()
        self.recordNum.hide()
        self.openFile.hide()
        self.previousMCA.hide()
        self.nextMCA.hide()

        self.exposure_l.setEnabled(True)
        self.exposure.setEnabled(True)
        self.exposureCount_l.show()
        self.exposureCount.show()
        self.start_b.show()
        self.stop_b.show()
        return

    def show_mca(self):
        """
        Modifies UI to reflect features for static file data processing
        """
        self.recordNum_l.show()
        self.recordNum.show()
        self.openFile.show()
        self.previousMCA.show()
        self.nextMCA.show()

        self.exposure_l.setEnabled(False)
        self.exposure.setEnabled(False)
        self.exposureCount_l.hide()
        self.exposureCount.hide()
        self.start_b.hide()
        self.stop_b.hide()
        return

    def connect_data(self):
        """
        Responsible for the steps required for processing live data. This includes creating 
        and connecting to the PyDM channel, and showing the proper UI widgets. 
        """
        if (self.macro_dict is not None) and ("DEVICE" in self.macro_dict):
            self.epics = PyDMChannel(
                address="ca://" + self.macro_dict["DEVICE"] +
                ":ARR1:ArrayData",
                value_slot=self.live_data,
                connection_slot=self.connect_data_settings)
            self.epics.connect()
        self.show_exposure()
        return

    def connect_data_settings(self):
        """
        Displays in the UI that a current connection is active
        """
        self.connectStatusLabel.setText("Connected")
        self.display_state = "DATA"
        self.openFilename.setText("None")
        return

    def disconnect_data(self):
        """
        Responsible for severing the connection to live data and updating the UI
        """
        if self.epics:
            self.epics.disconnect()
            self.connectStatusLabel.setText("Disconnected")
        self.display_state = "FILE"
        return

    def live_data(self, new_waveform):
        self.record = new_waveform
        if not self.record.any():
            return
        self.handle_mca()

    def open_file(self, *args, **kwargs):
        """
        Opens the data file specified by the user and sends for MCA processing
        """
        fname = QtWidgets.QFileDialog.getOpenFileName(
            self, "Open file", "", "Data files (*.dat);;All files (*.*)")
        # No file selected
        if (fname[0] == ""):
            return

        # Windows-based paths will return full path
        base_filename = path.basename(fname[0])
        self.openFilename.setText(base_filename)

        with open(fname[0]) as f:
            self.record = [line.rstrip() for line in f]
        self.record_i = 0

        if (len(self.record) > 0):
            if (len(self.record) > 1):
                self.nextMCA.setEnabled(True)
            self.handle_mca()
        self.previousMCA.setEnabled(False)

    def previous_mca(self, *args, **kwargs):
        self.logger.debug("\nPrevious MCA ...{}".format(self.separator))
        self.record_i = self.record_i - 1
        if (self.record_i == 0):
            self.previousMCA.setEnabled(False)

        self.nextMCA.setEnabled(True)

        self.handle_mca()

    def next_mca(self, *args, **kwargs):
        self.logger.debug("\nNext MCA ...{}".format(self.separator))
        self.record_i = self.record_i + 1
        if (self.record_i == len(self.record) - 1):
            self.nextMCA.setEnabled(False)

        self.previousMCA.setEnabled(True)

        self.handle_mca()

    def find_peak(self, y_array):
        start = math.floor(int(self.start0.text()) / 10.)

        ret_i = []
        work_d = []
        for ri in range(self.num_ROI):
            if not (self.ROI[ri].isChecked()):
                continue
            try:
                xl = math.floor(int(self.start[ri].text()) / 10.)
                xr = math.floor(int(self.end[ri].text()) / 10.)
                points = xr - xl + 1
            except BaseException:
                continue

            self.logger.debug("\nROI {0}\nxl - {1}\nxr - {2}{3}".format(
                ri, xl, xr, self.separator))
            if (points < 12):
                continue

            xl = xl - start
            xr = xr - start

            ypeak = max(y_array[xl:xr + 1])
            xpeak = y_array[xl:xr + 1].index(ypeak) + xl

            self.logger.debug(
                "\nFit 0: \nri - {0}\nxl - {1}, xr - {2}\nxpeak - {3}, ypeak - {4}{5}"
                .format(ri, xl, xr, xpeak, ypeak, self.separator))
            try:
                if (self.fitc == "Cauchy"):
                    fit, tmp = curve_fit(
                        cauchy,
                        list(range(xl, xr + 1)),
                        y_array[xl:xr + 1],
                        p0=[ypeak, (xr + xl) / 2., (xr - xl) / 4.])
                else:
                    fit, tmp = curve_fit(
                        gaussian,
                        list(range(xl, xr + 1)),
                        y_array[xl:xr + 1],
                        p0=[ypeak, (xr + xl) / 2., (xr - xl) / 4.])
                fit = list(fit)
            except BaseException:
                fit = []

            # try to fit 2 curves
            if (fit != []) and (((fit[1] - xl) / (xr - xl) < 0.35) or
                                ((fit[1] - xl) / (xr - xl) > 0.65)):
                try:
                    fit2, tmp = curve_fit(gaussian2,
                                          list(range(xl, xr + 1)),
                                          y_array[xl:xr + 1],
                                          p0=[
                                              fit[0], fit[1], fit[2],
                                              self.num_ROI, xl + xr - fit[1],
                                              (xr - xl) / 4.
                                          ])
                    self.logger.debug("\nFit2: {}{}".format(
                        fit2, self.separator))
                    fit = fit2
                except BaseException:
                    pass

            self.logger.debug("\nFit i: \nxl - {}, xr - {}\nfit - {}{}".format(
                xl, xr, fit, self.separator))

            ret_i.append([xl, xr, ypeak, xpeak, fit])

            work_d.append([xl, xr])

        work_i = sorted(work_d, key=itemgetter(0))

        work_l = []
        xi = 0
        for wi in range(len(work_i)):
            xl = work_i[wi][0]
            xr = work_i[wi][1]

            if (xl - xi >= 12):
                ymax = max(y_array[xi:xl])
                if (ymax >= 80):
                    work_l.append([ymax, xi, xl - 1])

            xi = xr + 1

        if (len(y_array) - xi >= 12):
            ymax = max(y_array[xi:])
            if (ymax >= 80):
                work_l.append([ymax, xi, len(y_array) - 1])

        ret_l = []
        while (len(work_l) > 0):
            work_i = sorted(work_l, key=itemgetter(0), reverse=True)
            work_l = work_i[1:]
            self.logger.debug("\nWork List: {}{}".format(
                work_i, self.separator))

            if (work_i[0][0] < 80):
                continue  # counts too low

            ypeak = work_i[0][0]
            xmin = work_i[0][1]
            xmax = work_i[0][2]

            y = y_array[xmin:xmax + 1]
            xpeak = y.index(ypeak)

            # monotonically going up or down
            if ((ypeak == y[0]) and (min(y) == y[-1])) or                   \
               ((ypeak == y[-1]) and (min(y) == y[0])):
                continue

            # ending up
            xr = len(y) - 3
            while (xr >= self.num_ROI):
                slope, intercept = np.polyfit(range(3), y[xr:xr + 3], 1)
                if (slope < 0.):
                    break

                xr = xr - 3

            xr = xr + 3

            if (xr < 12):
                continue  # less than 12 points left

            xl = 0  # starting down
            while (xl <= xr - 12):  # xr is the length of the new y
                slope, intercept = np.polyfit(range(3), y[xl:xl + 3], 1)
                if (slope > 0.):
                    break

                xl = xl + 3

            if (xr - xl < 12):
                continue  # less than 12 points left

            xmax = xmin + xr - 1
            xmin = xmin + xl

            y = y_array[xmin:xmax + 1]
            ypeak = max(y)
            xpeak = y.index(ypeak)

            if (ypeak < 80):
                continue  # counts too low

            dx = 10
            smax = 0
            xl = xpeak - dx + 1
            while ((xl >= 0) and (xl > xpeak - 100)):
                slope, intercept = np.polyfit(range(dx), y[xl:xl + dx], 1)

                if (slope > smax):
                    smax = slope
                elif (slope < 0.5):
                    if (dx == 3):
                        if ((y[xl] + y[xl + dx - 1]) < ypeak * 0.8) or  \
                           (((y[xl] + y[xl + dx - 1]) < ypeak) and (slope < 0)):
                            xl = xl + 3
                            break
                    elif (dx == 5):
                        dx = 3
                        xl = xl + 8
                        continue
                    else:
                        dx = 5
                        xl = xl + 15
                        continue

                if (xl >= dx):
                    xl = xl - dx
                else:
                    break

            dx = 10
            smin = 0
            xr = xpeak
            while ((xr <= len(y) - dx) and (xr < xpeak + 100)):
                slope, intercept = np.polyfit(range(dx), y[xr:xr + dx], 1)

                if (slope < smin):
                    smin = slope
                elif (slope > -0.5):
                    if (dx == 3):
                        if ((y[xr] + y[xr + dx - 1]) < ypeak * 0.8) or  \
                           (((y[xr] + y[xr + dx - 1]) < ypeak) and (slope > 0)):
                            xr = xr - 3
                            break
                    elif (dx == 5):
                        dx = 3
                        xr = xr - 3
                        continue
                    else:
                        dx = 5
                        xr = xr - 5
                        continue

                xr = xr + dx

            try:
                if (self.fitc == "Cauchy"):
                    fit, tmp = curve_fit(
                        cauchy,
                        list(range(xl, xr + 1)),
                        y[xl:xr + 1],
                        p0=[ypeak, (xr + xl) / 2., (xr - xl) / 4.])
                else:
                    fit, tmp = curve_fit(
                        gaussian,
                        list(range(xl, xr + 1)),
                        y[xl:xr + 1],
                        p0=[ypeak, (xr + xl) / 2., (xr - xl) / 4.])
                fit = list(fit)
            except BaseException:
                fit = []

            ret_l.append([xl + xmin, xr + xmin, ypeak, xpeak + xmin, fit])

            self.logger.debug(
                "\nFit: \nxl+xmin - {}\nxr+xmin - {}\nfit - {}{}".format(
                    xl + xmin, xr + xmin, fit, self.separator))

            if (len(ret_i) + len(ret_l) == 10):
                break

            if (xl >= 12):
                work_l.append([max(y[:xl]), xmin, xmin + xl - 1])

            if (len(y) - xr >= 13):
                work_l.append([max(y[xr + 1:]), xmin + xr + 1, xmax])

        return ret_i + sorted(ret_l, key=itemgetter(0))

    def handle_mca(self):
        # Check for live data connection
        if (self.display_state == "DATA"):
            items = self.record
        # File connection
        else:
            items = list(map(int, self.record[self.record_i].split()))

        start = math.floor(int(self.start0.text()) / 10.)
        end = math.ceil(int(self.end0.text()) / 10.) + 1

        order = 3
        cutoff = 0.1
        B, A = signal.butter(order, cutoff, output='ba')

        y = signal.filtfilt(B, A, items[1:][start:end])
        y_array = list(y)

        #       y_array = items[1:][start:end]

        ymax = max(y_array)
        sum0 = sum(y_array)

        self.curve.receiveXWaveform(
            np.array(list(range(start * 10, end * 10, 10))))
        self.curve.receiveYWaveform(np.array(y_array))

        self.counts0.setText('{:.0f}'.format(sum0))

        ret_l = self.find_peak(y_array)
        self.logger.debug("\nret_l - {0}{1}".format(ret_l, self.separator))

        for il in range(self.num_ROI):
            if (len(ret_l) > il):
                self.croi[il].show()
            else:
                self.croi[il].hide()
                self.line[il * 2].hide()
                self.line[il * 2 + 1].hide()

                self.counts[il].setText("")
                self.lines[il].setText("")

                if not (self.ROI[il].isChecked()):
                    self.start[il].setText("")
                    self.end[il].setText("")

                continue

            start_i = start + ret_l[il][0]
            end_i = start + ret_l[il][1]

            if (start_i < 0):
                start_i = 0
            if (end_i > 2047):
                end_i = 2047

            end_i = end_i + 1

            y_array = items[1:][start_i:end_i]
            ysum = sum(y_array)

            self.counts[il].setText('{:.0f}'.format(ysum))

            self.croi[il].receiveXWaveform(
                np.array(list(range(start_i * 10, end_i * 10, 10))))

            l_text = ""
            fit = ret_l[il][4]
            if not (self.ROI[il].isChecked()):
                self.start[il].setText(str(10 * (start_i)))
                self.end[il].setText(str(10 * (end_i)))

                self.croi[il].receiveYWaveform(np.array(y_array))

                if (len(fit) > 2):
                    efit = 10 * (start + fit[1])
                    efit = 10 * (start + ret_l[il][3])
                    emin = efit - 10 * fit[2]
                    emax = efit + 10 * fit[2]
                else:
                    efit = 10 * (start + ret_l[il][3])
                    emin = efit * 0.99
                    emax = efit * 1.01

                if (ret_l[il][2] > 0.4 * ymax):
                    scale = 1.1
                else:
                    scale = 0.5

                self.line[il * 2].receiveXWaveform(np.array([efit, efit]))
                self.line[il * 2].receiveYWaveform(np.array([0, scale * ymax]))
                self.line[il * 2].show()
                self.line[il * 2 + 1].hide()

                line_e = [
                    ei for ei in self.energy
                    if ((ei[0] > emin) and (ei[0] < emax))
                ]
                line_p = sorted(line_e, key=itemgetter(2))
                self.logger.debug("\nEfit - {}\nline - {}{}".format(
                    efit, line_p, self.separator))

                for ip in range(min(6, len(line_p))):
                    if (ip > 0):
                        l_text = l_text + ", "

                    l_text = l_text + line_p[ip][1] + "-" + line_p[ip][2]
            elif (len(fit) < 3):
                self.croi[il].receiveYWaveform(np.array(y_array))

                efit = 10 * (start + ret_l[il][3])

                if (ret_l[il][2] > 0.4 * ymax):
                    scale = 1.1
                else:
                    scale = 0.5

                self.line[il * 2].receiveXWaveform(np.array([efit, efit]))
                self.line[il * 2].receiveYWaveform(np.array([0, scale * ymax]))
                self.line[il * 2].show()
                self.line[il * 2 + 1].hide()

                l_text = "Failed to fit"
            elif (len(fit) == 6):
                self.croi[il].receiveYWaveform(
                    gaussian2(list(range(start_i * 10, end_i * 10, 10)),
                              fit[0], (fit[1] + start) * 10, fit[2] * 10,
                              fit[3], (fit[4] + start) * 10, fit[5] * 10))

                if (fit[0] >= 50):
                    efit = 10 * (start + fit[1])

                    if (fit[0] > 0.4 * ymax):
                        scale = 1.1
                    else:
                        scale = 0.5

                    self.line[il * 2].receiveXWaveform(np.array([efit, efit]))
                    self.line[il * 2].receiveYWaveform(
                        np.array([0, scale * ymax]))
                    self.line[il * 2].show()
                else:
                    self.line[il * 2].hide()

                if (fit[3] >= 50):
                    efit = 10 * (start + fit[4])

                    if (fit[3] > 0.4 * ymax):
                        scale = 1.1
                    else:
                        scale = 0.5

                    self.line[il * 2 + 1].receiveXWaveform(
                        np.array([efit, efit]))
                    self.line[il * 2 + 1].receiveYWaveform(
                        np.array([0, scale * ymax]))
                    self.line[il * 2 + 1].show()
                else:
                    self.line[il * 2 + 1].hide()

                l_text = str(int(fit[0])) + " " + str(int((start + fit[1]) * 10)) + " " + str(int(fit[2] * 10)) + "; " + \
                    str(int(fit[3])) + " " + str(int((start + fit[4])
                                                     * 10)) + " " + str(int(fit[5] * 10))
            elif (self.fitc == "Cauchy"):
                self.croi[il].receiveYWaveform(
                    cauchy(list(range(start_i * 10, end_i * 10, 10)),
                           fit[0] * 10, (fit[1] + start) * 10, fit[2] * 10))

                efit = 10 * (start + fit[1])

                if (ret_l[il][2] > 0.4 * ymax):
                    scale = 1.1
                else:
                    scale = 0.5

                self.line[il * 2].receiveXWaveform(np.array([efit, efit]))
                self.line[il * 2].receiveYWaveform(np.array([0, scale * ymax]))
                self.line[il * 2].show()
                self.line[il * 2 + 1].hide()

                l_text = str(int(fit[0])) + " " + str(
                    int((start + fit[1]) * 10)) + " " + str(int(fit[2] * 10))
            else:
                self.croi[il].receiveYWaveform(
                    gaussian(list(range(start_i * 10, end_i * 10, 10)), fit[0],
                             (fit[1] + start) * 10, fit[2] * 10))

                efit = 10 * (start + fit[1])

                if (ret_l[il][2] > 0.4 * ymax):
                    scale = 1.1
                else:
                    scale = 0.5

                self.line[il * 2].receiveXWaveform(np.array([efit, efit]))
                self.line[il * 2].receiveYWaveform(np.array([0, scale * ymax]))
                self.line[il * 2].show()
                self.line[il * 2 + 1].hide()

                l_text = str(int(fit[0])) + " " + str(
                    int((start + fit[1]) * 10)) + " " + str(int(fit[2] * 10))

            self.lines[il].setText(l_text)

        self.recordNum.setText(str(items[0]))
Beispiel #10
0
    def sector_change_connect(self):
        self.tab = TAB[self.tabWidget.currentIndex()]
        self.sector_change_disconnect()

        channels = []
        self.board_sensors = {}

        logger.info("Area {}; Sector {}".format(self.tab, self.sector.value()))

        try:
            info_request = requests.get(
                self.url, verify=False, params={"type": "mbtemp"}, timeout=5
            )
        except Exception:
            QtWidgets.QMessageBox.warning(
                self, "Warning", "Impossible connect to {}".format(self.url)
            )
            logger.warning("Impossible connect to {}".format(self.url))
            sys.exit()
        dev = info_request.json()

        if self.tab in ["RF", "TB", "TS", "LA", "PA"]:
            self.sector.setEnabled(False)

        elif self.tab == "BO":
            self.sector.setSingleStep(2)
            self.sector.setEnabled(True)
            self.sector.setMaximum(19)

            if self.sector.value() % 2 == 0:
                self.sector.setValue(1)

            sectorFrom = 2 + (self.sector.value() // 2) * 5
            sectorTo = 7 + (self.sector.value() // 2) * 5

            for x, y in enumerate(range(sectorFrom, sectorTo)):
                if y != 51:
                    getattr(self, "BO_Sec_{}".format(x + 1)).setText(
                        "Booster Sector: {}".format(y)
                    )
                else:
                    self.BO_Sec_5.setText("Booster Sector: 1")
            self.setBOImage(sectorFrom)
        else:
            self.sector.setSingleStep(1)
            self.sector.setEnabled(True)
            self.sector.setMaximum(20)

        for ip in DEVICES_IP[self.tab]:  # dict -> {MBTemp:[CH1,CH2...]}
            for board in dev[ip[0].format(self.sector.value())][ip[1][0] : ip[1][1]]:
                for enabled in range(1, 9):
                    channels.append(board["channels"]["CH{}".format(enabled)]["prefix"])
                self.board_sensors[board["prefix"]] = channels
                channels = []

        for mbtemp in self.board_sensors:
            for coef in ["Alpha", "LinearCoef-Mon", "AngularCoef-Mon"]:
                slot = partial(
                    self.update_mbtemp,
                    pvname=mbtemp,
                    coef=coef,
                    sector=self.sector.value(),
                )
                mb = PyDMChannel(
                    address="ca://{}:{}".format(mbtemp, coef),
                    value_slot=slot,
                    connection_slot=slot,
                )
                self.addr.append(mb)
                mb.connect()

            for number, pv in enumerate(self.board_sensors[mbtemp]):
                slot = partial(
                    self.update_channel,
                    name_pv=pv,
                    mbtemp_name=mbtemp,
                    mbtemp_ch=number + 1,
                )
                temp = PyDMChannel(
                    address="ca://" + pv, value_slot=slot, connection_slot=slot
                )
                self.addr.append(temp)
                temp.connect()
Beispiel #11
0
class PLCIOCStatus(Display):
    _on_color = QColor(0, 255, 0)
    _off_color = QColor(100, 100, 100)
    plc_status_ch = None

    def __init__(self, parent=None, args=None, macros=None):
        super(PLCIOCStatus, self).__init__(parent=parent,
                                           args=args,
                                           macros=macros)
        self.config = macros
        self.ffs_count_map = {}
        self.ffs_label_map = {}
        self.setup_ui()
        if self.plc_status_ch:
            self.destroyed.connect(
                functools.partial(clear_channel, self.plc_status_ch))

    def setup_ui(self):
        self.setup_plc_ioc_status()

    def setup_plc_ioc_status(self):
        ffs = self.config.get('fastfaults')
        if not ffs:
            return
        if self.plc_ioc_container is None:
            return

        for ff in ffs:
            prefix = ff.get('prefix')
            ffo_start = ff.get('ffo_start')
            ffo_end = ff.get('ffo_end')
            ff_start = ff.get('ff_start')
            ff_end = ff.get('ff_end')

            ffos_zfill = len(str(ffo_end)) + 1
            ffs_zfill = len(str(ff_end)) + 1
            entries = itertools.product(range(ffo_start, ffo_end + 1),
                                        range(ff_start, ff_end + 1))

            plc_name = prefix.strip(':')
            plc_macros = dict(P=prefix)
            # get the heartbeat of the IOC to
            ico_heart_ch = Template('ca://${P}HEARTBEAT').safe_substitute(
                **plc_macros)
            # the get PLC process cycle count
            plc_task_info_1 = Template(
                'ca://${P}TaskInfo:1:CycleCount').safe_substitute(**plc_macros)
            plc_task_info_2 = Template(
                'ca://${P}TaskInfo:2:CycleCount').safe_substitute(**plc_macros)
            plc_task_info_3 = Template(
                'ca://${P}TaskInfo:3:CycleCount').safe_substitute(**plc_macros)

            label_name = QtWidgets.QLabel(str(plc_name))
            label_online = QtWidgets.QLabel()
            label_in_use = QtWidgets.QLabel()
            label_alarmed = QtWidgets.QLabel()
            label_heartbeat = PyDMLabel(init_channel=ico_heart_ch)
            label_plc_task_info_1 = PyDMLabel(init_channel=plc_task_info_1)
            label_plc_task_info_2 = PyDMLabel(init_channel=plc_task_info_2)
            label_plc_task_info_3 = PyDMLabel(init_channel=plc_task_info_3)

            # if alarm of plc_task_info_1 == INVALID => plc down
            # if the count does not update and alarm == NO_ALARM =>
            # plc online but stopped
            self.plc_status_ch = PyDMChannel(
                plc_task_info_1,
                severity_slot=functools.partial(
                    self.plc_cycle_count_severity_changed, plc_name))
            self.plc_status_ch.connect()

            # if we can get the plc_cycle_count the PLC should be ON, if not OFF
            # if we get the plc_cycle_count and the .SERV is INVALID, the PLC is OFF
            plc_status_indicator = PyDMBitIndicator(circle=True)
            plc_status_indicator.setColor(self._off_color)
            # TODO - maybe add the case where PLC On but stopped

            # total initial number of ffs to initialize the dictionaries with
            # num_ffo * num_ff
            all_ffos = ((ffo_end - ffo_start) + 1) * (ff_end - ff_start + 1)
            self.ffs_count_map[plc_name] = {
                'online': [False] * all_ffos,
                'in_use': [False] * all_ffos,
                'alarmed': [False] * all_ffos,
                'plc_status': False
            }
            self.ffs_label_map[plc_name] = {
                'online': label_online,
                'in_use': label_in_use,
                'alarmed': label_alarmed,
                'plc_status': plc_status_indicator
            }

            count = 0
            for _ffo, _ff in entries:
                s_ffo = str(_ffo).zfill(ffos_zfill)
                s_ff = str(_ff).zfill(ffs_zfill)
                ch_macros = dict(index=count, P=prefix, FFO=s_ffo, FF=s_ff)

                ch = Template('ca://${P}FFO:${FFO}:FF:${FF}:Info:InUse_RBV'
                              ).safe_substitute(**ch_macros)
                channel = PyDMChannel(
                    ch,
                    connection_slot=functools.partial(
                        self.ffo_connection_callback, plc_name, count),
                    value_slot=functools.partial(self.ffo_value_changed,
                                                 plc_name, count),
                    severity_slot=functools.partial(self.ffo_severity_changed,
                                                    plc_name, count))
                # should not be adding a new connection because this address
                # already exists in the connections,
                # instead should just add a listener
                channel.connect()
                count += 1

            widget = QtWidgets.QWidget()
            widget_layout = QtWidgets.QHBoxLayout()

            # this is the same width as the labels in the plc_ioc_header
            max_width = 150
            min_width = 130
            widget_list = [
                label_name, label_online, label_in_use, label_alarmed,
                label_heartbeat, label_plc_task_info_1, label_plc_task_info_2,
                label_plc_task_info_3, plc_status_indicator
            ]
            widget.setLayout(widget_layout)

            # set minimum height of the widget
            widget.setMinimumHeight(40)
            self.setup_widget_size(max_width=max_width,
                                   min_width=min_width,
                                   widget_list=widget_list)
            widget.layout().addWidget(label_name)
            widget.layout().addWidget(label_online)
            widget.layout().addWidget(label_in_use)
            widget.layout().addWidget(label_alarmed)
            widget.layout().addWidget(label_heartbeat)
            widget.layout().addWidget(label_plc_task_info_1)
            widget.layout().addWidget(label_plc_task_info_2)
            widget.layout().addWidget(label_plc_task_info_3)
            widget.layout().addWidget(plc_status_indicator)

            self.plc_ioc_container.layout().addWidget(widget)
            vertical_spacer = (QtWidgets.QSpacerItem(
                20, 20, QtWidgets.QSizePolicy.Preferred,
                QtWidgets.QSizePolicy.Maximum))
            self.plc_ioc_container.layout().addItem(vertical_spacer)
        b_vertical_spacer = (QtWidgets.QSpacerItem(
            20, 20, QtWidgets.QSizePolicy.Preferred,
            QtWidgets.QSizePolicy.Expanding))
        self.plc_ioc_container.layout().addItem(b_vertical_spacer)
        self.plc_ioc_container.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
                                             QtWidgets.QSizePolicy.Preferred)

    def setup_widget_size(self, max_width, min_width, widget_list):
        for widget in widget_list:
            widget.setMinimumWidth(min_width)
            widget.setMaximumWidth(max_width)

    def plc_cycle_count_severity_changed(self, key, alarm):
        """
        Process PLC Cycle Count PV severity change.

        Parameters
        ----------
        key : str
            Prefix of PLC
        alarm : int
            New alarm.

        Note
        ----
        alarm == 0 => NO_ALARM, if NO_ALARM and counter does not change,
        PLC is till online but stopped
        alarm == 3 => INVALID - PLC is Offline
        """
        plc = self.ffs_count_map.get(key)
        if alarm == 3:
            plc['plc_status'] = False
        else:
            plc['plc_status'] = True
        self.update_status_labels(key)

    def ffo_connection_callback(self, key, idx, conn):
        # Update ffos count for connected In_Use PVs
        plc = self.ffs_count_map.get(key)
        plc['online'][idx] = conn
        # Call routine to update proper label
        self.update_plc_labels(key)

    def ffo_value_changed(self, key, idx, value):
        # Update ffos count for In_Use == True Pvs
        plc = self.ffs_count_map.get(key)
        plc['in_use'][idx] = value
        self.update_plc_labels(key)

    def ffo_severity_changed(self, key, idx, alarm):
        # 0 = NO_ALARM, 1 = MINOR, 2 = MAJOR, 3 = INVALID
        plc = self.ffs_count_map.get(key)
        if alarm != 0:
            plc['alarmed'][idx] = True
        else:
            plc['alarmed'][idx] = False
        self.update_plc_labels(key)

    def update_plc_labels(self, key):
        # Fetch value from count
        # TODO maybe have some checks here....?
        counts = self.ffs_count_map.get(key)
        online_cnt = sum(counts['online'])
        in_use_cnt = sum(counts['in_use'])
        alarmed_cnt = sum(counts['alarmed'])
        # Pick the label from the map
        # Update label with new count
        labels = self.ffs_label_map.get(key)
        labels['online'].setText(str(online_cnt))
        labels['in_use'].setText(str(in_use_cnt))
        labels['alarmed'].setText(str(alarmed_cnt))

    def update_status_labels(self, key):
        status = self.ffs_count_map.get(key)
        plc_status = status['plc_status']
        labels = self.ffs_label_map.get(key)
        if plc_status is True:
            labels['plc_status'].setColor(self._on_color)
        else:
            labels['plc_status'].setColor(self._off_color)

    def ui_filename(self):
        return 'plc_ioc_status.ui'
Beispiel #12
0
class TyphosPositionerWidget(utils.TyphosBase, widgets.TyphosDesignerMixin):
    """
    Widget to interact with a :class:`ophyd.Positioner`.

    Standard positioner motion requires a large amount of context for
    operators. For most motors, it may not be enough to simply have a text
    field where setpoints can be punched in. Instead, information like soft
    limits and hardware limit switches are crucial for a full understanding of
    the position and behavior of a motor. The widget will work with any object
    that implements the method ``set``, however to get other relevant
    information, we see if we can find other useful signals.  Below is a table
    of attributes that the widget looks for to inform screen design.

    ============== ===========================================================
    Widget         Attribute Selection
    ============== ===========================================================
    User Readback  The ``readback_attribute`` property is used, which defaults
                   to ``user_readback``. Linked to UI element
                   ``user_readback``.

    User Setpoint  The ``setpoint_attribute`` property is used, which defaults
                   to ``user_setpoint``. Linked to UI element
                   ``user_setpoint``.

    Limit Switches The ``low_limit_switch_attribute`` and
                   ``high_limit_switch_attribute`` properties are used, which
                   default to ``low_limit_switch`` and ``high_limit_switch``,
                   respectively.

    Soft Limits    The ``low_limit_travel_attribute`` and
                   ``high_limit_travel_attribute`` properties are used, which
                   default to ``low_limit_travel`` and ``high_limit_travel``,
                   respectively.  As a fallback, the ``limit`` property on the
                   device may be queried directly.

    Set and Tweak  Both of these methods simply use ``Device.set`` which is
                   expected to take a ``float`` and return a ``status`` object
                   that indicates the motion completeness. Must be implemented.

    Stop           ``Device.stop()``, if available, otherwise hide the button.
                   If you have a non-functional ``stop`` method inherited from
                   a parent device, you can hide it from ``typhos`` by
                   overriding it with a property that raises
                   ``AttributeError`` on access.

    Move Indicator The ``moving_attribute`` property is used, which defaults
                   to ``motor_is_moving``. Linked to UI element
                   ``moving_indicator``.

    Error Message  The ``error_message_attribute`` property is used, which
                   defaults to ``error_message``. Linked to UI element
                   ``error_label``.

    Clear Error    ``Device.clear_error()``, if applicable. This also clears
                   any visible error messages from the status returned by
                   ``Device.set``.

    Alarm Circle   Uses the ``TyphosAlarmCircle`` widget to summarize the
                   alarm state of all of the device's ``normal`` and
                   ``hinted`` signals.
    ============== ===========================================================
    """

    ui_template = os.path.join(utils.ui_dir, 'widgets', 'positioner.ui')
    _readback_attr = 'user_readback'
    _setpoint_attr = 'user_setpoint'
    _low_limit_switch_attr = 'low_limit_switch'
    _high_limit_switch_attr = 'high_limit_switch'
    _low_limit_travel_attr = 'low_limit_travel'
    _high_limit_travel_attr = 'high_limit_travel'
    _velocity_attr = 'velocity'
    _acceleration_attr = 'acceleration'
    _moving_attr = 'motor_is_moving'
    _error_message_attr = 'error_message'
    _min_visible_operation = 0.1

    def __init__(self, parent=None):
        self._moving = False
        self._last_move = None
        self._readback = None
        self._setpoint = None
        self._status_thread = None
        self._initialized = False
        self._moving_channel = None

        super().__init__(parent=parent)

        self.ui = uic.loadUi(self.ui_template, self)
        self.ui.tweak_positive.clicked.connect(self.positive_tweak)
        self.ui.tweak_negative.clicked.connect(self.negative_tweak)
        self.ui.stop_button.clicked.connect(self.stop)
        self.ui.clear_error_button.clicked.connect(self.clear_error)

        self.ui.alarm_circle.kindLevel = self.ui.alarm_circle.NORMAL
        self.ui.alarm_circle.alarm_changed.connect(self.update_alarm_text)

        self.show_expert_button = False
        self._after_set_moving(False)

    def _clear_status_thread(self):
        """Clear a previous status thread."""
        if self._status_thread is None:
            return

        logger.debug("Clearing current active status")
        self._status_thread.disconnect()
        self._status_thread = None

    def _start_status_thread(self, status, timeout):
        """Start the status monitoring thread for the given status object."""
        self._status_thread = thread = TyphosStatusThread(
            status, start_delay=self._min_visible_operation, timeout=timeout)
        thread.status_started.connect(self.move_changed)
        thread.status_finished.connect(self._status_finished)
        thread.start()

    def _get_timeout(self, set_position, settle_time):
        """Use positioner's configuration to select a timeout."""
        pos_sig = getattr(self.device, self._readback_attr, None)
        vel_sig = getattr(self.device, self._velocity_attr, None)
        acc_sig = getattr(self.device, self._acceleration_attr, None)
        # Not enough info == no timeout
        if pos_sig is None or vel_sig is None:
            return None
        delta = pos_sig.get() - set_position
        speed = vel_sig.get()
        # Bad speed == no timeout
        if speed == 0:
            return None
        # Bad acceleration == ignore acceleration
        if acc_sig is None:
            acc_time = 0
        else:
            acc_time = acc_sig.get()
        # This time is always greater than the kinematic calc
        return abs(delta / speed) + 2 * abs(acc_time) + abs(settle_time)

    def _set(self, value):
        """Inner `set` routine - call device.set() and monitor the status."""
        self._clear_status_thread()
        self._last_move = None
        if isinstance(self.ui.set_value, widgets.NoScrollComboBox):
            set_position = value
        else:
            set_position = float(value)

        try:
            timeout = self._get_timeout(set_position, 5)
        except Exception:
            # Something went wrong, just run without a timeout.
            logger.exception('Unable to estimate motor timeout.')
            timeout = None
        logger.debug("Setting device %r to %r with timeout %r", self.device,
                     value, timeout)
        # Send timeout through thread because status timeout stops the move
        status = self.device.set(set_position)
        self._start_status_thread(status, timeout)

    @QtCore.Slot(int)
    def combo_set(self, index):
        self.set()

    @QtCore.Slot()
    def set(self):
        """Set the device to the value configured by ``ui.set_value``"""
        if not self.device:
            return

        try:
            if isinstance(self.ui.set_value, widgets.NoScrollComboBox):
                value = self.ui.set_value.currentText()
            else:
                value = self.ui.set_value.text()
            self._set(value)
        except Exception as exc:
            logger.exception("Error setting %r to %r", self.devices, value)
            self._last_move = False
            utils.reload_widget_stylesheet(self, cascade=True)
            utils.raise_to_operator(exc)

    def tweak(self, offset):
        """Tweak by the given ``offset``."""
        try:
            setpoint = self._get_position() + float(offset)
        except Exception:
            logger.exception('Tweak failed')
            return

        self.ui.set_value.setText(str(setpoint))
        self.set()

    @QtCore.Slot()
    def positive_tweak(self):
        """Tweak positive by the amount listed in ``ui.tweak_value``"""
        try:
            self.tweak(float(self.tweak_value.text()))
        except Exception:
            logger.exception('Tweak failed')

    @QtCore.Slot()
    def negative_tweak(self):
        """Tweak negative by the amount listed in ``ui.tweak_value``"""
        try:
            self.tweak(-float(self.tweak_value.text()))
        except Exception:
            logger.exception('Tweak failed')

    @QtCore.Slot()
    def stop(self):
        """Stop device"""
        for device in self.devices:
            device.stop()

    @QtCore.Slot()
    def clear_error(self):
        """
        Clear the error messages from the device and screen.

        The device may have errors in the IOC. These will be cleared by calling
        the clear_error method.

        The screen may have errors from the status of the last move. These will
        be cleared from view.
        """
        for device in self.devices:
            clear_error_in_background(device)
        self._set_status_text('')
        # This variable holds True if last move was good, False otherwise
        # It also controls whether or not we have a red box on the widget
        # False = Red, True = Green, None = no box (in motion is yellow)
        if not self._last_move:
            self._last_move = None
        utils.reload_widget_stylesheet(self, cascade=True)

    def _get_position(self):
        if not self._readback:
            raise Exception("No Device configured for widget!")
        return self._readback.get()

    @utils.linked_attribute('readback_attribute', 'ui.user_readback', True)
    def _link_readback(self, signal, widget):
        """Link the positioner readback with the ui element."""
        self._readback = signal

    @utils.linked_attribute('setpoint_attribute', 'ui.user_setpoint', True)
    def _link_setpoint(self, signal, widget):
        """Link the positioner setpoint with the ui element."""
        self._setpoint = signal
        if signal is not None:
            # Seed the set_value text with the user_setpoint channel value.
            if hasattr(widget, 'textChanged'):
                widget.textChanged.connect(self._user_setpoint_update)

    @utils.linked_attribute('low_limit_switch_attribute',
                            'ui.low_limit_switch', True)
    def _link_low_limit_switch(self, signal, widget):
        """Link the positioner lower limit switch with the ui element."""
        if signal is None:
            widget.hide()

    @utils.linked_attribute('high_limit_switch_attribute',
                            'ui.high_limit_switch', True)
    def _link_high_limit_switch(self, signal, widget):
        """Link the positioner high limit switch with the ui element."""
        if signal is None:
            widget.hide()

    @utils.linked_attribute('low_limit_travel_attribute', 'ui.low_limit', True)
    def _link_low_travel(self, signal, widget):
        """Link the positioner lower travel limit with the ui element."""
        return signal is not None

    @utils.linked_attribute('high_limit_travel_attribute', 'ui.high_limit',
                            True)
    def _link_high_travel(self, signal, widget):
        """Link the positioner high travel limit with the ui element."""
        return signal is not None

    def _link_limits_by_limits_attr(self):
        """Link limits by using ``device.limits``."""
        device = self.device
        try:
            low_limit, high_limit = device.limits
        except Exception:
            ...
        else:
            if low_limit < high_limit:
                self.ui.low_limit.setText(str(low_limit))
                self.ui.high_limit.setText(str(high_limit))
                return

        # If not found or invalid, hide them:
        self.ui.low_limit.hide()
        self.ui.high_limit.hide()

    @utils.linked_attribute('moving_attribute', 'ui.moving_indicator', True)
    def _link_moving(self, signal, widget):
        """Link the positioner moving indicator with the ui element."""
        if signal is None:
            widget.hide()
            return False
        widget.show()
        # Additional handling for updating self.moving
        if self._moving_channel is not None:
            self._moving_channel.disconnect()
        chname = utils.channel_from_signal(signal)
        self._moving_channel = PyDMChannel(
            address=chname,
            value_slot=self._set_moving,
        )
        self._moving_channel.connect()
        return True

    @utils.linked_attribute('error_message_attribute', 'ui.error_label', True)
    def _link_error_message(self, signal, widget):
        """Link the IOC error message with the ui element."""
        if signal is None:
            widget.hide()

    def _define_setpoint_widget(self):
        """
        Leverage information at describe to define whether to use a
        PyDMLineEdit or a PyDMEnumCombobox as setpoint widget.
        """
        try:
            setpoint_signal = getattr(self.device, self.setpoint_attribute)
            selection = setpoint_signal.enum_strs is not None
        except Exception:
            selection = False

        if selection:
            self.ui.set_value = widgets.NoScrollComboBox()
            self.ui.set_value.addItems(setpoint_signal.enum_strs)
            # Activated signal triggers only when the user selects an option
            self.ui.set_value.activated.connect(self.set)
            self.ui.set_value.setSizePolicy(
                QtWidgets.QSizePolicy.Expanding,
                QtWidgets.QSizePolicy.Fixed,
            )
            self.ui.set_value.setMinimumContentsLength(20)
            self.ui.tweak_widget.setVisible(False)
        else:
            self.ui.set_value = QtWidgets.QLineEdit()
            self.ui.set_value.setAlignment(QtCore.Qt.AlignCenter)
            self.ui.set_value.returnPressed.connect(self.set)

        self.ui.setpoint_layout.addWidget(self.ui.set_value)

    @property
    def device(self):
        """The associated device."""
        try:
            return self.devices[0]
        except Exception:
            ...

    def add_device(self, device):
        """Add a device to the widget"""
        # Add device to cache
        self.devices.clear()  # only one device allowed
        super().add_device(device)

        self._define_setpoint_widget()
        self._link_readback()
        self._link_setpoint()
        self._link_low_limit_switch()
        self._link_high_limit_switch()

        # If the stop method is missing, hide the button
        try:
            device.stop
            self.ui.stop_button.show()
        except AttributeError:
            self.ui.stop_button.hide()

        if not (self._link_low_travel() and self._link_high_travel()):
            self._link_limits_by_limits_attr()

        if self._link_moving():
            self.ui.moving_indicator_label.show()
        else:
            self.ui.moving_indicator_label.hide()

        self._link_error_message()

        if self.show_expert_button:
            self.ui.expert_button.devices.clear()
            self.ui.expert_button.add_device(device)

        self.ui.alarm_circle.clear_all_alarm_configs()
        self.ui.alarm_circle.add_device(device)

    @QtCore.Property(bool, designable=False)
    def moving(self):
        """
        Current state of widget

        This will lag behind the actual state of the positioner in order to
        prevent unnecessary rapid movements
        """
        return self._moving

    @moving.setter
    def moving(self, value):
        if value != self._moving:
            self._moving = value
            self._after_set_moving(value)

    def _after_set_moving(self, value):
        """
        Common updates needed after a change to the moving state.

        This is pulled out as a separate method because we need
        to initialize the label here during __init__ without
        modifying self.moving.
        """
        utils.reload_widget_stylesheet(self, cascade=True)
        if value:
            self.ui.moving_indicator_label.setText('moving')
        else:
            self.ui.moving_indicator_label.setText('done')

    def _set_moving(self, value):
        """
        Slot for updating the self.moving property.

        This is used e.g. in updating the moving state when the
        motor starts moving in EPICS but not by the request of
        this widget.
        """
        self.moving = bool(value)

    @QtCore.Property(bool, designable=False)
    def successful_move(self):
        """The last requested move was successful"""
        return self._last_move is True

    @QtCore.Property(bool, designable=False)
    def failed_move(self):
        """The last requested move failed"""
        return self._last_move is False

    @QtCore.Property(str, designable=True)
    def readback_attribute(self):
        """The attribute name for the readback signal."""
        return self._readback_attr

    @readback_attribute.setter
    def readback_attribute(self, value):
        self._readback_attr = value

    @QtCore.Property(str, designable=True)
    def setpoint_attribute(self):
        """The attribute name for the setpoint signal."""
        return self._setpoint_attr

    @setpoint_attribute.setter
    def setpoint_attribute(self, value):
        self._setpoint_attr = value

    @QtCore.Property(str, designable=True)
    def low_limit_switch_attribute(self):
        """The attribute name for the low limit switch signal."""
        return self._low_limit_switch_attr

    @low_limit_switch_attribute.setter
    def low_limit_switch_attribute(self, value):
        self._low_limit_switch_attr = value

    @QtCore.Property(str, designable=True)
    def high_limit_switch_attribute(self):
        """The attribute name for the high limit switch signal."""
        return self._high_limit_switch_attr

    @high_limit_switch_attribute.setter
    def high_limit_switch_attribute(self, value):
        self._high_limit_switch_attr = value

    @QtCore.Property(str, designable=True)
    def low_limit_travel_attribute(self):
        """The attribute name for the low limit signal."""
        return self._low_limit_travel_attr

    @low_limit_travel_attribute.setter
    def low_limit_travel_attribute(self, value):
        self._low_limit_travel_attr = value

    @QtCore.Property(str, designable=True)
    def high_limit_travel_attribute(self):
        """The attribute name for the high (soft) limit travel signal."""
        return self._high_limit_travel_attr

    @high_limit_travel_attribute.setter
    def high_limit_travel_attribute(self, value):
        self._high_limit_travel_attr = value

    @QtCore.Property(str, designable=True)
    def velocity_attribute(self):
        """The attribute name for the velocity signal."""
        return self._velocity_attr

    @velocity_attribute.setter
    def velocity_attribute(self, value):
        self._velocity_attr = value

    @QtCore.Property(str, designable=True)
    def acceleration_attribute(self):
        """The attribute name for the acceleration time signal."""
        return self._acceleration_attr

    @acceleration_attribute.setter
    def acceleration_attribute(self, value):
        self._acceleration_attr = value

    @QtCore.Property(str, designable=True)
    def moving_attribute(self):
        """The attribute name for the motor moving indicator."""
        return self._moving_attr

    @moving_attribute.setter
    def moving_attribute(self, value):
        self._moving_attr = value

    @QtCore.Property(str, designable=True)
    def error_message_attribute(self):
        """The attribute name for the IOC error message label."""
        return self._error_message_attr

    @error_message_attribute.setter
    def error_message_attribute(self, value):
        self._error_message_attr = value

    @QtCore.Property(bool, designable=True)
    def show_expert_button(self):
        """
        If True, show the expert button.

        The expert button opens a full suite for the device.
        You typically want this False when you're already inside the
        suite that the button would open.
        You typically want this True when you're using the positioner widget
        inside of an unrelated screen.
        This will default to False.
        """
        return self._show_expert_button

    @show_expert_button.setter
    def show_expert_button(self, show):
        self._show_expert_button = show
        if show:
            self.ui.expert_button.show()
        else:
            self.ui.expert_button.hide()

    def move_changed(self):
        """Called when a move is begun"""
        logger.debug("Begin showing move in TyphosPositionerWidget")
        self.moving = True

    def _set_status_text(self, text, *, max_length=60):
        """Set the status text label to ``text``."""
        if len(text) >= max_length:
            self.ui.status_label.setToolTip(text)
            text = text[:max_length] + '...'
        else:
            self.ui.status_label.setToolTip('')

        self.ui.status_label.setText(text)

    def _status_finished(self, result):
        """Called when a move is complete."""
        if isinstance(result, Exception):
            text = f'<b>{result.__class__.__name__}</b> {result}'
        else:
            text = ''

        self._set_status_text(text)

        success = not isinstance(result, Exception)
        logger.debug("Completed move in TyphosPositionerWidget (result=%r)",
                     result)
        self._last_move = success
        self.moving = False

    @QtCore.Slot(str)
    def _user_setpoint_update(self, text):
        """Qt slot - indicating the ``user_setpoint`` widget text changed."""
        try:
            text = text.strip().split(' ')[0]
            text = text.strip()
        except Exception:
            return

        # Update set_value if it's not being edited.
        if not self.ui.set_value.hasFocus():
            if isinstance(self.ui.set_value, widgets.NoScrollComboBox):
                try:
                    idx = int(text)
                    self.ui.set_value.setCurrentIndex(idx)
                    self._initialized = True
                except ValueError:
                    logger.debug('Failed to convert value to int. %s', text)
            else:
                self._initialized = True
                self.ui.set_value.setText(text)

    def update_alarm_text(self, alarm_level):
        """
        Label the alarm circle with a short text bit.
        """
        alarms = self.ui.alarm_circle.AlarmLevel
        if alarm_level == alarms.NO_ALARM:
            text = 'no alarm'
        elif alarm_level == alarms.MINOR:
            text = 'minor'
        elif alarm_level == alarms.MAJOR:
            text = 'major'
        elif alarm_level == alarms.DISCONNECTED:
            text = 'no conn'
        else:
            text = 'invalid'
        self.ui.alarm_label.setText(text)
Beispiel #13
0
class LineBeamParametersControl(Display):
    """
    Class to handle display for the Line Beam Parameters Control tab.
    """
    # object names for all energy range bits checkboxes, set them all
    # to unchecked to start with
    _bits = {f'bit{num}': False for num in reversed(range(32))}

    # signal to emit when energy range is changed
    energy_range_signal = QtCore.Signal(int)

    # this is a gate to break an infinite loop of
    # - Update from channel value
    # - Write back to channel
    _setting_bits = False
    energy_channel = None

    def __init__(self, parent=None, args=None, macros=None):
        super(LineBeamParametersControl, self).__init__(parent=parent,
                                                        args=args,
                                                        macros=macros)
        self.config = macros
        self.setup_ui()

        if self.energy_channel:
            self.destroyed.connect(functools.partial(clear_channel,
                                                     self.energy_channel))

    def setup_ui(self):
        self.setup_bits_connections()
        self.setup_bit_indicators()
        self.setup_energy_range_channel()

    def setup_bits_connections(self):
        """
        Connect all the check boxes bits with the calc_energy_range method to
        calculate the range upon changing a bit state.
        """
        for key, item in self._bits.items():
            cb = self.findChild(QtWidgets.QCheckBox, key)
            cb.stateChanged.connect(functools.partial(
                self.calc_energy_range, key))

    def setup_bit_indicators(self):
        """
        Borrowed function from fast_faults, to help morph the labels vertically
        """
        for key in self._bits.keys():
            label = self.findChild(PyDMLabel, f"label_{key}")
            if label is not None:
                morph_into_vertical(label)

    def calc_energy_range(self, key, state):
        """
        Catch when a check box is checked/unchecked and calculate
        the current bitmask.

        Parameters
        ----------
        key : str
            The check box object name.
        state : int
            The state of the check box.
            0 = unchecked
            2 = checked

        Note
        ----
        The checkboxes can be tri-states - here we use the states 0 and 2
        for unchecked and checked respectively.
        """
        status = state == 2
        self._bits[key] = status
        decimal_value = functools.reduce(
            (lambda x, y: (x << 1) | y),
            map(int, [item for key, item in self._bits.items()])
        )

        if not self._setting_bits:
            # emit the decimal value to the PhotonEnergyRange
            self.energy_range_signal.emit(decimal_value)

    def setup_energy_range_channel(self):
        prefix = self.config.get('line_arbiter_prefix')

        ch_macros = dict(PREFIX=prefix)
        ch = Template(
            'ca://${PREFIX}BeamParamCntl:ReqBP:PhotonEnergyRanges').safe_substitute(**ch_macros)
        self.energy_channel = PyDMChannel(
            ch,
            value_slot=self.energy_range_changed,
            value_signal=self.energy_range_signal
        )
        self.energy_channel.connect()

    def energy_range_changed(self, energy_range):
        """
        This slot is supposed to handled the initial value of the
        Photon Energy Range coming in as soon as we connect, as well
        as whenever this value is changed outside this application.

        Parameters
        ----------
        energy_range : int
            The decimal value of the photon energy range.
        """
        if energy_range is None:
            return

        # EPICS is signed but we want the unsigned 32-bit int
        if energy_range < 0:
            energy_range = 2**32 + energy_range

        binary_range = list(bin(energy_range).replace("0b", ""))
        binary_list = list(map(int, binary_range))
        self._setting_bits = True
        for key, status in zip(self._bits.keys(), binary_list):
            self._bits[key] = bool(status)
            cb = self.findChild(QtWidgets.QCheckBox, f"{key}")
            state = 2 if status == 1 else 0
            cb.setCheckState(state)
        # set this value back to false so we don't create a infinite
        # loop between this slot and the energy_range_signal signal.
        self._setting_bits = False

    def ui_filename(self):
        return 'line_beam_parameters.ui'
Beispiel #14
0
    def __init__(self, parent=None, args=None, macros=None):
        super(
            MCADisplay,
            self).__init__(
            parent=parent,
            args=args,
            macros=macros)
        # Debug Logger
        self.logger = logging.getLogger('mca_logger')
        self.separator = "\n" + ("-" * 20) + "\n"

        self.num_ROI = 9
        self.ROI = []
        self.start = []
        self.end = []
        self.counts = []
        self.lines = []
        self.set_ROI_widgets()
        cli_args = self.parse_args(args)

        self.energy, self.element = build_dic(cli_args)

        self.waveform.plotItem.scene().sigMouseMoved.connect(self.mouse_moved)
        self.waveform.setXLabels(["Energy (eV)"])
        self.waveform.setYLabels(["Count"])

        # Add Channels
        self.waveform.addChannel(None, None, name="Full", color="white")
        color_list = ["red", "green", "blue"]
        for wave in range(self.num_ROI):
            name = f"ROI{wave+1}"
            color = color_list[wave % len(color_list)]
            self.waveform.addChannel(
                None, None, name=name, color=color, lineWidth=2)

        # TODO: Is 18 just double the number of ROI's?
        for wave in range(18):
            name = f"Line{wave+1:02d}"
            self.waveform.addChannel(
                None,
                None,
                name=name,
                color="white",
                lineWidth=2,
                lineStyle=Qt.DashLine)

        self.curve = self.waveform._curves[0]
        self.croi = self.waveform._curves[1:10]
        self.line = self.waveform._curves[10:28]

        if (macros is not None) and ("FIT" in macros):
            if (macros["FIT"].lower() == "cauchy"):
                self.fitc = "Cauchy"
            else:
                self.fitc = "Gaussian"
        else:
            self.fitc = "Gaussian"

        if (macros is not None) and ("DEVICE" in macros):
            self.dataSource.addItem("Live EPICS")

            # TODO: Other file uses macros["DEVICE"]+":RAW:ArrayData"
            epics = PyDMChannel(address="ca://" +
                                macros["DEVICE"] + ":ARR1:ArrayData",
                                value_slot=self.live_data)
            epics.connect()

            self.show_exposure()
        else:
            self.show_mca()

        self.dataSource.addItem("Playback")
        self.dataSource.setCurrentIndex(0)
        self.dataSource.currentIndexChanged.connect(self.change_source)

        self.openFile   .clicked           .connect(self.open_file)
        self.previousMCA.clicked           .connect(self.previous_mca)
        self.nextMCA    .clicked           .connect(self.next_mca)
        self.fullView   .clicked           .connect(self.full_view)

        self.previousMCA.setEnabled(False)
        self.nextMCA    .setEnabled(False)

        self.record = []
        self.record_i = 0
Beispiel #15
0
class ChannelTableWidgetItem(QtWidgets.QTableWidgetItem):
    """
    QTableWidgetItem that gets values from a PyDMChannel

    Parameters
    ----------
    header : str
        The name of the header of the column
    default : any, optional
        Starting value for the cell
    channel : str, optional
        PyDM channel address for value and connection updates.
    deadband : float, optional
        Only update the table if the change is more than the deadband.
        This can help make large tables less resource-hungry.
    """
    header: str
    channel: Optional[str]
    deadband: float
    pydm_channel: Optional[PyDMChannel]

    def __init__(self,
                 header: str,
                 default: Optional[Any] = None,
                 channel: Optional[str] = None,
                 deadband: float = 0.0,
                 parent: Optional[QtWidgets.QWidget] = None):
        super().__init__(parent)
        self.header = header
        self.update_value(default)
        self.channel = channel
        self.deadband = deadband
        if channel is None:
            self.update_connection(True)
            self.pydm_channel = None
        else:
            self.update_connection(False)
            self.pydm_channel = PyDMChannel(
                channel,
                value_slot=self.update_value,
                connection_slot=self.update_connection,
            )
            self.pydm_channel.connect()

    def update_value(self, value: Any) -> str:
        """
        Store the value for sorting and display in the table if visible.

        By setting the text, we also notify the table that a cell has updated.
        """
        try:
            if abs(self._value - value) < self.deadband:
                return
        except Exception:
            pass
        self._value = value
        self.setText(str(value))

    def update_connection(self, connected: bool) -> None:
        """
        When our PV connects or disconnects, store the state as an attribute.
        """
        self.connected = connected

    def get_value(self) -> Any:
        return self._value

    def __lt__(self, other: ChannelTableWidgetItem) -> bool:
        """
        Two special sorting rules:
        1. None is the greatest
        2. Empty string is the greatest string

        This means that disconnected and empty string sort as "high"
        (sort ascending is most common)
        """
        # Make sure None sorts as greatest
        if self.get_value() is None:
            return False
        elif other.get_value() is None:
            return True
        # Make sure empty string sorts as next greatest
        elif self.get_value() == '':
            return False
        elif other.get_value() == '':
            return True
        return self.get_value() < other.get_value()
Beispiel #16
0
    def setup_plc_ioc_status(self):
        ffs = self.config.get('fastfaults')
        if not ffs:
            return
        if self.plc_ioc_container is None:
            return

        for ff in ffs:
            prefix = ff.get('prefix')
            ffo_start = ff.get('ffo_start')
            ffo_end = ff.get('ffo_end')
            ff_start = ff.get('ff_start')
            ff_end = ff.get('ff_end')

            ffos_zfill = len(str(ffo_end)) + 1
            ffs_zfill = len(str(ff_end)) + 1
            entries = itertools.product(range(ffo_start, ffo_end + 1),
                                        range(ff_start, ff_end + 1))

            plc_name = prefix.strip(':')
            plc_macros = dict(P=prefix)
            # get the heartbeat of the IOC to
            ico_heart_ch = Template('ca://${P}HEARTBEAT').safe_substitute(
                **plc_macros)
            # the get PLC process cycle count
            plc_task_info_1 = Template(
                'ca://${P}TaskInfo:1:CycleCount').safe_substitute(**plc_macros)
            plc_task_info_2 = Template(
                'ca://${P}TaskInfo:2:CycleCount').safe_substitute(**plc_macros)
            plc_task_info_3 = Template(
                'ca://${P}TaskInfo:3:CycleCount').safe_substitute(**plc_macros)

            label_name = QtWidgets.QLabel(str(plc_name))
            label_online = QtWidgets.QLabel()
            label_in_use = QtWidgets.QLabel()
            label_alarmed = QtWidgets.QLabel()
            label_heartbeat = PyDMLabel(init_channel=ico_heart_ch)
            label_plc_task_info_1 = PyDMLabel(init_channel=plc_task_info_1)
            label_plc_task_info_2 = PyDMLabel(init_channel=plc_task_info_2)
            label_plc_task_info_3 = PyDMLabel(init_channel=plc_task_info_3)

            # if alarm of plc_task_info_1 == INVALID => plc down
            # if the count does not update and alarm == NO_ALARM =>
            # plc online but stopped
            self.plc_status_ch = PyDMChannel(
                plc_task_info_1,
                severity_slot=functools.partial(
                    self.plc_cycle_count_severity_changed, plc_name))
            self.plc_status_ch.connect()

            # if we can get the plc_cycle_count the PLC should be ON, if not OFF
            # if we get the plc_cycle_count and the .SERV is INVALID, the PLC is OFF
            plc_status_indicator = PyDMBitIndicator(circle=True)
            plc_status_indicator.setColor(self._off_color)
            # TODO - maybe add the case where PLC On but stopped

            # total initial number of ffs to initialize the dictionaries with
            # num_ffo * num_ff
            all_ffos = ((ffo_end - ffo_start) + 1) * (ff_end - ff_start + 1)
            self.ffs_count_map[plc_name] = {
                'online': [False] * all_ffos,
                'in_use': [False] * all_ffos,
                'alarmed': [False] * all_ffos,
                'plc_status': False
            }
            self.ffs_label_map[plc_name] = {
                'online': label_online,
                'in_use': label_in_use,
                'alarmed': label_alarmed,
                'plc_status': plc_status_indicator
            }

            count = 0
            for _ffo, _ff in entries:
                s_ffo = str(_ffo).zfill(ffos_zfill)
                s_ff = str(_ff).zfill(ffs_zfill)
                ch_macros = dict(index=count, P=prefix, FFO=s_ffo, FF=s_ff)

                ch = Template('ca://${P}FFO:${FFO}:FF:${FF}:Info:InUse_RBV'
                              ).safe_substitute(**ch_macros)
                channel = PyDMChannel(
                    ch,
                    connection_slot=functools.partial(
                        self.ffo_connection_callback, plc_name, count),
                    value_slot=functools.partial(self.ffo_value_changed,
                                                 plc_name, count),
                    severity_slot=functools.partial(self.ffo_severity_changed,
                                                    plc_name, count))
                # should not be adding a new connection because this address
                # already exists in the connections,
                # instead should just add a listener
                channel.connect()
                count += 1

            widget = QtWidgets.QWidget()
            widget_layout = QtWidgets.QHBoxLayout()

            # this is the same width as the labels in the plc_ioc_header
            max_width = 150
            min_width = 130
            widget_list = [
                label_name, label_online, label_in_use, label_alarmed,
                label_heartbeat, label_plc_task_info_1, label_plc_task_info_2,
                label_plc_task_info_3, plc_status_indicator
            ]
            widget.setLayout(widget_layout)

            # set minimum height of the widget
            widget.setMinimumHeight(40)
            self.setup_widget_size(max_width=max_width,
                                   min_width=min_width,
                                   widget_list=widget_list)
            widget.layout().addWidget(label_name)
            widget.layout().addWidget(label_online)
            widget.layout().addWidget(label_in_use)
            widget.layout().addWidget(label_alarmed)
            widget.layout().addWidget(label_heartbeat)
            widget.layout().addWidget(label_plc_task_info_1)
            widget.layout().addWidget(label_plc_task_info_2)
            widget.layout().addWidget(label_plc_task_info_3)
            widget.layout().addWidget(plc_status_indicator)

            self.plc_ioc_container.layout().addWidget(widget)
            vertical_spacer = (QtWidgets.QSpacerItem(
                20, 20, QtWidgets.QSizePolicy.Preferred,
                QtWidgets.QSizePolicy.Maximum))
            self.plc_ioc_container.layout().addItem(vertical_spacer)
        b_vertical_spacer = (QtWidgets.QSpacerItem(
            20, 20, QtWidgets.QSizePolicy.Preferred,
            QtWidgets.QSizePolicy.Expanding))
        self.plc_ioc_container.layout().addItem(b_vertical_spacer)
        self.plc_ioc_container.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
                                             QtWidgets.QSizePolicy.Preferred)
Beispiel #17
0
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap,
                            ReadingOrder):
    """
    A SpectrogramView with support for Channels and more from PyDM.

    If there is no :attr:`channelWidth` it is possible to define the width of
    the image with the :attr:`width` property.

    The :attr:`normalizeData` property defines if the colors of the images are
    relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to
    the minimum and maximum values of the image.

    Use the :attr:`newImageSignal` to hook up to a signal that is emitted when
    a new image is rendered in the widget.

    Parameters
    ----------
    parent : QWidget
        The parent widget for the Label
    image_channel : str, optional
        The channel to be used by the widget for the image data.
    xaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Clike), and to set the xaxis values
    yaxis_channel : str, optional
        The channel to be used by the widget to receive the image width
        (if ReadingOrder == Fortranlike), and to set the yaxis values
    background : QColor, optional
        QColor to set the background color of the GraphicsView
    """

    Q_ENUMS(PyDMColorMap)
    Q_ENUMS(ReadingOrder)

    color_maps = cmaps

    def __init__(self,
                 parent=None,
                 image_channel=None,
                 xaxis_channel=None,
                 yaxis_channel=None,
                 roioffsetx_channel=None,
                 roioffsety_channel=None,
                 roiwidth_channel=None,
                 roiheight_channel=None,
                 title='',
                 background='w',
                 image_width=0,
                 image_height=0):
        """Initialize widget."""
        GraphicsLayoutWidget.__init__(self, parent)
        PyDMWidget.__init__(self)
        self.thread = None
        self._imagechannel = None
        self._xaxischannel = None
        self._yaxischannel = None
        self._roioffsetxchannel = None
        self._roioffsetychannel = None
        self._roiwidthchannel = None
        self._roiheightchannel = None
        self._channels = 7 * [
            None,
        ]
        self.image_waveform = np.zeros(0)
        self._image_width = image_width if not xaxis_channel else 0
        self._image_height = image_height if not yaxis_channel else 0
        self._roi_offsetx = 0
        self._roi_offsety = 0
        self._roi_width = 0
        self._roi_height = 0
        self._normalize_data = False
        self._auto_downsample = True
        self._last_yaxis_data = None
        self._last_xaxis_data = None
        self._auto_colorbar_lims = True
        self.format_tooltip = '{0:.4g}, {1:.4g}'

        # ViewBox and imageItem.
        self._view = ViewBox()
        self._image_item = ImageItem()
        self._view.addItem(self._image_item)

        # ROI
        self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0])
        self.ROIColor = QColor('red')
        pen = mkPen()
        pen.setColor(QColor('transparent'))
        pen.setWidth(1)
        self.ROICurve.setPen(pen)
        self._view.addItem(self.ROICurve)

        # Axis.
        self.xaxis = AxisItem('bottom')
        self.xaxis.setPen(QColor(0, 0, 0))
        if not xaxis_channel:
            self.xaxis.setVisible(False)
        self.yaxis = AxisItem('left')
        self.yaxis.setPen(QColor(0, 0, 0))
        if not yaxis_channel:
            self.yaxis.setVisible(False)

        # Colorbar legend.
        self.colorbar = _GradientLegend()

        # Title.
        start_row = 0
        if title:
            self.title = LabelItem(text=title, color='#000000')
            self.addItem(self.title, 0, 0, 1, 3)
            start_row = 1

        # Set layout.
        self.addItem(self._view, start_row, 1)
        self.addItem(self.yaxis, start_row, 0)
        self.addItem(self.colorbar, start_row, 2)
        self.addItem(self.xaxis, start_row + 1, 1)
        self.setBackground(background)
        self.ci.layout.setColumnSpacing(0, 0)
        self.ci.layout.setRowSpacing(start_row, 0)

        # Set color map limits.
        self.cm_min = 0.0
        self.cm_max = 255.0

        # Set default reading order of numpy array data to Clike.
        self._reading_order = ReadingOrder.Clike

        # Make a right-click menu for changing the color map.
        self.cm_group = QActionGroup(self)
        self.cmap_for_action = {}
        for cm in self.color_maps:
            action = self.cm_group.addAction(cmap_names[cm])
            action.setCheckable(True)
            self.cmap_for_action[action] = cm

        # Set the default colormap.
        self._cm_colors = None
        self.colorMap = PyDMColorMap.Inferno

        # Setup the redraw timer.
        self.needs_redraw = False
        self.redraw_timer = QTimer(self)
        self.redraw_timer.timeout.connect(self.redrawImage)
        self._redraw_rate = 30
        self.maxRedrawRate = self._redraw_rate
        self.newImageSignal = self._image_item.sigImageChanged

        # Set Channels.
        self.imageChannel = image_channel
        self.xAxisChannel = xaxis_channel
        self.yAxisChannel = yaxis_channel
        self.ROIOffsetXChannel = roioffsetx_channel
        self.ROIOffsetYChannel = roioffsety_channel
        self.ROIWidthChannel = roiwidth_channel
        self.ROIHeightChannel = roiheight_channel

    # --- Context menu ---
    def widget_ctx_menu(self):
        """
        Fetch the Widget specific context menu.

        It will be populated with additional tools by `assemble_tools_menu`.

        Returns
        -------
        QMenu or None
            If the return of this method is None a new QMenu will be created by
            `assemble_tools_menu`.
        """
        self.menu = ViewBoxMenu(self._view)
        cm_menu = self.menu.addMenu("Color Map")
        for act in self.cmap_for_action.keys():
            cm_menu.addAction(act)
        cm_menu.triggered.connect(self._changeColorMap)
        return self.menu

    # --- Colormap methods ---
    def _changeColorMap(self, action):
        """
        Method invoked by the colormap Action Menu.

        Changes the current colormap used to render the image.

        Parameters
        ----------
        action : QAction
        """
        self.colorMap = self.cmap_for_action[action]

    @Property(float)
    def colorMapMin(self):
        """
        Minimum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_min

    @colorMapMin.setter
    @Slot(float)
    def colorMapMin(self, new_min):
        """
        Set the minimum value for the colormap.

        Parameters
        ----------
        new_min : float
        """
        if self.cm_min != new_min:
            self.cm_min = new_min
            if self.cm_min > self.cm_max:
                self.cm_max = self.cm_min

    @Property(float)
    def colorMapMax(self):
        """
        Maximum value for the colormap.

        Returns
        -------
        float
        """
        return self.cm_max

    @colorMapMax.setter
    @Slot(float)
    def colorMapMax(self, new_max):
        """
        Set the maximum value for the colormap.

        Parameters
        ----------
        new_max : float
        """
        if self.cm_max != new_max:
            self.cm_max = new_max
            if self.cm_max < self.cm_min:
                self.cm_min = self.cm_max

    def setColorMapLimits(self, mn, mx):
        """
        Set the limit values for the colormap.

        Parameters
        ----------
        mn : int
            The lower limit
        mx : int
            The upper limit
        """
        if mn >= mx:
            return
        self.cm_max = mx
        self.cm_min = mn

    @Property(PyDMColorMap)
    def colorMap(self):
        """
        Return the color map used by the SpectrogramView.

        Returns
        -------
        PyDMColorMap
        """
        return self._colormap

    @colorMap.setter
    def colorMap(self, new_cmap):
        """
        Set the color map used by the SpectrogramView.

        Parameters
        -------
        new_cmap : PyDMColorMap
        """
        self._colormap = new_cmap
        self._cm_colors = self.color_maps[new_cmap]
        self.setColorMap()
        for action in self.cm_group.actions():
            if self.cmap_for_action[action] == self._colormap:
                action.setChecked(True)
            else:
                action.setChecked(False)

    def setColorMap(self, cmap=None):
        """
        Update the image colormap.

        Parameters
        ----------
        cmap : ColorMap
        """
        if not cmap:
            if not self._cm_colors.any():
                return
            # Take default values
            pos = np.linspace(0.0, 1.0, num=len(self._cm_colors))
            cmap = ColorMap(pos, self._cm_colors)
        self._view.setBackgroundColor(cmap.map(0))
        lut = cmap.getLookupTable(0.0, 1.0, alpha=False)
        self.colorbar.setIntColorScale(colors=lut)
        self._image_item.setLookupTable(lut)

    # --- Connection Slots ---
    @Slot(bool)
    def image_connection_state_changed(self, conn):
        """
        Callback invoked when the Image Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        if conn:
            self.redraw_timer.start()
        else:
            self.redraw_timer.stop()

    @Slot(bool)
    def yaxis_connection_state_changed(self, connected):
        """
        Callback invoked when the TimeAxis Channel connection state is changed.

        Parameters
        ----------
        conn : bool
            The new connection state.
        """
        self._timeaxis_connected = connected

    @Slot(bool)
    def roioffsetx_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetX Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsetx = 0

    @Slot(bool)
    def roioffsety_connection_state_changed(self, conn):
        """
        Run when the ROIOffsetY Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_offsety = 0

    @Slot(bool)
    def roiwidth_connection_state_changed(self, conn):
        """
        Run when the ROIWidth Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_width = 0

    @Slot(bool)
    def roiheight_connection_state_changed(self, conn):
        """
        Run when the ROIHeight Channel connection state changes.

        Parameters
        ----------
        conn : bool
            The new connection state.

        """
        if not conn:
            self._roi_height = 0

    # --- Value Slots ---
    @Slot(np.ndarray)
    def image_value_changed(self, new_image):
        """
        Callback invoked when the Image Channel value is changed.

        We try to do as little as possible in this method, because it
        gets called every time the image channel updates, which might
        be extremely often.  Basically just store the data, and set
        a flag requesting that the image be redrawn.

        Parameters
        ----------
        new_image : np.ndarray
            The new image data.  This can be a flat 1D array, or a 2D array.
        """
        if new_image is None or new_image.size == 0:
            return
        logging.debug("SpectrogramView Received New Image: Needs Redraw->True")
        self.image_waveform = new_image
        self.needs_redraw = True
        if not self._image_height and self._image_width:
            self._image_height = new_image.size / self._image_width
        elif not self._image_width and self._image_height:
            self._image_width = new_image.size / self._image_height

    @Slot(np.ndarray)
    @Slot(float)
    def xaxis_value_changed(self, new_array):
        """
        Callback invoked when the Image Width Channel value is changed.

        Parameters
        ----------
        new_array : np.ndarray
            The new x axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_xaxis_data = new_array
        if self._reading_order == self.Clike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(np.ndarray)
    @Slot(float)
    def yaxis_value_changed(self, new_array):
        """
        Callback invoked when the TimeAxis Channel value is changed.

        Parameters
        ----------
        new_array : np.array
            The new y axis array
        """
        if new_array is None:
            return
        if isinstance(new_array, float):
            new_array = np.array([
                new_array,
            ])
        self._last_yaxis_data = new_array
        if self._reading_order == self.Fortranlike:
            self._image_width = new_array.size
        else:
            self._image_height = new_array.size
        self.needs_redraw = True

    @Slot(int)
    def roioffsetx_value_changed(self, new_offset):
        """
        Run when the ROIOffsetX Channel value changes.

        Parameters
        ----------
        new_offsetx : int
            The new image ROI horizontal offset

        """
        if new_offset is None:
            return
        self._roi_offsetx = new_offset
        self.redrawROI()

    @Slot(int)
    def roioffsety_value_changed(self, new_offset):
        """
        Run when the ROIOffsetY Channel value changes.

        Parameters
        ----------
        new_offsety : int
            The new image ROI vertical offset

        """
        if new_offset is None:
            return
        self._roi_offsety = new_offset
        self.redrawROI()

    @Slot(int)
    def roiwidth_value_changed(self, new_width):
        """
        Run when the ROIWidth Channel value changes.

        Parameters
        ----------
        new_width : int
            The new image ROI width

        """
        if new_width is None:
            return
        self._roi_width = int(new_width)
        self.redrawROI()

    @Slot(int)
    def roiheight_value_changed(self, new_height):
        """
        Run when the ROIHeight Channel value changes.

        Parameters
        ----------
        new_height : int
            The new image ROI height

        """
        if new_height is None:
            return
        self._roi_height = int(new_height)
        self.redrawROI()

    # --- Image update methods ---
    def process_image(self, image):
        """
        Boilerplate method.

        To be used by applications in order to add calculations and also modify
        the image before it is displayed at the widget.

        .. warning::
           This code runs in a separated QThread so it **MUST** not try to
           write to QWidgets.

        Parameters
        ----------
        image : np.ndarray
            The Image Data as a 2D numpy array

        Returns
        -------
        np.ndarray
            The Image Data as a 2D numpy array after processing.
        """
        return image

    def redrawImage(self):
        """
        Set the image data into the ImageItem, if needed.

        If necessary, reshape the image to 2D first.
        """
        if self.thread is not None and not self.thread.isFinished():
            logger.warning(
                "Image processing has taken longer than the refresh rate.")
            return
        self.thread = SpectrogramUpdateThread(self)
        self.thread.updateSignal.connect(self._updateDisplay)
        logging.debug("SpectrogramView RedrawImage Thread Launched")
        self.thread.start()

    @Slot(list)
    def _updateDisplay(self, data):
        logging.debug("SpectrogramView Update Display with new image")

        # Update axis
        if self._last_xaxis_data is not None:
            szx = self._last_xaxis_data.size
            xMin = self._last_xaxis_data.min()
            xMax = self._last_xaxis_data.max()
        else:
            szx = self.imageWidth if self.readingOrder == self.Clike \
                else self.imageHeight
            xMin = 0
            xMax = szx

        if self._last_yaxis_data is not None:
            szy = self._last_yaxis_data.size
            yMin = self._last_yaxis_data.min()
            yMax = self._last_yaxis_data.max()
        else:
            szy = self.imageHeight if self.readingOrder == self.Clike \
                else self.imageWidth
            yMin = 0
            yMax = szy

        self.xaxis.setRange(xMin, xMax)
        self.yaxis.setRange(yMin, yMax)
        self._view.setLimits(xMin=0,
                             xMax=szx,
                             yMin=0,
                             yMax=szy,
                             minXRange=szx,
                             maxXRange=szx,
                             minYRange=szy,
                             maxYRange=szy)

        # Update image
        if self.autoSetColorbarLims:
            self.colorbar.setLimits(data)
        mini, maxi = data[0], data[1]
        img = data[2]
        self._image_item.setLevels([mini, maxi])
        self._image_item.setImage(img,
                                  autoLevels=False,
                                  autoDownsample=self.autoDownsample)

    # ROI update methods
    def redrawROI(self):
        startx = self._roi_offsetx
        endx = self._roi_offsetx + self._roi_width
        starty = self._roi_offsety
        endy = self._roi_offsety + self._roi_height
        self.ROICurve.setData([startx, startx, endx, endx, startx],
                              [starty, endy, endy, starty, starty])

    def showROI(self, show):
        """Set ROI visibility."""
        pen = mkPen()
        if show:
            pen.setColor(self.ROIColor)
        else:
            pen.setColor(QColor('transparent'))
        self.ROICurve.setPen(pen)

    # --- Properties ---
    @Property(bool)
    def autoDownsample(self):
        """
        Return if we should or not apply the autoDownsample option.

        Return
        ------
        bool
        """
        return self._auto_downsample

    @autoDownsample.setter
    def autoDownsample(self, new_value):
        """
        Whether we should or not apply the autoDownsample option.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_downsample:
            self._auto_downsample = new_value

    @Property(bool)
    def autoSetColorbarLims(self):
        """
        Return if we should or not auto set colorbar limits.

        Return
        ------
        bool
        """
        return self._auto_colorbar_lims

    @autoSetColorbarLims.setter
    def autoSetColorbarLims(self, new_value):
        """
        Whether we should or not auto set colorbar limits.

        Parameters
        ----------
        new_value: bool
        """
        if new_value != self._auto_colorbar_lims:
            self._auto_colorbar_lims = new_value

    @Property(int)
    def imageWidth(self):
        """
        Return the width of the image.

        Return
        ------
        int
        """
        return self._image_width

    @imageWidth.setter
    def imageWidth(self, new_width):
        """
        Set the width of the image.

        Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`.

        Parameters
        ----------
        new_width: int
        """
        boo = self._image_width != int(new_width)
        boo &= not self._xaxischannel
        boo &= not self._yaxischannel
        if boo:
            self._image_width = int(new_width)

    @Property(int)
    def imageHeight(self):
        """
        Return the height of the image.

        Return
        ------
        int
        """
        return self._image_height

    @Property(int)
    def ROIOffsetX(self):
        """
        Return the ROI offset in X axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsetx

    @ROIOffsetX.setter
    def ROIOffsetX(self, new_offset):
        """
        Set the ROI offset in X axis in pixels.

        Can be overridden by :attr:`ROIOffsetXChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsetx != int(new_offset)
        boo &= not self._roioffsetxchannel
        if boo:
            self._roi_offsetx = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIOffsetY(self):
        """
        Return the ROI offset in Y axis in pixels.

        Return
        ------
        int
        """
        return self._roi_offsety

    @ROIOffsetY.setter
    def ROIOffsetY(self, new_offset):
        """
        Set the ROI offset in Y axis in pixels.

        Can be overridden by :attr:`ROIOffsetYChannel`.

        Parameters
        ----------
        new_offset: int
        """
        if new_offset is None:
            return
        boo = self._roi_offsety != int(new_offset)
        boo &= not self._roioffsetychannel
        if boo:
            self._roi_offsety = int(new_offset)
            self.redrawROI()

    @Property(int)
    def ROIWidth(self):
        """
        Return the ROI width in pixels.

        Return
        ------
        int
        """
        return self._roi_width

    @ROIWidth.setter
    def ROIWidth(self, new_width):
        """
        Set the ROI width in pixels.

        Can be overridden by :attr:`ROIWidthChannel`.

        Parameters
        ----------
        new_width: int
        """
        if new_width is None:
            return
        boo = self._roi_width != int(new_width)
        boo &= not self._roiwidthchannel
        if boo:
            self._roi_width = int(new_width)
            self.redrawROI()

    @Property(int)
    def ROIHeight(self):
        """
        Return the ROI height in pixels.

        Return
        ------
        int
        """
        return self._roi_height

    @ROIHeight.setter
    def ROIHeight(self, new_height):
        """
        Set the ROI height in pixels.

        Can be overridden by :attr:`ROIHeightChannel`.

        Parameters
        ----------
        new_height: int
        """
        if new_height is None:
            return
        boo = self._roi_height != int(new_height)
        boo &= not self._roiheightchannel
        if boo:
            self._roi_height = int(new_height)
            self.redrawROI()

    @Property(bool)
    def normalizeData(self):
        """
        Return True if the colors are relative to data maximum and minimum.

        Returns
        -------
        bool
        """
        return self._normalize_data

    @normalizeData.setter
    @Slot(bool)
    def normalizeData(self, new_norm):
        """
        Define if the colors are relative to minimum and maximum of the data.

        Parameters
        ----------
        new_norm: bool
        """
        if self._normalize_data != new_norm:
            self._normalize_data = new_norm

    @Property(ReadingOrder)
    def readingOrder(self):
        """
        Return the reading order of the :attr:`imageChannel` array.

        Returns
        -------
        ReadingOrder
        """
        return self._reading_order

    @readingOrder.setter
    def readingOrder(self, order):
        """
        Set reading order of the :attr:`imageChannel` array.

        Parameters
        ----------
        order: ReadingOrder
        """
        if self._reading_order != order:
            self._reading_order = order

        if order == self.Clike:
            if self._last_xaxis_data is not None:
                self._image_width = self._last_xaxis_data.size
            if self._last_yaxis_data is not None:
                self._image_height = self._last_yaxis_data.size
        elif order == self.Fortranlike:
            if self._last_yaxis_data is not None:
                self._image_width = self._last_yaxis_data.size
            if self._last_xaxis_data is not None:
                self._image_height = self._last_xaxis_data.size

    @Property(int)
    def maxRedrawRate(self):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Returns
        -------
        int
        """
        return self._redraw_rate

    @maxRedrawRate.setter
    def maxRedrawRate(self, redraw_rate):
        """
        The maximum rate (in Hz) at which the plot will be redrawn.

        The plot will not be redrawn if there is not new data to draw.

        Parameters
        -------
        redraw_rate : int
        """
        self._redraw_rate = redraw_rate
        self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000))

    # --- Events rederivations ---
    def keyPressEvent(self, ev):
        """Handle keypress events."""
        return

    def mouseMoveEvent(self, ev):
        if not self._image_item.width() or not self._image_item.height():
            super().mouseMoveEvent(ev)
            return
        pos = ev.pos()
        posaux = self._image_item.mapFromDevice(ev.pos())
        if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \
                posaux.y() < 0 or posaux.y() >= self._image_item.height():
            super().mouseMoveEvent(ev)
            return

        pos_scene = self._view.mapSceneToView(pos)
        x = round(pos_scene.x())
        y = round(pos_scene.y())

        if self.xAxisChannel and self._last_xaxis_data is not None:
            maxx = len(self._last_xaxis_data) - 1
            x = x if x < maxx else maxx
            valx = self._last_xaxis_data[x]
        else:
            valx = x

        if self.yAxisChannel and self._last_yaxis_data is not None:
            maxy = len(self._last_yaxis_data) - 1
            y = y if y < maxy else maxy
            valy = self._last_yaxis_data[y]
        else:
            valy = y

        txt = self.format_tooltip.format(valx, valy)
        QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(),
                          5000)
        super().mouseMoveEvent(ev)

    # --- Channels ---
    @Property(str)
    def imageChannel(self):
        """
        The channel address in use for the image data .

        Returns
        -------
        str
            Channel address
        """
        if self._imagechannel:
            return str(self._imagechannel.address)
        else:
            return ''

    @imageChannel.setter
    def imageChannel(self, value):
        """
        The channel address in use for the image data .

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._imagechannel != value:
            # Disconnect old channel
            if self._imagechannel:
                self._imagechannel.disconnect()
            # Create and connect new channel
            self._imagechannel = PyDMChannel(
                address=value,
                connection_slot=self.image_connection_state_changed,
                value_slot=self.image_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[0] = self._imagechannel
            self._imagechannel.connect()

    @Property(str)
    def xAxisChannel(self):
        """
        The channel address in use for the x-axis of image.

        Returns
        -------
        str
            Channel address
        """
        if self._xaxischannel:
            return str(self._xaxischannel.address)
        else:
            return ''

    @xAxisChannel.setter
    def xAxisChannel(self, value):
        """
        The channel address in use for the x-axis of image.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._xaxischannel != value:
            # Disconnect old channel
            if self._xaxischannel:
                self._xaxischannel.disconnect()
            # Create and connect new channel
            self._xaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.connectionStateChanged,
                value_slot=self.xaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[1] = self._xaxischannel
            self._xaxischannel.connect()

    @Property(str)
    def yAxisChannel(self):
        """
        The channel address in use for the time axis.

        Returns
        -------
        str
            Channel address
        """
        if self._yaxischannel:
            return str(self._yaxischannel.address)
        else:
            return ''

    @yAxisChannel.setter
    def yAxisChannel(self, value):
        """
        The channel address in use for the time axis.

        Parameters
        ----------
        value : str
            Channel address
        """
        if self._yaxischannel != value:
            # Disconnect old channel
            if self._yaxischannel:
                self._yaxischannel.disconnect()
            # Create and connect new channel
            self._yaxischannel = PyDMChannel(
                address=value,
                connection_slot=self.yaxis_connection_state_changed,
                value_slot=self.yaxis_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[2] = self._yaxischannel
            self._yaxischannel.connect()

    @Property(str)
    def ROIOffsetXChannel(self):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetxchannel:
            return str(self._roioffsetxchannel.address)
        else:
            return ''

    @ROIOffsetXChannel.setter
    def ROIOffsetXChannel(self, value):
        """
        Return the channel address in use for the image ROI horizontal offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetxchannel != value:
            # Disconnect old channel
            if self._roioffsetxchannel:
                self._roioffsetxchannel.disconnect()
            # Create and connect new channel
            self._roioffsetxchannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsetx_connection_state_changed,
                value_slot=self.roioffsetx_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[3] = self._roioffsetxchannel
            self._roioffsetxchannel.connect()

    @Property(str)
    def ROIOffsetYChannel(self):
        """
        Return the channel address in use for the image ROI vertical offset.

        Returns
        -------
        str
            Channel address

        """
        if self._roioffsetychannel:
            return str(self._roioffsetychannel.address)
        else:
            return ''

    @ROIOffsetYChannel.setter
    def ROIOffsetYChannel(self, value):
        """
        Return the channel address in use for the image ROI vertical offset.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roioffsetychannel != value:
            # Disconnect old channel
            if self._roioffsetychannel:
                self._roioffsetychannel.disconnect()
            # Create and connect new channel
            self._roioffsetychannel = PyDMChannel(
                address=value,
                connection_slot=self.roioffsety_connection_state_changed,
                value_slot=self.roioffsety_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[4] = self._roioffsetychannel
            self._roioffsetychannel.connect()

    @Property(str)
    def ROIWidthChannel(self):
        """
        Return the channel address in use for the image ROI width.

        Returns
        -------
        str
            Channel address

        """
        if self._roiwidthchannel:
            return str(self._roiwidthchannel.address)
        else:
            return ''

    @ROIWidthChannel.setter
    def ROIWidthChannel(self, value):
        """
        Return the channel address in use for the image ROI width.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiwidthchannel != value:
            # Disconnect old channel
            if self._roiwidthchannel:
                self._roiwidthchannel.disconnect()
            # Create and connect new channel
            self._roiwidthchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiwidth_connection_state_changed,
                value_slot=self.roiwidth_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[5] = self._roiwidthchannel
            self._roiwidthchannel.connect()

    @Property(str)
    def ROIHeightChannel(self):
        """
        Return the channel address in use for the image ROI height.

        Returns
        -------
        str
            Channel address

        """
        if self._roiheightchannel:
            return str(self._roiheightchannel.address)
        else:
            return ''

    @ROIHeightChannel.setter
    def ROIHeightChannel(self, value):
        """
        Return the channel address in use for the image ROI height.

        Parameters
        ----------
        value : str
            Channel address

        """
        if self._roiheightchannel != value:
            # Disconnect old channel
            if self._roiheightchannel:
                self._roiheightchannel.disconnect()
            # Create and connect new channel
            self._roiheightchannel = PyDMChannel(
                address=value,
                connection_slot=self.roiheight_connection_state_changed,
                value_slot=self.roiheight_value_changed,
                severity_slot=self.alarmSeverityChanged)
            self._channels[6] = self._roiheightchannel
            self._roiheightchannel.connect()

    def channels(self):
        """
        Return the channels being used for this Widget.

        Returns
        -------
        channels : list
            List of PyDMChannel objects
        """
        return self._channels

    def channels_for_tools(self):
        """Return channels for tools."""
        return [self._imagechannel]
Beispiel #18
0
class PMPSTableWidgetItem(QTableWidgetItem):
    """
    QTableWidgetItem with extra utilities for the PMPS UI

    Adds the following features:
    - Fill value and connection state from PyDMChannel
    - Configurable sorting
    - Process inputs before storing in the table
    - Able to sort as a non-str type

    Parameters
    ----------
    store_type : callable
        Function or type to call on the value from the PyDMChannel before
        storing in the table.
    data_type : callable
        Function or type to call on the str stored in the table before
        comparing with other PMPSTableWidgetItem instances for sorting.
    default : Any
        A starting value for the widget item.
    channel : str, optional
        PyDM channel address for value and connection updates.
    """
    def __init__(self,
                 store_type,
                 data_type,
                 default,
                 channel=None,
                 parent=None):
        super().__init__(parent)
        self.store_type = store_type
        self.data_type = data_type
        self.setText(str(default))
        self.channel = channel
        self.connected = False
        if channel is not None:
            self.pydm_channel = PyDMChannel(
                channel,
                value_slot=self.update_value,
                connection_slot=self.update_connection,
            )
            self.pydm_channel.connect()

    def update_value(self, value):
        """
        Use the hidden text field to store the value from the PV.

        This is pre-processed by the "store_type" attribute because the
        value can only be saved as a string via setText.

        We use the setText instead of an attribute to help with debugging,
        this means you can see what the table is being sorted on
        if you unhide the columns.
        """
        self.setText(str(self.store_type(value)))

    def update_connection(self, connected):
        """
        When our PV connects or disconnects, store the state as an attribute.
        """
        self.connected = connected

    def get_value(self):
        """The value in canonical python type (not string)."""
        return self.data_type(self.text())

    def __lt__(self, other):
        """Use order of defined type, not alphabetical."""
        return self.get_value() < other.get_value()
Beispiel #19
0
    def __init__(self,
                 parent=None,
                 macros=None,
                 args=None,
                 average="Gamma Detectors"):
        super().__init__(parent=parent,
                         args=args,
                         macros=macros,
                         ui_filename=OVERVIEW_UI)
        self.setWindowTitle("Overview of Time Bases")
        self.alpha = [0, 0, 0, 0, 0]  # Initially doesn't show none graph
        self.average = average
        self.groups = ["{:0>2d}".format(sec) for sec in range(1, 21)]
        self.x = numpy.arange(len(self.groups))
        self.width = 0.185

        self.dict_pvs_tb = {}
        self.dict_macro_gamma = {}

        self.gamma_1 = [0] * 20
        self.gamma_2 = [0] * 20
        self.gamma_3 = [0] * 20
        self.gamma_4 = [0] * 20
        self.gamma_5 = [0] * 20

        self.fig, self.ax = plt.subplots(figsize=(12, 8))  #
        self.fig.canvas.set_window_title("Overview")  #
        self.fig.subplots_adjust(left=0.05, bottom=0.08, right=0.95,
                                 top=0.95)  # Adjustments of graphics
        plt.subplots_adjust(left=0.1)  #

        self.fig.text(0.03, 0.25, "Control of\n Graphic", ha="center")
        self.ani = FuncAnimation(fig=self.fig,
                                 func=self.animate,
                                 interval=10000)
        self.animate()
        self.checkButtons_setting()
        plt.show()

        if self.average == "Gamma Detectors":  # If user chose 'Counting - Overview'
            for PV in range(1, 21):
                for s_sec in range(2):
                    self.dict_pvs_tb["valueTB{}{}".format(
                        PV, counters[s_sec]
                    )] = "ca://SI-{:0>2d}{}:CO-Counter:TimeBase-SP".format(
                        PV, counters[s_sec])
                if PV != 20:
                    self.dict_pvs_tb["valueTB{}M1".format(
                        PV
                    )] = "ca://SI-{:0>2d}M1:CO-Counter:TimeBase-SP".format(PV +
                                                                           1)
                else:
                    self.dict_pvs_tb["valueTB{}M1".format(
                        PV)] = "ca://SI-01M1:CO-Counter:TimeBase-SP"

            for location in range(1, 21):
                for s_sec in range(len(Det_Location)):
                    self.dict_macro_gamma["DET{}".format(
                        s_sec)] = "SI-{:0>2d}{}:CO-Gamma".format(
                            location, Det_Location[s_sec])
                    if s_sec < 3:
                        self.dict_macro_gamma["TimeBase{}".format(
                            s_sec)] = "{}".format(
                                self.dict_pvs_tb["valueTB{}{}".format(
                                    location, counters[s_sec])])

                    a = PyDMChannel(
                        address="ca://SI-{:0>2d}{}:CO-Gamma:Count-Mon".format(
                            location, Det_Location[s_sec]),
                        value_slot=partial(self.plot,
                                           location=location,
                                           det=s_sec),
                    )  # Connect to Counting PVs
                    a.connect()

                self.disp = PyDMEmbeddedDisplay(
                    parent=self)  # Creates the window of Time Bases
                PyDMApplication.instance().close_widget_connections(self.disp)
                self.disp.macros = json.dumps(self.dict_macro_gamma)
                self.disp.filename = LAYOUT_OVERVIEW_UI
                self.disp.setMinimumWidth(300)
                self.disp.setMinimumHeight(140)
                self.verticalLayout.addWidget(self.disp)

                PyDMApplication.instance().establish_widget_connections(
                    self.disp)
        else:  # If user chose some Average
            for location in range(1, 21):
                for s_sec in range(len(Det_Location)):
                    a = PyDMChannel(
                        address="ca://SI-{:0>2d}{}:CO-Gamma:{}-Mon".format(
                            location, Det_Location[s_sec], self.average),
                        value_slot=partial(self.plot,
                                           location=location,
                                           det=s_sec),
                    )  # Connect to Averages PVs
                    a.connect()
Beispiel #20
0
class Regatron(Display):
    def __init__(self, parent=None, macros=None, **kwargs):
        super().__init__(parent=parent, macros=macros, ui_filename=COMPLETE_UI)
        self.setup_icons()

        self.btnErr.filenames = [ERR_MAIN]
        self.btnWarn.filenames = [WARN_MAIN]

        self.btnSysHistory.filenames = [ALARM_MAIN]
        self.btnSysHistory.base_macros = {'P': macros['P'], 'T': 'Sys'}
        self.btnModHistory.filenames = [ALARM_MAIN]
        self.btnModHistory.base_macros = {'P': macros['P'], 'T': 'Mod'}

        # Warning Groups
        self.ch_mod_std_warn_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Mod-StdWarnGroup-Mon',
            value_slot=self.get_mod_std_warn_report)
        self.ch_mod_std_warn_report.connect()

        self.ch_sys_std_warn_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Sys-StdWarnGroup-Mon',
            value_slot=self.get_sys_std_warn_report)
        self.ch_sys_std_warn_report.connect()

        self.ch_mod_ext_warn_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Mod-ExtWarnGroup-Mon',
            value_slot=self.get_mod_ext_warn_report)
        self.ch_mod_ext_warn_report.connect()

        self.ch_sys_ext_warn_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Sys-ExtWarnGroup-Mon',
            value_slot=self.get_sys_ext_warn_report)
        self.ch_sys_ext_warn_report.connect()

        # Error Groups
        self.ch_mod_std_error_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Mod-StdErrGroup-Mon',
            value_slot=self.get_mod_std_error_report)
        self.ch_mod_std_error_report.connect()

        self.ch_sys_std_error_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Sys-StdErrGroup-Mon',
            value_slot=self.get_sys_std_error_report)
        self.ch_sys_std_error_report.connect()

        self.ch_mod_ext_error_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Mod-ExtErrGroup-Mon',
            value_slot=self.get_mod_ext_error_report)
        self.ch_mod_std_error_report.connect()

        self.ch_sys_ext_error_report = PyDMChannel(
            address='ca://' + macros['P'] + ':Sys-ExtErrGroup-Mon',
            value_slot=self.get_sys_ext_error_report)
        self.ch_sys_ext_error_report.connect()

    # Warning
    def get_mod_ext_warn_report(self, value):
        self.lblModGenWarnExt.setText('\n'.join(
            get_report(value, EXTENDED_MAP, "Module extended")))

    def get_sys_ext_warn_report(self, value):
        self.lblSysGenWarnExt.setText('\n'.join(
            get_report(value, EXTENDED_MAP, "System extended")))

    def get_mod_std_warn_report(self, value):
        self.lblModGenWarnStd.setText('\n'.join(
            get_report(value, STANDARD_MAP, "Module standard")))

    def get_sys_std_warn_report(self, value):
        self.lblSysGenWarnStd.setText('\n'.join(
            get_report(value, STANDARD_MAP, "System standard")))

    # Error
    def get_mod_std_error_report(self, value):
        self.lblModGenErrStd.setText('\n'.join(
            get_report(value, STANDARD_MAP, "Module standard")))

    def get_mod_ext_error_report(self, value):
        self.lblModGenErrExt.setText('\n'.join(
            get_report(value, EXTENDED_MAP, "Module extended")))

    def get_sys_std_error_report(self, value):
        self.lblSysGenErrStd.setText('\n'.join(
            get_report(value, STANDARD_MAP, "System standard")))

    def get_sys_ext_error_report(self, value):
        self.lblSysGenErrExt.setText('\n'.join(
            get_report(value, EXTENDED_MAP, "System extended")))

    def setup_icons(self):
        REFRESH_ICON = IconFont().icon('refresh')
        # Overview
        self.btnSstate.setIcon(REFRESH_ICON)
        self.btnSCtrlMode.setIcon(REFRESH_ICON)
        self.btnMState.setIcon(REFRESH_ICON)
        self.btnMCtrlMode.setIcon(REFRESH_ICON)
        self.btnActIFace.setIcon(REFRESH_ICON)

        self.btnSave.setIcon(IconFont().icon('download'))
        self.btnClear.setIcon(IconFont().icon('eraser'))

        # Module
        self.btnMMV.setIcon(REFRESH_ICON)
        self.btnMMC.setIcon(REFRESH_ICON)
        self.btnMMinC.setIcon(REFRESH_ICON)
        self.btnMMP.setIcon(REFRESH_ICON)
        self.btnMMinV.setIcon(REFRESH_ICON)
        self.btnMMinP.setIcon(REFRESH_ICON)
        self.btnMRes.setIcon(REFRESH_ICON)
        self.btnNomDCV.setIcon(REFRESH_ICON)
        self.btnDCV.setIcon(REFRESH_ICON)
        self.btnMOV.setIcon(REFRESH_ICON)
        self.btnMOC.setIcon(REFRESH_ICON)
        self.btnMOP.setIcon(REFRESH_ICON)

        self.btnMVPRb.setIcon(REFRESH_ICON)
        self.btnMVLQ4Rb.setIcon(REFRESH_ICON)
        self.btnMCPRb.setIcon(REFRESH_ICON)
        self.btnMCQLRb.setIcon(REFRESH_ICON)
        self.btnMPPRb.setIcon(REFRESH_ICON)
        self.btnMPLQRb.setIcon(REFRESH_ICON)
        self.btnMRPRb.setIcon(REFRESH_ICON)

        # System
        self.PyDMPushButton_17.setIcon(REFRESH_ICON)
        self.PyDMPushButton_18.setIcon(REFRESH_ICON)
        self.PyDMPushButton_19.setIcon(REFRESH_ICON)
        self.PyDMPushButton_20.setIcon(REFRESH_ICON)
        self.PyDMPushButton_21.setIcon(REFRESH_ICON)
        self.PyDMPushButton_22.setIcon(REFRESH_ICON)
        self.PyDMPushButton_23.setIcon(REFRESH_ICON)
        self.PyDMPushButton_28.setIcon(REFRESH_ICON)
        self.PyDMPushButton_29.setIcon(REFRESH_ICON)
        self.PyDMPushButton_45.setIcon(REFRESH_ICON)
        self.PyDMPushButton_46.setIcon(REFRESH_ICON)
        self.PyDMPushButton_47.setIcon(REFRESH_ICON)
        self.PyDMPushButton_48.setIcon(REFRESH_ICON)
        self.PyDMPushButton_49.setIcon(REFRESH_ICON)
        self.PyDMPushButton_57.setIcon(REFRESH_ICON)
        self.PyDMPushButton_71.setIcon(REFRESH_ICON)
        self.PyDMPushButton_73.setIcon(REFRESH_ICON)

        # Advanced
        self.PyDMPushButton_41.setIcon(REFRESH_ICON)
        self.PyDMPushButton_50.setIcon(REFRESH_ICON)
        self.PyDMPushButton_51.setIcon(REFRESH_ICON)
        self.PyDMPushButton_52.setIcon(REFRESH_ICON)