diff --git a/instruments/doc/examples/ex_thorlabstc200.py b/instruments/doc/examples/ex_thorlabstc200.py new file mode 100644 index 000000000..10e9db72b --- /dev/null +++ b/instruments/doc/examples/ex_thorlabstc200.py @@ -0,0 +1,41 @@ +#Thorlabs Temperature Controller example + +import instruments as ik +import quantities +tc = ik.thorlabs.TC200.open_serial('/dev/tc200', 115200) + + +tc.temperature = 70*quantities.degF +print("The current temperature is: ", tc.temperature) + +tc.mode = tc.Mode.normal +print("The current mode is: ", tc.mode) + +tc.enable = True +print("The current enabled state is: ", tc.enable) + +tc.p = 200 +print("The current p gain is: ", tc.p) + +tc.i = 2 +print("The current i gain is: ", tc.i) + +tc.d = 2 +print("The current d gain is: ", tc.d) + +tc.degrees = quantities.degF +print("The current degrees settings is: ", tc.degrees) + +tc.sensor = tc.Sensor.ptc100 +print("The current sensor setting is: ", tc.sensor) + +tc.beta = 3900 +print("The current beta settings is: ", tc.beta) + +tc.max_temperature = 150*quantities.degC +print("The current max temperature setting is: ", tc.max_temperature) + +tc.max_power = 1000*quantities.mW +print("The current max power setting is: ", tc.max_power) + + diff --git a/instruments/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/instruments/abstract_instruments/comm/loopback_communicator.py index 81988c0d7..9ba95fee3 100644 --- a/instruments/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/instruments/abstract_instruments/comm/loopback_communicator.py @@ -1,14 +1,14 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -## -# loopback_communicator.py: Loopback communicator, just prints what it receives +# +# loopback_communicator.py: Loopback communicator, just prints what it receives # or queries return empty string -## -# © 2013-2015 Steven Casagrande (scasagrande@galvant.ca). +# +# © 2013-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. -## +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -21,76 +21,83 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -## -## +# +# -## IMPORTS ##################################################################### +# IMPORTS ##################################################################### import io from instruments.abstract_instruments.comm import AbstractCommunicator import sys -## CLASSES ##################################################################### +# CLASSES ##################################################################### + class LoopbackCommunicator(io.IOBase, AbstractCommunicator): + """ Used for testing various controllers """ - + def __init__(self, stdin=None, stdout=None): AbstractCommunicator.__init__(self) self._terminator = '\n' self._stdout = stdout self._stdin = stdin - - ## PROPERTIES ## - + + # PROPERTIES ## + @property def address(self): return sys.stdin.name + @address.setter def address(self, newval): raise NotImplementedError() - + @property def terminator(self): return self._terminator + @terminator.setter def terminator(self, newval): if not isinstance(newval, str): raise TypeError('Terminator must be specified ' - 'as a single character string.') + 'as a single character string.') if len(newval) > 1: raise ValueError('Terminator for LoopbackCommunicator must only be 1 ' - 'character long.') + 'character long.') self._terminator = newval - + @property def timeout(self): return 0 + @timeout.setter def timeout(self, newval): pass - - ## FILE-LIKE METHODS ## - + + # FILE-LIKE METHODS ## + def close(self): try: self._stdin.close() except: pass - - def read(self, size): + + def read(self, size=-1): """ Gets desired response command from user - + + :param int size: Number of characters to read. Default value of -1 + will read until termination character is found. :rtype: `str` """ if self._stdin is not None: - if (size >= 0): + if size >= 0: input_var = self._stdin.read(size) return input_var - elif (size == -1): + elif size == -1: result = bytearray() c = 0 while c != self._terminator: @@ -105,43 +112,41 @@ def read(self, size): else: input_var = raw_input("Desired Response: ") return input_var - + def write(self, msg): if self._stdout is not None: self._stdout.write(msg) else: print " <- {} ".format(repr(msg)) - - + def seek(self, offset): return NotImplemented - + def tell(self): return NotImplemented - + def flush_input(self): pass - - ## METHODS ## - + + # METHODS ## + def sendcmd(self, msg): - ''' + """ Receives a command and passes off to write function - + :param str msg: The command to be received - ''' + """ if msg is not '': - msg = msg + self._terminator + msg = "{}{}".format(msg, self._terminator) self.write(msg) - + def query(self, msg, size=-1): - ''' + """ Receives a query and returns the generated Response - + :param str msg: The message to received :rtype: `str` - ''' + """ self.sendcmd(msg) resp = self.read(size) return resp - diff --git a/instruments/instruments/abstract_instruments/comm/serial_communicator.py b/instruments/instruments/abstract_instruments/comm/serial_communicator.py index 9fcb3abfd..b61ff86b4 100644 --- a/instruments/instruments/abstract_instruments/comm/serial_communicator.py +++ b/instruments/instruments/abstract_instruments/comm/serial_communicator.py @@ -104,10 +104,10 @@ def close(self): self._conn.close() def read(self, size): - if (size >= 0): + if size >= 0: resp = self._conn.read(size) return resp - elif (size == -1): + elif size == -1: result = bytearray() c = 0 while c != self._terminator: diff --git a/instruments/instruments/abstract_instruments/instrument.py b/instruments/instruments/abstract_instruments/instrument.py index 7ef5a612d..a55afeb90 100644 --- a/instruments/instruments/abstract_instruments/instrument.py +++ b/instruments/instruments/abstract_instruments/instrument.py @@ -1,13 +1,13 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -## +# # instrument.py: Provides base class for all instruments. -## -# © 2013-2015 Steven Casagrande (scasagrande@galvant.ca). +# +# © 2013-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. -## +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -20,14 +20,11 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -## -## +# +# -## IMPORTS ##################################################################### +# IMPORTS ##################################################################### -import serial -import time -import struct import socket import urlparse @@ -40,9 +37,9 @@ GPIBCommunicator, AbstractCommunicator, USBTMCCommunicator, - SerialCommunicator, serial_manager ) +from instruments.errors import AcknowledgementError, PromptError import os @@ -66,7 +63,7 @@ import collections -## CONSTANTS ################################################################### +# CONSTANTS ################################################################### _DEFAULT_FORMATS = collections.defaultdict(lambda: '>b') _DEFAULT_FORMATS.update({ @@ -75,39 +72,56 @@ 4: '>i' }) -## CLASSES ##################################################################### +# CLASSES ##################################################################### + class Instrument(object): - # Set a default terminator. - # This can and should be overriden in subclasses for instruments - # that use different terminators. - _terminator = "\n" - def __init__(self, filelike): # Check to make sure filelike is a subclass of AbstractCommunicator if isinstance(filelike, AbstractCommunicator): self._file = filelike else: raise TypeError('Instrument must be initialized with a filelike ' - 'object that is a subclass of AbstractCommunicator.') - - ## COMMAND-HANDLING METHODS ## - + 'object that is a subclass of ' + 'AbstractCommunicator.') + self._prompt = None + self._terminator = "\n" + + # COMMAND-HANDLING METHODS # + + def _ack_expected(self, msg=""): + return None + def sendcmd(self, cmd): """ - Sends a command without waiting for a response. - + Sends a command without waiting for a response. + :param str cmd: String containing the command to be sent. """ self._file.sendcmd(str(cmd)) - + ack_expected = self._ack_expected(cmd) + if ack_expected is not None: + ack = self.read() + if ack != ack_expected: + raise AcknowledgementError( + "Incorrect ACK message received: got {} " + "expected {}".format(ack, ack_expected) + ) + if self.prompt is not None: + prompt = self.read() + if prompt != self.prompt: + raise PromptError( + "Incorrect prompt message received: got {} " + "expected {}".format(prompt, self.prompt) + ) + def query(self, cmd, size=-1): """ Executes the given query. - - :param str cmd: String containing the query to + + :param str cmd: String containing the query to execute. :param int size: Number of bytes to be read. Default is read until termination character is found. @@ -115,12 +129,30 @@ def query(self, cmd, size=-1): connected instrument. :rtype: `str` """ - return self._file.query(cmd, size) + ack_expected = self._ack_expected(cmd) + if ack_expected is not None: + ack = self._file.query(cmd) + if ack != ack_expected: + raise AcknowledgementError( + "Incorrect ACK message received: got {} " + "expected {}".format(ack, ack_expected) + ) + value = self.read(size) + else: + value = self._file.query(cmd, size) + if self.prompt is not None: + prompt = self.read() + if prompt is not self.prompt: + raise PromptError( + "Incorrect prompt message received: got {} " + "expected {}".format(prompt, self.prompt) + ) + return value def read(self, size=-1): """ Read the last line. - + :param int size: Number of bytes to be read. Default is read until termination character is found. :return: The result of the read as returned by the @@ -129,109 +161,150 @@ def read(self, size=-1): """ return self._file.read(size) - - ## PROPERTIES ## - + def readline(self): + """ + Read a full line + + :return: the read line + :rtype: `str` + """ + return self._file.readline() + + # PROPERTIES # + @property def timeout(self): - ''' + """ Gets/sets the communication timeout for this instrument. Note that setting this value after opening the connection is not supported for all connection types. - + :type: `int` - ''' + """ return self._file.timeout + @timeout.setter def timeout(self, newval): self._file.timeout = newval - + @property def address(self): - ''' + """ Gets/sets the target communication of the instrument. - + This is useful for situations when running straight from a Python shell and your instrument has enumerated with a different address. An example when this can happen is if you are using a USB to Serial adapter and you disconnect/reconnect it. - + :type: `int` for GPIB address, `str` for other - ''' + """ return self._file.address + @address.setter def address(self, newval): self._file.address = newval - + @property def terminator(self): - ''' + """ Gets/sets the terminator used for communication. - - For communication options where this is applicable, the value - corresponds to the ASCII character used for termination in decimal + + For communication options where this is applicable, the value + corresponds to the ASCII character used for termination in decimal format. Example: 10 sets the character to NEWLINE. - - :type: `int`, or `str` for GPIB adapters. - ''' + + :type: `int`, or `str` for GPIB adapters. + """ return self._file.terminator + @terminator.setter def terminator(self, newval): self._file.terminator = newval - - ## BASIC I/O METHODS ## - + + @property + def prompt(self): + """ + Gets/sets the prompt used for communication. + + The prompt refers to a character that is sent back from the instrument + after it has finished processing your last command. Typically this is + used to indicate to an end-user that the device is ready for input when + connected to a serial-terminal interface. + + In IK, the prompt is specified that that it (and its associated + termination character) are read in. The value read in from the device + is also checked against the stored prompt value to make sure that + everything is still in sync. + + :type: `str` + """ + return self._prompt + + @prompt.setter + def prompt(self, newval): + self._prompt = newval + + # BASIC I/O METHODS # + def write(self, msg): - ''' - Write data string to GPIB connected instrument. - This function sends all the necessary GI-GPIB adapter internal commands - that are required for the specified instrument. - ''' - self._file.write(msg) - + """ + Write data string to the connected instrument. This will call + the write method for the attached filelike object. This will typically + bypass attaching any termination characters or other communication + channel related work. + + .. seealso:: `Instrument.sendcmd` if you wish to send a string to the + instrument, while still having InstrumentKit handle termination + characters and other communication channel related work. + + :param str msg: String that will be written to the filelike object + (`Instrument._file`) attached to this instrument. + """ + self._file.write(msg) + def binblockread(self, data_width, fmt=None): - ''' + """" Read a binary data block from attached instrument. This requires that the instrument respond in a particular manner as EOL terminators naturally can not be used in binary transfers. - + The format is as follows: #{number of following digits:1-9}{num of bytes to be read}{data bytes} :param int data_width: Specify the number of bytes wide each data - point is. One of [1,2]. - + point is. One of [1,2,4]. + :param str fmt: Format string as specified by the :mod:`struct` module, or `None` to choose a format automatically based on the data - width. - ''' - #if(data_width not in [1,2]): - # print 'Error: Data width must be 1 or 2.' - # return 0 + width. Typically you can just specify `data_width` and leave this + default. + """ # This needs to be a # symbol for valid binary block symbol = self._file.read(1) - if(symbol != '#'): # Check to make sure block is valid + if symbol != '#': # Check to make sure block is valid raise IOError('Not a valid binary block start. Binary blocks ' - 'require the first character to be #.') + 'require the first character to be #.') else: # Read in the num of digits for next part digits = int(self._file.read(1)) - + # Read in the num of bytes to be read num_of_bytes = int(self._file.read(digits)) - + # Make or use the required format string. if fmt is None: fmt = _DEFAULT_FORMATS[data_width] - + # Read in the data bytes, and pass them to numpy using the specified # data type (format). return np.frombuffer(self._file.read(num_of_bytes), dtype=fmt) - - ## CLASS METHODS ## - URI_SCHEMES = ['serial', 'tcpip', 'gpib+usb', 'gpib+serial', 'visa', 'file', 'usbtmc'] - + # CLASS METHODS # + + URI_SCHEMES = ['serial', 'tcpip', 'gpib+usb', + 'gpib+serial', 'visa', 'file', 'usbtmc'] + @classmethod def open_from_uri(cls, uri): """ @@ -240,7 +313,7 @@ def open_from_uri(cls, uri): followed by a location that is interpreted differently for each scheme. The following examples URIs demonstrate the currently supported schemes and location formats:: - + serial://COM3 serial:///dev/ttyACM0 tcpip://192.168.0.10:4100 @@ -254,28 +327,28 @@ def open_from_uri(cls, uri): using the query parameter ``baud=``, as in the example ``serial://COM9?baud=115200``. If not specified, the baud rate is assumed to be 115200. - + :param str uri: URI for the instrument to be loaded. :rtype: `Instrument` - + .. seealso:: `PySerial`_ documentation for serial port URI format - + .. _PySerial: http://pyserial.sourceforge.net/ """ # Make sure that urlparse knows that we want query strings. for scheme in cls.URI_SCHEMES: if scheme not in urlparse.uses_query: urlparse.uses_query.append(scheme) - + # Break apart the URI using urlparse. This returns a named tuple whose # parts describe the incoming URI. parsed_uri = urlparse.urlparse(uri) - + # We always want the query string to provide keyword args to the # class method. # FIXME: This currently won't work, as everything is strings, - # but the other class methods expect ints or floats, depending. + # but the other class methods expect ints or floats, depending. kwargs = urlparse.parse_qs(parsed_uri.query) if parsed_uri.scheme == "serial": # Ex: serial:///dev/ttyACM0 @@ -293,7 +366,7 @@ def open_from_uri(cls, uri): kwargs['baud'] = int(kwargs['baud'][0]) else: kwargs['baud'] = 115200 - + return cls.open_serial( dev_name, **kwargs) @@ -302,7 +375,8 @@ def open_from_uri(cls, uri): host, port = parsed_uri.netloc.split(":") port = int(port) return cls.open_tcpip(host, port, **kwargs) - elif parsed_uri.scheme == "gpib+usb" or parsed_uri.scheme == "gpib+serial": + elif parsed_uri.scheme == "gpib+usb" \ + or parsed_uri.scheme == "gpib+serial": # Ex: gpib+usb://COM3/15 # scheme="gpib+usb", netloc="COM3", path="/15" # Make a new device path by joining the netloc (if any) @@ -324,23 +398,25 @@ def open_from_uri(cls, uri): # Ex: usbtmc can take URIs exactly like visa://. return cls.open_visa(parsed_uri.netloc, **kwargs) elif parsed_uri.scheme == 'file': - return cls.open_file(os.path.join(parsed_uri.netloc, - parsed_uri.path), **kwargs) + return cls.open_file(os.path.join( + parsed_uri.netloc, + parsed_uri.path + ), **kwargs) else: raise NotImplementedError("Invalid scheme or not yet " - "implemented.") - + "implemented.") + @classmethod def open_tcpip(cls, host, port): """ Opens an instrument, connecting via TCP/IP to a given host and TCP port. - + :param str host: Name or IP address of the instrument. :param int port: TCP port on which the insturment is listening. - + :rtype: `Instrument` :return: Object representing the connected instrument. - + .. seealso:: `~socket.socket.connect` for description of `host` and `port` parameters in the TCP/IP address family. @@ -348,42 +424,44 @@ def open_tcpip(cls, host, port): conn = socket.socket() conn.connect((host, port)) return cls(SocketCommunicator(conn)) - + @classmethod - def open_serial(cls, port, baud, timeout=3, writeTimeout=3): + def open_serial(cls, port, baud, timeout=3, write_timeout=3): """ Opens an instrument, connecting via a physical or emulated serial port. Note that many instruments which connect via USB are exposed to the operating system as serial ports, so this method will very commonly be used for connecting instruments via USB. - + :param str port: Name of the the port or device file to open a connection on. For example, ``"COM10"`` on Windows or ``"/dev/ttyUSB0"`` on Linux. :param int baud: The baud rate at which instrument communicates. :param float timeout: Number of seconds to wait when reading from the instrument before timing out. - :param float writeTimeout: Number of seconds to wait when writing to the + :param float write_timeout: Number of seconds to wait when writing to the instrument before timing out. - + :rtype: `Instrument` :return: Object representing the connected instrument. - + .. seealso:: `~serial.Serial` for description of `port`, baud rates and timeouts. """ - ser = serial_manager.new_serial_connection(port, - baud, - timeout, - writeTimeout) + ser = serial_manager.new_serial_connection( + port, + baud=baud, + timeout=timeout, + write_timeout=write_timeout + ) return cls(ser) - + @classmethod - def open_gpibusb(cls, port, gpib_address, timeout=3, writeTimeout=3): + def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3): """ Opens an instrument, connecting via a `Galvant Industries GPIB-USB adapter`_. - + :param str port: Name of the the port or device file to open a connection on. Note that because the GI GPIB-USB adapter identifies as a serial port to the operating system, this @@ -392,22 +470,25 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, writeTimeout=3): the instrument. :param float timeout: Number of seconds to wait when reading from the instrument before timing out. - :param float writeTimeout: Number of seconds to wait when writing to the + :param float write_timeout: Number of seconds to wait when writing to the instrument before timing out. - + :rtype: `Instrument` :return: Object representing the connected instrument. - + .. seealso:: `~serial.Serial` for description of `port` and timeouts. - - .. _Galvant Industries GPIB-USB adapter: http://galvant.ca/shop/gpibusb/ + + .. _Galvant Industries GPIB-USB adapter: galvant.ca/#!/store/gpibusb """ - ser = serialManager.newSerialConnection(port, - timeout=timeout, - writeTimeout=writeTimeout) + ser = serial_manager.new_serial_connection( + port, + baud=460800, + timeout=timeout, + write_timeout=write_timeout + ) return cls(GPIBCommunicator(ser, gpib_address)) - + @classmethod def open_gpibethernet(cls, host, port, gpib_address): conn = socket.socket() @@ -420,23 +501,23 @@ def open_visa(cls, resource_name): Opens an instrument, connecting using the VISA library. Note that `PyVISA`_ and a VISA implementation must both be present and installed for this method to function. - + :param str resource_name: Name of a VISA resource representing the given instrument. - + :rtype: `Instrument` :return: Object representing the connected instrument. - + .. seealso:: `National Instruments help page on VISA resource names `_. - + .. _PyVISA: http://pyvisa.sourceforge.net/ """ if visa is None: raise ImportError("PyVISA is required for loading VISA " - "instruments.") - if int(visa.__version__.replace('.',''))>= 160: + "instruments.") + if int(visa.__version__.replace('.', '')) >= 160: ins = visa.ResourceManager().open_resource(resource_name) else: ins = visa.instrument(resource_name) @@ -456,7 +537,7 @@ def open_usbtmc(cls, *args, **kwargs): def open_usb(cls, vid, pid): """ Opens an instrument, connecting via a raw USB stream. - + .. note:: Note that raw USB a very uncommon of connecting to instruments, even for those that are connected by USB. Most will identify as @@ -467,16 +548,16 @@ def open_usb(cls, vid, pid): kernel module is loaded. On Windows, some such devices can be opened using the VISA library and the `~instruments.Instrument.open_visa` method. - + :param str vid: Vendor ID of the USB device to open. :param int pid: Product ID of the USB device to open. - + :rtype: `Instrument` :return: Object representing the connected instrument. """ if usb is None: raise ImportError("USB support not imported. Do you have PyUSB " - "version 1.0 or later?") + "version 1.0 or later?") dev = usb.core.find(idVendor=vid, idProduct=pid) if dev is None: @@ -488,24 +569,24 @@ def open_usb(cls, vid, pid): # Copied from the tutorial at: # http://pyusb.sourceforge.net/docs/1.0/tutorial.html cfg = dev.get_active_configuration() - interface_number = cfg[(0,0)].bInterfaceNumber + interface_number = cfg[(0, 0)].bInterfaceNumber alternate_setting = usb.control.get_interface(dev, interface_number) intf = usb.util.find_descriptor( - cfg, bInterfaceNumber = interface_number, - bAlternateSetting = alternate_setting + cfg, bInterfaceNumber=interface_number, + bAlternateSetting=alternate_setting ) ep = usb.util.find_descriptor( intf, - custom_match = lambda e: \ - usb.util.endpoint_direction(e.bEndpointAddress) == \ + custom_match=lambda e: + usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT - ) + ) if ep is None: raise IOError("USB descriptor not found.") return cls(USBCommunicator(ep)) - + @classmethod def open_file(cls, filename): """ @@ -513,9 +594,9 @@ def open_file(cls, filename): be read from and written to in order to communicate with the instrument. This may be the case, for instance, if the instrument is connected by the Linux ``usbtmc`` kernel driver. - + :param str filename: Name of the character device to open. - + :rtype: `Instrument` :return: Object representing the connected instrument. """ diff --git a/instruments/instruments/errors.py b/instruments/instruments/errors.py new file mode 100644 index 000000000..fd825f025 --- /dev/null +++ b/instruments/instruments/errors.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +## +# errors.py: Custom exception errors used by various instruments. +## +# © 2016 Steven Casagrande (scasagrande@galvant.ca). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +## +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +## + +# IMPORTS ##################################################################### + +# CLASSES ##################################################################### + + +class AcknowledgementError(IOError): + pass + + +class PromptError(IOError): + pass diff --git a/instruments/instruments/tests/test_property_factories.py b/instruments/instruments/tests/test_property_factories.py index a7fcabaec..df6f9cad3 100644 --- a/instruments/instruments/tests/test_property_factories.py +++ b/instruments/instruments/tests/test_property_factories.py @@ -187,7 +187,7 @@ class BoolMock(MockInstrument): mock_instrument = BoolMock({'MOCK1?': 'OFF'}) - mock_instrument.mock1 = "OFF" + mock_instrument.mock1 = False ## Enum Property Factories ## @@ -212,6 +212,19 @@ class EnumMock(MockInstrument): eq_(mock.value, 'MOCK:A?\nMOCK:B?\nMOCK:A bb\nMOCK:B aa\nMOCK:B bb\n') +@raises(ValueError) +def test_enum_property_invalid(): + class SillyEnum(Enum): + a = 'aa' + b = 'bb' + + class EnumMock(MockInstrument): + a = enum_property('MOCK:A', SillyEnum) + + mock = EnumMock({'MOCK:A?': 'aa', 'MOCK:B?': 'bb'}) + + mock.a = 'c' + def test_enum_property_set_fmt(): class SillyEnum(Enum): a = 'aa' @@ -525,6 +538,35 @@ class UnitfulMock(MockInstrument): eq_(mock_inst.unitful_property, 1 * pq.hertz) +def test_unitful_property_valid_range(): + class UnitfulMock(MockInstrument): + unitful_property = unitful_property('MOCK', pq.hertz, valid_range=(0, 10)) + + mock_inst = UnitfulMock() + + mock_inst.unitful_property = 0 + mock_inst.unitful_property = 10 + + eq_(mock_inst.value, 'MOCK {:e}\nMOCK {:e}\n'.format(0, 10)) + +@raises(ValueError) +def test_unitful_property_minimum_value(): + class UnitfulMock(MockInstrument): + unitful_property = unitful_property('MOCK', pq.hertz, valid_range=(0, 10)) + + mock_inst = UnitfulMock() + + mock_inst.unitful_property = -1 + +@raises(ValueError) +def test_unitful_property_maximum_value(): + class UnitfulMock(MockInstrument): + unitful_property = unitful_property('MOCK', pq.hertz, valid_range=(0, 10)) + + mock_inst = UnitfulMock() + + mock_inst.unitful_property = 11 + ## String Property ## def test_string_property_basics(): diff --git a/instruments/instruments/tests/test_thorlabs/__init__.py b/instruments/instruments/tests/test_thorlabs/__init__.py index a945ba1cd..03eb7b6e5 100644 --- a/instruments/instruments/tests/test_thorlabs/__init__.py +++ b/instruments/instruments/tests/test_thorlabs/__init__.py @@ -1,13 +1,13 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -## +# # __init__.py: Tests for Thorlabs-brand instruments. -## -# © 2014 Steven Casagrande (scasagrande@galvant.ca). +# +# © 2014-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. -## +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -20,265 +20,1353 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -## +# -## IMPORTS #################################################################### +# IMPORTS #################################################################### import instruments as ik from instruments.tests import expected_protocol, make_name_test, unit_eq - -import cStringIO as StringIO +from nose.tools import raises +from flufl.enum import IntEnum import quantities as pq -## TESTS ###################################################################### +# TESTS ###################################################################### + + +def test_lcc25_name(): + with expected_protocol( + ik.thorlabs.LCC25, + [ + "*idn?" + ], + [ + "*idn?", + "bloopbloop", + ">" + ], + sep="\r" + ) as lcc: + name = lcc.name + assert name == "bloopbloop", "got {} expected bloopbloop".format(name) + def test_lcc25_frequency(): with expected_protocol( ik.thorlabs.LCC25, - "freq?\rfreq=10.0\r", - "\r>20\r" + [ + "freq?", + "freq=10.0" + ], + [ + "freq?", + "20", + ">", + "freq=10.0", + ">" + ], + sep="\r" ) as lcc: unit_eq(lcc.frequency, pq.Quantity(20, "Hz")) lcc.frequency = 10.0 + +@raises(ValueError) +def test_lcc25_frequency_lowlimit(): + with expected_protocol( + ik.thorlabs.LCC25, + [ + "freq=0.0" + ], + [ + "freq=0.0", + ">" + ], + sep="\r" + ) as lcc: + lcc.frequency = 0.0 + + +@raises(ValueError) +def test_lcc25_frequency_highlimit(): + with expected_protocol( + ik.thorlabs.LCC25, + [ + "freq=160.0" + ], + [ + "freq=160.0", + ">" + ], + sep="\r" + ) as lcc: + lcc.frequency = 160.0 + + def test_lcc25_mode(): with expected_protocol( ik.thorlabs.LCC25, - "mode?\rmode=1\r", - "\r>2\r" + [ + "mode?", + "mode=1" + ], + [ + "mode?", + "2", + ">", + "mode=1", + ">" + ], + sep="\r" ) as lcc: assert lcc.mode == ik.thorlabs.LCC25.Mode.voltage2 lcc.mode = ik.thorlabs.LCC25.Mode.voltage1 + +@raises(ValueError) +def test_lcc25_mode_invalid(): + with expected_protocol( + ik.thorlabs.LCC25, + [], + [] + ) as lcc: + lcc.mode = "blo" + + +@raises(ValueError) +def test_lcc25_mode_invalid2(): + with expected_protocol( + ik.thorlabs.LCC25, + [], + [] + ) as lcc: + blo = IntEnum("blo", "beep boop bop") + lcc.mode = blo[0] + + def test_lcc25_enable(): with expected_protocol( ik.thorlabs.LCC25, - "enable?\renable=1\r", - ">\r>0\r" + [ + "enable?", + "enable=1" + ], + [ + "enable?", + "0", + ">", + "enable=1", + ">" + ], + sep="\r" ) as lcc: - assert lcc.enable == False + assert lcc.enable is False lcc.enable = True + +@raises(TypeError) +def test_lcc25_enable_invalid_type(): + with expected_protocol( + ik.thorlabs.LCC25, + [], + [] + ) as lcc: + lcc.enable = "blo" + + def test_lcc25_extern(): with expected_protocol( ik.thorlabs.LCC25, - "extern?\rextern=1\r", - ">\r>0\r" + [ + "extern?", + "extern=1" + ], + [ + "extern?", + "0", + ">", + "extern=1", + ">" + ], + sep="\r" ) as lcc: - assert lcc.extern == False + assert lcc.extern is False lcc.extern = True + +@raises(TypeError) +def test_tc200_extern_invalid_type(): + with expected_protocol( + ik.thorlabs.LCC25, + [], + [] + ) as tc: + tc.extern = "blo" + + def test_lcc25_remote(): with expected_protocol( ik.thorlabs.LCC25, - "remote?\rremote=1\r", - ">\r>0\r" + [ + "remote?", + "remote=1" + ], + [ + "remote?", + "0", + ">", + "remote=1", + ">" + ], + sep="\r" ) as lcc: - assert lcc.remote == False + assert lcc.remote is False lcc.remote = True + +@raises(TypeError) +def test_tc200_remote_invalid_type(): + with expected_protocol( + ik.thorlabs.LCC25, + [], + [] + ) as tc: + tc.remote = "blo" + + def test_lcc25_voltage1(): with expected_protocol( ik.thorlabs.LCC25, - "volt1?\rvolt1=10.0\r", - "\r20\r" + [ + "volt1?", + "volt1=10.0" + ], + [ + "volt1?", + "20", + ">", + "volt1=10.0", + ">" + ], + sep="\r" ) as lcc: unit_eq(lcc.voltage1, pq.Quantity(20, "V")) lcc.voltage1 = 10.0 + +def test_check_cmd(): + assert ik.thorlabs.thorlabs_utils.check_cmd("blo") == 1 + assert ik.thorlabs.thorlabs_utils.check_cmd("CMD_NOT_DEFINED") == 0 + assert ik.thorlabs.thorlabs_utils.check_cmd("CMD_ARG_INVALID") == 0 + + def test_lcc25_voltage2(): with expected_protocol( ik.thorlabs.LCC25, - "volt2?\rvolt2=10.0\r", - "\r20\r" + [ + "volt2?", + "volt2=10.0", + ], + [ + "volt2?", + "20", + ">", + "volt2=10.0", + ">" + ], + sep="\r" ) as lcc: unit_eq(lcc.voltage2, pq.Quantity(20, "V")) lcc.voltage2 = 10.0 + def test_lcc25_minvoltage(): with expected_protocol( ik.thorlabs.LCC25, - "min?\rmin=10.0\r", - "\r>20\r" + [ + "min?", + "min=10.0" + ], + [ + "min?", + "20", + ">", + "min=10.0", + ">" + ], + sep="\r" ) as lcc: unit_eq(lcc.min_voltage, pq.Quantity(20, "V")) lcc.min_voltage = 10.0 + +def test_lcc25_maxvoltage(): + with expected_protocol( + ik.thorlabs.LCC25, + [ + "max?", + "max=10.0" + ], + [ + "max?", + "20", + ">", + "max=10.0", + ">" + ], + sep="\r" + ) as lcc: + unit_eq(lcc.max_voltage, pq.Quantity(20, "V")) + lcc.max_voltage = 10.0 + + def test_lcc25_dwell(): with expected_protocol( ik.thorlabs.LCC25, - "dwell?\rdwell=10\r", - "\r>20\r" + [ + "dwell?", + "dwell=10" + ], + [ + "dwell?", + "20", + ">", + "dwell=10", + ">" + ], + sep="\r" ) as lcc: unit_eq(lcc.dwell, pq.Quantity(20, "ms")) lcc.dwell = 10 + +@raises(ValueError) +def test_lcc25_dwell_positive(): + with expected_protocol( + ik.thorlabs.LCC25, + [ + "dwell=-10" + ], + [ + "dwell=-10", + ">" + ], + sep="\r" + ) as lcc: + lcc.dwell = -10 + + def test_lcc25_increment(): with expected_protocol( ik.thorlabs.LCC25, - "increment?\rincrement=10.0\r", - "\r>20\r" + [ + "increment?", + "increment=10.0" + ], + [ + "increment?", + "20", + ">", + "increment=10.0", + ">" + ], + sep="\r" ) as lcc: unit_eq(lcc.increment, pq.Quantity(20, "V")) lcc.increment = 10.0 + +@raises(ValueError) +def test_lcc25_increment_positive(): + with expected_protocol( + ik.thorlabs.LCC25, + [ + "increment=-10" + ], + [ + "increment=-10", + ">" + ], + sep="\r" + ) as lcc: + lcc.increment = -10 + + def test_lcc25_default(): with expected_protocol( ik.thorlabs.LCC25, - "default\r", - "\r>\r" + [ + "default" + ], + [ + "default", + "1", + ">" + ], + sep="\r" ) as lcc: lcc.default() + def test_lcc25_save(): with expected_protocol( ik.thorlabs.LCC25, - "save\r", - "\r>\r" + [ + "save" + ], + [ + "save", + "1", + ">" + ], + sep="\r" ) as lcc: lcc.save() -def test_lcc25_save_settings(): + +def test_lcc25_set_settings(): with expected_protocol( ik.thorlabs.LCC25, - "set=2\r", - "\r>\r" + [ + "set=2" + ], + [ + "set=2", + "1", + ">" + ], + sep="\r" ) as lcc: lcc.set_settings(2) + +@raises(ValueError) +def test_lcc25_set_settings_invalid(): + with expected_protocol( + ik.thorlabs.LCC25, + [], + [], + sep="\r" + ) as lcc: + lcc.set_settings(5) + + def test_lcc25_get_settings(): with expected_protocol( ik.thorlabs.LCC25, - "get=2\r", - "\r>\r" + [ + "get=2" + ], + [ + "get=2", + "1", + ">" + ], + sep="\r" ) as lcc: lcc.get_settings(2) + +@raises(ValueError) +def test_lcc25_get_settings_invalid(): + with expected_protocol( + ik.thorlabs.LCC25, + [], + [], + sep="\r" + ) as lcc: + lcc.get_settings(5) + + def test_lcc25_test_mode(): with expected_protocol( ik.thorlabs.LCC25, - "test\r", - "\r>\r" + [ + "test" + ], + [ + "test", + "1", + ">" + ], + sep="\r" ) as lcc: lcc.test_mode() + +def test_sc10_name(): + with expected_protocol( + ik.thorlabs.SC10, + [ + "id?" + ], + [ + "id?", + "bloopbloop", + ">" + ], + sep="\r" + ) as sc: + assert sc.name == "bloopbloop" + + def test_sc10_enable(): with expected_protocol( ik.thorlabs.SC10, - "ens?\rens=1\r", - "\r>0\r>\r" + [ + "ens?", + "ens=1" + ], + [ + "ens?", + "0", + ">", + "ens=1", + ">" + ], + sep="\r" + ) as sc: + assert sc.enable == False + sc.enable = True + + +@raises(TypeError) +def test_sc10_enable_invalid(): + with expected_protocol( + ik.thorlabs.SC10, + [], + [], + sep="\r" ) as sc: - assert sc.enable == 0 - sc.enable = 1 + sc.enable = 10 + def test_sc10_repeat(): with expected_protocol( ik.thorlabs.SC10, - "rep?\rrep=10\r", - "\r>20\r>\r" + [ + "rep?", + "rep=10" + ], + [ + "rep?", + "20", + ">", + "rep=10", + ">" + ], + sep="\r" ) as sc: assert sc.repeat == 20 sc.repeat = 10 + +@raises(ValueError) +def test_sc10_repeat_invalid(): + with expected_protocol( + ik.thorlabs.SC10, + [], + [], + sep="\r" + ) as sc: + sc.repeat = -1 + + def test_sc10_mode(): with expected_protocol( ik.thorlabs.SC10, - "mode?\rmode=2\r", - "\r>1\r>\r" + [ + "mode?", + "mode=2" + ], + [ + "mode?", + "1", + ">", + "mode=2", + ">" + ], + sep="\r" ) as sc: assert sc.mode == ik.thorlabs.SC10.Mode.manual sc.mode = ik.thorlabs.SC10.Mode.auto + +@raises(ValueError) +def test_sc10_mode_invalid(): + with expected_protocol( + ik.thorlabs.SC10, + [], + [], + sep="\r" + ) as sc: + sc.mode = "blo" + + +@raises(ValueError) +def test_sc10_mode_invalid2(): + with expected_protocol( + ik.thorlabs.SC10, + [], + [], + sep="\r" + ) as sc: + blo = IntEnum("blo", "beep boop bop") + sc.mode = blo[0] + + def test_sc10_trigger(): with expected_protocol( - ik.thorlabs.SC10, - "trig?\rtrig=1\r", - "\r>0\r>\r" + ik.thorlabs.SC10, + [ + "trig?", + "trig=1" + ], + [ + "trig?", + "0", + ">", + "trig=1", + ">" + ], + sep="\r" ) as sc: assert sc.trigger == 0 sc.trigger = 1 + def test_sc10_out_trigger(): with expected_protocol( ik.thorlabs.SC10, - "xto?\rxto=1\r", - "\r>0\r>\r" + [ + "xto?", + "xto=1" + ], + [ + "xto?", + "0", + ">", + "xto=1", + ">" + ], + sep="\r" ) as sc: assert sc.out_trigger == 0 sc.out_trigger = 1 + def test_sc10_open_time(): with expected_protocol( ik.thorlabs.SC10, - "open?\ropen=10\r", - "\r20\r>\r" + [ + "open?", + "open=10" + ], + [ + "open?", + "20", + ">", + "open=10", + ">" + ], + sep="\r" ) as sc: unit_eq(sc.open_time, pq.Quantity(20, "ms")) - sc.open_time = 10.0 + sc.open_time = 10 + def test_sc10_shut_time(): with expected_protocol( ik.thorlabs.SC10, - "shut?\rshut=10\r", - "\r20\r>\r" + [ + "shut?", + "shut=10" + ], + [ + "shut?", + "20", + ">", + "shut=10", + ">" + ], + sep="\r" ) as sc: unit_eq(sc.shut_time, pq.Quantity(20, "ms")) sc.shut_time = 10.0 -''' -unit test for baud rate should be done very carefully, testing to change the -baud rate to something other then the current baud rate will cause the -connection to be unreadable. + def test_sc10_baud_rate(): - with expected_protocol(ik.thorlabs.SC10, "baud?\rbaud=1\r", "\r>0\r>\r") as sc: - assert sc.baud_rate ==0 - sc.baud_rate = 1 -''' + with expected_protocol( + ik.thorlabs.SC10, + [ + "baud?", + "baud=1" + ], + [ + "baud?", + "0", + ">", + "baud=1", + ">" + ], + sep="\r" + ) as sc: + assert sc.baud_rate == 9600 + sc.baud_rate = 115200 + + +@raises(ValueError) +def test_sc10_baud_rate_error(): + with expected_protocol( + ik.thorlabs.SC10, + [], + [], + sep="\r" + ) as sc: + sc.baud_rate = 115201 + def test_sc10_closed(): with expected_protocol( ik.thorlabs.SC10, - "closed?\r", - "\r1\r" + [ + "closed?" + ], + [ + "closed?", + "1", + ">" + ], + sep="\r" ) as sc: assert sc.closed + def test_sc10_interlock(): with expected_protocol( ik.thorlabs.SC10, - "interlock?\r", - "\r1\r" + [ + "interlock?" + ], + [ + "interlock?", + "1", + ">" + ], + sep="\r" ) as sc: assert sc.interlock + def test_sc10_default(): with expected_protocol( ik.thorlabs.SC10, - "default\r", - "\r1\r" + [ + "default" + ], + [ + "default", + "1", + ">" + ], + sep="\r" ) as sc: assert sc.default() + def test_sc10_save(): with expected_protocol( ik.thorlabs.SC10, - "savp\r", - "\r1\r" + [ + "savp" + ], + [ + "savp", + "1", + ">" + ], + sep="\r" ) as sc: assert sc.save() + def test_sc10_save_mode(): with expected_protocol( ik.thorlabs.SC10, - "save\r", - "\r1\r" + [ + "save" + ], + [ + "save", + "1", + ">" + ], + sep="\r" ) as sc: assert sc.save_mode() + def test_sc10_restore(): with expected_protocol( ik.thorlabs.SC10, - "resp\r", - "\r1\r" + [ + "resp" + ], + [ + "resp", + "1", + ">" + ], + sep="\r" ) as sc: - assert sc.restore() \ No newline at end of file + assert sc.restore() + + +def test_tc200_name(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "*idn?" + ], + [ + "*idn?", + "bloopbloop", + ">" + ], + sep="\r" + ) as tc: + assert tc.name() == "bloopbloop" + + +def test_tc200_mode(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "stat?", + "stat?", + "mode=cycle" + ], + [ + "stat?", + "0", + ">", + "stat?", + "2", + ">", + "mode=cycle", + ">" + ], + sep="\r" + ) as tc: + assert tc.mode == tc.Mode.normal + assert tc.mode == tc.Mode.cycle + tc.mode = ik.thorlabs.TC200.Mode.cycle + + +@raises(TypeError) +def test_tc200_mode_error(): + with expected_protocol( + ik.thorlabs.TC200, + [], + [], + sep="\r" + ) as tc: + tc.mode = "blo" + + +@raises(TypeError) +def test_tc200_mode_error2(): + with expected_protocol( + ik.thorlabs.TC200, + [], + [], + sep="\r" + ) as tc: + blo = IntEnum("blo", "beep boop bop") + tc.mode = blo.beep + + +def test_tc200_enable(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "stat?", + "stat?", + "ens", + "stat?", + "ens" + ], + [ + "stat?", + "54", + ">", + + "stat?", + "54", + ">", + "ens", + ">", + + "stat?", + "55", + ">", + "ens", + ">" + ], + sep="\r" + ) as tc: + assert tc.enable == 0 + tc.enable = True + tc.enable = False + + +@raises(TypeError) +def test_tc200_enable_type(): + with expected_protocol( + ik.thorlabs.TC200, + [], + [], + sep="\r" + ) as tc: + tc.enable = "blo" + + +def test_tc200_temperature(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "tact?", + "tmax?", + "tset=40.0" + ], + [ + "tact?", + "30 C", + ">", + "tmax?", + "250", + ">", + "tset=40.0", + ">" + ], + sep="\r" + ) as tc: + assert tc.temperature == 30.0 * pq.degC + tc.temperature = 40 * pq.degC + + +@raises(ValueError) +def test_tc200_temperature_range(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "tmax?" + ], + [ + "tmax?", + "40", + ">" + ], + sep="\r" + ) as tc: + tc.temperature = 50 * pq.degC + + +def test_tc200_pid(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "pid?", + "pgain=2" + ], + [ + "pid?", + "2 0 220", + ">", + "pgain=2", + ">" + ], + sep="\r" + ) as tc: + assert tc.p == 2 + tc.p = 2 + + with expected_protocol( + ik.thorlabs.TC200, + [ + "pid?", + "igain=0" + ], + [ + "pid?", + "2 0 220", + ">", + "igain=0", + ">" + ], + sep="\r" + ) as tc: + assert tc.i == 0 + tc.i = 0 + + with expected_protocol( + ik.thorlabs.TC200, + [ + "pid?", + "dgain=220" + ], + [ + "pid?", + "2 0 220", + ">", + "dgain=220", + ">" + ], + sep="\r" + ) as tc: + assert tc.d == 220 + tc.d = 220 + + +@raises(ValueError) +def test_tc200_pmin(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "pgain=-1" + ], + [ + "pgain=-1", + ">" + ], + sep="\r" + ) as tc: + tc.p = -1 + + +@raises(ValueError) +def test_tc200_pmax(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "pgain=260" + ], + [ + "pgain=260", + ">" + ], + sep="\r" + ) as tc: + tc.p = 260 + + +@raises(ValueError) +def test_tc200_imin(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "igain=-1" + ], + [ + "igain=-1", + ">" + ], + sep="\r" + ) as tc: + tc.i = -1 + + +@raises(ValueError) +def test_tc200_imax(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "igain=260" + ], + [ + "igain=260", + ">" + ], + sep="\r" + ) as tc: + tc.i = 260 + + +@raises(ValueError) +def test_tc200_dmin(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "dgain=-1" + ], + [ + "dgain=-1", + ">" + ], + sep="\r" + ) as tc: + tc.d = -1 + + +@raises(ValueError) +def test_tc200_dmax(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "dgain=260" + ], + [ + "dgain=260", + ">" + ], + sep="\r" + ) as tc: + tc.d = 260 + + +def test_tc200_degrees(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "stat?", + "stat?", + "stat?", + "unit=c", + "unit=f", + "unit=k" + ], + [ + "stat?", + "44", + ">", + "stat?", + "54", + ">", + "stat?", + "0", + ">", + "unit=c", + ">", + "unit=f", + ">", + "unit=k", + ">" + ], + sep="\r" + ) as tc: + assert str(tc.degrees).split(" ")[1] == "K" + assert str(tc.degrees).split(" ")[1] == "degC" + assert tc.degrees == pq.degF + + tc.degrees = pq.degC + tc.degrees = pq.degF + tc.degrees = pq.degK + + +@raises(TypeError) +def test_tc200_degrees_invalid(): + + with expected_protocol( + ik.thorlabs.TC200, + [], + [], + sep="\r" + ) as tc: + tc.degrees = "blo" + + +def test_tc200_sensor(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "sns?", + "sns=ptc100" + ], + [ + "sns?", + "Sensor = NTC10K, Beta = 5600", + ">", + "sns=ptc100", + ">" + ], + sep="\r" + ) as tc: + assert tc.sensor == tc.Sensor.ntc10k + tc.sensor = tc.Sensor.ptc100 + + +@raises(TypeError) +def test_tc200_sensor_error(): + with expected_protocol( + ik.thorlabs.TC200, + [], + [] + ) as tc: + tc.sensor = "blo" + + +@raises(TypeError) +def test_tc200_sensor_error2(): + with expected_protocol( + ik.thorlabs.TC200, + [], + [] + ) as tc: + blo = IntEnum("blo", "beep boop bop") + tc.sensor = blo.beep + + +def test_tc200_beta(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "beta?", + "beta=2000" + ], + [ + "beta?", + "5600", + ">", + "beta=2000", + ">" + ], + sep="\r" + ) as tc: + assert tc.beta == 5600 + tc.beta = 2000 + + +@raises(ValueError) +def test_tc200_beta_min(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "beta=200" + ], + [ + "beta=200", + ">" + ], + sep="\r" + ) as tc: + tc.beta = 200 + + +@raises(ValueError) +def test_tc200_beta_max(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "beta=20000" + ], + [ + "beta=20000", + ">" + ], + sep="\r" + ) as tc: + tc.beta = 20000 + + +def test_tc200_max_power(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "pmax?", + "PMAX=12.0" + ], + [ + "pmax?", + "15.0", + ">", + "PMAX=12.0", + ">" + ], + sep="\r" + ) as tc: + assert tc.max_power == 15.0 * pq.W + tc.max_power = 12 * pq.W + + +@raises(ValueError) +def test_tc200_power_min(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "PMAX=-2" + ], + [ + "PMAX=-2", + ">" + ], + sep="\r" + ) as tc: + tc.max_power = -1 + + +@raises(ValueError) +def test_tc200_power_max(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "PMAX=20000" + ], + [ + "PMAX=20000", + ">" + ], + sep="\r" + ) as tc: + tc.max_power = 20000 + + +def test_tc200_max_temperature(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "tmax?", + "TMAX=180.0" + ], + [ + "tmax?", + "200.0", + ">", + "TMAX=180.0", + ">" + ], + sep="\r" + ) as tc: + assert tc.max_temperature == 200.0 * pq.degC + print "second test" + tc.max_temperature = 180 * pq.degC + + +@raises(ValueError) +def test_tc200_temp_min(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "TMAX=-2" + ], + [ + "TMAX=-2", + ">" + ], + sep="\r" + ) as tc: + tc.max_temperature = -1 + + +@raises(ValueError) +def test_tc200_temp_max(): + with expected_protocol( + ik.thorlabs.TC200, + [ + "TMAX=20000" + ], + [ + "TMAX=20000", + ">" + ], + sep="\r" + ) as tc: + tc.max_temperature = 20000 diff --git a/instruments/instruments/tests/test_util_fns.py b/instruments/instruments/tests/test_util_fns.py index d3053fdea..86295aa0b 100644 --- a/instruments/instruments/tests/test_util_fns.py +++ b/instruments/instruments/tests/test_util_fns.py @@ -31,7 +31,7 @@ from instruments.util_fns import ( ProxyList, - assume_units + assume_units, convert_temperature ) from flufl.enum import Enum @@ -134,7 +134,37 @@ def test_assume_units_correct(): # Check that raw scalars are made unitful. eq_(assume_units(1, 'm').rescale('mm').magnitude, 1000) - + +def test_temperature_conversion(): + blo = 70.0*pq.degF + out = convert_temperature(blo, pq.degC) + eq_(out.magnitude, 21.11111111111111) + out = convert_temperature(blo, pq.degK) + eq_(out.magnitude, 294.2055555555555) + out = convert_temperature(blo, pq.degF) + eq_(out.magnitude, 70.0) + + blo = 20.0*pq.degC + out = convert_temperature(blo, pq.degF) + eq_(out.magnitude, 68) + out = convert_temperature(blo, pq.degC) + eq_(out.magnitude, 20.0) + out = convert_temperature(blo, pq.degK) + eq_(out.magnitude, 293.15) + + blo = 270*pq.degK + out = convert_temperature(blo, pq.degC) + eq_(out.magnitude, -3.1499999999999773) + out = convert_temperature(blo, pq.degF) + eq_(out.magnitude, 141.94736842105263) + out = convert_temperature(blo, pq.K) + eq_(out.magnitude, 270) + +@raises(ValueError) +def test_temperater_conversion_failure(): + blo = 70.0*pq.degF + convert_temperature(blo, pq.V) + @raises(ValueError) def test_assume_units_failures(): assume_units(1, 'm').rescale('s') diff --git a/instruments/instruments/thorlabs/__init__.py b/instruments/instruments/thorlabs/__init__.py index c23d86ccf..520df7761 100644 --- a/instruments/instruments/thorlabs/__init__.py +++ b/instruments/instruments/thorlabs/__init__.py @@ -1,6 +1,9 @@ +from __future__ import absolute_import + from instruments.thorlabs.thorlabsapt import ( ThorLabsAPT, APTPiezoStage, APTStrainGaugeReader, APTMotorController ) from instruments.thorlabs.pm100usb import PM100USB from instruments.thorlabs.lcc25 import LCC25 from instruments.thorlabs.sc10 import SC10 +from instruments.thorlabs.tc200 import TC200 diff --git a/instruments/instruments/thorlabs/_abstract.py b/instruments/instruments/thorlabs/_abstract.py index 8851919e9..5315cad79 100644 --- a/instruments/instruments/thorlabs/_abstract.py +++ b/instruments/instruments/thorlabs/_abstract.py @@ -3,7 +3,7 @@ ## # _packets.py: Module for working with ThorLabs packets. ## -# © 2013 Steven Casagrande (scasagrande@galvant.ca). +# © 2013-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. @@ -22,12 +22,15 @@ # along with this program. If not, see . ## -## IMPORTS ##################################################################### +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division from instruments.thorlabs import _packets from instruments.abstract_instruments.instrument import Instrument -## CLASSES ##################################################################### +# CLASSES ##################################################################### class ThorLabsInstrument(Instrument): diff --git a/instruments/instruments/thorlabs/_cmds.py b/instruments/instruments/thorlabs/_cmds.py index a5e6a5315..09c971a21 100644 --- a/instruments/instruments/thorlabs/_cmds.py +++ b/instruments/instruments/thorlabs/_cmds.py @@ -3,7 +3,7 @@ ## # _cmds.py: Command mneonics for APT protocol. ## -# © 2013 Steven Casagrande (scasagrande@galvant.ca). +# © 2013-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. @@ -22,15 +22,14 @@ # along with this program. If not, see . ## -## FEATURES #################################################################### +# IMPORTS ##################################################################### +from __future__ import absolute_import from __future__ import division - -## IMPORTS ##################################################################### - from flufl.enum import IntEnum -## CLASSES ##################################################################### +# CLASSES ##################################################################### + class ThorLabsCommands(IntEnum): # General System Commands diff --git a/instruments/instruments/thorlabs/_packets.py b/instruments/instruments/thorlabs/_packets.py index eaedf00b9..423691b21 100644 --- a/instruments/instruments/thorlabs/_packets.py +++ b/instruments/instruments/thorlabs/_packets.py @@ -3,7 +3,7 @@ ## # _packets.py: Module for working with ThorLabs packets. ## -# © 2013 Steven Casagrande (scasagrande@galvant.ca). +# © 2013-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. @@ -22,16 +22,19 @@ # along with this program. If not, see . ## -## IMPORTS ##################################################################### +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division import struct -## STRUCTS ##################################################################### +# STRUCTS ##################################################################### message_header_nopacket = struct.Struct('. -## +# # LCC25 Class contributed by Catherine Holloway # -## IMPORTS ##################################################################### + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +from builtins import range import quantities as pq from flufl.enum import IntEnum +from instruments.thorlabs.thorlabs_utils import check_cmd + from instruments.abstract_instruments import Instrument -from instruments.util_fns import assume_units +from instruments.util_fns import enum_property, bool_property, unitful_property + +# CLASSES ##################################################################### -## CLASSES ##################################################################### class LCC25(Instrument): + """ The LCC25 is a controller for the thorlabs liquid crystal modules. - it can set two voltages and then oscillate between them at a specific + it can set two voltages and then oscillate between them at a specific repetition rate. - + The user manual can be found here: http://www.thorlabs.com/thorcat/18800/LCC25-Manual.pdf """ + def __init__(self, filelike): super(LCC25, self).__init__(filelike) self.terminator = "\r" - self.end_terminator = ">" - - ## ENUMS ## - + self.prompt = ">" + + def _ack_expected(self, msg=""): + return msg + + # ENUMS # + class Mode(IntEnum): - modulate = 0 + normal = 0 voltage1 = 1 voltage2 = 2 - - ## PROPERTIES ## + # PROPERTIES # + + @property def name(self): """ - gets the name and version number of the device - """ - response = self.check_command("*idn?") - if response is "CMD_NOT_DEFINED": - self.name() - else: - return response + Gets the name and version number of the device - @property - def frequency(self): + :rtype: `str` """ - Gets/sets the frequency at which the LCC oscillates between the + return self.query("*idn?") + + frequency = unitful_property( + "freq", + pq.Hz, + format_code="{:.1f}", + set_fmt="{}={}", + valid_range=(5, 150), + doc=""" + Gets/sets the frequency at which the LCC oscillates between the two voltages. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Hertz. - :type: `~quantities.Quantity` - """ - response = self.check_command("freq?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.Hz - @frequency.setter - def frequency(self, newval): - newval = assume_units(newval, pq.Hz).rescale(pq.Hz).magnitude - if newval < 5: - raise ValueError("Frequency is too low.") - if newval >150: - raise ValueError("Frequency is too high") - self.sendcmd("freq={}".format(newval)) - @property - def mode(self): - """ - Gets/sets the output mode of the LCC25 - - :type: `LCC25.Mode` + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units Hertz. + :rtype: `~quantities.quantity.Quantity` """ - response = self.check_command("mode?") - if not response is "CMD_NOT_DEFINED": - return LCC25.Mode[int(response)] + ) - @mode.setter - def mode(self, newval): - if (newval.enum is not LCC25.Mode): - raise TypeError("Mode setting must be a `LCC25.Mode` value, " - "got {} instead.".format(type(newval))) - response = self.query("mode={}".format(newval.value)) + mode = enum_property( + "mode", + Mode, + input_decoration=int, + set_fmt="{}={}", + doc=""" + Gets/sets the output mode of the LCC25 - @property - def enable(self): + :rtype: `LCC25.Mode` """ + ) + + enable = bool_property( + "enable", + "1", + "0", + set_fmt="{}={}", + doc=""" Gets/sets the output enable status. - + If output enable is on (`True`), there is a voltage on the output. - - :type: `bool` - """ - response = self.check_command("enable?") - if not response is "CMD_NOT_DEFINED": - return True if int(response) is 1 else False - @enable.setter - def enable(self, newval): - if not isinstance(newval, bool): - raise TypeError("LLC25 enable property must be specified with a " - "boolean.") - self.sendcmd("enable={}".format(int(newval))) - - @property - def extern(self): + + :rtype: `bool` """ + ) + + extern = bool_property( + "extern", + "1", + "0", + set_fmt="{}={}", + doc=""" Gets/sets the use of the external TTL modulation. - + Value is `True` for external TTL modulation and `False` for internal modulation. - - :type: `bool` - """ - response = self.check_command("extern?") - if not response is "CMD_NOT_DEFINED": - return True if int(response) is 1 else False - @extern.setter - def extern(self, newval): - if not isinstance(newval, bool): - raise TypeError("LLC25 extern property must be specified with a " - "boolean.") - self.sendcmd("extern={}".format(int(newval))) - @property - def remote(self): + :rtype: `bool` """ + ) + + remote = bool_property( + "remote", + "1", + "0", + set_fmt="{}={}", + doc=""" Gets/sets front panel lockout status for remote instrument operation. - + Value is `False` for normal operation and `True` to lock out the front panel buttons. - - :type: `bool` - """ - response = self.check_command("remote?") - if not response is "CMD_NOT_DEFINED": - return True if int(response) is 1 else False - @remote.setter - def remote(self, newval): - if not isinstance(newval, bool): - raise TypeError("LLC25 remote property must be specified with a " - "boolean.") - self.sendcmd("remote={}".format(int(newval))) - @property - def voltage1(self): + :rtype: `bool` """ + ) + + voltage1 = unitful_property( + "volt1", + pq.V, + format_code="{:.1f}", + set_fmt="{}={}", + valid_range=(0, 25), + doc=""" Gets/sets the voltage value for output 1. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `~quantities.Quantity` - """ - response = self.check_command("volt1?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.V - @voltage1.setter - def voltage1(self, newval): - newval = assume_units(newval, pq.V).rescale(pq.V).magnitude - if newval < 0: - raise ValueError("Voltage is too low.") - if newval > 25: - raise ValueError("Voltage is too high") - self.sendcmd("volt1={}".format(newval)) - @property - def voltage2(self): + :units: As specified (if a `~quantities.quantity.Quantity`) or + assumed to be of units Volts. + :rtype: `~quantities.quantity.Quantity` """ + ) + + voltage2 = unitful_property( + "volt2", + pq.V, + format_code="{:.1f}", + set_fmt="{}={}", + valid_range=(0, 25), + doc=""" Gets/sets the voltage value for output 2. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `~quantities.Quantity` - """ - response = self.check_command("volt2?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.V - @voltage2.setter - def voltage2(self, newval): - newval = assume_units(newval, pq.V).rescale(pq.V).magnitude - if newval < 0: - raise ValueError("Voltage is too low.") - if newval > 25: - raise ValueError("Voltage is too high") - self.sendcmd("volt2={}".format(newval)) - @property - def min_voltage(self): + :units: As specified (if a `~quantities.quantity.Quantity`) or + assumed to be of units Volts. + :rtype: `~quantities.quantity.Quantity` """ + ) + + min_voltage = unitful_property( + "min", + pq.V, + format_code="{:.1f}", + set_fmt="{}={}", + valid_range=(0, 25), + doc=""" Gets/sets the minimum voltage value for the test mode. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `~quantities.Quantity` - """ - response = self.check_command("min?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.V - @min_voltage.setter - def min_voltage(self, newval): - newval = assume_units(newval, pq.V).rescale(pq.V).magnitude - if newval < 0: - raise ValueError("Voltage is too low.") - if newval > 25: - raise ValueError("Voltage is too high") - self.sendcmd("min={}".format(newval)) - @property - def max_voltage(self): + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units Volts. + :rtype: `~quantities.quantity.Quantity` """ - Gets/sets the maximum voltage value for the test mode. If the maximum + ) + + max_voltage = unitful_property( + "max", + pq.V, + format_code="{:.1f}", + set_fmt="{}={}", + valid_range=(0, 25), + doc=""" + Gets/sets the maximum voltage value for the test mode. If the maximum voltage is less than the minimum voltage, nothing happens. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `~quantities.Quantity` - - """ - response = self.check_command("max?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.V - @max_voltage.setter - def max_voltage(self, newval): - newval = assume_units(newval, pq.V).rescale(pq.V).magnitude - if newval < 0: - raise ValueError("Voltage is too low.") - if newval > 25: - raise ValueError("Voltage is too high") - self.sendcmd("max={}".format(newval)) - @property - def dwell(self): + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units Volts. + :rtype: `~quantities.quantity.Quantity` """ + ) + + dwell = unitful_property( + "dwell", + units=pq.ms, + format_code="{:n}", + set_fmt="{}={}", + valid_range=(0, None), + doc=""" Gets/sets the dwell time for voltages for the test mode. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units milliseconds. - :type: `~quantities.Quantity` - """ - response = self.check_command("dwell?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.ms - @dwell.setter - def dwell(self, newval): - newval = int(assume_units(newval, pq.ms).rescale(pq.ms).magnitude) - if newval < 0: - raise ValueError("Dwell time must be positive") - self.sendcmd("dwell={}".format(newval)) - @property - def increment(self): + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units milliseconds. + :rtype: `~quantities.quantity.Quantity` """ + ) + + increment = unitful_property( + "increment", + units=pq.V, + format_code="{:.1f}", + set_fmt="{}={}", + valid_range=(0, None), + doc=""" Gets/sets the voltage increment for voltages for the test mode. - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `~quantities.Quantity` - """ - response = self.check_command("increment?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.V - @increment.setter - def increment(self, newval): - newval = assume_units(newval, pq.V).rescale(pq.V).magnitude - if newval < 0: - raise ValueError("Increment voltage must be positive") - self.sendcmd("increment={}".format(newval)) - - ## METHODS ## - - def check_command(self,command): - """ - Checks for the \"Command error CMD_NOT_DEFINED\" error, which can - sometimes occur if there were incorrect terminators on the previous - command. If the command is successful, it returns the value, if not, - it returns CMD_NOT_DEFINED - - check_command will also clear out the query string + + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units Volts. + :rtype: `~quantities.quantity.Quantity` """ - response = self.query(command) - response = self.read() - cmd_find = response.find("CMD_NOT_DEFINED") - if cmd_find ==-1: - error_find = response.find("CMD_ARG_INVALID") - if error_find ==-1: - output_str = response.replace(command,"") - output_str = output_str.replace(self.terminator,"") - output_str = output_str.replace(self.end_terminator,"") - else: - output_str = "CMD_ARG_INVALID" - else: - output_str = "CMD_NOT_DEFINED" - return output_str - + ) + + # METHODS # + def default(self): """ Restores instrument to factory settings. - + Returns 1 if successful, 0 otherwise - + :rtype: `int` """ - response = self.check_command("default") - if not response is "CMD_NOT_DEFINED": - return 1 - else: - return 0 + response = self.query("default") + return check_cmd(response) def save(self): """ Stores the parameters in static memory - + Returns 1 if successful, zero otherwise. - + :rtype: `int` """ - response = self.check_command("save") - if not response is "CMD_NOT_DEFINED": - return 1 - else: - return 0 + response = self.query("save") + return check_cmd(response) def set_settings(self, slot): """ - Saves the current settings to memory - + Saves the current settings to memory. + Returns 1 if successful, zero otherwise. - + :param slot: Memory slot to use, valid range `[1,4]` :type slot: `int` - :rtype: `int` """ - if slot < 0: - raise ValueError("Slot number is less than 0") - if slot > 4: - raise ValueError("Slot number is greater than 4") - response = self.check_command("set={}".format(slot)) - if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID": - return 1 - else: - return 0 - - def get_settings(self,slot): + if slot not in range(1, 5): + raise ValueError("Cannot set memory out of `[1,4]` range") + response = self.query("set={}".format(slot)) + return check_cmd(response) + + def get_settings(self, slot): """ - Gets the current settings to memory - + Gets the current settings to memory. + Returns 1 if successful, zero otherwise. - + :param slot: Memory slot to use, valid range `[1,4]` :type slot: `int` - :rtype: `int` """ - if slot < 0: - raise ValueError("Slot number is less than 0") - if slot > 4: - raise ValueError("Slot number is greater than 4") - response = self.check_command("get={}".format(slot)) - if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID": - return 1 - else: - return 0 + if slot not in range(1, 5): + raise ValueError("Cannot set memory out of `[1,4]` range") + response = self.query("get={}".format(slot)) + return check_cmd(response) def test_mode(self): """ - Puts the LCC in test mode - meaning it will increment the output - voltage from the minimum value to the maximum value, in increments, + Puts the LCC in test mode - meaning it will increment the output + voltage from the minimum value to the maximum value, in increments, waiting for the dwell time - + Returns 1 if successful, zero otherwise. - + :rtype: `int` """ - response = self.check_command("test") - if not response is "CMD_NOT_DEFINED": - return 1 - else: - return 0 + response = self.query("test") + return check_cmd(response) diff --git a/instruments/instruments/thorlabs/pm100usb.py b/instruments/instruments/thorlabs/pm100usb.py index 6f965a549..0b295d89f 100644 --- a/instruments/instruments/thorlabs/pm100usb.py +++ b/instruments/instruments/thorlabs/pm100usb.py @@ -3,7 +3,7 @@ ## # pm100usb.py: Driver class for the PM100USB power meter. ## -# © 2013 Steven Casagrande (scasagrande@galvant.ca). +# © 2013-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. @@ -22,12 +22,11 @@ # along with this program. If not, see . ## -## FEATURES #################################################################### +# IMPORTS ##################################################################### +from __future__ import absolute_import from __future__ import division -## IMPORTS ##################################################################### - from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import enum_property @@ -37,13 +36,14 @@ import quantities as pq from collections import namedtuple -## LOGGING ##################################################################### +# LOGGING ##################################################################### import logging logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -## CLASSES ##################################################################### +# CLASSES ##################################################################### + class PM100USB(SCPIInstrument): """ diff --git a/instruments/instruments/thorlabs/sc10.py b/instruments/instruments/thorlabs/sc10.py index e23a0e72f..12f87896c 100644 --- a/instruments/instruments/thorlabs/sc10.py +++ b/instruments/instruments/thorlabs/sc10.py @@ -1,13 +1,13 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -## +# # sc10.py: Class for the thorlabs sc10 Shutter Controller -## -# © 2014 Steven Casagrande (scasagrande@galvant.ca). +# +# © 2014-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. -## +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or @@ -20,317 +20,251 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -## +# # SC10 Class contributed by Catherine Holloway # -## IMPORTS ##################################################################### -import quantities as pq +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +from builtins import range + from flufl.enum import IntEnum +import quantities as pq from instruments.abstract_instruments import Instrument -from instruments.util_fns import assume_units +from instruments.util_fns import ( + bool_property, enum_property, int_property, unitful_property +) +from instruments.thorlabs.thorlabs_utils import check_cmd + +# CLASSES ##################################################################### -## CLASSES ##################################################################### class SC10(Instrument): + """ The SC10 is a shutter controller, to be used with the Thorlabs SH05 and SH1. The user manual can be found here: http://www.thorlabs.com/thorcat/8600/SC10-Manual.pdf """ + def __init__(self, filelike): super(SC10, self).__init__(filelike) self.terminator = '\r' - self.end_terminator = '>' - - ## ENUMS ## - + self.prompt = '>' + + def _ack_expected(self, msg=""): + return msg + + # ENUMS # + class Mode(IntEnum): manual = 1 auto = 2 single = 3 repeat = 4 external = 5 - - ## PROPERTIES ## + + # PROPERTIES # @property def name(self): """ Gets the name and version number of the device. - """ - response = self.check_command("id?") - if response is "CMD_NOT_DEFINED": - self.name() - else: - return response - @property - def enable(self): - """ - Gets/sets the shutter enable status, 0 for disabled, 1 if enabled - - :type: `int` + :return: Name and verison number of the device + :rtype: `str` """ - response = self.check_command("ens?") - if not response is "CMD_NOT_DEFINED": - return int(response) - @enable.setter - def enable(self, newval): - if newval == 0 or newval ==1: - self.sendcmd("ens={}".format(newval)) - self.read() - else: - raise ValueError("Invalid value for enable, must be 0 or 1") + return self.query("id?") - @property - def repeat(self): + enable = bool_property( + "ens", + "1", + "0", + set_fmt="{}={}", + doc=""" + Gets/sets the shutter enable status, False for disabled, True if + enabled + + If output enable is on (`True`), there is a voltage on the output. + + :rtype: `bool` """ + ) + + repeat = int_property( + "rep", + valid_set=range(1, 100), + set_fmt="{}={}", + doc=""" Gets/sets the repeat count for repeat mode. Valid range is [1,99] inclusive. - + :type: `int` """ - response = self.check_command("rep?") - if not response is "CMD_NOT_DEFINED": - return int(response) - @repeat.setter - def repeat(self, newval): - if newval >0 or newval <100: - self.sendcmd("rep={}".format(newval)) - self.read() - else: - raise ValueError("Invalid value for repeat count, must be " - "between 1 and 99") + ) - @property - def mode(self): - """ - Gets/sets the output mode of the SC10. + mode = enum_property( + "mode", + Mode, + input_decoration=int, + set_fmt="{}={}", + doc=""" + Gets/sets the output mode of the LCC25 - :type: `SC10.Mode` - """ - response = self.check_command("mode?") - if not response is "CMD_NOT_DEFINED": - return SC10.Mode[int(response)] - @mode.setter - def mode(self, newval): - if (newval.enum is not SC10.Mode): - raise TypeError("Mode setting must be a `SC10.Mode` value, " - "got {} instead.".format(type(newval))) - - self.sendcmd("mode={}".format(newval.value)) - self.read() - - @property - def trigger(self): + :rtype: `LCC25.Mode` """ + ) + + trigger = int_property( + "trig", + valid_set=range(0, 2), + set_fmt="{}={}", + doc=""" Gets/sets the trigger source. - + 0 for internal trigger, 1 for external trigger - + :type: `int` """ - response = self.check_command("trig?") - if not response is "CMD_NOT_DEFINED": - return int(response) - @trigger.setter - def trigger(self, newval): - if newval != 0 and newval != 1: - raise ValueError("Not a valid value for trigger mode") - self.sendcmd("trig={}".format(newval)) - self.read() - - @property - def out_trigger(self): - """ + ) + + out_trigger = int_property( + "xto", + valid_set=range(0, 2), + set_fmt="{}={}", + doc=""" Gets/sets the out trigger source. - - 0 trigger out follows shutter output, 1 trigger out follows + + 0 trigger out follows shutter output, 1 trigger out follows controller output - + :type: `int` """ - response = self.check_command("xto?") - if not response is "CMD_NOT_DEFINED": - return int(response) - @out_trigger.setter - def out_trigger(self, newval): - if newval != 0 and newval != 1: - raise ValueError("Not a valid value for output trigger mode") - self.sendcmd("xto={}".format(newval)) - self.read() - - ###I'm not sure how to handle checking for the number of digits yet. - @property - def open_time(self): - """ + ) + + open_time = unitful_property( + "open", + pq.ms, + format_code="{:.0f}", + set_fmt="{}={}", + valid_range=(0, 999999), + doc=""" Gets/sets the amount of time that the shutter is open, in ms - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units milliseconds. - :type: `~quantities.Quantity` - """ - response = self.check_command("open?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.ms - @open_time.setter - def open_time(self, newval): - newval = int(assume_units(newval, pq.ms).rescale(pq.ms).magnitude) - if newval < 0: - raise ValueError("Shutter open time cannot be negative") - if newval >999999: - raise ValueError("Shutter open duration is too long") - self.sendcmd("open={}".format(newval)) - self.read() - - @property - def shut_time(self): + + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units milliseconds. + :type: `~quantities.quantity.Quantity` """ + ) + + shut_time = unitful_property( + "shut", + pq.ms, + format_code="{:.0f}", + set_fmt="{}={}", + valid_range=(0, 999999), + doc=""" Gets/sets the amount of time that the shutter is closed, in ms - - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units milliseconds. - :type: `~quantities.Quantity` + + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units milliseconds. + :type: `~quantities.quantity.Quantity` """ - response = self.check_command("shut?") - if not response is "CMD_NOT_DEFINED": - return float(response)*pq.ms - @shut_time.setter - def shut_time(self, newval): - newval = int(assume_units(newval, pq.ms).rescale(pq.ms).magnitude) - if newval < 0: - raise ValueError("Time cannot be negative") - if newval >999999: - raise ValueError("Duration is too long") - self.sendcmd("shut={}".format(newval)) - self.read() - + ) + @property def baud_rate(self): """ Gets/sets the instrument baud rate. - + Valid baud rates are 9600 and 115200. - + :type: `int` """ - response = self.check_command("baud?") - if not response is "CMD_NOT_DEFINED": - return 115200 if response else 9600 + response = self.query("baud?") + return 115200 if int(response) else 9600 + @baud_rate.setter def baud_rate(self, newval): - if newval != 9600 and newval !=115200: + if newval != 9600 and newval != 115200: raise ValueError("Invalid baud rate mode") else: self.sendcmd("baud={}".format(0 if newval == 9600 else 1)) - self.read() - - @property - def closed(self): - """ + + closed = bool_property( + "closed", + "1", + "0", + readonly=True, + doc=""" Gets the shutter closed status. - + `True` represents the shutter is closed, and `False` for the shutter is open. - + :rtype: `bool` """ - response = self.check_command("closed?") - if not response is "CMD_NOT_DEFINED": - return True if int(response) is 1 else False - - @property - def interlock(self): - """ + ) + + interlock = bool_property( + "interlock", + "1", + "0", + readonly=True, + doc=""" Gets the interlock tripped status. - + Returns `True` if the interlock is tripped, and `False` otherwise. - + :rtype: `bool` """ - response = self.check_command("interlock?") - if not response is "CMD_NOT_DEFINED": - return True if int(response) is 1 else False + ) + + # Methods # - ## Methods ## - - def check_command(self,command): - """ - Checks for the \"Command error CMD_NOT_DEFINED\" error, which can sometimes occur if there were - incorrect terminators on the previous command. If the command is successful, it returns the value, - if not, it returns CMD_NOT_DEFINED - check_command will also clear out the query string - """ - response = self.query(command) - #This device will echo the commands sent, so another line must be read to catch the response. - response = self.read() - cmd_find = response.find("CMD_NOT_DEFINED") - if cmd_find ==-1: - error_find = response.find("CMD_ARG_INVALID") - if error_find ==-1: - output_str = response.replace(command,"") - output_str = output_str.replace(self.terminator,"") - output_str = output_str.replace(self.end_terminator,"") - else: - output_str = "CMD_ARG_INVALID" - else: - output_str = "CMD_NOT_DEFINED" - return output_str - def default(self): """ Restores instrument to factory settings. - + Returns 1 if successful, zero otherwise. - + :rtype: `int` """ - response = self.check_command("default") - if not response is "CMD_NOT_DEFINED": - return 1 - else: - return 0 + response = self.query("default") + return check_cmd(response) def save(self): """ Stores the parameters in static memory - + Returns 1 if successful, zero otherwise. - + :rtype: `int` """ - response = self.check_command("savp") - if not response is "CMD_NOT_DEFINED": - return 1 - else: - return 0 + response = self.query("savp") + return check_cmd(response) def save_mode(self): """ Stores output trigger mode and baud rate settings in memory. - + Returns 1 if successful, zero otherwise. - + :rtype: `int` """ - response = self.check_command("save") - if not response is "CMD_NOT_DEFINED": - return 1 - else: - return 0 + response = self.query("save") + return check_cmd(response) def restore(self): """ Loads the settings from memory. - + Returns 1 if successful, zero otherwise. - + :rtype: `int` """ - response = self.check_command("resp") - if not response is "CMD_NOT_DEFINED": - return 1 - else: - return 0 + response = self.query("resp") + return check_cmd(response) diff --git a/instruments/instruments/thorlabs/tc200.py b/instruments/instruments/thorlabs/tc200.py new file mode 100644 index 000000000..a66b9edd9 --- /dev/null +++ b/instruments/instruments/thorlabs/tc200.py @@ -0,0 +1,325 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# tc200.py: class for the Thorlabs TC200 Temperature Controller +# +# © 2016 Steven Casagrande (scasagrande@galvant.ca). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# TC200 Class contributed by Catherine Holloway +# + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division + +import quantities as pq +from flufl.enum import IntEnum + +from instruments.abstract_instruments import Instrument +from instruments.util_fns import assume_units, convert_temperature + +# CLASSES ##################################################################### + + +class TC200(Instrument): + + """ + The TC200 is is a controller for the voltage across a heating element. + It can also read in the temperature off of a thermistor and implements + a PID control to keep the temperature at a set value. + + The user manual can be found here: + http://www.thorlabs.com/thorcat/12500/TC200-Manual.pdf + """ + + def __init__(self, filelike): + super(TC200, self).__init__(filelike) + self.terminator = "\r" + self.prompt = ">" + + def _ack_expected(self, msg=""): + return msg + + # ENUMS # + + class Mode(IntEnum): + normal = 0 + cycle = 1 + + class Sensor(IntEnum): + ptc100 = 0 + ptc1000 = 1 + th10k = 2 + ntc10k = 3 + + # PROPERTIES # + + def name(self): + """ + Gets the name and version number of the device + + :return: the name string of the device + :rtype: str + """ + response = self.query("*idn?") + return response + + @property + def mode(self): + """ + Gets/sets the output mode of the TC200 + + :type: `TC200.Mode` + """ + response = self.query("stat?") + response_code = (int(response) >> 1) % 2 + return TC200.Mode[response_code] + + @mode.setter + def mode(self, newval): + if not hasattr(newval, 'enum'): + raise TypeError("Mode setting must be a `TC200.Mode` value, " + "got {} instead.".format(type(newval))) + if newval.enum is not TC200.Mode: + raise TypeError("Mode setting must be a `TC200.Mode` value, " + "got {} instead.".format(type(newval))) + out_query = "mode={}".format(newval.name) + self.sendcmd(out_query) + + @property + def enable(self): + """ + Gets/sets the heater enable status. + + If output enable is on (`True`), there is a voltage on the output. + + :type: `bool` + """ + response = self.query("stat?") + return True if int(response) % 2 is 1 else False + + @enable.setter + def enable(self, newval): + if not isinstance(newval, bool): + raise TypeError("TC200 enable property must be specified with a " + "boolean.") + # the "ens" command is a toggle, we need to track two different cases, + # when it should be on and it is off, and when it is off and + # should be on + if newval and not self.enable: + self.sendcmd("ens") + elif not newval and self.enable: + self.sendcmd("ens") + + @property + def temperature(self): + """ + Gets/sets the temperature + + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + to be of units degrees C. + :type: `~quantities.quantity.Quantity` or `int` + :return: the temperature (in degrees C) + :rtype: `~quantities.quantity.Quantity` + """ + response = self.query("tact?").replace( + " C", "").replace(" F", "").replace(" K", "") + return float(response) * pq.degC + + @temperature.setter + def temperature(self, newval): + # the set temperature is always in celsius + newval = convert_temperature(newval, pq.degC).magnitude + if newval < 20.0 or newval > self.max_temperature: + raise ValueError("Temperature is out of range.") + out_query = "tset={}".format(newval) + self.sendcmd(out_query) + + @property + def p(self): + """ + Gets/sets the p-gain. Valid numbers are [1,250]. + + :return: the p-gain (in nnn) + :rtype: `int` + """ + response = self.query("pid?") + return int(response.split(" ")[0]) + + @p.setter + def p(self, newval): + if newval < 1: + raise ValueError("P-Value is too low.") + if newval > 250: + raise ValueError("P-Value is too high") + self.sendcmd("pgain={}".format(newval)) + + @property + def i(self): + """ + Gets/sets the i-gain. Valid numbers are [1,250] + + :return: the i-gain (in nnn) + :rtype: `int` + """ + response = self.query("pid?") + return int(response.split(" ")[1]) + + @i.setter + def i(self, newval): + if newval < 0: + raise ValueError("I-Value is too low.") + if newval > 250: + raise ValueError("I-Value is too high") + self.sendcmd("igain={}".format(newval)) + + @property + def d(self): + """ + Gets/sets the d-gain. Valid numbers are [0, 250] + + :return: the d-gain (in nnn) + :type: `int` + """ + response = self.query("pid?") + return int(response.split(" ")[2]) + + @d.setter + def d(self, newval): + if newval < 0: + raise ValueError("D-Value is too low.") + if newval > 250: + raise ValueError("D-Value is too high") + self.sendcmd("dgain={}".format(newval)) + + @property + def degrees(self): + """ + Gets/sets the units of the temperature measurement. + + :return: The temperature units (degC/F/K) the TC200 is measuring in + :type: `~quantities.unitquantity.UnitTemperature` + """ + response = self.query("stat?") + response = int(response) + if (response >> 4) % 2 and (response >> 5) % 2: + return pq.degC + elif (response >> 5) % 2: + return pq.degK + else: + return pq.degF + + @degrees.setter + def degrees(self, newval): + if newval is pq.degC: + self.sendcmd("unit=c") + elif newval is pq.degF: + self.sendcmd("unit=f") + elif newval is pq.degK: + self.sendcmd("unit=k") + else: + raise TypeError("Invalid temperature type") + + @property + def sensor(self): + """ + Gets/sets the current thermistor type. Used for converting resistances + to temperatures. + + :return: The thermistor type + :type: `TC200.Sensor` + """ + response = self.query("sns?") + response = response.split(",")[0].replace( + "Sensor = ", '').replace(self.terminator, "").replace(" ", "") + return TC200.Sensor(response.lower()) + + @sensor.setter + def sensor(self, newval): + if not hasattr(newval, 'enum'): + raise TypeError("Sensor setting must be a `TC200.Sensor` value, " + "got {} instead.".format(type(newval))) + + if newval.enum is not TC200.Sensor: + raise TypeError("Sensor setting must be a `TC200.Sensor` value, " + "got {} instead.".format(type(newval))) + self.sendcmd("sns={}".format(newval.name)) + + @property + def beta(self): + """ + Gets/sets the beta value of the thermistor curve. + + :return: the gain (in nnn) + :type: `int` + """ + response = self.query("beta?") + return int(response) + + @beta.setter + def beta(self, newval): + if newval < 2000: + raise ValueError("Beta Value is too low.") + if newval > 6000: + raise ValueError("Beta Value is too high") + self.sendcmd("beta={}".format(newval)) + + @property + def max_power(self): + """ + Gets/sets the maximum power + + :return: The maximum power + :units: Watts (linear units) + :type: `~quantities.quantity.Quantity` + """ + response = self.query("pmax?") + return float(response) * pq.W + + @max_power.setter + def max_power(self, newval): + newval = assume_units(newval, pq.W).rescale(pq.W).magnitude + if newval < 0.1: + raise ValueError("Power is too low.") + if newval > 18.0: + raise ValueError("Power is too high") + self.sendcmd("PMAX={}".format(newval)) + + @property + def max_temperature(self): + """ + Gets/sets the maximum temperature + + :return: the maximum temperature (in deg C) + :units: As specified or assumed to be degree Celsius. Returns with + units degC. + :rtype: `~quantities.quantity.Quantity` + """ + response = self.query("tmax?").replace(" C", "") + return float(response) * pq.degC + + @max_temperature.setter + def max_temperature(self, newval): + newval = convert_temperature(newval, pq.degC).magnitude + if newval < 20: + raise ValueError("Temperature is too low.") + if newval > 205.0: + raise ValueError("Temperature is too high") + self.sendcmd("TMAX={}".format(newval)) diff --git a/instruments/instruments/thorlabs/thorlabs_utils.py b/instruments/instruments/thorlabs/thorlabs_utils.py new file mode 100644 index 000000000..f75aa94f0 --- /dev/null +++ b/instruments/instruments/thorlabs/thorlabs_utils.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# thorlabs_utils.py: Utility functions for Thorlabs-brand instruments. +# +# © 2016 Steven Casagrande (scasagrande@galvant.ca). +# +# This file is a part of the InstrumentKit project. +# Licensed under the AGPL version 3. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + + +def check_cmd(response): + """ + Checks the for the two common Thorlabs error messages; CMD_NOT_DEFINED and + CMD_ARG_INVALID + + :param response: the response from the device + :return: 1 if not found, 0 otherwise + :rtype: int + """ + if response != "CMD_NOT_DEFINED" and response != "CMD_ARG_INVALID": + return 1 + else: + return 0 diff --git a/instruments/instruments/thorlabs/thorlabsapt.py b/instruments/instruments/thorlabs/thorlabsapt.py index e75d2a243..a1ecca67b 100644 --- a/instruments/instruments/thorlabs/thorlabsapt.py +++ b/instruments/instruments/thorlabs/thorlabsapt.py @@ -3,7 +3,7 @@ ## # thorlabsapt.py: Driver for the Thorlabs APT Controller. ## -# © 2013 Steven Casagrande (scasagrande@galvant.ca). +# © 2013-2016 Steven Casagrande (scasagrande@galvant.ca). # # This file is a part of the InstrumentKit project. # Licensed under the AGPL version 3. @@ -22,24 +22,20 @@ # along with this program. If not, see . ## -## FEATURES #################################################################### +# IMPORTS ##################################################################### +from __future__ import absolute_import from __future__ import division +from builtins import range -## IMPORTS ##################################################################### - -from instruments.thorlabs import _abstract -from instruments.thorlabs import _packets -from instruments.thorlabs import _cmds - -from flufl.enum import IntEnum +import re +import struct import quantities as pq -import re -import struct +from instruments.thorlabs import _abstract, _packets, _cmds -## LOGGING ##################################################################### +# LOGGING ##################################################################### import logging from instruments.util_fns import NullHandler @@ -47,7 +43,8 @@ logger = logging.getLogger(__name__) logger.addHandler(NullHandler()) -## CLASSES ##################################################################### +# CLASSES ##################################################################### + class ThorLabsAPT(_abstract.ThorLabsInstrument): ''' @@ -143,7 +140,7 @@ def __init__(self, filelike): # Create a tuple of channels of length _n_channel_type if self._n_channels > 0: - self._channel = list(self._channel_type(self, chan_idx) for chan_idx in xrange(self._n_channels) ) + self._channel = list(self._channel_type(self, chan_idx) for chan_idx in range(self._n_channels) ) @property def serial_number(self): @@ -175,7 +172,7 @@ def n_channels(self, nch): # If we remove channels, remove them from the end of the list. if nch > self._n_channels: self._channel = self._channel + \ - list( self._channel_type(self, chan_idx) for chan_idx in xrange(self._n_channels, nch) ) + list( self._channel_type(self, chan_idx) for chan_idx in range(self._n_channels, nch) ) elif nch < self._n_channels: self._channel = self._channel[:nch] self._n_channels = nch @@ -248,7 +245,7 @@ def led_intensity(self, intensity): param2=None, dest=self._dest, source=0x01, - data=struct.pack(' 0)) - for key, bit_mask in self.__STATUS_BIT_MASK.iteritems() + for key, bit_mask in self.__STATUS_BIT_MASK.items() ) return status_dict diff --git a/instruments/instruments/util_fns.py b/instruments/instruments/util_fns.py index a46362d3b..1b2410217 100644 --- a/instruments/instruments/util_fns.py +++ b/instruments/instruments/util_fns.py @@ -54,6 +54,41 @@ def assume_units(value, units): value = pq.Quantity(value, units) return value + +def convert_temperature(temperature, base): + """ + convert the temperature to the specified base + :param temperature: a quantity with units of Kelvin, Celsius, or Fahrenheit + :type temperature: `quantities.Quantity` + :param base: a temperature unit to convert to + :type base: `unitquantity.UnitTemperature` + :return: the converted temperature + :rtype: `quantities.Quantity` + """ + # quantities reports equivalence between degC and degK, so a string comparison is needed + newval = assume_units(temperature, pq.degC) + if newval.units == pq.degF and str(base).split(" ")[1] == 'degC': + return ((newval.magnitude-32.0)*5.0/9.0)*base + elif str(newval.units).split(" ")[1] == 'K' and str(base).split(" ")[1] == 'degC': + return (newval.magnitude-273.15)*base + elif str(newval.units).split(" ")[1] == 'K' and base == pq.degF: + return (newval.magnitude/1.8-459/57)*base + elif str(newval.units).split(" ")[1] == 'degC' and base == pq.degF: + return (newval.magnitude*9.0/5.0+32.0)*base + elif newval.units == pq.degF and str(base).split(" ")[1] == 'K': + return ((newval.magnitude+459.57)*5.0/9.0)*base + elif str(newval.units).split(" ")[1] == 'degC' and str(base).split(" ")[1] == 'K': + return (newval.magnitude+273.15)*base + elif str(newval.units).split(" ")[1] == 'degC' and str(base).split(" ")[1] == 'degC': + return newval + elif newval.units == pq.degF and base == pq.degF: + return newval + elif str(newval.units).split(" ")[1] == 'K' and str(base).split(" ")[1] == 'K': + return newval + else: + raise ValueError("Unable to convert "+str(newval.units)+" to "+str(base)) + + def split_unit_str(s, default_units=pq.dimensionless, lookup=None): """ Given a string of the form "12 C" or "14.7 GHz", returns a tuple of the @@ -140,6 +175,9 @@ def bool_property(name, inst_true, inst_false, doc=None, readonly=False, writeon def getter(self): return self.query(name + "?").strip() == inst_true def setter(self, newval): + if not isinstance(newval, bool): + raise TypeError("Bool properties must be specified with a " + "boolean value") self.sendcmd(set_fmt.format(name, inst_true if newval else inst_false)) return rproperty(fget=getter, fset=setter, doc=doc, readonly=readonly, writeonly=writeonly) @@ -177,6 +215,10 @@ def out_decor_fcn(val): def getter(self): return enum[in_decor_fcn(self.query("{}?".format(name)).strip())] def setter(self, newval): + try: + enum[newval] + except ValueError: + raise ValueError("Enum property new value not in enum.") self.sendcmd(set_fmt.format(name, out_decor_fcn(enum[newval].value))) return rproperty(fget=getter, fset=setter, doc=doc, readonly=readonly, writeonly=writeonly) @@ -247,7 +289,7 @@ def setter(self, newval): return rproperty(fget=getter, fset=setter, doc=doc, readonly=readonly, writeonly=writeonly) -def unitful_property(name, units, format_code='{:e}', doc=None, readonly=False, writeonly=False, set_fmt="{} {}"): +def unitful_property(name, units, format_code='{:e}', doc=None, readonly=False, writeonly=False, set_fmt="{} {}", valid_range=(None,None)): """ Called inside of SCPI classes to instantiate properties with unitful numeric values. This function assumes that the instrument only accepts @@ -270,11 +312,23 @@ def unitful_property(name, units, format_code='{:e}', doc=None, readonly=False, non-query to the instrument. The default is "{} {}" which places a space between the SCPI command the associated parameter. By switching to "{}={}" an equals sign would instead be used as the separator. + :param valid_range: Tuple containing min & max values when setting + the property. Index 0 is minimum value, index 1 is maximum value. + Setting `None` in either disables bounds checking for that end of the + range. The default of `(None, None)` has no min or max constraints. + The valid set is inclusive of the values provided. + :type valid_range: `tuple` or `list` of `int` or `float` """ def getter(self): raw = self.query("{}?".format(name)) return float(raw) * units def setter(self, newval): + if valid_range[0] is not None and newval < valid_range[0]: + raise ValueError("Unitful quantity is too low. Got {}, minimum " + "value is {}".format(newval, valid_range[0])) + if valid_range[1] is not None and newval > valid_range[1]: + raise ValueError("Unitful quantity is too high. Got {}, maximum " + "value is {}".format(newval, valid_range[1])) # Rescale to the correct unit before printing. This will also catch bad units. strval = format_code.format(assume_units(newval, units).rescale(units).item()) self.sendcmd(set_fmt.format(name, strval)) diff --git a/instruments/requirements.txt b/instruments/requirements.txt index e4813b160..b560bbf7c 100644 --- a/instruments/requirements.txt +++ b/instruments/requirements.txt @@ -2,4 +2,4 @@ numpy pyserial quantities flufl.enum - +future