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()
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()
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()
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")))
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()
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()
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()
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()
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]))
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()
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'
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)
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'
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
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()
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)
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]
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()
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()
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)