From dd3d93de7dc9a64f004a56f4ac44d0c76327905c Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Mon, 10 Apr 2017 20:21:27 +1000 Subject: [PATCH 01/22] Added driver/motor tables for T/KDC001 APT devs. --- instruments/thorlabs/thorlabsapt.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index d79ab01ad..60ea57a5d 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -476,6 +476,13 @@ class MotorChannel(ThorLabsAPT.APTChannel): 'FW103': (pq.Quantity(25600 / 360, 'ct/deg'),) * 3, 'NR360': (pq.Quantity(25600 / 5.4546, 'ct/deg'),) * 3 }, + + re.compile('[TK]DC001'): { + 'MTS25-Z8': (1 / pq.Quantity(34304, 'mm/ct'), NotImplemented, NotImplemented), + 'MTS50-Z8': (1 / pq.Quantity(34304, 'mm/ct'), NotImplemented, NotImplemented), + # TODO: Z8xx and Z6xx models. Need to add regex support to motor models, too. + 'PRM1-Z8': (1 / pq.Quantity(1919.64, 'deg/ct'), NotImplemented, NotImplemented), + } # TODO: add other tables here. } From 4427bf16f050a26563e2746575c8453fabcae304 Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Tue, 11 Apr 2017 10:31:37 +1000 Subject: [PATCH 02/22] Moved TODO comment to avoid pylint error. --- instruments/thorlabs/thorlabsapt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index 60ea57a5d..7c3077614 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -464,6 +464,7 @@ class MotorChannel(ThorLabsAPT.APTChannel): scale_factors = (pq.Quantity(1, 'dimensionless'), ) * 3 __SCALE_FACTORS_BY_MODEL = { + # TODO: add other tables here. re.compile('TST001|BSC00.|BSC10.|MST601'): { # Note that for these drivers, the scale factors are identical # for position, velcoity and acceleration. This is not true for @@ -483,7 +484,6 @@ class MotorChannel(ThorLabsAPT.APTChannel): # TODO: Z8xx and Z6xx models. Need to add regex support to motor models, too. 'PRM1-Z8': (1 / pq.Quantity(1919.64, 'deg/ct'), NotImplemented, NotImplemented), } - # TODO: add other tables here. } __STATUS_BIT_MASK = { From 0165a3cb93b957d590d6dd6d8824f1e926232672 Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Tue, 11 Apr 2017 20:12:39 +1000 Subject: [PATCH 03/22] Misc Py3k changes for ThorLabs APT --- .../comm/serial_communicator.py | 16 +++++++++++++--- instruments/thorlabs/_abstract.py | 5 +++-- instruments/thorlabs/_packets.py | 2 +- instruments/thorlabs/thorlabsapt.py | 16 ++++++++-------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/instruments/abstract_instruments/comm/serial_communicator.py b/instruments/abstract_instruments/comm/serial_communicator.py index 02b88f23c..40bc5b30d 100644 --- a/instruments/abstract_instruments/comm/serial_communicator.py +++ b/instruments/abstract_instruments/comm/serial_communicator.py @@ -118,13 +118,23 @@ def read_raw(self, size=-1): return resp elif size == -1: result = bytes() - while result.endswith(self._terminator.encode("utf-8")) is False: + # If the terminator is empty, we can't use endswith, but must + # read as many bytes as are available. + # On the other hand, if terminator is nonempty, we can check + # that the tail end of the buffer matches it. + c = None + term = self._terminator.encode('utf-8') if self._terminator else None + while not ( + result.endswith(term) + if term is not None else + c == b'' + ): c = self._conn.read(1) - if c == b'': + if c == b'' and term is not None: raise IOError("Serial connection timed out before reading " "a termination character.") result += c - return result[:-len(self._terminator)] + return result[:-len(term)] if term is not None else result else: raise ValueError("Must read a positive value of characters.") diff --git a/instruments/thorlabs/_abstract.py b/instruments/thorlabs/_abstract.py index 820950dac..84d354542 100644 --- a/instruments/thorlabs/_abstract.py +++ b/instruments/thorlabs/_abstract.py @@ -35,7 +35,7 @@ def sendpacket(self, packet): :param packet: The thorlabs data packet that will be queried :type packet: `ThorLabsPacket` """ - self.sendcmd(packet.pack()) + self._file.write_raw(packet.pack()) # pylint: disable=protected-access def querypacket(self, packet, expect=None): @@ -56,7 +56,8 @@ def querypacket(self, packet, expect=None): a thorlabs packet :rtype: `ThorLabsPacket` """ - resp = self.query(packet.pack()) + self._file.write_raw(packet.pack()) + resp = self._file.read_raw() if not resp: if expect is None: return None diff --git a/instruments/thorlabs/_packets.py b/instruments/thorlabs/_packets.py index 83c6decb9..3cf132e30 100644 --- a/instruments/thorlabs/_packets.py +++ b/instruments/thorlabs/_packets.py @@ -166,7 +166,7 @@ def unpack(cls, bytes): # Check if 0x80 is set on header byte 4. If so, then this packet # has data. - if struct.unpack("B", header[4])[0] & 0x80: + if struct.unpack("B", header[4:5])[0] & 0x80: msg_id, length, dest, source = message_header_wpacket.unpack( header) dest = dest ^ 0x80 # Turn off 0x80. diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index 7c3077614..ab881342d 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -12,6 +12,7 @@ import re import struct import logging +import codecs from builtins import range import quantities as pq @@ -109,11 +110,10 @@ def __init__(self, filelike): 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() + self._serial_number = codecs.encode(hw_info.data[0:4], 'hex').decode('ascii') + self._model_number = hw_info.data[4:12].decode('ascii').replace('\x00', '').strip() - hw_type_int = struct.unpack(' Date: Tue, 11 Apr 2017 21:09:03 +1000 Subject: [PATCH 04/22] motion_timeout for APT motor cmds, fix scale factor --- instruments/thorlabs/_abstract.py | 35 +++++++++++++++++++++++++---- instruments/thorlabs/thorlabsapt.py | 26 +++++++++++++++++---- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/instruments/thorlabs/_abstract.py b/instruments/thorlabs/_abstract.py index 84d354542..585616915 100644 --- a/instruments/thorlabs/_abstract.py +++ b/instruments/thorlabs/_abstract.py @@ -11,6 +11,11 @@ from instruments.thorlabs import _packets from instruments.abstract_instruments.instrument import Instrument +from instruments.util_fns import assume_units + +from quantities import second + +import time # CLASSES ##################################################################### @@ -38,7 +43,7 @@ def sendpacket(self, packet): self._file.write_raw(packet.pack()) # pylint: disable=protected-access - def querypacket(self, packet, expect=None): + def querypacket(self, packet, expect=None, timeout=None): """ Sends a packet to the connected APT instrument, and waits for a packet in response. Optionally, checks whether the received packet type is @@ -52,12 +57,33 @@ def querypacket(self, packet, expect=None): with the default value of `None` then no checking occurs. :type expect: `str` or `None` + :param timeout: Sets a timeout to wait before returning `None`, indicating + no packet was received. If the timeout is set to `None`, then the + timeout is inherited from the underlying communicator and no additional + timeout is added. If timeout is set to `False`, then this method waits + indefinitely. If timeout is set to a unitful quantity, then it is interpreted + as a time and used as the timeout value. Finally, if the timeout is a unitless + number (e.g. `float` or `int`), then seconds are assumed. + :return: Returns the response back from the instrument wrapped up in - a thorlabs packet + a ThorLabs APT packet, or None if no packet was received. :rtype: `ThorLabsPacket` """ - self._file.write_raw(packet.pack()) - resp = self._file.read_raw() + t_start = time.time() + + if timeout: + timeout = assume_units(timeout, second).rescale('second').magnitude + + while True: + self._file.write_raw(packet.pack()) + resp = self._file.read_raw() + if resp or timeout is None: + break + else: + tic = time.time() + if tic - t_start > timeout: + break + if not resp: if expect is None: return None @@ -72,4 +98,5 @@ def querypacket(self, packet, expect=None): raise IOError("APT returned message ID {}, expected {}".format( pkt._message_id, expect )) + return pkt diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index ab881342d..d2261d42a 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -18,6 +18,7 @@ import quantities as pq from instruments.thorlabs import _abstract, _packets, _cmds +from instruments.util_fns import assume_units # LOGGING ##################################################################### @@ -463,6 +464,8 @@ class MotorChannel(ThorLabsAPT.APTChannel): #: For more details, see the APT protocol documentation. scale_factors = (pq.Quantity(1, 'dimensionless'), ) * 3 + _motion_timeout = pq.Quantity(10, 'second') + __SCALE_FACTORS_BY_MODEL = { # TODO: add other tables here. re.compile('TST001|BSC00.|BSC10.|MST601'): { @@ -478,11 +481,11 @@ class MotorChannel(ThorLabsAPT.APTChannel): 'NR360': (pq.Quantity(25600 / 5.4546, 'ct/deg'),) * 3 }, - re.compile('[TK]DC001'): { + re.compile('TDC001|KDC101'): { 'MTS25-Z8': (1 / pq.Quantity(34304, 'mm/ct'), NotImplemented, NotImplemented), 'MTS50-Z8': (1 / pq.Quantity(34304, 'mm/ct'), NotImplemented, NotImplemented), # TODO: Z8xx and Z6xx models. Need to add regex support to motor models, too. - 'PRM1-Z8': (1 / pq.Quantity(1919.64, 'deg/ct'), NotImplemented, NotImplemented), + 'PRM1-Z8': (pq.Quantity(1919.64, 'ct/deg'), NotImplemented, NotImplemented), } } @@ -501,6 +504,17 @@ class MotorChannel(ThorLabsAPT.APTChannel): 'INTERLOCK_STATE': 0x00001000 } + # IK-SPECIFIC PROPERTIES # + # These properties don't correspond to any particular functionality + # of the underlying device, but control how we interact with it. + + @property + def motion_timeout(self): + return self._motion_timeout + @motion_timeout.setter + def motion_timeout(self, newval): + self._motion_timeout = assume_units(newval, pq.second) + # UNIT CONVERSION METHODS # def set_scale(self, motor_model): @@ -613,7 +627,10 @@ def go_home(self): source=0x01, data=None ) - self._apt.sendpacket(pkt) + _ = self._apt.querypacket(pkt, + expect=_cmds.ThorLabsCommands.MOT_MOVE_HOMED, + timeout=self.motion_timeout + ) def move(self, pos, absolute=True): """ @@ -662,7 +679,8 @@ def move(self, pos, absolute=True): _ = self._apt.querypacket( pkt, - expect=_cmds.ThorLabsCommands.MOT_MOVE_COMPLETED + expect=_cmds.ThorLabsCommands.MOT_MOVE_COMPLETED, + timeout=self.motion_timeout ) _channel_type = MotorChannel From b7e7559ec36347808e72701ad9816da3874210c3 Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Wed, 12 Apr 2017 12:57:19 +1000 Subject: [PATCH 05/22] ThorLabsAPT: Example of new config support. --- instruments/thorlabs/thorlabsapt.py | 34 ++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index d2261d42a..7bbeec22e 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -13,6 +13,7 @@ import struct import logging import codecs +import warnings from builtins import range import quantities as pq @@ -444,6 +445,8 @@ class MotorChannel(ThorLabsAPT.APTChannel): # INSTANCE VARIABLES # + _motor_model = None + #: Sets the scale between the encoder counts and physical units #: for the position, velocity and acceleration parameters of this #: channel. By default, set to dimensionless, indicating that the proper @@ -517,7 +520,7 @@ def motion_timeout(self, newval): # UNIT CONVERSION METHODS # - def set_scale(self, motor_model): + def _set_scale(self, motor_model): """ Sets the scale factors for this motor channel, based on the model of the attached motor and the specifications of the driver of which @@ -538,6 +541,35 @@ def set_scale(self, motor_model): logger.warning("Scale factors for controller %s and motor %s are " "unknown", self._apt.model_number, motor_model) + def set_scale(self, motor_model): + warnings.warn( + "The set_scale method has been deprecated in favor " + "of the motor_model property.", + DeprecationWarning + ) + return self._set_scale(motor_model) + + set_scale.__doc__ = _set_scale.__doc__ + + @property + def motor_model(self): + """ + Gets or sets the model name of the attached motor. + Note that the scale factors for this motor channel are based on the model + of the attached motor and the specifications of the driver of which + this is a channel, such that setting a new motor model will update + the scale factors accordingly. + + :type: `str` or `None` + """ + return self._motor_model + + @motor_model.setter + def motor_model(self, newval): + self._set_scale(newval) + self._motor_model = newval + + # MOTOR COMMANDS # @property From 9a7bea41838ada7a83c2c43b6e80a0cd018cdfbc Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Wed, 12 Apr 2017 17:15:48 +1000 Subject: [PATCH 06/22] More pylint fixes --- instruments/thorlabs/thorlabsapt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index 7bbeec22e..9d7752b74 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -541,6 +541,9 @@ def _set_scale(self, motor_model): logger.warning("Scale factors for controller %s and motor %s are " "unknown", self._apt.model_number, motor_model) + # We copy the docstring below, so it's OK for this method + # to not have a docstring of its own. + # pylint: disable=missing-docstring def set_scale(self, motor_model): warnings.warn( "The set_scale method has been deprecated in favor " @@ -563,12 +566,12 @@ def motor_model(self): :type: `str` or `None` """ return self._motor_model - + @motor_model.setter def motor_model(self, newval): self._set_scale(newval) self._motor_model = newval - + # MOTOR COMMANDS # From c6471a236ce72acdfe81000acc1b11d261362b5f Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Thu, 20 Apr 2017 09:14:44 +1000 Subject: [PATCH 07/22] Fix for line continuation convention. --- instruments/thorlabs/thorlabsapt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index 9d7752b74..57128b663 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -663,9 +663,9 @@ def go_home(self): data=None ) _ = self._apt.querypacket(pkt, - expect=_cmds.ThorLabsCommands.MOT_MOVE_HOMED, - timeout=self.motion_timeout - ) + expect=_cmds.ThorLabsCommands.MOT_MOVE_HOMED, + timeout=self.motion_timeout + ) def move(self, pos, absolute=True): """ From 11822f0a5ec6c553073d922e2f837a9eaaf3ceaa Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Thu, 20 Apr 2017 09:20:27 +1000 Subject: [PATCH 08/22] Rearranged imports into standard order. --- instruments/thorlabs/_abstract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instruments/thorlabs/_abstract.py b/instruments/thorlabs/_abstract.py index 585616915..2de132b4b 100644 --- a/instruments/thorlabs/_abstract.py +++ b/instruments/thorlabs/_abstract.py @@ -9,14 +9,14 @@ from __future__ import absolute_import from __future__ import division +import time + from instruments.thorlabs import _packets from instruments.abstract_instruments.instrument import Instrument from instruments.util_fns import assume_units from quantities import second -import time - # CLASSES ##################################################################### From 0941e4d8ad8605774f6539190c023638704ad123 Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Thu, 20 Apr 2017 15:16:59 +1000 Subject: [PATCH 09/22] Added an APT test. Not working yet. --- .../tests/test_thorlabs/test_thorlabs_apt.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 instruments/tests/test_thorlabs/test_thorlabs_apt.py diff --git a/instruments/tests/test_thorlabs/test_thorlabs_apt.py b/instruments/tests/test_thorlabs/test_thorlabs_apt.py new file mode 100644 index 000000000..757122c30 --- /dev/null +++ b/instruments/tests/test_thorlabs/test_thorlabs_apt.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the Thorlabs TC200 +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +from nose.tools import raises +import quantities as pq +import struct + +import instruments as ik +from instruments.thorlabs._packets import ThorLabsPacket, hw_info +from instruments.thorlabs._cmds import ThorLabsCommands +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + + +def test_apt_hw_info(): + with expected_protocol( + ik.thorlabs.ThorLabsAPT, + [ + ThorLabsPacket( + message_id=ThorLabsCommands.HW_REQ_INFO, + param1=0x00, param2=0x00, + dest=0x50, + source=0x01, + data=None + ).pack() + ], + [ + ThorLabsPacket( + message_id=ThorLabsCommands.HW_GET_INFO, + dest=0x01, + source=0x50, + data=hw_info.pack( + # Serial number + 0x1234, + # Model number + "ABC-123".encode('ascii'), + # HW type + 3, + # FW version, + 0xa1, 0xa2, 0xa3, + # Notes + "abcdefg".encode('ascii'), + # HW version + 42, + # Mod state + 43, + # Number of channels + 2 + ) + ).pack() + ], + sep="" + ) as apt: + # Check internal representations. + # NB: we shouldn't do this in some sense, but these fields + # act as an API to the APT subclasses. + assert apt._hw_type == "Unknown type: 3" + assert apt._fw_version == "a1.a2.a3" + assert apt._notes == "abcdefg" + assert apt._hw_version == 42 + assert apt._mod_state == 43 + + # Check external API. + assert apt.serial_number == 0x1234 + assert apt.model_number == 'ABC-123' + assert apt.name == "ThorLabs APT Instrument model ABC-123, serial 1234 (HW version 42, FW version a1.a2.a3)" + From 68cb90fe6c6714b91a3161f54cd288f97cabc043 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Sun, 23 Apr 2017 13:28:34 -0400 Subject: [PATCH 10/22] Fix linting issues --- .../tests/test_thorlabs/test_thorlabs_apt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/instruments/tests/test_thorlabs/test_thorlabs_apt.py b/instruments/tests/test_thorlabs/test_thorlabs_apt.py index 757122c30..a098e076e 100644 --- a/instruments/tests/test_thorlabs/test_thorlabs_apt.py +++ b/instruments/tests/test_thorlabs/test_thorlabs_apt.py @@ -6,11 +6,14 @@ # IMPORTS #################################################################### +# pylint: disable=unused-import + from __future__ import absolute_import +import struct + from nose.tools import raises import quantities as pq -import struct import instruments as ik from instruments.thorlabs._packets import ThorLabsPacket, hw_info @@ -19,6 +22,8 @@ # TESTS ###################################################################### +# pylint: disable=protected-access,unused-argument + def test_apt_hw_info(): with expected_protocol( @@ -39,13 +44,13 @@ def test_apt_hw_info(): source=0x50, data=hw_info.pack( # Serial number - 0x1234, + 0x1234, # Model number - "ABC-123".encode('ascii'), + "ABC-123".encode('ascii'), # HW type - 3, + 3, # FW version, - 0xa1, 0xa2, 0xa3, + 0xa1, 0xa2, 0xa3, # Notes "abcdefg".encode('ascii'), # HW version @@ -71,5 +76,5 @@ def test_apt_hw_info(): # Check external API. assert apt.serial_number == 0x1234 assert apt.model_number == 'ABC-123' - assert apt.name == "ThorLabs APT Instrument model ABC-123, serial 1234 (HW version 42, FW version a1.a2.a3)" - + assert apt.name == "ThorLabs APT Instrument model ABC-123, serial " \ + "1234 (HW version 42, FW version a1.a2.a3)" From 07f28367c4f7d87043389b20979e41564e1e1f8b Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Wed, 12 Jul 2017 15:44:21 +1000 Subject: [PATCH 11/22] New handling in loopback for empty terminator. --- .../comm/loopback_communicator.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index 85020ec3c..89422dc3c 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -108,16 +108,20 @@ def read_raw(self, size=-1): :rtype: `bytes` """ if self._stdin is not None: - if size >= 0: + if size and size >= 0: input_var = self._stdin.read(size) return bytes(input_var) - elif size == -1: + elif size == -1 or size is None: result = bytes() - while result.endswith(self._terminator.encode("utf-8")) is False: - c = self._stdin.read(1) - if c == b'': - break - result += c + if self._terminator: + while result.endswith(self._terminator.encode("utf-8")) is False: + c = self._stdin.read(1) + if c == b'': + break + result += c + else: + result = self._stdin.read(-1) + return result[:-len(self._terminator)] else: raise ValueError("Must read a positive value of characters.") From 389944abadf9de9077eae2a4be96a14acba1c4af Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Wed, 12 Jul 2017 15:44:50 +1000 Subject: [PATCH 12/22] struct.Struct for contents of hw_info packets --- instruments/thorlabs/_packets.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/instruments/thorlabs/_packets.py b/instruments/thorlabs/_packets.py index 3cf132e30..c12e91233 100644 --- a/instruments/thorlabs/_packets.py +++ b/instruments/thorlabs/_packets.py @@ -15,6 +15,18 @@ message_header_nopacket = struct.Struct(' Date: Wed, 12 Jul 2017 15:45:14 +1000 Subject: [PATCH 13/22] Support for specifying expected apt pkt sizes --- instruments/thorlabs/_abstract.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/instruments/thorlabs/_abstract.py b/instruments/thorlabs/_abstract.py index 2de132b4b..423e1d465 100644 --- a/instruments/thorlabs/_abstract.py +++ b/instruments/thorlabs/_abstract.py @@ -43,7 +43,7 @@ def sendpacket(self, packet): self._file.write_raw(packet.pack()) # pylint: disable=protected-access - def querypacket(self, packet, expect=None, timeout=None): + def querypacket(self, packet, expect=None, timeout=None, expect_data_len=None): """ Sends a packet to the connected APT instrument, and waits for a packet in response. Optionally, checks whether the received packet type is @@ -65,6 +65,9 @@ def querypacket(self, packet, expect=None, timeout=None): as a time and used as the timeout value. Finally, if the timeout is a unitless number (e.g. `float` or `int`), then seconds are assumed. + :param int expect_data_len: Number of bytes to expect as the + data for the returned packet. + :return: Returns the response back from the instrument wrapped up in a ThorLabs APT packet, or None if no packet was received. :rtype: `ThorLabsPacket` @@ -76,7 +79,11 @@ def querypacket(self, packet, expect=None, timeout=None): while True: self._file.write_raw(packet.pack()) - resp = self._file.read_raw() + resp = self._file.read_raw( + expect_data_len + 6 # the header is six bytes. + if expect_data_len else + 6 + ) if resp or timeout is None: break else: From 56585f85bbceb98f4acb6858507d367c40417299 Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Wed, 12 Jul 2017 15:45:28 +1000 Subject: [PATCH 14/22] Fixes to APT and APT tests --- instruments/tests/test_thorlabs/test_thorlabs_apt.py | 10 +++++----- instruments/thorlabs/thorlabsapt.py | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/instruments/tests/test_thorlabs/test_thorlabs_apt.py b/instruments/tests/test_thorlabs/test_thorlabs_apt.py index 757122c30..178ce3024 100644 --- a/instruments/tests/test_thorlabs/test_thorlabs_apt.py +++ b/instruments/tests/test_thorlabs/test_thorlabs_apt.py @@ -13,7 +13,7 @@ import struct import instruments as ik -from instruments.thorlabs._packets import ThorLabsPacket, hw_info +from instruments.thorlabs._packets import ThorLabsPacket, hw_info_data from instruments.thorlabs._cmds import ThorLabsCommands from instruments.tests import expected_protocol @@ -37,9 +37,9 @@ def test_apt_hw_info(): message_id=ThorLabsCommands.HW_GET_INFO, dest=0x01, source=0x50, - data=hw_info.pack( + data=hw_info_data.pack( # Serial number - 0x1234, + b'\x01\x02\x03\x04', # Model number "ABC-123".encode('ascii'), # HW type @@ -69,7 +69,7 @@ def test_apt_hw_info(): assert apt._mod_state == 43 # Check external API. - assert apt.serial_number == 0x1234 + assert apt.serial_number == '01020304' assert apt.model_number == 'ABC-123' - assert apt.name == "ThorLabs APT Instrument model ABC-123, serial 1234 (HW version 42, FW version a1.a2.a3)" + assert apt.name == "ThorLabs APT Instrument model ABC-123, serial 01020304 (HW version 42, FW version a1.a2.a3)" diff --git a/instruments/thorlabs/thorlabsapt.py b/instruments/thorlabs/thorlabsapt.py index 57128b663..d3c746648 100644 --- a/instruments/thorlabs/thorlabsapt.py +++ b/instruments/thorlabs/thorlabsapt.py @@ -110,7 +110,9 @@ def __init__(self, filelike): data=None ) hw_info = self.querypacket( - req_packet, expect=_cmds.ThorLabsCommands.HW_GET_INFO) + req_packet, expect=_cmds.ThorLabsCommands.HW_GET_INFO, + expect_data_len=84 + ) self._serial_number = codecs.encode(hw_info.data[0:4], 'hex').decode('ascii') self._model_number = hw_info.data[4:12].decode('ascii').replace('\x00', '').strip() @@ -126,10 +128,10 @@ def __init__(self, filelike): # 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( - codecs.encode(hw_info.data[14:18], 'hex') + self._fw_version = "{0[0]:x}.{0[1]:x}.{0[2]:x}".format( + hw_info.data[14:18] ) - self._notes = str(hw_info.data[18:66]).replace('\x00', '').strip() + self._notes = hw_info.data[18:66].replace(b'\x00', b'').decode('ascii').strip() self._hw_version = struct.unpack( ' Date: Wed, 12 Jul 2017 16:03:13 +1000 Subject: [PATCH 15/22] Missed a conflict marker. --- instruments/tests/test_thorlabs/test_thorlabs_apt.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/instruments/tests/test_thorlabs/test_thorlabs_apt.py b/instruments/tests/test_thorlabs/test_thorlabs_apt.py index 5fbbfdbb5..538562070 100644 --- a/instruments/tests/test_thorlabs/test_thorlabs_apt.py +++ b/instruments/tests/test_thorlabs/test_thorlabs_apt.py @@ -44,11 +44,7 @@ def test_apt_hw_info(): source=0x50, data=hw_info_data.pack( # Serial number -<<<<<<< HEAD b'\x01\x02\x03\x04', -======= - 0x1234, ->>>>>>> 68cb90fe6c6714b91a3161f54cd288f97cabc043 # Model number "ABC-123".encode('ascii'), # HW type From fefd40b99fbcb4d3531052a9cb7092b58b975381 Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Wed, 12 Jul 2017 18:36:33 +1000 Subject: [PATCH 16/22] Fixed bug due to `if size` falling through on size == 0. --- .../abstract_instruments/comm/loopback_communicator.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index 89422dc3c..bb0bfd7bd 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -108,10 +108,7 @@ def read_raw(self, size=-1): :rtype: `bytes` """ if self._stdin is not None: - if size and size >= 0: - input_var = self._stdin.read(size) - return bytes(input_var) - elif size == -1 or size is None: + if size == -1 or size is None: result = bytes() if self._terminator: while result.endswith(self._terminator.encode("utf-8")) is False: @@ -123,6 +120,11 @@ def read_raw(self, size=-1): result = self._stdin.read(-1) return result[:-len(self._terminator)] + + elif size >= 0: + input_var = self._stdin.read(size) + return bytes(input_var) + else: raise ValueError("Must read a positive value of characters.") else: From 5bc5a6e8cc46fde783db1197d1de4e74d4100bad Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Wed, 12 Jul 2017 18:42:57 +1000 Subject: [PATCH 17/22] Removed trailing whitespace. --- instruments/abstract_instruments/comm/loopback_communicator.py | 2 +- instruments/tests/test_thorlabs/test_thorlabs_apt.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index bb0bfd7bd..a975e8af1 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -118,7 +118,7 @@ def read_raw(self, size=-1): result += c else: result = self._stdin.read(-1) - + return result[:-len(self._terminator)] elif size >= 0: diff --git a/instruments/tests/test_thorlabs/test_thorlabs_apt.py b/instruments/tests/test_thorlabs/test_thorlabs_apt.py index 538562070..9dca095c9 100644 --- a/instruments/tests/test_thorlabs/test_thorlabs_apt.py +++ b/instruments/tests/test_thorlabs/test_thorlabs_apt.py @@ -44,7 +44,7 @@ def test_apt_hw_info(): source=0x50, data=hw_info_data.pack( # Serial number - b'\x01\x02\x03\x04', + b'\x01\x02\x03\x04', # Model number "ABC-123".encode('ascii'), # HW type From f5db0ee565c034f101127168b92a28f40683a5c3 Mon Sep 17 00:00:00 2001 From: Christopher Granade Date: Wed, 12 Jul 2017 12:19:33 +1000 Subject: [PATCH 18/22] Locked requirements.txt; see #174. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 30a1a88d6..f76e8b075 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy +numpy==1.12.1 pyserial quantities future>=0.15 From 22d967587f4efe0571c1284b277a778943679e92 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Thu, 7 Feb 2019 14:50:53 -0500 Subject: [PATCH 19/22] Remove numpy version pinning in requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7e9f179f2..c6153e9af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.12.1 +numpy pyserial pyvisa>=1.9 quantities>=0.12.1 From da0b6c830dd23eead3056c3f51d7bbed5c3ce2d2 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Thu, 7 Feb 2019 15:41:20 -0500 Subject: [PATCH 20/22] Add tests to cover additional loopback comm behaviour --- .../comm/loopback_communicator.py | 8 ++++---- instruments/tests/test_comm/test_loopback.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index a975e8af1..560b61b01 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -110,18 +110,18 @@ def read_raw(self, size=-1): if self._stdin is not None: if size == -1 or size is None: result = bytes() - if self._terminator: + if len(self._terminator) > 0: while result.endswith(self._terminator.encode("utf-8")) is False: c = self._stdin.read(1) if c == b'': break result += c + return result[:-len(self._terminator)] else: result = self._stdin.read(-1) + return result - return result[:-len(self._terminator)] - - elif size >= 0: + elif size > 0: input_var = self._stdin.read(size) return bytes(input_var) diff --git a/instruments/tests/test_comm/test_loopback.py b/instruments/tests/test_comm/test_loopback.py index c02c36024..0cbf751f3 100644 --- a/instruments/tests/test_comm/test_loopback.py +++ b/instruments/tests/test_comm/test_loopback.py @@ -99,6 +99,25 @@ def test_loopbackcomm_read_raw_2char_terminator(): assert mock_stdin.read.call_count == 5 +def test_loopbackcomm_read_raw_terminator_is_empty_string(): + mock_stdin = mock.MagicMock() + mock_stdin.read.side_effect = [b"abc"] + comm = LoopbackCommunicator(stdin=mock_stdin) + comm._terminator = "" + + assert comm.read_raw() == b"abc" + mock_stdin.read.assert_has_calls([mock.call(-1)]) + assert mock_stdin.read.call_count == 1 + + +def test_loopbackcomm_read_raw_size_zero(): + with pytest.raises(ValueError): + mock_stdin = mock.MagicMock() + mock_stdin.read.side_effect = [b"abc"] + comm = LoopbackCommunicator(stdin=mock_stdin) + comm.read_raw(size=0) + + def test_loopbackcomm_write_raw(): mock_stdout = mock.MagicMock() comm = LoopbackCommunicator(stdout=mock_stdout) From 3cec5a9c546232313f1a0dc9d726bbcec467a190 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Thu, 7 Feb 2019 15:46:17 -0500 Subject: [PATCH 21/22] Make pylint happy --- .../abstract_instruments/comm/loopback_communicator.py | 6 ++---- instruments/tests/test_thorlabs/test_thorlabs_apt.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index 560b61b01..022c6b06c 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -110,16 +110,14 @@ def read_raw(self, size=-1): if self._stdin is not None: if size == -1 or size is None: result = bytes() - if len(self._terminator) > 0: + if self._terminator: while result.endswith(self._terminator.encode("utf-8")) is False: c = self._stdin.read(1) if c == b'': break result += c return result[:-len(self._terminator)] - else: - result = self._stdin.read(-1) - return result + return self._stdin.read(-1) elif size > 0: input_var = self._stdin.read(size) diff --git a/instruments/tests/test_thorlabs/test_thorlabs_apt.py b/instruments/tests/test_thorlabs/test_thorlabs_apt.py index 9dca095c9..286d585f0 100644 --- a/instruments/tests/test_thorlabs/test_thorlabs_apt.py +++ b/instruments/tests/test_thorlabs/test_thorlabs_apt.py @@ -12,7 +12,7 @@ import struct -from nose.tools import raises +import pytest import quantities as pq import instruments as ik From e2e59f1cfdee2a97963f0457f8a5030c4d1ee128 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Thu, 7 Feb 2019 15:57:58 -0500 Subject: [PATCH 22/22] Revert changes to size=0 behaviour in loopback comm --- .../abstract_instruments/comm/loopback_communicator.py | 2 +- instruments/tests/test_comm/test_loopback.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index 022c6b06c..029c9d34f 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -119,7 +119,7 @@ def read_raw(self, size=-1): return result[:-len(self._terminator)] return self._stdin.read(-1) - elif size > 0: + elif size >= 0: input_var = self._stdin.read(size) return bytes(input_var) diff --git a/instruments/tests/test_comm/test_loopback.py b/instruments/tests/test_comm/test_loopback.py index 0cbf751f3..943fd6a0e 100644 --- a/instruments/tests/test_comm/test_loopback.py +++ b/instruments/tests/test_comm/test_loopback.py @@ -110,12 +110,12 @@ def test_loopbackcomm_read_raw_terminator_is_empty_string(): assert mock_stdin.read.call_count == 1 -def test_loopbackcomm_read_raw_size_zero(): +def test_loopbackcomm_read_raw_size_invalid(): with pytest.raises(ValueError): mock_stdin = mock.MagicMock() mock_stdin.read.side_effect = [b"abc"] comm = LoopbackCommunicator(stdin=mock_stdin) - comm.read_raw(size=0) + comm.read_raw(size=-2) def test_loopbackcomm_write_raw():