def max_travel(self): """ Gets the maximum travel for the specified piezo channel. :type: `~quantities.Quantity` :units: Nanometers """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.PZ_REQ_MAXTRAVEL, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None) resp = self._apt.querypacket(pkt) # Not all APT piezo devices support querying the maximum travel # distance. Those that do not simply ignore the PZ_REQ_MAXTRAVEL # packet, so that the response is empty. if resp is None: return NotImplemented # chan, int_maxtrav _, int_maxtrav = struct.unpack('<HH', resp.data) return int_maxtrav * u.Quantity(100, 'nm')
def status_bits(self): """ Gets the status bits for the specified motor channel. :type: `dict` """ # NOTE: the difference between MOT_REQ_STATUSUPDATE and # MOT_REQ_DCSTATUSUPDATE confuses me pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_REQ_STATUSUPDATE, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None) # The documentation claims there are 14 data bytes, but it seems # there are sometimes some extra random ones... resp_data = self._apt.querypacket(pkt).data[:14] # ch_ident, position, enc_count, status_bits _, _, _, status_bits = struct.unpack('<HLLL', resp_data) status_dict = dict( (key, (status_bits & bit_mask > 0)) for key, bit_mask in self.__STATUS_BIT_MASK.items()) return status_dict
def move(self, pos, absolute=True): # Handle units as follows: # 1. Treat raw numbers as encoder counts. # 2. If units are provided (as a Quantity), check if they're encoder # counts. If they aren't, apply scale factor. if not isinstance(pos, pq.Quantity): pos_ec = int(pos) else: if pos.units == pq.counts: pos_ec = int(pos.magnitude) else: scaled_pos = (pos * self.scale_factors[0]) # Force a unit error. try: pos_ec = int(scaled_pos.rescale(pq.counts).magnitude) except: raise ValueError("Provided units are not compatible with current motor scale factor.") # Now that we have our position as an integer number of encoder # counts, we're good to move. pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.MOT_MOVE_ABSOLUTE if absolute else _cmds.ThorLabsCommands.MOT_MOVE_RELATIVE, param1=None, param2=None, dest=self._apt._dest, source=0x01, data=struct.pack('<Hl', self._idx_chan, pos_ec)) response = self._apt.querypacket(pkt, expect=_cmds.ThorLabsCommands.MOT_MOVE_COMPLETED)
def test_pack_with_data(params_or_data): """Pack a package with data.""" message_id = 0x0000 param1 = params_or_data[0] param2 = params_or_data[1] dest = 0x50 source = 0x01 data = params_or_data[2] pkt = _packets.ThorLabsPacket( message_id=message_id, param1=param1, param2=param2, dest=dest, source=source, data=data, ) if params_or_data[3]: message_header = struct.Struct("<HHBB") pkt_expected = message_header.pack(message_id, len(data), 0x80 | dest, source) + data else: message_header = struct.Struct("<HBBBB") pkt_expected = message_header.pack(message_id, param1, param2, dest, source) assert pkt.pack() == pkt_expected
def test_string(params_or_data): """Return a string of the class.""" message_id = 0x0000 param1 = params_or_data[0] param2 = params_or_data[1] dest = 0x50 source = 0x01 data = params_or_data[2] pkt = _packets.ThorLabsPacket( message_id=message_id, param1=param1, param2=param2, dest=dest, source=source, data=data, ) string_expected = """ ThorLabs APT packet: Message ID {0} Parameter 1 {1} Parameter 2 {2} Destination {3} Source {4} Data {5} """.format(f"0x{message_id:x}", f"0x{param1:x}" if not params_or_data[3] else "None", f"0x{param2:x}" if not params_or_data[3] else "None", f"0x{dest:x}", f"0x{source:x}", f"{data}" if params_or_data[3] else "None") assert pkt.__str__() == string_expected
def __init__(self, filelike): super(ThorLabsAPT, self).__init__(filelike) self._dest = 0x50 # Generic USB device; make this configurable later. # Provide defaults in case an exception occurs below. self._serial_number = None self._model_number = None self._hw_type = None self._fw_version = None self._notes = "" self._hw_version = None self._mod_state = None self._n_channels = 0 self._channel = () # Perform a HW_REQ_INFO to figure out the model number, serial number, # etc. try: req_packet = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.HW_REQ_INFO, param1=0x00, param2=0x00, dest=self._dest, source=0x01, data=None) hw_info = self.querypacket( req_packet, expect=_cmds.ThorLabsCommands.HW_GET_INFO) self._serial_number = str(hw_info.data[0:4]).encode('hex') self._model_number = str(hw_info.data[4:12]).replace('\x00', '').strip() hw_type_int = struct.unpack('<H', str(hw_info.data[12:14]))[0] if hw_type_int == 45: self._hw_type = 'Multi-channel controller motherboard' elif hw_type_int == 44: self._hw_type = 'Brushless DC controller' else: self._hw_type = 'Unknown type: {}'.format(hw_type_int) # Note that the fourth byte is padding, so we strip out the first # three bytes and format them. # pylint: disable=invalid-format-index self._fw_version = "{0[0]}.{0[1]}.{0[2]}".format( str(hw_info.data[14:18]).encode('hex')) self._notes = str(hw_info.data[18:66]).replace('\x00', '').strip() self._hw_version = struct.unpack('<H', str(hw_info.data[78:80]))[0] self._mod_state = struct.unpack('<H', str(hw_info.data[80:82]))[0] self._n_channels = struct.unpack('<H', str(hw_info.data[82:84]))[0] except IOError as e: logger.error("Exception occured while fetching hardware info: %s", e) # Create a tuple of channels of length _n_channel_type if self._n_channels > 0: self._channel = tuple( self._channel_type(self, chan_idx) for chan_idx in range(self._n_channels))
def go_home(self): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.MOT_MOVE_HOME, param1=self._idx_chan, param2=0x00, dest=self._apt._dest, source=0x01, data=None) self._apt.sendpacket(pkt)
def output_position(self, pos): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.PZ_SET_OUTPUTPOS, param1=None, param2=None, dest=self._apt._dest, source=0x01, data=struct.pack('<HH', self._idx_chan, pos)) self._apt.sendpacket(pkt)
def enabled(self, newval): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.MOD_SET_CHANENABLESTATE, param1=self._idx_chan, param2=0x01 if newval else 0x02, dest=self._apt._dest, source=0x01, data=None) self._apt.sendpacket(pkt)
def enabled(self): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.MOD_REQ_CHANENABLESTATE, param1=self._idx_chan, param2=0x00, dest=self._apt._dest, source=0x01, data=None) resp = self._apt.querypacket(pkt, expect=_cmds.ThorLabsCommands.MOD_GET_CHANENABLESTATE) return not bool(resp._param2 - 1)
def is_position_control_closed(self): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.PZ_REQ_POSCONTROLMODE, param1=self._idx_chan, param2=0x00, dest=self._apt._dest, source=0x01, data=None) resp = self._apt.querypacket(pkt, expect=_cmds.ThorLabsCommands.PZ_GET_POSCONTROLMODE) return bool((resp._param2 - 1) & 1)
def change_position_control_mode(self, closed, smooth=True): mode = 1 + (int(closed) | int(smooth) << 1) pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.PZ_SET_POSCONTROLMODE, param1=self._idx_chan, param2=mode, dest=self._apt._dest, source=0x01, data=None) self._apt.sendpacket(pkt)
def position_encoder(self): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.MOT_REQ_ENCCOUNTER, param1=self._idx_chan, param2=0x00, dest=self._apt._dest, source=0x01, data=None) response = self._apt.querypacket(pkt, expect=_cmds.ThorLabsCommands.MOT_GET_ENCCOUNTER) chan, pos = struct.unpack('<Hl', response._data) return pq.Quantity(pos, 'counts')
def test_source(): """Get / set source.""" pkt = _packets.ThorLabsPacket(message_id=0x000, param1=0x01, param2=0x02, dest=0x50, source=0x01, data=None) pkt.source = 0x2A assert pkt.source == 0x2A
def test_destination(): """Get / set destination.""" pkt = _packets.ThorLabsPacket(message_id=0x000, param1=0x01, param2=0x02, dest=0x50, source=0x01, data=None) pkt.destination = 0x2A assert pkt.destination == 0x2A
def output_position(self): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.PZ_REQ_OUTPUTPOS, param1=self._idx_chan, param2=0x00, dest=self._apt._dest, source=0x01, data=None) resp = self._apt.querypacket(pkt, expect=_cmds.ThorLabsCommands.PZ_GET_OUTPUTPOS) chan, pos = struct.unpack('<HH', resp._data) return pos
def test_parameters(): """Get / set both parameters.""" pkt = _packets.ThorLabsPacket(message_id=0x000, param1=0x01, param2=0x02, dest=0x50, source=0x01, data=None) pkt.parameters = (0x0D, 0x2A) assert pkt.parameters == (0x0D, 0x2A)
def led_intensity(self, intensity): # pylint: disable=round-builtin pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.PZ_SET_TPZ_DISPSETTINGS, param1=None, param2=None, dest=self._dest, source=0x01, data=struct.pack('<H', int(round(255 * intensity)))) self.sendpacket(pkt)
def test_data(): """Get / set data.""" pkt = _packets.ThorLabsPacket(message_id=0x000, param1=None, param2=None, dest=0x50, source=0x01, data=0x00) data = struct.pack("<H", 0x2A) pkt.data = data assert pkt.data == data
def identify(self): ''' Causes a light on the APT instrument to blink, so that it can be identified. ''' pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.MOD_IDENTIFY, param1=0x00, param2=0x00, dest=self._dest, source=0x01, data=None) self.sendpacket(pkt)
def test_unpack_empty_packet(): """Raise ValueError if trying to unpack empty string.""" pkt = _packets.ThorLabsPacket(message_id=0x000, param1=0x01, param2=0x02, dest=0x50, source=0x01, data=None) with pytest.raises(ValueError) as err_info: pkt.unpack("") err_msg = err_info.value.args[0] assert err_msg == "Expected a packet, got an empty string instead."
def test_unpack_too_short(): """Raise ValueError if trying to unpack to short value.""" pkt = _packets.ThorLabsPacket(message_id=0x000, param1=0x01, param2=0x02, dest=0x50, source=0x01, data=None) too_short_package = struct.pack("<HH", 0x01, 0x02) with pytest.raises(ValueError) as err_info: pkt.unpack(too_short_package) err_msg = err_info.value.args[0] assert err_msg == "Packet must be at least 6 bytes long."
def led_intensity(self, intensity): ''' The output intensity of the LED display. :param float intensity: Ranges from 0 to 1. ''' pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.PZ_SET_TPZ_DISPSETTINGS, param1=None, param2=None, dest=self._dest, source=0x01, data=struct.pack('<H', int(round(255*intensity)))) self.sendpacket(pkt)
def go_home(self): """ Instructs the specified motor channel to return to its home position """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_MOVE_HOME, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None) self._apt.sendpacket(pkt)
def led_intensity(self): ''' The output intensity of the LED display. :type: `float` between 0 and 1. ''' pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.PZ_REQ_TPZ_DISPSETTINGS, param1=0x01, param2=0x00, dest=self._dest, source=0x01, data=None) resp = self.querypacket(pkt) return float(struct.unpack('<H', resp._data)[0])/255
def move(self, pos, absolute=True): """ Instructs the specified motor channel to move to a specific location. The provided position can be either an absolute or relative position. :param pos: The position to move to. Provided value will be converted to encoder counts. :type pos: `~quantities.Quantity` :units pos: As specified, or assumed to of units encoder counts :param bool absolute: Specify if the position is a relative or absolute position. ``True`` means absolute, while ``False`` is for a relative move. """ # Handle units as follows: # 1. Treat raw numbers as encoder counts. # 2. If units are provided (as a Quantity), check if they're encoder # counts. If they aren't, apply scale factor. if not isinstance(pos, pq.Quantity): pos_ec = int(pos) else: if pos.units == pq.counts: pos_ec = int(pos.magnitude) else: scaled_pos = (pos * self.scale_factors[0]) # Force a unit error. try: pos_ec = int(scaled_pos.rescale(pq.counts).magnitude) except: raise ValueError("Provided units are not compatible " "with current motor scale factor.") # Now that we have our position as an integer number of encoder # counts, we're good to move. pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_MOVE_ABSOLUTE if absolute else _cmds.ThorLabsCommands.MOT_MOVE_RELATIVE, param1=None, param2=None, dest=self._apt.destination, source=0x01, data=struct.pack('<Hl', self._idx_chan, pos_ec) ) _ = self._apt.querypacket( pkt, expect=_cmds.ThorLabsCommands.MOT_MOVE_COMPLETED, timeout=self.motion_timeout )
def enabled(self): """ Gets/sets the enabled status for the specified APT channel :type: `bool` """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOD_REQ_CHANENABLESTATE, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None) resp = self._apt.querypacket( pkt, expect=_cmds.ThorLabsCommands.MOD_GET_CHANENABLESTATE) return not bool(resp.parameters[1] - 1)
def change_position_control_mode(self, closed, smooth=True): """ Changes the position control mode of the piezo channel :param bool closed: `True` for closed, `False` for open :param bool smooth: `True` for smooth, `False` for otherwise. Default is `True`. """ mode = 1 + (int(closed) | int(smooth) << 1) pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.PZ_SET_POSCONTROLMODE, param1=self._idx_chan, param2=mode, dest=self._apt.destination, source=0x01, data=None) self._apt.sendpacket(pkt)
def max_travel(self): pkt = _packets.ThorLabsPacket(message_id=_cmds.ThorLabsCommands.PZ_REQ_MAXTRAVEL, param1=self._idx_chan, param2=0x00, dest=self._apt._dest, source=0x01, data=None) resp = self._apt.querypacket(pkt) # Not all APT piezo devices support querying the maximum travel # distance. Those that do not simply ignore the PZ_REQ_MAXTRAVEL # packet, so that the response is empty. if resp is None: return NotImplemented chan, int_maxtrav = struct.unpack('<HH', resp._data) return int_maxtrav * pq.Quantity(100, 'nm')
def position(self): """ Gets the current position of the specified motor channel :type: `~quantities.Quantity` """ pkt = _packets.ThorLabsPacket( message_id=_cmds.ThorLabsCommands.MOT_REQ_POSCOUNTER, param1=self._idx_chan, param2=0x00, dest=self._apt.destination, source=0x01, data=None) response = self._apt.querypacket( pkt, expect=_cmds.ThorLabsCommands.MOT_GET_POSCOUNTER) # chan, pos _, pos = struct.unpack('<Hl', response.data) return u.Quantity(pos, 'counts') / self.scale_factors[0]