def set_calibration_file(self, path, enabled=True): """Set the calibration file. Parameters ---------- path : :obj:`str` The path to the calibration file. enabled : :obj:`bool`, optional Whether to enable or disable the calibration file. """ if not self._supports_calibration: prompt.critical( 'The translation stage, {}, does not support a calibration file' .format(self._connection)) return try: self._connection.set_calibration_file(path, enabled) except IOError: prompt.critical('Cannot find calibration file\n' + path) if self._connection.is_calibration_active(): device_cal_path = self._connection.get_calibration_file() self._uncalibrated_mm, self._calibrated_mm = np.loadtxt( device_cal_path, unpack=True) self._calibration_label = 'Calibration file: {}'.format( os.path.basename(device_cal_path)) else: self._uncalibrated_mm, self._calibrated_mm = np.array( []), np.array([]) self._calibration_label = ''
def set_jog(self, value, millimeters=True): """Set the jog step size. Parameters ---------- value : :obj:`int` or :obj:`float` The jog step size. millimeters : :obj:`bool`, optional Whether the `value` is in ``device units`` or in ``real-world units`` (i.e., in millimeters). """ if not millimeters: jog = int(value) jog_mm = self._connection.get_real_value_from_device_unit( jog, UnitType.DISTANCE) msg = '{} device units'.format(jog) else: jog_mm = float(value) jog = self._connection.get_device_unit_from_real_value( jog_mm, UnitType.DISTANCE) msg = '{} mm'.format(jog) if jog_mm > self._max_pos_mm or jog_mm < self._min_pos_mm: prompt.critical('Invalid jog size of ' + msg) else: self._connection.set_jog_step_size(jog) self._update_jog_tooltip()
def _execute_start(self): """start/stop the execution thread""" self._abort_execution = self._execute_thread.isRunning() if not self._execute_thread.isRunning(): if not self._generate_command_list(): # then there was an error generating the command list return if not self._command_list: prompt.critical('There are no messages to send') return self._execute_thread._error = False self._start_thread()
def move_to(self, value, wait=True, millimeters=True): """Move to an absolute position. Parameters ---------- value : :obj:`int`, :obj:`float` or :obj:`str` If :obj:`str` then the name of a preset. Otherwise an absolute position to move to. wait : :obj:`bool` Wait until the move is finished before returning control to the calling program. If :obj:`True` then this is a blocking method. millimeters : :obj:`bool`, optional Whether the `value` is in ``device units`` or in ``real-world units`` (i.e., in millimeters). """ if isinstance(value, str): if value not in self.preset_positions: prompt.critical('{} is not a preset. Must be one of: ' + ','.join(self.preset_positions.keys())) return value = self.preset_positions[value] millimeters = True # the preset values are in real-world units if not self._connection.can_move_without_homing_first(): res = prompt.question( 'The motor should be homed before a move can be performed.\n\nHome the motor?' ) if res: self.go_home(False) return if not millimeters: value_du = value value_mm = self._connection.get_real_value_from_device_unit( value, UnitType.DISTANCE) else: value_du = self._connection.get_device_unit_from_real_value( value, UnitType.DISTANCE) value_mm = value if self._min_pos_mm <= value_mm <= self._max_pos_mm: self._requested_mm = value_mm self._connection.move_to_position(value_du) if wait: self._wait(value_du) self._update_preset_text_block_signals(value) else: m = 'Invalid move request.\n\n{} is outside the allowed range [{}, {}]' prompt.critical(m.format(value, self._min_pos_mm, self._max_pos_mm))
def _generate_command_list(self): """Generate the list of commands to execute""" # determine which rows to execute if self._use_rows.text(): rows = [] for item in self._use_rows.text().split(','): item = item.strip() if not item: continue if item.isdigit(): rows.append(int(item)) elif '-' in item: item_split = item.split('-') if len(item_split) != 2: prompt.critical('Invalid range: ' + item) return False try: start, end = map(int, item_split) except ValueError as err: prompt.critical( 'Invalid start-end range: {}'.format(err)) return False if start > end: prompt.critical('Start row {} > End row {}'.format( start, end)) return False rows.extend([value for value in range(start, end + 1)]) else: prompt.critical('Invalid row: ' + item) return False else: rows = [i + 1 for i in range(self._table.rowCount())] # determine which rows have commands self._command_list = [] for row in rows: if row < 1 or row > self._table.rowCount(): prompt.critical('Row {} does not exist'.format(row)) return False index = row - 1 action = self._table.cellWidget(index, 0).currentText() delay = self._table.cellWidget(index, 1).value() * 1e-3 message = self._table.cellWidget(index, 2).text().strip() if not message and action not in ('read', 'delay'): continue self._command_list.append((index, action, delay, message)) return True
def _new_file(self, path): """Loads a new configuration file""" try: cfg = Config(path) database = cfg.database() except Exception as e: prompt.critical('Invalid configuration file\n\n{}'.format(e)) return self._filebox.setText(path) self._equipment_records_table.set_database(database) self._connection_records_table.set_database(database) self._equipment_table.set_database(database) self._constants_table.update_table(cfg.root) self._tree.populate_tree() self._apply_filter()
def _apply_filter(self): """Apply the filter""" if not self._filebox.text(): prompt.warning('You have not loaded a configuration file') else: try: self._equipment_records_table.filter(self._filter.text()) self._connection_records_table.filter(self._filter.text()) self._equipment_table.filter(self._filter.text()) except Exception as e: self._equipment_records_table.setRowCount(0) self._equipment_records_table.resizeColumnsToContents() self._connection_records_table.setRowCount(0) self._connection_records_table.resizeColumnsToContents() self._equipment_table.setRowCount(0) self._equipment_table.resizeColumnsToContents() prompt.critical(e.message)
def __init__(self, connection, config=None, parent=None): """A :class:`~QtWidgets.QWidget` for controlling a Thorlabs translation stage. Parameters ---------- connection : :class:`~msl.equipment.connection.Connection` The connection to the translational stage motor controller (e.g., LTS150, LTS300, KST101, KDC101, ...). config : :class:`~msl.equipment.config.Config`, optional A configuration file. The following elements can be defined in a :class:`~msl.equipment.config.Config` file to initialize a :class:`TranslationStage`: .. code-block:: xml <!-- The following attributes can be defined for a "preset" and a "jog size" element. For a "preset" you must define a name attribute: units - can be either "mm" or "device". If omitted then the default unit value is "mm" name - the text that will displayed in the GUI as the name of the preset If multiple translation stages are being used then you can uniquely identify which stage will have its properties updated by including one of the additional attributes: serial - the serial number of the translation stage motor controller alias - the same alias that is used in the <equipment> XML tag If you do not include one of 'serial' or 'alias' then all stages will be updated to the XML element value. --> <thorlabs_translation_stage_preset name='Si-PD' serial="123456789">54.232</thorlabs_translation_stage_preset> <thorlabs_translation_stage_preset name='InGaAs-PD' units="mm" serial="123456789">75.2</thorlabs_translation_stage_preset> <thorlabs_translation_stage_preset name='Reference' units="device" serial="123456789">10503037</thorlabs_translation_stage_preset> <!-- Note: In the following you can also specify the calibration path to be a path relative to the configuration file --> <thorlabs_translation_stage_calibration_path serial="123456789">path/to/calibration/file.dat</thorlabs_translation_stage_calibration_path> <!-- Since the 'serial', 'alias' and 'unit' attributes are not defined then all stages will have the jog size set to 2.0 mm --> <thorlabs_translation_stage_jog_size>2.0</thorlabs_translation_stage_jog_size> parent : :class:`QtWidgets.QWidget`, optional The parent widget. """ super(TranslationStage, self).__init__(parent=parent) if signaler is None: raise ImportError( 'This widget requires that the MSL-Equipment package is installed' ) if config is not None and not issubclass(config.__class__, Config): raise TypeError( 'Must pass in a MSL Equipment configuration object. Received {}' .format(config.__class__)) self._connection = connection self._supports_calibration = hasattr(self._connection, 'set_calibration_file') self._uncalibrated_mm = np.array([]) self._calibrated_mm = np.array([]) self._calibration_label = '' # set the calibration file if config is not None and self._supports_calibration: elements = self._find_xml_elements( config, 'thorlabs_translation_stage_calibration_path') if elements: cal_path = elements[0].text rel_path = os.path.join(os.path.dirname(config.path), cal_path) if os.path.isfile(cal_path): self.set_calibration_file(cal_path) elif os.path.isfile(rel_path): self.set_calibration_file(rel_path) else: prompt.critical('Cannot find calibration file\n' + cal_path) # set the presets self._preset_combobox = QtWidgets.QComboBox() self._preset_combobox.setToolTip('Preset positions') self._preset_combobox.addItems(['', 'Home']) self.preset_positions = {} if config is not None: for element in self._find_xml_elements( config, 'thorlabs_translation_stage_preset'): self.add_preset(element.attrib['name'], float(element.text), element.attrib.get('units', 'mm') == 'mm') self._min_pos_mm, self._max_pos_mm = self._connection.get_motor_travel_limits( ) self._position_display = QtWidgets.QLineEdit() self._position_display.setReadOnly(True) self._position_display.setFont(QtGui.QFont('Helvetica', 24)) self._position_display.mouseDoubleClickEvent = self._ask_move_to fm = QtGui.QFontMetrics(self._position_display.font()) self._position_display.setFixedWidth( fm.width(' {}.xxx'.format(int(self._max_pos_mm)))) self._home_button = QtWidgets.QPushButton() self._home_button.setToolTip('Go to the Home position') self._home_button.clicked.connect(self.go_home) self._home_button.setIcon(get_icon('ieframe|0')) self._stop_button = QtWidgets.QPushButton('Stop') self._stop_button.setToolTip('Stop moving immediately') self._stop_button.clicked.connect(self._connection.stop_immediate) self._stop_button.setIcon(get_icon('wmploc|155')) if config is not None: elements = self._find_xml_elements( config, 'thorlabs_translation_stage_jog_size') if elements: element = elements[0] if element.attrib.get('units', 'mm') == 'mm': jog_mm = float(element.text) jog = self._connection.get_device_unit_from_real_value( jog_mm, UnitType.DISTANCE) s = element.text + ' mm' else: jog = int(float(element.text)) jog_mm = self._connection.get_real_value_from_device_unit( jog, UnitType.DISTANCE) s = element.text + ' device units' if jog_mm > self._max_pos_mm or jog_mm < self._min_pos_mm: prompt.critical('Invalid jog size of ' + s) else: self._connection.set_jog_step_size(jog) self._jog_forward_button = QtWidgets.QPushButton() self._jog_forward_button.clicked.connect( lambda: self.jog_forward(False)) self._jog_forward_button.setIcon(get_icon(QtWidgets.QStyle.SP_ArrowUp)) self._jog_backward_button = QtWidgets.QPushButton() self._jog_backward_button.clicked.connect( lambda: self.jog_backward(False)) self._jog_backward_button.setIcon( get_icon(QtWidgets.QStyle.SP_ArrowDown)) settings_button = QtWidgets.QPushButton() settings_button.clicked.connect(self._show_settings) settings_button.setIcon(get_icon('shell32|71')) settings_button.setToolTip('Edit the jog and move settings') grid = QtWidgets.QGridLayout() grid.addWidget(QtWidgets.QLabel('Presets:'), 0, 0, alignment=QtCore.Qt.AlignRight) grid.addWidget(self._preset_combobox, 0, 1) grid.addWidget(self._stop_button, 0, 2, 1, 2) grid.addWidget(self._position_display, 1, 0, 2, 2) grid.addWidget(self._home_button, 1, 2) grid.addWidget(self._jog_forward_button, 1, 3) grid.addWidget(settings_button, 2, 2) grid.addWidget(self._jog_backward_button, 2, 3) grid.setSpacing(0) grid.setRowStretch(3, 1) grid.setColumnStretch(4, 1) self.setLayout(grid) self._connection.start_polling(200) self._polling_duration = self._connection.polling_duration() * 1e-3 self._connection.register_message_callback(callback) signaler.signal.connect(self._update_display) self._requested_mm = None self._update_jog_tooltip() self._update_display() self._requested_mm = float(self._position_display.text()) self._preset_combobox.setCurrentText( self._get_preset_name(self._requested_mm)) self._preset_combobox.currentIndexChanged[str].connect( self._go_to_preset)
def _execute_error(self, message): """called if there was an exception raised in the execution thread""" prompt.critical(message) self._show_execute_icon()