diff --git a/docs/source/upcoming_release_notes/1275-SmarAct_UI_Overhaul.rst b/docs/source/upcoming_release_notes/1275-SmarAct_UI_Overhaul.rst new file mode 100644 index 00000000000..0bda41a0d97 --- /dev/null +++ b/docs/source/upcoming_release_notes/1275-SmarAct_UI_Overhaul.rst @@ -0,0 +1,33 @@ +1275 SmarAct UI Overhaul +################# + +API Breaks +---------- +- N/A + +Library Features +---------------- +- Added SmarAct.detailed.ui screen +- Accompanying SmarAct.detailed.py screen to handle closed loop and picoscale +- Added SmarActTipTilt.embedded ui and py screens for operational support +- Designed and implemented huge improvements for all the SmarAct typhos screens, a monumental win for laser folk. + +Device Features +--------------- +- Added long_name fields to various SmarAct classes in pcsdevices.epics_motor + +New Devices +----------- +- N/A + +Bugfixes +-------- +- N/A + +Maintenance +----------- +- N/A + +Contributors +------------ +- aberges-SLAC diff --git a/pcdsdevices/epics_motor.py b/pcdsdevices/epics_motor.py index 1257c526019..ffbac373b85 100644 --- a/pcdsdevices/epics_motor.py +++ b/pcdsdevices/epics_motor.py @@ -1562,6 +1562,20 @@ class SmarActOpenLoop(Device): module_temp = Cpt(EpicsSignalRO, ':MODTEMP', kind='normal', doc='Temperature of the MCS2 Module in the rack') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Long name shenanigans + self.step_voltage.long_name = 'Step Voltage' + self.step_freq.long_name = 'Step Frequency' + self.jog_step_size.long_name = 'Jog Step Size' + self.jog_fwd.long_name = 'Jog Forward' + self.jog_rev.long_name = 'Jog Backward' + self.total_step_count.long_name = 'Total Step Count' + self.step_clear_cmd.long_name = 'Clear Step Count' + self.scan_move.long_name = 'Scan Voltage' + self.channel_temp.long_name = 'Channel Temp. (°C)' + self.module_temp.long_name = 'Module Temp. (°C)' + class SmarActTipTilt(Device): """ @@ -1652,6 +1666,41 @@ class SmarAct(EpicsMotorInterface): # useful open_loop = Cpt(SmarActOpenLoop, '', kind='omitted') + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Long name shenanigans + # Motor Record long name overrides + self.velocity.long_name = 'Velocity' + self.velocity_base.long_name = 'Velocity Min' + self.velocity_max.long_name = 'Velocity Max' + self.acceleration.long_name = 'Acceleration' + self.motor_stop.long_name = 'Stop Motor' + self.motor_is_moving.long_name = 'Actively Moving' + self.dial_position.long_name = 'Dial Position' + self.direction_of_travel.long_name = 'Direction of Travel' + self.home_forward.long_name = 'Home Forward' + self.home_reverse.long_name = 'Home Backward' + self.low_limit_switch.long_name = 'Low Limit Switch' + self.high_limit_switch.long_name = 'High Limit Switch' + self.high_limit_travel.long_name = 'High Limit Travel' + self.low_limit_travel.long_name = 'Low Limit Travel' + self.user_setpoint.long_name = 'Setpoint' + self.user_offset.long_name = 'User Offset' + self.user_offset_dir.long_name = 'User Offset Direction' + self.motor_egu.long_name = 'EGU' + self.description.long_name = 'Description' + # SmarAct specific long names + self.pos_type.long_name = 'Positioner Type' + self.needs_calib.long_name = 'Needs Calibration?' + self.do_calib.long_name = 'Calibrate' + self.log_scale_offset.long_name = 'Logical Scale Offset' + self.def_range_min.long_name = 'Default Range Min.' + self.def_range_max.long_name = 'Default Range Max' + self.log_scale_inv.long_name = 'Logical Scale Inversion' + self.dist_code_inv.long_name = 'Distance Code Inversion' + self.channel_temp.long_name = 'Channel Temp. (°C)' + self.module_temp.long_name = 'Module Temp. (°C)' + class SmarActEncodedTipTilt(Device): """ @@ -1718,6 +1767,18 @@ class SmarActPicoscale(SmarAct): def __init__(self, prefix, *, ioc_base, **kwargs): self._ioc_base = ioc_base super().__init__(prefix, **kwargs) + self.pico_adj_done.long_name = 'Auto Adjustment Done?' + self.pico_adj_state.long_name = 'Auto Adjustment State' + self.pico_curr_adj_prog.long_name = 'Auto Adjustment Progress' + self.pico_enable.long_name = 'PicoScale Enabled?' + self.pico_exists.long_name = 'PicoScale Exists?' + self.pico_name.long_name = 'PicoScale Name' + self.pico_present.long_name = 'PicoScale Present?' + self.pico_sig_qual.long_name = 'Signal Quality' + self.pico_valid.long_name = 'PicoScale Valid?' + self.pico_stable.long_name = 'PicoScale Stable?' + self.pico_wmax.long_name = 'Working distance (max)' + self.pico_wmin.long_name = 'Working distance (min)' class PI_M824(PVPositionerIsClose): diff --git a/pcdsdevices/ui/SmarAct.detailed.py b/pcdsdevices/ui/SmarAct.detailed.py new file mode 100644 index 00000000000..320cfa4a05a --- /dev/null +++ b/pcdsdevices/ui/SmarAct.detailed.py @@ -0,0 +1,378 @@ +from __future__ import annotations + +import logging +import re + +import pydm +from pydm import Display +from qtpy import QtCore, QtWidgets +from typhos import utils + +logger = logging.getLogger(__name__) + + +class _SmarActDetailedUI(QtWidgets.QWidget): + """Annotations helper for SmarAct.detailed.ui. Do not instantiate.""" + # Status bar + calibrated_bool: pydm.widgets.byte.PyDMByteIndicator + has_encoder_bool: pydm.widgets.byte.PyDMByteIndicator + referenced_bool: pydm.widgets.byte.PyDMByteIndicator + # Open-loop tab + jog_fwd_button: pydm.widgets.pushbutton.PyDMPushButton + jog_rev_button: pydm.widgets.pushbutton.PyDMPushButton + step_clear_cmd_button: pydm.widgets.pushbutton.PyDMPushButton + total_step_count_rbv: pydm.widgets.label.PyDMLabel + step_size_rbv: pydm.widgets.label.PyDMLabel + step_size_set: pydm.widgets.line_edit.PyDMLineEdit + step_volt_rbv: pydm.widgets.label.PyDMLabel + step_volt_set: pydm.widgets.line_edit.PyDMLineEdit + step_freq_rbv: pydm.widgets.label.PyDMLabel + step_freq_set: pydm.widgets.line_edit.PyDMLineEdit + scan_move_rbv: pydm.widgets.label.PyDMLabel + scan_move_set: pydm.widgets.line_edit.PyDMLineEdit + # Closed-loop tab + home_forward_button: pydm.widgets.pushbutton.PyDMPushButton + home_reverse_button: pydm.widgets.pushbutton.PyDMPushButton + curr_pos_rbv: pydm.widgets.label.PyDMLabel + curr_pos_set: pydm.widgets.line_edit.PyDMLineEdit + home_velocity_rbv: pydm.widgets.label.PyDMLabel + home_velocity_set: pydm.widgets.line_edit.PyDMLineEdit + velocity_rbv: pydm.widgets.label.PyDMLabel + velocity_set: pydm.widgets.line_edit.PyDMLineEdit + velocity_base_rbv: pydm.widgets.label.PyDMLabel + velocity_base_set: pydm.widgets.line_edit.PyDMLineEdit + velocity_max_rbv: pydm.widgets.label.PyDMLabel + velocity_max_set: pydm.widgets.line_edit.PyDMLineEdit + acceleration_rbv: pydm.widgets.label.PyDMLabel + acceleration_set: pydm.widgets.line_edit.PyDMLineEdit + closed_loop_freq_max_rbv: pydm.widgets.label.PyDMLabel + closed_loop_freq_max_set: pydm.widgets.line_edit.PyDMLineEdit + # Diagnostics tab + channel_temp_rbv: pydm.widgets.label.PyDMLabel + module_temp_rbv: pydm.widgets.label.PyDMLabel + motor_load_rbv: pydm.widgets.label.PyDMLabel + chan_error_rbv: pydm.widgets.label.PyDMLabel + diag_closed_loop_freq_max_rbv: pydm.widgets.label.PyDMLabel + diag_closed_loop_freq_avg_rbv: pydm.widgets.label.PyDMLabel + diag_closed_loop_freq_timebase_rbv: pydm.widgets.label.PyDMLabel + channel_states: pydm.widgets.byte.PyDMByteIndicator + # Config tab + desc_rbv: pydm.widgets.label.PyDMLabel + desc_set: pydm.widgets.line_edit.PyDMLineEdit + egu_rbv: pydm.widgets.label.PyDMLabel + egu_set: pydm.widgets.line_edit.PyDMLineEdit + pos_type_rbv: pydm.widgets.label.PyDMLabel + pos_type_set: pydm.widgets.line_edit.PyDMLineEdit + needs_calib_led: pydm.widgets.byte.PyDMByteIndicator + do_calib_button: pydm.widgets.pushbutton.PyDMPushButton + low_limit_rbv: pydm.widgets.label.PyDMLabel + low_limit_set: pydm.widgets.line_edit.PyDMLineEdit + high_limit_rbv: pydm.widgets.label.PyDMLabel + high_limit_set: pydm.widgets.line_edit.PyDMLineEdit + ttzv_rbv: pydm.widgets.label.PyDMLabel + ttzv_set: pydm.widgets.enum_combo_box.PyDMEnumComboBox + ttzv_threshold_rbv: pydm.widgets.label.PyDMLabel + ttzv_treshold_set: pydm.widgets.line_edit.PyDMLineEdit + log_scale_offset_rbv: pydm.widgets.label.PyDMLabel + log_scale_offset_set: pydm.widgets.line_edit.PyDMLineEdit + log_scale_inversion_rbv: pydm.widgets.label.PyDMLabel + log_scale_inversion_set: pydm.widgets.enum_combo_box.PyDMEnumComboBox + def_range_min_rbv: pydm.widgets.label.PyDMLabel + def_range_min_set: pydm.widgets.line_edit.PyDMLineEdit + def_range_max_rbv: pydm.widgets.label.PyDMLabel + def_range_max_set: pydm.widgets.line_edit.PyDMLineEdit + dist_code_inversion_rbv: pydm.widgets.label.PyDMLabel + dist_code_inversion_set: pydm.widgets.enum_combo_box.PyDMEnumComboBox + # Misc + controls_tabs: QtWidgets.QTabWidget + pico_adjustment_prog_bar: QtWidgets.QProgressBar + + +class SmarActDetailedWidget(Display, utils.TyphosBase): + """ + Custom widget for managing the SmarAct detailed screen + """ + ui: _SmarActDetailedUI + + def __init__(self, parent=None, ui_filename='SmarAct.detailed.ui', **kwargs): + super().__init__(parent=parent, ui_filename=ui_filename) + + @property + def device(self): + """The associated device.""" + try: + return self.devices[0] + except Exception: + ... + + def add_device(self, device): + """Typhos hook for adding a new device.""" + super().add_device(device) + # Gotta make sure to destroy this screen if you were handed an empty device + if device is None: + self.ui.device_name_label.setText("(no device)") + return + + self.post_typhos_init() + + def post_typhos_init(self): + """ + Once typhos has relinked the device and parent widget, we need to clean + Up some of the signals and maybe add new widgets to the display. + Add any other init-esque shenanigans you need here. + """ + self.fix_pvs() + self.maybe_add_pico() + self.add_tool_tips() + + def fix_pvs(self): + """ + Fix all the channel access and signal linking to various pydm objects in the screen, + since the macros aren't expanded when the UI is initialized. + """ + object_names = self.find_pydm_names() + for obj in object_names: + _widget = getattr(self, obj) + _channel = getattr(_widget, 'channel') + if not _channel: + _channel = '' + if re.search(r'\{prefix\}', _channel): + _widget.set_channel(_channel.replace('${prefix}', self.device.prefix)) + + # Now let's manually add the funky egu and description signals here to avoid terminal spam + self.desc_set.set_channel(f'sig://{self.device.name}_description') + self.egu_set.set_channel(f'sig://{self.device.name}_motor_egu') + + def add_tool_tips(self): + """ + Add hutch_python tooltips to label widgets, if they exist. + """ + _signals = list(self.device.component_names) + # Let's deal with the subdevice signals afterwards + _signals.remove('open_loop') + _open_loop_signals = list(self.device.open_loop.component_names) + + def _get_tooltip(device: any, signal: str) -> str: + """ + Get the tooltip from the Ophyd.Device signal, if it exists. + """ + _dotted_name = getattr(device, signal).dotted_name + try: + _tooltip = getattr(type(device), signal).doc + except AttributeError: + _tooltip = '' + _tooltip = (_dotted_name + '
' + + round(1.75*len(_dotted_name))*'-' + '
' + + _tooltip) + return _tooltip + + for sig in (_signals + _open_loop_signals): + # Assume the QLabel's object name is standard form + _label_name = sig + '_label' + _device = self.device + # open_loop is a component device, nests differently + if sig in _open_loop_signals: + _device = self.device.open_loop + # Now let's add the tooltip for widgets that exist + if hasattr(self, _label_name): + _tooltip = _get_tooltip(_device, sig) + getattr(self, _label_name).setToolTip(_tooltip) + + def find_pydm_names(self) -> list[str]: + """ + Find the object names for all PyDM objects using findChildren + + Returns + ------------ + result : list[str] + 1D list of object names + """ + _button = pydm.widgets.pushbutton.PyDMPushButton + _byte = pydm.widgets.byte.PyDMByteIndicator + _label = pydm.widgets.label.PyDMLabel + _line_edit = pydm.widgets.line_edit.PyDMLineEdit + _combo_box = pydm.widgets.enum_combo_box.PyDMEnumComboBox + + result = [] + + for obj_type in [_button, _byte, _label, _line_edit, _combo_box]: + result += [obj.objectName() for obj in self.findChildren(obj_type)] + + # get rid of the objects from the embedded TyphosPositioner widget + _omit = ['low_limit_switch', 'moving_indicator', 'high_limit_switch', + 'low_limit', 'user_readback', 'error_label', + 'high_limit', 'user_setpoint'] + + return [obj for obj in result if obj not in _omit] + + def maybe_add_pico(self): + """ + Maybe add the picoscale tab and signals _if_ they exist. + Sadly involves a lot of manual signal management that would normally be + handled in TyphosSignalPanel. + """ + if hasattr(self.device, 'pico_exists'): + if hasattr(self, 'picoscale'): + # Don't add infite tabs please :] + return + # Only start this timer if PicoScale exists + self.adj_prog_timer = QtCore.QTimer(parent=self) + self.adj_prog_timer.timeout.connect(self.update_adj_prog) + self.adj_prog_timer.setInterval(1000) + self.adj_prog_timer.start() + self._last_adj_prog = 0 + # Grab all the pico related signals + _pico_signals = [sig for sig in self.device.component_names if 'pico' in sig] + self.pico_signal_dict = {} + # Grab any usable info from the HAPPI device and rebuild the JSON + for sig in _pico_signals: + _d = {} + _sig = getattr(self.device, sig) + _d['name'] = sig + if hasattr(_sig, 'pvname'): + _d['read_pv'] = _sig.pvname + if hasattr(_sig, 'setpoint_pvname'): + _d['write_pv'] = _sig.setpoint_pvname + if hasattr(_sig, 'long_name'): + _d['label'] = _sig.long_name + self.pico_signal_dict[sig] = _d + + # Now let's add some metadata for customizing the display + _byte_sigs = ['pico_present', 'pico_exists', 'pico_valid', 'pico_enable', 'pico_stable', 'pico_adj_done'] + _enum_sigs = ['pico_adj_state'] + for sig in _byte_sigs: + self.pico_signal_dict[sig]['meta'] = 'byte' + for sig in _enum_sigs: + self.pico_signal_dict[sig]['meta'] = 'enum' + # Some exceptions that prove the rule + self.pico_signal_dict['pico_curr_adj_prog']['meta'] = 'progressbar' + + # Last but not least, let me manually dictate the signal order + _rows = ['pico_name', 'pico_present', 'pico_exists', 'pico_valid', 'pico_enable', + 'pico_stable', 'pico_adj_done', 'pico_wmin', 'pico_wmax', 'pico_sig_qual', + 'pico_adj_state', 'pico_curr_adj_prog'] + for pos, item in enumerate(_rows): + self.pico_signal_dict[item]['row'] = pos + + # Generate the tab + self.generate_pico_tab() + + def generate_pico_tab(self): + """ + Gather all the pico related signals from the device and format the signal panel. + A lot of this is recycled from TyphosSignalPanel with some alterations. + """ + + def add_signal_row(grid: QtWidgets.QGridLayout, signal_dict: dict): + """ + Similar to the typhos method but allows for some manual overhauling. + Bundles an EpicsSignal into a row of (Label, RBV widget, Setpoint widget) + and adds it to a grid layout. + + Parameters + ------------ + grid: QtWidgets.QGridLayout + The layout to add the signals to + signal_dict: dict + A JSON-like dictionary of device signals, names, and metadata + + Returns + ------------ + None + """ + # Make the row label first + row_label = QtWidgets.QLabel() + row_label.setText(signal_dict['label']) + # Set the object name as an attr for later shenanigans + setattr(self, signal_dict['name'] + '_label', row_label) + + # Then set the RBV widget + rbv_widget = pydm.widgets.label.PyDMLabel() + rbv_widget.setAlignment(QtCore.Qt.AlignCenter) + # Unless they're special + if 'meta' in signal_dict: + if signal_dict['meta'] == 'byte': + rbv_widget = pydm.widgets.byte.PyDMByteIndicator() + rbv_widget.circles = 1 + rbv_widget.showLabels = 0 + if signal_dict['meta'] == 'progressbar': + rbv_widget = QtWidgets.QProgressBar() + rbv_widget.setRange(0, 100) + rbv_widget.hide() + row_label.hide() + + # Add the widget as an attr for later shenanigans + setattr(self, signal_dict['name'] + '_rbv', rbv_widget) + + if hasattr(rbv_widget, 'set_channel'): + rbv_widget.set_channel(signal_dict['read_pv']) + + # Create setpoint widgets if they exist + if 'write_pv' in signal_dict: + # handle tricky enums + if 'meta' in signal_dict and signal_dict['meta'] == 'enum': + setpoint_widget = pydm.widgets.enum_combo_box.PyDMEnumComboBox() + setpoint_widget.set_channel(signal_dict['write_pv']) + # lean on HAPPI if possible + _signal_metadata = getattr(self.device, signal_dict['name']).metadata + if 'enum_strs' in _signal_metadata: + for item in _signal_metadata['enum_strs']: + setpoint_widget.addItem(item) + # Otherwise just give it some defaults + else: + for item in ['Disable', 'Enable']: + setpoint_widget.addItem(item) + # Default line edits + else: + setpoint_widget = pydm.widgets.line_edit.PyDMLineEdit() + setpoint_widget.set_channel(signal_dict['write_pv']) + + # Add the widget as an attr for later shenanigans + setattr(self, signal_dict['name'] + '_set', setpoint_widget) + + # Now finally add these signals + grid.addWidget(row_label, signal_dict['row'], 0) + grid.addWidget( + getattr(self, signal_dict['name'] + '_rbv'), signal_dict['row'], 1) + if 'write_pv' in signal_dict: + grid.addWidget(setpoint_widget, signal_dict['row'], 2) + + # Make the tab + self.picoscale = QtWidgets.QWidget() + # Format the scroll area + pico_scroll_area = QtWidgets.QScrollArea() + pico_scroll_area.setFrameStyle(QtWidgets.QFrame.NoFrame) + pico_scroll_area.setWidget(self.picoscale) + # Format the tab layout + self.picoscale.layout = QtWidgets.QVBoxLayout() + # Add signals + self.picoscale.panel = QtWidgets.QGridLayout() + for _d in self.pico_signal_dict: + add_signal_row(self.picoscale.panel, self.pico_signal_dict[_d]) + # Set the layout and add to the tab widget + self.picoscale.setLayout(self.picoscale.panel) + self.controls_tabs.addTab(self.picoscale, 'Picoscale') + + def update_adj_prog(self): + """ + Function for displaying and updating the QProgressBar when the PicoScale + Auto-adjustment is taking place. + """ + # Check to see if the system can even enter adjustment and display the bar + if self.device.pico_adj_state.get(): + self.adj_prog_timer.start() + if not self._last_adj_prog: + self.pico_curr_adj_prog_rbv.show() + self.pico_curr_adj_prog_label.show() + # Then check to see if the progress has updated + if self._last_adj_prog < self.device.pico_curr_adj_prog.get(): + self.pico_curr_adj_prog_rbv.setValue(self.device.pico_curr_adj_prog.get()) + # Now finally check to see if it is complete. Since the threaded behavior in the IOC + # toggles the adjustment PV for ALL stages on a PicoScale when adjustment completes, + # check for that state change too. + if self.device.pico_adj_done.get() or not self.device.pico_adj_state.get(): + self.pico_curr_adj_prog_rbv.hide() + self.pico_curr_adj_prog_rbv.reset() + self.pico_curr_adj_prog_label.hide() + # reset the timer too in case you need re-adjust later on + self.adj_prog_timer.start() diff --git a/pcdsdevices/ui/SmarAct.detailed.ui b/pcdsdevices/ui/SmarAct.detailed.ui new file mode 100644 index 00000000000..a2f4af7d26f --- /dev/null +++ b/pcdsdevices/ui/SmarAct.detailed.ui @@ -0,0 +1,4708 @@ + + + Form + + + + 0 + 0 + 543 + 577 + + + + + 0 + 0 + + + + + 543 + 577 + + + + Form + + + + 0 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + 0 + + + + + + + + + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + + + + false + + + true + + + + + + ca://${prefix}:STATE_RBV.B5 + + + true + + + false + + + false + + + 1 + + + 0 + + + + Has Encoder + + + + + + + + + 0 + 0 + + + + + + + false + + + true + + + + + + ca://${prefix}:STATE_RBV.B6 + + + true + + + false + + + false + + + 1 + + + 0 + + + + Calibrated + + + + + + + + + 0 + 0 + + + + + + + false + + + true + + + + + + ca://${prefix}:STATE_RBV.B7 + + + true + + + false + + + false + + + 1 + + + 0 + + + + Homed + + + + + + + + + + + 0 + 0 + + + + + 0 + 200 + + + + + 16777215 + 150 + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 15 + + + + + + + + true + + + + 0 + 0 + + + + + 0 + 200 + + + + 0 + + + + + 0 + 0 + + + + Open-loop + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + QFrame::StyledPanel + + + QFrame::Sunken + + + 1 + + + Qt::ScrollBarAsNeeded + + + QAbstractScrollArea::AdjustIgnored + + + true + + + Qt::AlignCenter + + + + + 0 + 0 + 509 + 300 + + + + + 0 + 0 + + + + + 0 + 300 + + + + + + + + + + 0 + 0 + + + + Jog + + + Qt::AlignCenter + + + + + + + QLayout::SetDefaultConstraint + + + + + Forward + + + + + + + + 0 + 0 + + + + + 100 + 25 + + + + + + + + + + + 25 + 25 + + + + false + + + false + + + + + + false + + + ca://${prefix}:STEP_FORWARD + + + caret-square-right + + + + 90 + 90 + 90 + + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 5 + 25 + + + + + + + + + 0 + 0 + + + + + 100 + 25 + + + + + + + + + + + 25 + 25 + + + + false + + + false + + + + + + false + + + ca://${prefix}:STEP_REVERSE.PROC + + + caret-square-left + + + + 90 + 90 + 90 + + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + Reverse + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + + QLayout::SetDefaultConstraint + + + 6 + + + + + Scan Voltage + + + Qt::AlignCenter + + + + + + + + + + 0 + 0 + + + + + 55 + 31 + + + + + + + + 25 + 25 + + + + false + + + false + + + + + + false + + + ca://${prefix}:CLEAR_COUNT.PROC + + + eraser + + + + 255 + 85 + 255 + + + + false + + + + + + + + + true + + + Are you sure you want to clear the total step count? + + + 1 + + + None + + + false + + + false + + + + + + + Clear Count + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:STEP_VOLTAGE + + + + + + + Step Frequency + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}:STEP_FREQ + + + false + + + + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}:STEP_COUNT + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 5 + 20 + + + + + + + + Step Size + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:SCAN_MOVE + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + 16777215 + 1677215 + + + + Total Step Count + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:STEP_COUNT + + + + + + + Step Voltage + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:STEP_FREQ + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}:STEP_VOLTAGE + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}:TOTAL_STEP_COUNT + + + false + + + + + + + + 0 + 0 + + + + + 50 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Scan Voltage as a % expressed as Uint16 (i.e. 0-65535) + + + ca://${prefix}:SCAN_POS + + + false + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 5 + + + + + + + + + + + + + + 0 + 0 + + + + Closed-loop + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + + 0 + 0 + + + + true + + + Qt::AlignCenter + + + + + 0 + 0 + 527 + 400 + + + + + 0 + 0 + + + + + 0 + 400 + + + + + + + + + + 0 + 25 + + + + Home + + + Qt::AlignCenter + + + + + + + QLayout::SetNoConstraint + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 5 + 25 + + + + + + + + + 0 + 0 + + + + + 75 + 25 + + + + + + + + 25 + 25 + + + + false + + + false + + + + + + false + + + ca://${prefix}.HOMR + + + fast-backward + + + false + + + + + + + + + true + + + Are you sure you want to proceed? This will move your stage until it reaches the next reference position + + + 1 + + + None + + + false + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Reverse + + + + + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Forward + + + + + + + + 0 + 0 + + + + + 75 + 25 + + + + + + + + 25 + 25 + + + + false + + + false + + + + + + false + + + ca://${prefix}.HOMF + + + fast-forward + + + false + + + + + + + + + true + + + Are you sure you want to proceed? This will move your stage until it reaches the next reference position + + + 1 + + + None + + + false + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + + + + + 0 + 25 + + + + Acceleration + + + Qt::AlignCenter + + + + + + + + 0 + 25 + + + + Velocity + + + Qt::AlignCenter + + + + + + + + 0 + 25 + + + + Max Velocity + + + Qt::AlignCenter + + + + + + + + 0 + 25 + + + + Current Position + + + Qt::AlignCenter + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.VELO + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.HVEL + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + Velocity for closed loop moves + + + ca://${prefix}.VELO + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Closed Loop Frequency Max + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Maximum closed loop frequency in Hz. Sets an upper threshold on the velocity of a stage. Setting > 5 kHz can affect piezo health + + + ca://${prefix}:MAX_CLF_RBV + + + false + + + + + + + + 0 + 25 + + + + Home Velocity + + + Qt::AlignCenter + + + + + + + + 0 + 25 + + + + Base Velocity + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + Base (minimum) velocity + + + ca://${prefix}.VBAS + + + false + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.VMAX + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + + + + ca://${prefix}.RBV + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + Velocity of Homing sequence + + + ca://${prefix}.HVEL + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Acceleration of the closed loop move + + + ca://${prefix}.ACCL + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 5 + 20 + + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.VBAS + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.VAL + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:MAX_CLF + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.ACCL + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + Maximum velocity + + + ca://${prefix}.VMAX + + + false + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + 0 + 0 + + + + Diagnostics + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + true + + + + + 0 + 0 + 655 + 250 + + + + + 0 + 0 + + + + + 0 + 250 + + + + + + + + 0 + 0 + + + + + 150 + 200 + + + + true + + + + + 0 + 0 + 150 + 600 + + + + + 0 + 0 + + + + + 150 + 600 + + + + + + + + 0 + 0 + + + + + 125 + 0 + + + + + + + false + + + true + + + + + + ca://${prefix}:STATE_RBV + + + true + + + false + + + true + + + 21 + + + 0 + + + + Actively Moving + Closed Loop Active + Calibrating + Referencing + Move Delayed + Sensor Present + Is Calibrated + Is Referenced + End Stop Reached + Range Limit Reached + Following Limit Reached + Movement Failed + Is Streaming + Positioner OVLD + Over temperature + Reference Mark + Is Phased + Positioner Fault + Amplifier Enabled + In Position + Brake Enabled + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 2 + + + + + + 0 + 0 + + + + Motor Load + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Avg CLF measured in 2 s interval + + + ca://${prefix}:DIAG_CLF_AVG + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Timebase for measuring CLF diagnostics + + + DCLF Timebase + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + thermal load as a % + + + ca://${prefix}:MOTOR_LOAD + + + false + + + + + + + + 0 + 0 + + + + Channel Temp. + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Max CLF measured in 2 s interval + + + DCLF Max + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Max CLF measured in 2 s interval + + + ca://${prefix}:DIAG_CLF_MAX + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Most recent channel error + + + ca://${prefix}:CHAN_ERR + + + false + + + PyDMLabel::Hex + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Timebase for measuring CLF diagnostics + + + ca://${prefix}:DIAG_CLF_TIMEBASE_RBV + + + false + + + + + + + + 0 + 0 + + + + Module Temp. + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + + + + ca://${prefix}:CHANTEMP + + + false + + + + + + + + 0 + 0 + + + + Channel Error + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Avg CLF measured in 2 s interval + + + DCLF Avg + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + + + + ca://${prefix}:MODTEMP + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Timebase for measuring CLF diagnostics + + + false + + + ca://${prefix}:DIAG_CLF_TIMEBASE + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Celsius + + + + + + + Celsius + + + + + + + % + + + + + + + + + + + + + + + 0 + 0 + + + + Configuration + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + true + + + + + 0 + 0 + 509 + 478 + + + + + 0 + 0 + + + + + 0 + 350 + + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + 6 + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Closed-loop ONLY: Whether TTZV is enabled or not + + + ca://${prefix}:TTZV_MODE_RBV + + + false + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 13 + 20 + + + + + + + + + 0 + 25 + + + + Description + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Default minimum range for logical scale. Non-volatile. + + + Default Range Min + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:SET_LSCO + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + TTZV Threshold + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Target to Zero Voltage + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 25 + + + + PTYPE + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 80 + 20 + + + + + + + + + 0 + 0 + + + + + 55 + 25 + + + + + 9 + + + + + + + false + + + true + + + + + + ca://${prefix}:NEED_CALIB + + + + 255 + 255 + 0 + + + + Qt::Horizontal + + + true + + + false + + + true + + + 1 + + + 0 + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 90 + 20 + + + + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.LLM + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}.LLM + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}.EGU + + + false + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}.HLM + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Sets the distance code inversion flag. Non-volatile. + + + ca://${prefix}:DCIN_RBV + + + false + + + + + + + + 0 + 0 + + + + + 100 + 25 + + + + + + + false + + + true + + + Closed-loop ONLY: toggle TTZV state + + + false + + + ca://${prefix}:TTZV_MODE + + + + + + + + 0 + 25 + + + + High Limit + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Invert the logical scale. Must home after changing to take effect. Non-volatile. + + + ca://${prefix}:LSCI_RBV + + + false + + + + + + + + 0 + 25 + + + + Low Limit + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Sets the distance code inversion flag. Non-volatile. + + + Distance Code Inversion + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + Shift the logical scale in mm. Non-volatile. + + + ca://${prefix}:LSCO + + + false + + + + + + + + 0 + 0 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + Current TTZV position threshold in encoder ticks (pm or ndeg) + + + ca://${prefix}:TTZ_THRESHOLD_RBV + + + false + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:SET_DRMIN + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Default Range Max + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 100 + 25 + + + + + + + false + + + true + + + + + + false + + + ca://${prefix}:SET_DCIN + + + + + + + + 0 + 25 + + + + EGU + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Default maximum range for logical scale. Non-volatile. + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + Default maximum range for logical scale. Non-volatile. + + + ca://${prefix}:DRMAX + + + false + + + + + + + + 0 + 25 + + + + + 10 + 75 + true + + + + + + + Calibrate + + + + 25 + 25 + + + + false + + + false + + + + + + false + + + ca://${prefix}:DO_CALIB.PROC + + + sync + + + + 0 + 85 + 255 + + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + 0 + 0 + + + + + 100 + 25 + + + + + + + false + + + true + + + + + + false + + + ca://${prefix}:SET_LSCI + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}:PTYPE_RBV + + + false + + + + + + + + 0 + 25 + + + + Invert the logical scale. Must home after changing to take effect. Non-volatile. + + + Logical Scale Inversion + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + Shift the logical scale in mm. Non-volatile. + + + false + + + Logical Scale Offset + + + false + + + Qt::AlignCenter + + + true + + + 0 + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:PTYPE + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + TTZV position threshold in encoder ticks (pm or ndeg). Range: 0 - 1E7 + + + false + + + ca://${prefix}:TTZ_THRESHOLD_RBV + + + + + + + + 0 + 25 + + + + Needs Calibration + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}.DESC + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + true + + + true + + + false + + + true + + + Default minimum range for logical scale. Non-volatile. + + + ca://${prefix}:DRMIN + + + false + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + ca://${prefix}.HLM + + + false + + + + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + 0 + + + false + + + true + + + false + + + true + + + + + + false + + + ca://${prefix}:SET_DRMAX + + + + + + + + + Qt::Vertical + + + + 453 + 37 + + + + + + + + + + + + + + + + + PyDMLabel + QLabel +
pydm.widgets.label
+
+ + PyDMByteIndicator + QWidget +
pydm.widgets.byte
+
+ + PyDMEnumComboBox + QComboBox +
pydm.widgets.enum_combo_box
+
+ + PyDMLineEdit + QLineEdit +
pydm.widgets.line_edit
+
+ + PyDMPushButton + QPushButton +
pydm.widgets.pushbutton
+
+ + TyphosDisplayTitle + QFrame +
typhos.display
+
+ + TyphosPositionerWidget + QWidget +
typhos.positioner
+
+
+ + +
diff --git a/pcdsdevices/ui/SmarActTipTilt.embedded.py b/pcdsdevices/ui/SmarActTipTilt.embedded.py new file mode 100644 index 00000000000..f417ca5fb65 --- /dev/null +++ b/pcdsdevices/ui/SmarActTipTilt.embedded.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import logging +from typing import Optional + +import ophyd +import pydm +from pydm import Display +from qtpy import QtCore, QtGui, QtWidgets +from typhos import utils +from typhos.panel import SignalOrder, TyphosSignalPanel + +logger = logging.getLogger(__name__) + + +class _SmarActTipTiltEmbeddedUI(QtWidgets.QWidget): + """Annotations helper for SmarActTipTilt.embedded.ui. Do not instantiate.""" + dpad_label: QtWidgets.QLabel + tip_forward: pydm.widgets.pushbutton.PyDMPushButton + tip_reverse: pydm.widgets.pushbutton.PyDMPushButton + tip_step_count: pydm.widgets.label.PyDMLabel + tilt_forward: pydm.widgets.pushbutton.PyDMPushButton + tilt_reverse: pydm.widgets.pushbutton.PyDMPushButton + tilt_step_count: pydm.widgets.label.PyDMLabel + settings_button: pydm.widgets.pushbutton.PyDMPushButton + + +class SmarActTipTiltWidget(Display, utils.TyphosBase): + """Custom widget for controlling a tip-tilt with d-pad buttons""" + ui: _SmarActTipTiltEmbeddedUI + + def __init__(self, parent=None, ui_filename='SmarActTipTilt.embedded.ui', **kwargs,): + super().__init__(parent=parent, ui_filename=ui_filename) + + self._omit_names = ['jog_fwd', 'jog_rev'] + self.ui.extended_signal_panel = None + + self.ui.settings_button.clicked.connect(self._expand_layout) + + @property + def device(self): + """The associated device.""" + try: + return self.devices[0] + except Exception: + ... + + def add_device(self, device): + """Typhos hook for adding a new device.""" + super().add_device(device) + # Gotta make sure to destroy this screen if you were handed an empty device + if device is None: + self.ui.device_name_label.setText("(no device)") + if self.ui.extended_signal_panel is not None: + self.layout().removeWidget(self.ui.extended_signal_panel) + self.ui.extended_signal_panel.destroyLater() + self.ui.extended_signal_panel = None + return + + # Can't do this during init because the device doesn't exist yet! + self.update_pvs() + + @QtCore.Property("QStringList") + def omitNames(self) -> list[str]: + """Get or set the list of names to omit in the expanded signal panel.""" + return self._omit_names + + @omitNames.setter + def omitNames(self, omit_names: list[str]) -> None: + if omit_names == self._omit_names: + return + + self._omit_names = list(omit_names or []) + if self.ui.extended_signal_panel is not None: + self.ui.extended_signal_panel.omitNames = self._omit_names + + def _create_signal_panel(self) -> Optional[TyphosSignalPanel]: + """Create the 'extended' TyphosSignalPanel for the device.""" + if self.device is None: + return None + + return SettingsPanel(mirror=self, parent=self, flags=QtCore.Qt.Window) + + def _expand_layout(self) -> None: + """Toggle the expansion of the signal panel.""" + if self.ui.extended_signal_panel is None: + self.ui.extended_signal_panel = self._create_signal_panel() + if self.ui.extended_signal_panel is None: + return + to_show = True + else: + to_show = not self.ui.extended_signal_panel.isVisible() + + self.ui.extended_signal_panel.setVisible(to_show) + + def update_pvs(self): + """ + Once we have the tip-tilt device, set the TIP and TILT channels to + the buttons and labels. + """ + + if self.device is None: + print('No device set!') + return + + self.ui.tip_forward.set_channel(f'ca://{self.device.tip.prefix}:STEP_FORWARD.PROC') + self.ui.tip_reverse.set_channel(f'ca://{self.device.tip.prefix}:STEP_REVERSE.PROC') + self.ui.tip_step_count.set_channel(f'ca://{self.device.tip.prefix}:TOTAL_STEP_COUNT') + self.ui.tilt_forward.set_channel(f'ca://{self.device.tilt.prefix}:STEP_FORWARD.PROC') + self.ui.tilt_reverse.set_channel(f'ca://{self.device.tilt.prefix}:STEP_REVERSE.PROC') + self.ui.tilt_step_count.set_channel(f'ca://{self.device.tilt.prefix}:TOTAL_STEP_COUNT') + + def get_names_to_omit(self) -> list[str]: + """ + Get a list of signal names to omit in the extended panel. + + Returns + ------- + list[str] + """ + device: Optional[ophyd.Device] = self.device + if device is None: + return [] + + to_omit = set(['jog_fwd', 'jog_rev']) + + # TODO: move these to a Qt designable property + for name in self.omitNames: + to_omit.add(name) + + if device.name in to_omit: + # Don't let the renamed position signal stop us from showing any + # signals: + to_omit.remove(device.name) + return sorted(to_omit) + + +class _StageSettingsUI(): + """helper for the stages basic settings. Do not instantiate.""" + tip_label: QtWidgets.QLabel + tilt_label: QtWidgets.QLabel + step_size_label: QtWidgets.QLabel + tip_step_size_rbv: pydm.widgets.label.PyDMLabel + tip_step_size_set: pydm.widgets.line_edit.PyDMLineEdit + tilt_step_size_rbv: pydm.widgets.label.PyDMLabel + tilt_step_size_set: pydm.widgets.line_edit.PyDMLineEdit + step_count_label: QtWidgets.QLabel + step_count_rbv: pydm.widgets.label.PyDMLabel + step_volt_label: QtWidgets.QLabel + tip_step_volt_rbv: pydm.widgets.label.PyDMLabel + tip_step_volt_set: pydm.widgets.line_edit.PyDMLineEdit + tilt_step_volt_rbv: pydm.widgets.label.PyDMLabel + tilt_step_volt_set: pydm.widgets.label.PyDMLineEdit + + +class SettingsPanel(QtWidgets.QWidget): + """ + Container class for basic settings that accompany open-loop movement for SmarAct tip-tilts. Largely lifted from TyphosPositionerRow + """ + mirror: SmarActTipTiltWidget + resize_timer: QtCore.QTimer + + def __init__(self, mirror: SmarActTipTiltWidget, parent: QtWidgets.QWidget | None = None, **kwargs): + super().__init__(parent=parent, **kwargs) + + self.mirror = mirror + # Make the subdevice labels + self.tip_label = QtWidgets.QLabel() + self.format_label(self.tip_label, 'Tip') + + self.tilt_label = QtWidgets.QLabel() + self.format_label(self.tilt_label, 'Tilt') + + # Then add panels, widgets, devices, and scroll areas + self.tip_panel = TyphosSignalPanel() + self.tip_panel.sortBy = SignalOrder.byName + self.tip_panel.omitNames = mirror.get_names_to_omit() + self.tip_panel.add_device(mirror.device.tip) + self.tip_scroll_area = QtWidgets.QScrollArea() + self.format_scroll_area(self.tip_panel, self.tip_scroll_area) + + self.tilt_panel = TyphosSignalPanel() + self.tilt_panel.sortBy = SignalOrder.byName + self.tilt_panel.omitNames = mirror.get_names_to_omit() + self.tilt_panel.add_device(mirror.device.tilt) + self.tilt_scroll_area = QtWidgets.QScrollArea() + self.format_scroll_area(self.tilt_panel, self.tilt_scroll_area) + + # Then add them to the layout! + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.tip_label) + layout.addWidget(self.tip_scroll_area) + layout.addWidget(self.tilt_label) + layout.addWidget(self.tilt_scroll_area) + + # Set the layout and then do some resize timer set-up + self.setLayout(layout) + self.resize_timer = QtCore.QTimer(parent=self) + self.resize_timer.timeout.connect(self.fix_scroll_size) + self.resize_timer.setInterval(1) + self.resize_timer.setSingleShot(True) + + # Capture the initial min widths + for panel in [self.tip_panel, self.tilt_panel]: + panel.original_panel_min_width = panel.minimumWidth() + panel.last_resize_width = 0 + + self.resize_done = False + + def format_label(self, label, label_text): + """Create and format the text for each subdevice""" + _label = label + _label.setText(label_text) + _font = _label.font() + _font.setPointSize(_font.pointSize() + 4) + _label.setFont(_font) + _label.setMaximumHeight( + QtGui.QFontMetrics(_font).boundingRect(_label.text()).height() + ) + + def format_scroll_area(self, panel, panel_scroll_area): + """Format the scroll area for each subdevice""" + panel_scroll_area.setFrameStyle(QtWidgets.QFrame.NoFrame) + panel_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + panel_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + panel_scroll_area.setWidgetResizable(True) + panel_scroll_area.setWidget(panel) + + def hideEvent(self, event: QtGui.QHideEvent): + """ + After hide, update button text, even if we were hidden via clicking the "x". + Shamelessly stolen from TyphosPositionerRow. + """ + button = self.mirror.ui.settings_button + button.PyDMIcon = 'SP_ToolBarHorizontalExtensionButton' + return super().hideEvent(event) + + def showEvent(self, event: QtGui.QShowEvent): + """ + Before show, update button text and move window to just under button. + Shamelessly stolen from TyphosPositionerRow. + """ + button = self.mirror.ui.settings_button + button.PyDMIcon = 'SP_ToolBarVerticalExtensionButton' + offset = button.mapToGlobal(QtCore.QPoint(0, 0)) + self.move( + button.mapToGlobal( + QtCore.QPoint( + button.pos().x() + button.width(), + button.pos().y() + button.height() + + self.style().pixelMetric(QtWidgets.QStyle.PM_TitleBarHeight) + - offset.y(), + ) + ) + ) + if not self.resize_done: + self.resize_timer.start() + return super().showEvent(event) + + def fix_scroll_size(self): + """ + Slot that ensures the panel gets enough space in the scroll area. + + The panel, when created, has smaller sizing information than it does + a few moments after being shown for the first time. This might + update several times before settling down. + + We want to watch for this resize and set the scroll area width such + that there's enough room to see the widget at its minimum size. + -------------------------------------------------------------------- + Also shamelessly stolen from TyphosPositionerRow + """ + if (self.tip_panel.minimumWidth() <= self.tip_panel.original_panel_min_width or + self.tilt_panel.minimumWidth() <= self.tilt_panel.original_panel_min_width): + # No change + self.resize_timer.start() + return + elif (self.tip_panel.last_resize_width != self.tip_panel.minimumWidth() or + self.tilt_panel.last_resize_width != self.tilt_panel.minimumWidth()): + # We are not stable yet! + self.tip_panel.last_resize_width = self.tip_panel.minimumWidth() + self.tilt_panel.last_resize_width = self.tilt_panel.minimumWidth() + self.resize_timer.start() + return + + def make_space(self, scroll_area, panel): + """Generalize fixing the dimensions of the scroll areas for multiple panels""" + scroll_area.setMinimumWidth( + panel.minimumWidth() + + self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent) + + 2 * self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollView_ScrollBarOverlap) + + 2 * self.style().pixelMetric(QtWidgets.QStyle.PM_ScrollView_ScrollBarSpacing) + ) + + make_space(self, self.tip_scroll_area, self.tip_panel) + make_space(self, self.tilt_scroll_area, self.tilt_panel) + + self.resize_done = True diff --git a/pcdsdevices/ui/SmarActTipTilt.embedded.ui b/pcdsdevices/ui/SmarActTipTilt.embedded.ui new file mode 100644 index 00000000000..72b73a712ca --- /dev/null +++ b/pcdsdevices/ui/SmarActTipTilt.embedded.ui @@ -0,0 +1,580 @@ + + + Form + + + + 0 + 0 + 320 + 323 + + + + + 0 + 0 + + + + Form + + + + QLayout::SetMinimumSize + + + + + + 0 + 0 + + + + + + + + + + + + + Step Count + + + + + + + + 0 + 0 + + + + Tilt + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Tip + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + Qt::AlignCenter + + + + + + + + + + 0 + 0 + + + + + + + + + + 0 + 0 + + + + + 35 + 35 + + + + + + + + 30 + 30 + + + + false + + + false + + + + + + false + + + + + + caret-right + + + + 0 + 0 + 0 + + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + 0 + 0 + + + + + 30 + 30 + + + + + 12 + 75 + true + + + + Step + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 35 + 35 + + + + + + + + 30 + 30 + + + + false + + + false + + + + + + false + + + + + + caret-left + + + + 0 + 0 + 0 + + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + 0 + 0 + + + + + 35 + 35 + + + + + + + + 30 + 30 + + + + false + + + false + + + + + + false + + + + + + caret-down + + + + 0 + 0 + 0 + + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + 0 + 0 + + + + + 35 + 35 + + + + + + + + 30 + 30 + + + + false + + + false + + + + + + false + + + + + + caret-up + + + + 0 + 0 + 0 + + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + 1 + + + None + + + false + + + false + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 25 + + + + + + + + + + false + + + false + + + + + + false + + + + + + SP_ToolBarHorizontalExtensionButton + + + false + + + + + + + + + false + + + Are you sure you want to proceed? + + + None + + + None + + + false + + + false + + + + + + + + PyDMLabel + QLabel +
pydm.widgets.label
+
+ + PyDMPushButton + QPushButton +
pydm.widgets.pushbutton
+
+ + TyphosDisplayTitle + QFrame +
typhos.display
+
+
+ + +