From fad35d1c89e9f4a7b0dbb76872e24ee762057fb1 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Thu, 7 Feb 2019 17:31:39 -0500 Subject: [PATCH 01/86] Drop support for Python 3.4 (#200) --- .travis.yml | 1 - README.rst | 4 ++-- setup.py | 1 - tox.ini | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b46392035..1d34c5b7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ sudo: false language: python python: - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" diff --git a/README.rst b/README.rst index bd4e5d44c..d37d1674a 100644 --- a/README.rst +++ b/README.rst @@ -111,7 +111,7 @@ send, one can use the following functions to do so: Python Version Compatibility ---------------------------- -At this time, Python 2.7, 3.4, 3.5, 3.6, and 3.7 are supported. Should you encounter +At this time, Python 2.7, 3.5, 3.6, and 3.7 are supported. Should you encounter any problems with this library that occur in one version or another, please do not hesitate to let us know. @@ -134,7 +134,7 @@ To run the tests against all supported version of Python, you will need to have the binary for each installed, as well as any requirements needed to install ``numpy`` under each Python version. On Debian/Ubuntu systems this means you will need to install the ``python-dev`` package for each version of Python -supported (``python2.7-dev``, ``python3.4-dev``, etc). +supported (``python2.7-dev``, ``python3.7-dev``, etc). With the required system packages installed, all tests can be run with ``tox``: diff --git a/setup.py b/setup.py index 7e5e6b1c4..f7f6f75c0 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,6 @@ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", diff --git a/tox.ini b/tox.ini index 508210fb1..d4a105448 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,py37 +envlist = py27,py35,py36,py37 [testenv] deps = -rdev-requirements.txt commands = pytest From c081174c1d227ff723ac047c8fcdb2a17e84b29f Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Fri, 8 Feb 2019 16:20:41 -0500 Subject: [PATCH 02/86] Fix issue #197 (#201) Where checking if value is in enum will raise value error in Py38 --- instruments/hp/hp6632b.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/instruments/hp/hp6632b.py b/instruments/hp/hp6632b.py index b07407293..06e352558 100644 --- a/instruments/hp/hp6632b.py +++ b/instruments/hp/hp6632b.py @@ -468,6 +468,9 @@ def check_error_queue(self): done = True else: result.append( - self.ErrorCodes(err) if err in self.ErrorCodes else err) + self.ErrorCodes(err) + if any(err == item.value for item in self.ErrorCodes) + else err + ) return result From ca5fb15eac3e761c3829cab2f1945d67b8f15c1d Mon Sep 17 00:00:00 2001 From: Chris Granade Date: Sat, 9 Feb 2019 16:10:24 -0800 Subject: [PATCH 03/86] Improvements to APT (#167) * Added driver/motor tables for T/KDC001 APT devs. * Moved TODO comment to avoid pylint error. * Misc Py3k changes for ThorLabs APT * motion_timeout for APT motor cmds, fix scale factor * ThorLabsAPT: Example of new config support. * More pylint fixes * Fix for line continuation convention. * Rearranged imports into standard order. * Added an APT test. Not working yet. * Fix linting issues * New handling in loopback for empty terminator. * struct.Struct for contents of hw_info packets * Support for specifying expected apt pkt sizes * Fixes to APT and APT tests * Missed a conflict marker. * Fixed bug due to `if size` falling through on size == 0. * Removed trailing whitespace. * Locked requirements.txt; see #174. * Remove numpy version pinning in requirements.txt * Add tests to cover additional loopback comm behaviour * Make pylint happy * Revert changes to size=0 behaviour in loopback comm --- .../comm/loopback_communicator.py | 22 +++-- .../comm/serial_communicator.py | 16 +++- instruments/tests/test_comm/test_loopback.py | 19 ++++ .../tests/test_thorlabs/test_thorlabs_apt.py | 82 +++++++++++++++++ instruments/thorlabs/_abstract.py | 43 ++++++++- instruments/thorlabs/_packets.py | 14 ++- instruments/thorlabs/thorlabsapt.py | 92 ++++++++++++++++--- 7 files changed, 256 insertions(+), 32 deletions(-) create mode 100644 instruments/tests/test_thorlabs/test_thorlabs_apt.py diff --git a/instruments/abstract_instruments/comm/loopback_communicator.py b/instruments/abstract_instruments/comm/loopback_communicator.py index 85020ec3c..029c9d34f 100644 --- a/instruments/abstract_instruments/comm/loopback_communicator.py +++ b/instruments/abstract_instruments/comm/loopback_communicator.py @@ -108,17 +108,21 @@ def read_raw(self, size=-1): :rtype: `bytes` """ if self._stdin is not None: - if size >= 0: + if size == -1 or size is None: + result = bytes() + if self._terminator: + while result.endswith(self._terminator.encode("utf-8")) is False: + c = self._stdin.read(1) + if c == b'': + break + result += c + return result[:-len(self._terminator)] + return self._stdin.read(-1) + + elif size >= 0: input_var = self._stdin.read(size) return bytes(input_var) - elif size == -1: - result = bytes() - while result.endswith(self._terminator.encode("utf-8")) is False: - c = self._stdin.read(1) - if c == b'': - break - result += c - return result[:-len(self._terminator)] + else: raise ValueError("Must read a positive value of characters.") else: diff --git a/instruments/abstract_instruments/comm/serial_communicator.py b/instruments/abstract_instruments/comm/serial_communicator.py index 02b88f23c..40bc5b30d 100644 --- a/instruments/abstract_instruments/comm/serial_communicator.py +++ b/instruments/abstract_instruments/comm/serial_communicator.py @@ -118,13 +118,23 @@ def read_raw(self, size=-1): return resp elif size == -1: result = bytes() - while result.endswith(self._terminator.encode("utf-8")) is False: + # If the terminator is empty, we can't use endswith, but must + # read as many bytes as are available. + # On the other hand, if terminator is nonempty, we can check + # that the tail end of the buffer matches it. + c = None + term = self._terminator.encode('utf-8') if self._terminator else None + while not ( + result.endswith(term) + if term is not None else + c == b'' + ): c = self._conn.read(1) - if c == b'': + if c == b'' and term is not None: raise IOError("Serial connection timed out before reading " "a termination character.") result += c - return result[:-len(self._terminator)] + return result[:-len(term)] if term is not None else result else: raise ValueError("Must read a positive value of characters.") diff --git a/instruments/tests/test_comm/test_loopback.py b/instruments/tests/test_comm/test_loopback.py index c02c36024..943fd6a0e 100644 --- a/instruments/tests/test_comm/test_loopback.py +++ b/instruments/tests/test_comm/test_loopback.py @@ -99,6 +99,25 @@ def test_loopbackcomm_read_raw_2char_terminator(): assert mock_stdin.read.call_count == 5 +def test_loopbackcomm_read_raw_terminator_is_empty_string(): + mock_stdin = mock.MagicMock() + mock_stdin.read.side_effect = [b"abc"] + comm = LoopbackCommunicator(stdin=mock_stdin) + comm._terminator = "" + + assert comm.read_raw() == b"abc" + mock_stdin.read.assert_has_calls([mock.call(-1)]) + assert mock_stdin.read.call_count == 1 + + +def test_loopbackcomm_read_raw_size_invalid(): + with pytest.raises(ValueError): + mock_stdin = mock.MagicMock() + mock_stdin.read.side_effect = [b"abc"] + comm = LoopbackCommunicator(stdin=mock_stdin) + comm.read_raw(size=-2) + + def test_loopbackcomm_write_raw(): mock_stdout = mock.MagicMock() comm = LoopbackCommunicator(stdout=mock_stdout) diff --git a/instruments/tests/test_thorlabs/test_thorlabs_apt.py b/instruments/tests/test_thorlabs/test_thorlabs_apt.py new file mode 100644 index 000000000..286d585f0 --- /dev/null +++ b/instruments/tests/test_thorlabs/test_thorlabs_apt.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the Thorlabs TC200 +""" + +# IMPORTS #################################################################### + +# pylint: disable=unused-import + +from __future__ import absolute_import + +import struct + +import pytest +import quantities as pq + +import instruments as ik +from instruments.thorlabs._packets import ThorLabsPacket, hw_info_data +from instruments.thorlabs._cmds import ThorLabsCommands +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + +# pylint: disable=protected-access,unused-argument + + +def test_apt_hw_info(): + with expected_protocol( + ik.thorlabs.ThorLabsAPT, + [ + ThorLabsPacket( + message_id=ThorLabsCommands.HW_REQ_INFO, + param1=0x00, param2=0x00, + dest=0x50, + source=0x01, + data=None + ).pack() + ], + [ + ThorLabsPacket( + message_id=ThorLabsCommands.HW_GET_INFO, + dest=0x01, + source=0x50, + data=hw_info_data.pack( + # Serial number + b'\x01\x02\x03\x04', + # Model number + "ABC-123".encode('ascii'), + # HW type + 3, + # FW version, + 0xa1, 0xa2, 0xa3, + # Notes + "abcdefg".encode('ascii'), + # HW version + 42, + # Mod state + 43, + # Number of channels + 2 + ) + ).pack() + ], + sep="" + ) as apt: + # Check internal representations. + # NB: we shouldn't do this in some sense, but these fields + # act as an API to the APT subclasses. + assert apt._hw_type == "Unknown type: 3" + assert apt._fw_version == "a1.a2.a3" + assert apt._notes == "abcdefg" + assert apt._hw_version == 42 + assert apt._mod_state == 43 + + # Check external API. + assert apt.serial_number == '01020304' + assert apt.model_number == 'ABC-123' + assert apt.name == ( + "ThorLabs APT Instrument model ABC-123, " + "serial 01020304 (HW version 42, FW version a1.a2.a3)" + ) diff --git a/instruments/thorlabs/_abstract.py b/instruments/thorlabs/_abstract.py index 820950dac..423e1d465 100644 --- a/instruments/thorlabs/_abstract.py +++ b/instruments/thorlabs/_abstract.py @@ -9,8 +9,13 @@ from __future__ import absolute_import from __future__ import division +import time + from instruments.thorlabs import _packets from instruments.abstract_instruments.instrument import Instrument +from instruments.util_fns import assume_units + +from quantities import second # CLASSES ##################################################################### @@ -35,10 +40,10 @@ def sendpacket(self, packet): :param packet: The thorlabs data packet that will be queried :type packet: `ThorLabsPacket` """ - self.sendcmd(packet.pack()) + self._file.write_raw(packet.pack()) # pylint: disable=protected-access - def querypacket(self, packet, expect=None): + def querypacket(self, packet, expect=None, timeout=None, expect_data_len=None): """ Sends a packet to the connected APT instrument, and waits for a packet in response. Optionally, checks whether the received packet type is @@ -52,11 +57,40 @@ def querypacket(self, packet, expect=None): with the default value of `None` then no checking occurs. :type expect: `str` or `None` + :param timeout: Sets a timeout to wait before returning `None`, indicating + no packet was received. If the timeout is set to `None`, then the + timeout is inherited from the underlying communicator and no additional + timeout is added. If timeout is set to `False`, then this method waits + indefinitely. If timeout is set to a unitful quantity, then it is interpreted + as a time and used as the timeout value. Finally, if the timeout is a unitless + number (e.g. `float` or `int`), then seconds are assumed. + + :param int expect_data_len: Number of bytes to expect as the + data for the returned packet. + :return: Returns the response back from the instrument wrapped up in - a thorlabs packet + a ThorLabs APT packet, or None if no packet was received. :rtype: `ThorLabsPacket` """ - resp = self.query(packet.pack()) + t_start = time.time() + + if timeout: + timeout = assume_units(timeout, second).rescale('second').magnitude + + while True: + self._file.write_raw(packet.pack()) + resp = self._file.read_raw( + expect_data_len + 6 # the header is six bytes. + if expect_data_len else + 6 + ) + if resp or timeout is None: + break + else: + tic = time.time() + if tic - t_start > timeout: + break + if not resp: if expect is None: return None @@ -71,4 +105,5 @@ def querypacket(self, packet, expect=None): raise IOError("APT returned message ID {}, expected {}".format( pkt._message_id, expect )) + return pkt diff --git a/instruments/thorlabs/_packets.py b/instruments/thorlabs/_packets.py index 7a9ef8ded..5c4171970 100644 --- a/instruments/thorlabs/_packets.py +++ b/instruments/thorlabs/_packets.py @@ -15,6 +15,18 @@ message_header_nopacket = struct.Struct(' Date: Mon, 11 Feb 2019 16:08:24 -0500 Subject: [PATCH 04/86] Fix bug with SCPIFunctionGenerator.function, add tests (#202) * Fix bug with SCPIFunctionGenerator.function, add tests * Fix linting --- .../generic_scpi/scpi_function_generator.py | 4 +- .../test_scpi_function_generator.py | 80 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 instruments/tests/test_generic_scpi/test_scpi_function_generator.py diff --git a/instruments/generic_scpi/scpi_function_generator.py b/instruments/generic_scpi/scpi_function_generator.py index 4877168ae..122d29894 100644 --- a/instruments/generic_scpi/scpi_function_generator.py +++ b/instruments/generic_scpi/scpi_function_generator.py @@ -87,7 +87,7 @@ def _set_amplitude_(self, magnitude, units): function = enum_property( command="FUNC", - enum=lambda: Function, # pylint: disable=undefined-variable + enum=FunctionGenerator.Function, doc=""" Gets/sets the output function of the function generator @@ -103,7 +103,7 @@ def _set_amplitude_(self, magnitude, units): Set value should be within correct bounds of instrument. - :units: As specified (if a `~quntities.quantity.Quantity`) or assumed + :units: As specified (if a `~quantities.quantity.Quantity`) or assumed to be of units volts. :type: `~quantities.quantity.Quantity` with units volts. """ diff --git a/instruments/tests/test_generic_scpi/test_scpi_function_generator.py b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py new file mode 100644 index 000000000..6920a3f4b --- /dev/null +++ b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for generic SCPI function generator instruments +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +import quantities as pq + +import instruments as ik +from instruments.tests import expected_protocol, make_name_test + +# TESTS ###################################################################### + +test_scpi_func_gen_name = make_name_test(ik.generic_scpi.SCPIFunctionGenerator) + + +def test_scpi_func_gen_amplitude(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + [ + "VOLT:UNIT?", + "VOLT?", + "VOLT:UNIT VPP", + "VOLT 2.0", + "VOLT:UNIT DBM", + "VOLT 1.5" + ], [ + "VPP", + "+1.000000E+00" + ] + ) as fg: + assert fg.amplitude == (1 * pq.V, fg.VoltageMode.peak_to_peak) + fg.amplitude = 2 * pq.V + fg.amplitude = (1.5 * pq.V, fg.VoltageMode.dBm) + + +def test_scpi_func_gen_frequency(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + [ + "FREQ?", + "FREQ 1.005000e+02" + ], [ + "+1.234000E+03" + ] + ) as fg: + assert fg.frequency == 1234 * pq.Hz + fg.frequency = 100.5 * pq.Hz + + +def test_scpi_func_gen_function(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + [ + "FUNC?", + "FUNC SQU" + ], [ + "SIN" + ] + ) as fg: + assert fg.function == fg.Function.sinusoid + fg.function = fg.Function.square + + +def test_scpi_func_gen_offset(): + with expected_protocol( + ik.generic_scpi.SCPIFunctionGenerator, + [ + "VOLT:OFFS?", + "VOLT:OFFS 4.321000e-01" + ], [ + "+1.234000E+01", + ] + ) as fg: + assert fg.offset == 12.34 * pq.V + fg.offset = 0.4321 * pq.V From 13845c9d3d23cacdf7bc7c479431b67077254de6 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Mon, 11 Feb 2019 18:50:31 -0500 Subject: [PATCH 05/86] Fix Agilent 33220a, add tests (#203) --- instruments/agilent/agilent33220a.py | 14 +- .../tests/test_agilent/test_agilent_33220a.py | 169 ++++++++++++++++++ 2 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 instruments/tests/test_agilent/test_agilent_33220a.py diff --git a/instruments/agilent/agilent33220a.py b/instruments/agilent/agilent33220a.py index 8afb868cf..b5282414d 100644 --- a/instruments/agilent/agilent33220a.py +++ b/instruments/agilent/agilent33220a.py @@ -78,14 +78,6 @@ class OutputPolarity(Enum): # PROPERTIES # - @property - def frequency(self): - return super(Agilent33220a, self).frequency - - @frequency.setter - def frequency(self, newval): - super(Agilent33220a, self).frequency = newval - function = enum_property( command="FUNC", enum=Function, @@ -182,13 +174,11 @@ def load_resistance(self): def load_resistance(self, newval): if isinstance(newval, self.LoadResistance): newval = newval.value - elif isinstance(newval, int): + else: + newval = assume_units(newval, pq.ohm).rescale(pq.ohm).magnitude if (newval < 0) or (newval > 10000): raise ValueError( "Load resistance must be between 0 and 10,000") - newval = assume_units(newval, pq.ohm).rescale(pq.ohm).magnitude - else: - raise TypeError("Not a valid load resistance type.") self.sendcmd("OUTP:LOAD {}".format(newval)) @property diff --git a/instruments/tests/test_agilent/test_agilent_33220a.py b/instruments/tests/test_agilent/test_agilent_33220a.py new file mode 100644 index 000000000..425e03a19 --- /dev/null +++ b/instruments/tests/test_agilent/test_agilent_33220a.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for generic SCPI function generator instruments +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +import quantities as pq + +import instruments as ik +from instruments.tests import expected_protocol, make_name_test + +# TESTS ###################################################################### + +test_scpi_func_gen_name = make_name_test(ik.agilent.Agilent33220a) + + +def test_agilent33220a_amplitude(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "VOLT:UNIT?", + "VOLT?", + "VOLT:UNIT VPP", + "VOLT 2.0", + "VOLT:UNIT DBM", + "VOLT 1.5" + ], [ + "VPP", + "+1.000000E+00" + ] + ) as fg: + assert fg.amplitude == (1 * pq.V, fg.VoltageMode.peak_to_peak) + fg.amplitude = 2 * pq.V + fg.amplitude = (1.5 * pq.V, fg.VoltageMode.dBm) + + +def test_agilent33220a_frequency(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "FREQ?", + "FREQ 1.005000e+02" + ], [ + "+1.234000E+03" + ] + ) as fg: + assert fg.frequency == 1234 * pq.Hz + fg.frequency = 100.5 * pq.Hz + + +def test_agilent33220a_function(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "FUNC?", + "FUNC:SQU" + ], [ + "SIN" + ] + ) as fg: + assert fg.function == fg.Function.sinusoid + fg.function = fg.Function.square + + +def test_agilent33220a_offset(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "VOLT:OFFS?", + "VOLT:OFFS 4.321000e-01" + ], [ + "+1.234000E+01", + ] + ) as fg: + assert fg.offset == 12.34 * pq.V + fg.offset = 0.4321 * pq.V + + +def test_agilent33220a_duty_cycle(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "FUNC:SQU:DCYC?", + "FUNC:SQU:DCYC 75" + ], [ + "53", + ] + ) as fg: + assert fg.duty_cycle == 53 + fg.duty_cycle = 75 + + +def test_agilent33220a_ramp_symmetry(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "FUNC:RAMP:SYMM?", + "FUNC:RAMP:SYMM 75" + ], [ + "53", + ] + ) as fg: + assert fg.ramp_symmetry == 53 + fg.ramp_symmetry = 75 + + +def test_agilent33220a_output(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "OUTP?", + "OUTP OFF" + ], [ + "ON", + ] + ) as fg: + assert fg.output is True + fg.output = False + + +def test_agilent33220a_output_sync(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "OUTP:SYNC?", + "OUTP:SYNC OFF" + ], [ + "ON", + ] + ) as fg: + assert fg.output_sync is True + fg.output_sync = False + + +def test_agilent33220a_output_polarity(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "OUTP:POL?", + "OUTP:POL NORM" + ], [ + "INV", + ] + ) as fg: + assert fg.output_polarity == fg.OutputPolarity.inverted + fg.output_polarity = fg.OutputPolarity.normal + + +def test_agilent33220a_load_resistance(): + with expected_protocol( + ik.agilent.Agilent33220a, + [ + "OUTP:LOAD?", + "OUTP:LOAD?", + "OUTP:LOAD 100.0", + "OUTP:LOAD MAX" + ], [ + "50", + "INF" + ] + ) as fg: + assert fg.load_resistance == 50 * pq.Ohm + assert fg.load_resistance == fg.LoadResistance.high_impedance + fg.load_resistance = 100 * pq.Ohm + fg.load_resistance = fg.LoadResistance.maximum From 7ccdc131091a0f19bacb9cc2ee52c2f13c0443ff Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Thu, 14 Feb 2019 15:24:54 -0500 Subject: [PATCH 06/86] Function Generator single/multi-channel consistency (#206) * Function Generator single/multi-channel consistency * Fix Py27 linting issue * Fix linting issue * Add tests for FunctionGenerator abstract instrument --- dev-requirements.txt | 1 + .../function_generator.py | 291 +++++++++++++----- instruments/tests/__init__.py | 17 +- .../test_function_generator.py | 171 ++++++++++ .../test_scpi_function_generator.py | 25 +- 5 files changed, 425 insertions(+), 80 deletions(-) create mode 100644 instruments/tests/test_abstract_inst/test_function_generator.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 4fe922cad..b4082436b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,6 @@ mock pytest +pytest-mock hypothesis pylint==1.7.1 astroid==1.5.3 diff --git a/instruments/abstract_instruments/function_generator.py b/instruments/abstract_instruments/function_generator.py index e9baa64be..107d6591f 100644 --- a/instruments/abstract_instruments/function_generator.py +++ b/instruments/abstract_instruments/function_generator.py @@ -12,13 +12,13 @@ import abc from enum import Enum +from builtins import range from future.utils import with_metaclass import quantities as pq - from instruments.abstract_instruments import Instrument import instruments.units as u -from instruments.util_fns import assume_units +from instruments.util_fns import assume_units, ProxyList # CLASSES ##################################################################### @@ -32,6 +32,175 @@ class FunctionGenerator(with_metaclass(abc.ABCMeta, Instrument)): provide a consistent interface to the user. """ + def __init__(self, filelike): + super(FunctionGenerator, self).__init__(filelike) + self._channel_count = 1 + + # pylint:disable=protected-access + class Channel(with_metaclass(abc.ABCMeta, object)): + """ + Abstract base class for physical channels on a function generator. + + All applicable concrete instruments should inherit from this ABC to + provide a consistent interface to the user. + + Function generators that only have a single channel do not need to + define their own concrete implementation of this class. Ones with + multiple channels need their own definition of this class, where + this class contains the concrete implementations of the below + abstract methods. Instruments with 1 channel have their concrete + implementations at the parent instrument level. + """ + def __init__(self, parent, name): + self._parent = parent + self._name = name + + # ABSTRACT PROPERTIES # + + @property + def frequency(self): + """ + Gets/sets the the output frequency of the function generator. This is + an abstract property. + + :type: `~quantities.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.frequency + else: + raise NotImplementedError() + + @frequency.setter + def frequency(self, newval): + if self._parent._channel_count == 1: + self._parent.frequency = newval + else: + raise NotImplementedError() + + @property + def function(self): + """ + Gets/sets the output function mode of the function generator. This is + an abstract property. + + :type: `~enum.Enum` + """ + if self._parent._channel_count == 1: + return self._parent.function + else: + raise NotImplementedError() + + @function.setter + def function(self, newval): + if self._parent._channel_count == 1: + self._parent.function = newval + else: + raise NotImplementedError() + + @property + def offset(self): + """ + Gets/sets the output offset voltage of the function generator. This is + an abstract property. + + :type: `~quantities.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.offset + else: + raise NotImplementedError() + + @offset.setter + def offset(self, newval): + if self._parent._channel_count == 1: + self._parent.offset = newval + else: + raise NotImplementedError() + + @property + def phase(self): + """ + Gets/sets the output phase of the function generator. This is an + abstract property. + + :type: `~quantities.Quantity` + """ + if self._parent._channel_count == 1: + return self._parent.phase + else: + raise NotImplementedError() + + @phase.setter + def phase(self, newval): + if self._parent._channel_count == 1: + self._parent.phase = newval + else: + raise NotImplementedError() + + def _get_amplitude_(self): + if self._parent._channel_count == 1: + return self._parent._get_amplitude_() + else: + raise NotImplementedError() + + def _set_amplitude_(self, magnitude, units): + if self._parent._channel_count == 1: + self._parent._set_amplitude_(magnitude=magnitude, units=units) + else: + raise NotImplementedError() + + @property + def amplitude(self): + """ + Gets/sets the output amplitude of the function generator. + + If set with units of :math:`\\text{dBm}`, then no voltage mode can + be passed. + + If set with units of :math:`\\text{V}` as a `~quantities.Quantity` or a + `float` without a voltage mode, then the voltage mode is assumed to be + peak-to-peak. + + :units: As specified, or assumed to be :math:`\\text{V}` if not + specified. + :type: Either a `tuple` of a `~quantities.Quantity` and a + `FunctionGenerator.VoltageMode`, or a `~quantities.Quantity` + if no voltage mode applies. + """ + mag, units = self._get_amplitude_() + + if units == self._parent.VoltageMode.dBm: + return pq.Quantity(mag, u.dBm) + + return pq.Quantity(mag, pq.V), units + + @amplitude.setter + def amplitude(self, newval): + # Try and rescale to dBm... if it succeeds, set the magnitude + # and units accordingly, otherwise handle as a voltage. + try: + newval_dbm = newval.rescale(u.dBm) + mag = float(newval_dbm.magnitude) + units = self._parent.VoltageMode.dBm + except (AttributeError, ValueError): + # OK, we have volts. Now, do we have a tuple? If not, assume Vpp. + if not isinstance(newval, tuple): + mag = newval + units = self._parent.VoltageMode.peak_to_peak + else: + mag, units = newval + + # Finally, convert the magnitude out to a float. + mag = float(assume_units(mag, pq.V).rescale(pq.V).magnitude) + + self._set_amplitude_(mag, units) + + def sendcmd(self, cmd): + self._parent.sendcmd(cmd) + + def query(self, cmd, size=-1): + return self._parent.query(cmd, size) + # ENUMS # class VoltageMode(Enum): @@ -53,20 +222,27 @@ class Function(Enum): noise = 'NOIS' arbitrary = 'ARB' - # ABSTRACT METHODS # + @property + def channel(self): + return ProxyList(self, self.Channel, range(self._channel_count)) + + # PASSTHROUGH PROPERTIES # + + @property + def amplitude(self): + return self.channel[0].amplitude + + @amplitude.setter + def amplitude(self, newval): + self.channel[0].amplitude = newval - @abc.abstractmethod def _get_amplitude_(self): - pass + raise NotImplementedError() - @abc.abstractmethod def _set_amplitude_(self, magnitude, units): - pass - - # ABSTRACT PROPERTIES # + raise NotImplementedError() @property - @abc.abstractmethod def frequency(self): """ Gets/sets the the output frequency of the function generator. This is @@ -74,15 +250,19 @@ def frequency(self): :type: `~quantities.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].frequency + else: + raise NotImplementedError() @frequency.setter - @abc.abstractmethod def frequency(self, newval): - pass + if self._channel_count > 1: + self.channel[0].frequency = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def function(self): """ Gets/sets the output function mode of the function generator. This is @@ -90,15 +270,19 @@ def function(self): :type: `~enum.Enum` """ - pass + if self._channel_count > 1: + return self.channel[0].function + else: + raise NotImplementedError() @function.setter - @abc.abstractmethod def function(self, newval): - pass + if self._channel_count > 1: + self.channel[0].function = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def offset(self): """ Gets/sets the output offset voltage of the function generator. This is @@ -106,15 +290,19 @@ def offset(self): :type: `~quantities.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].offset + else: + raise NotImplementedError() @offset.setter - @abc.abstractmethod def offset(self, newval): - pass + if self._channel_count > 1: + self.channel[0].offset = newval + else: + raise NotImplementedError() @property - @abc.abstractmethod def phase(self): """ Gets/sets the output phase of the function generator. This is an @@ -122,57 +310,14 @@ def phase(self): :type: `~quantities.Quantity` """ - pass + if self._channel_count > 1: + return self.channel[0].phase + else: + raise NotImplementedError() @phase.setter - @abc.abstractmethod def phase(self, newval): - pass - - # CONCRETE PROPERTIES # - - @property - def amplitude(self): - """ - Gets/sets the output amplitude of the function generator. - - If set with units of :math:`\\text{dBm}`, then no voltage mode can - be passed. - - If set with units of :math:`\\text{V}` as a `~quantities.Quantity` or a - `float` without a voltage mode, then the voltage mode is assumed to be - peak-to-peak. - - :units: As specified, or assumed to be :math:`\\text{V}` if not - specified. - :type: Either a `tuple` of a `~quantities.Quantity` and a - `FunctionGenerator.VoltageMode`, or a `~quantities.Quantity` - if no voltage mode applies. - """ - mag, units = self._get_amplitude_() - - if units == self.VoltageMode.dBm: - return pq.Quantity(mag, u.dBm) - - return pq.Quantity(mag, pq.V), units - - @amplitude.setter - def amplitude(self, newval): - # Try and rescale to dBm... if it succeeds, set the magnitude - # and units accordingly, otherwise handle as a voltage. - try: - newval_dbm = newval.rescale(u.dBm) - mag = float(newval_dbm.magnitude) - units = self.VoltageMode.dBm - except (AttributeError, ValueError): - # OK, we have volts. Now, do we have a tuple? If not, assume Vpp. - if not isinstance(newval, tuple): - mag = newval - units = self.VoltageMode.peak_to_peak - else: - mag, units = newval - - # Finally, convert the magnitude out to a float. - mag = float(assume_units(mag, pq.V).rescale(pq.V).magnitude) - - self._set_amplitude_(mag, units) + if self._channel_count > 1: + self.channel[0].phase = newval + else: + raise NotImplementedError() diff --git a/instruments/tests/__init__.py b/instruments/tests/__init__.py index ea901d886..b54a355ad 100644 --- a/instruments/tests/__init__.py +++ b/instruments/tests/__init__.py @@ -26,7 +26,7 @@ @contextlib.contextmanager -def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): +def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n", repeat=1): """ Given an instrument class, expected output from the host and expected input from the instrument, asserts that the protocol in a context block proceeds @@ -35,7 +35,8 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): For an example of how to write tests using this context manager, see the ``make_name_test`` function below. - :param type ins_class: Instrument class to use for the protocol assertion. + :param ins_class: Instrument class to use for the protocol assertion. + :type ins_class: `~instruments.Instrument` :param host_to_ins: Data to be sent by the host to the instrument; this is checked against the actual data sent by the instrument class during the execution of this context manager. @@ -46,9 +47,17 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): be used to assert correct behaviour within the context. :type ins_to_host: ``str`` or ``list``; if ``list``, each line is concatenated with the separator given by ``sep``. + :param str sep: Character to be inserted after each string in both + host_to_ins and ins_to_host parameters. This is typically the + termination character you would like to have inserted. + :param int repeat: The number of times the host_to_ins and + ins_to_host data sets should be duplicated. Typically the default + value of 1 is sufficient, but increasing this is useful when + testing multiple calls in the same test that should have the same + command transactions. """ if isinstance(sep, bytes): - sep = sep.encode("utf-8") + sep = sep.decode("utf-8") # Normalize assertion and playback strings. if isinstance(ins_to_host, list): @@ -60,6 +69,7 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): (sep.encode("utf-8") if ins_to_host else b"") elif isinstance(ins_to_host, str): ins_to_host = ins_to_host.encode("utf-8") + ins_to_host *= repeat if isinstance(host_to_ins, list): host_to_ins = [ @@ -70,6 +80,7 @@ def expected_protocol(ins_class, host_to_ins, ins_to_host, sep="\n"): (sep.encode("utf-8") if host_to_ins else b"") elif isinstance(host_to_ins, str): host_to_ins = host_to_ins.encode("utf-8") + host_to_ins *= repeat stdin = BytesIO(ins_to_host) stdout = BytesIO() diff --git a/instruments/tests/test_abstract_inst/test_function_generator.py b/instruments/tests/test_abstract_inst/test_function_generator.py new file mode 100644 index 000000000..00198669b --- /dev/null +++ b/instruments/tests/test_abstract_inst/test_function_generator.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the abstract function generator class +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +import pytest +import quantities as pq + +import instruments as ik + + +# TESTS ###################################################################### + +@pytest.fixture +def fg(): + return ik.abstract_instruments.FunctionGenerator.open_test() + + +def test_func_gen_default_channel_count(fg): + assert fg._channel_count == 1 + + +def test_func_gen_raises_not_implemented_error_one_channel_getting(fg): + fg._channel_count = 1 + with pytest.raises(NotImplementedError): + _ = fg.amplitude + with pytest.raises(NotImplementedError): + _ = fg.frequency + with pytest.raises(NotImplementedError): + _ = fg.function + with pytest.raises(NotImplementedError): + _ = fg.offset + with pytest.raises(NotImplementedError): + _ = fg.phase + + +def test_func_gen_raises_not_implemented_error_one_channel_setting(fg): + fg._channel_count = 1 + with pytest.raises(NotImplementedError): + fg.amplitude = 1 + with pytest.raises(NotImplementedError): + fg.frequency = 1 + with pytest.raises(NotImplementedError): + fg.function = 1 + with pytest.raises(NotImplementedError): + fg.offset = 1 + with pytest.raises(NotImplementedError): + fg.phase = 1 + + +def test_func_gen_raises_not_implemented_error_two_channel_getting(fg): + fg._channel_count = 2 + with pytest.raises(NotImplementedError): + _ = fg.channel[0].amplitude + with pytest.raises(NotImplementedError): + _ = fg.channel[0].frequency + with pytest.raises(NotImplementedError): + _ = fg.channel[0].function + with pytest.raises(NotImplementedError): + _ = fg.channel[0].offset + with pytest.raises(NotImplementedError): + _ = fg.channel[0].phase + + +def test_func_gen_raises_not_implemented_error_two_channel_setting(fg): + fg._channel_count = 2 + with pytest.raises(NotImplementedError): + fg.channel[0].amplitude = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].frequency = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].function = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].offset = 1 + with pytest.raises(NotImplementedError): + fg.channel[0].phase = 1 + + +def test_func_gen_two_channel_passes_thru_call_getter(fg, mocker): + mock_channel = mocker.MagicMock() + mock_properties = [mocker.PropertyMock(return_value=1) for _ in range(5)] + + mocker.patch("instruments.abstract_instruments.FunctionGenerator.Channel", new=mock_channel) + type(mock_channel()).amplitude = mock_properties[0] + type(mock_channel()).frequency = mock_properties[1] + type(mock_channel()).function = mock_properties[2] + type(mock_channel()).offset = mock_properties[3] + type(mock_channel()).phase = mock_properties[4] + + fg._channel_count = 2 + _ = fg.amplitude + _ = fg.frequency + _ = fg.function + _ = fg.offset + _ = fg.phase + + for mock_property in mock_properties: + mock_property.assert_called_once_with() + + +def test_func_gen_one_channel_passes_thru_call_getter(fg, mocker): + mock_properties = [mocker.PropertyMock(return_value=1) for _ in range(4)] + mock_method = mocker.MagicMock(return_value=(1, pq.V)) + + mocker.patch("instruments.abstract_instruments.FunctionGenerator.frequency", new=mock_properties[0]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator.function", new=mock_properties[1]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator.offset", new=mock_properties[2]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator.phase", new=mock_properties[3]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator._get_amplitude_", new=mock_method) + + fg._channel_count = 1 + _ = fg.channel[0].amplitude + _ = fg.channel[0].frequency + _ = fg.channel[0].function + _ = fg.channel[0].offset + _ = fg.channel[0].phase + + for mock_property in mock_properties: + mock_property.assert_called_once_with() + + mock_method.assert_called_once_with() + + +def test_func_gen_two_channel_passes_thru_call_setter(fg, mocker): + mock_channel = mocker.MagicMock() + mock_properties = [mocker.PropertyMock() for _ in range(5)] + + mocker.patch("instruments.abstract_instruments.FunctionGenerator.Channel", new=mock_channel) + type(mock_channel()).amplitude = mock_properties[0] + type(mock_channel()).frequency = mock_properties[1] + type(mock_channel()).function = mock_properties[2] + type(mock_channel()).offset = mock_properties[3] + type(mock_channel()).phase = mock_properties[4] + + fg._channel_count = 2 + fg.amplitude = 1 + fg.frequency = 1 + fg.function = 1 + fg.offset = 1 + fg.phase = 1 + + for mock_property in mock_properties: + mock_property.assert_called_once_with(1) + + +def test_func_gen_one_channel_passes_thru_call_setter(fg, mocker): + mock_properties = [mocker.PropertyMock() for _ in range(4)] + mock_method = mocker.MagicMock() + + mocker.patch("instruments.abstract_instruments.FunctionGenerator.frequency", new=mock_properties[0]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator.function", new=mock_properties[1]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator.offset", new=mock_properties[2]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator.phase", new=mock_properties[3]) + mocker.patch("instruments.abstract_instruments.FunctionGenerator._set_amplitude_", new=mock_method) + + fg._channel_count = 1 + fg.channel[0].amplitude = 1 + fg.channel[0].frequency = 1 + fg.channel[0].function = 1 + fg.channel[0].offset = 1 + fg.channel[0].phase = 1 + + for mock_property in mock_properties: + mock_property.assert_called_once_with(1) + + mock_method.assert_called_once_with(magnitude=1, units=fg.VoltageMode.peak_to_peak) diff --git a/instruments/tests/test_generic_scpi/test_scpi_function_generator.py b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py index 6920a3f4b..d32456728 100644 --- a/instruments/tests/test_generic_scpi/test_scpi_function_generator.py +++ b/instruments/tests/test_generic_scpi/test_scpi_function_generator.py @@ -31,12 +31,17 @@ def test_scpi_func_gen_amplitude(): ], [ "VPP", "+1.000000E+00" - ] + ], + repeat=2 ) as fg: assert fg.amplitude == (1 * pq.V, fg.VoltageMode.peak_to_peak) fg.amplitude = 2 * pq.V fg.amplitude = (1.5 * pq.V, fg.VoltageMode.dBm) + assert fg.channel[0].amplitude == (1 * pq.V, fg.VoltageMode.peak_to_peak) + fg.channel[0].amplitude = 2 * pq.V + fg.channel[0].amplitude = (1.5 * pq.V, fg.VoltageMode.dBm) + def test_scpi_func_gen_frequency(): with expected_protocol( @@ -46,11 +51,15 @@ def test_scpi_func_gen_frequency(): "FREQ 1.005000e+02" ], [ "+1.234000E+03" - ] + ], + repeat=2 ) as fg: assert fg.frequency == 1234 * pq.Hz fg.frequency = 100.5 * pq.Hz + assert fg.channel[0].frequency == 1234 * pq.Hz + fg.channel[0].frequency = 100.5 * pq.Hz + def test_scpi_func_gen_function(): with expected_protocol( @@ -60,11 +69,15 @@ def test_scpi_func_gen_function(): "FUNC SQU" ], [ "SIN" - ] + ], + repeat=2 ) as fg: assert fg.function == fg.Function.sinusoid fg.function = fg.Function.square + assert fg.channel[0].function == fg.Function.sinusoid + fg.channel[0].function = fg.Function.square + def test_scpi_func_gen_offset(): with expected_protocol( @@ -74,7 +87,11 @@ def test_scpi_func_gen_offset(): "VOLT:OFFS 4.321000e-01" ], [ "+1.234000E+01", - ] + ], + repeat=2 ) as fg: assert fg.offset == 12.34 * pq.V fg.offset = 0.4321 * pq.V + + assert fg.channel[0].offset == 12.34 * pq.V + fg.channel[0].offset = 0.4321 * pq.V From e05dad5149408362df9bac0ba47b2ab5d644e948 Mon Sep 17 00:00:00 2001 From: Catherine Holloway Date: Thu, 14 Feb 2019 18:58:03 -0500 Subject: [PATCH 07/86] Adding support for the minghe mhs5200 (#150) * added mhs5200 * added minghe function generator * added absolute_import * fixed scaling on frequency * switched to abstract instrument class * fixed a few docstrings * after testing with device * Minghe MHS5200 - Add instrument to docs * isolating changes from cc1 test station: * Revert "isolating changes from cc1 test station:" This reverts commit 87b8dec40d927460bb9cb0edf6d671c3535f27c2. * reverting changes and fixing duty cycle * Update for new FunctionGenerator multichannel consistency update --- doc/examples/minghe/ex_minghe_mhs5200.py | 28 +++ doc/source/apiref/minghe.rst | 15 ++ instruments/__init__.py | 1 + instruments/minghe/__init__.py | 7 + instruments/minghe/mhs5200a.py | 235 ++++++++++++++++++ .../tests/test_minghe/test_minghe_mhs5200a.py | 210 ++++++++++++++++ 6 files changed, 496 insertions(+) create mode 100644 doc/examples/minghe/ex_minghe_mhs5200.py create mode 100644 doc/source/apiref/minghe.rst create mode 100644 instruments/minghe/__init__.py create mode 100644 instruments/minghe/mhs5200a.py create mode 100644 instruments/tests/test_minghe/test_minghe_mhs5200a.py diff --git a/doc/examples/minghe/ex_minghe_mhs5200.py b/doc/examples/minghe/ex_minghe_mhs5200.py new file mode 100644 index 000000000..60b69bedb --- /dev/null +++ b/doc/examples/minghe/ex_minghe_mhs5200.py @@ -0,0 +1,28 @@ +#!/usr/bin/python +from instruments.minghe import MHS5200 +import quantities as pq + +mhs = MHS5200.open_serial(vid=6790, pid=29987, baud=57600) +print(mhs.serial_number) +mhs.channel[0].frequency = 3000000*pq.Hz +print(mhs.channel[0].frequency) +mhs.channel[0].function = MHS5200.Function.sawtooth_down +print(mhs.channel[0].function) +mhs.channel[0].amplitude = 9.0*pq.V +print(mhs.channel[0].amplitude) +mhs.channel[0].offset = -0.5 +print(mhs.channel[0].offset) +mhs.channel[0].phase = 90 +print(mhs.channel[0].phase) + +mhs.channel[1].frequency = 2000000*pq.Hz +print(mhs.channel[1].frequency) +mhs.channel[1].function = MHS5200.Function.square +print(mhs.channel[1].function) +mhs.channel[1].amplitude = 2.0*pq.V +print(mhs.channel[1].amplitude) +mhs.channel[1].offset = 0.0 +print(mhs.channel[1].offset) +mhs.channel[1].phase = 15 +print(mhs.channel[1].phase) + diff --git a/doc/source/apiref/minghe.rst b/doc/source/apiref/minghe.rst new file mode 100644 index 000000000..6a1763039 --- /dev/null +++ b/doc/source/apiref/minghe.rst @@ -0,0 +1,15 @@ +.. + TODO: put documentation license header here. + +.. currentmodule:: instruments.minghe + +====== +Minghe +====== + +:class:`MHS5200` Function Generator +=================================== + +.. autoclass:: MHS5200 + :members: + :undoc-members: diff --git a/instruments/__init__.py b/instruments/__init__.py index ca26f525b..a6b20794d 100644 --- a/instruments/__init__.py +++ b/instruments/__init__.py @@ -17,6 +17,7 @@ from . import hp from . import keithley from . import lakeshore +from . import minghe from . import newport from . import oxford from . import phasematrix diff --git a/instruments/minghe/__init__.py b/instruments/minghe/__init__.py new file mode 100644 index 000000000..997950f6c --- /dev/null +++ b/instruments/minghe/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing MingHe instruments +""" +from __future__ import absolute_import +from .mhs5200a import MHS5200 diff --git a/instruments/minghe/mhs5200a.py b/instruments/minghe/mhs5200a.py new file mode 100644 index 000000000..97e661f3f --- /dev/null +++ b/instruments/minghe/mhs5200a.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Provides the support for the MingHe low-cost function generator. + +Class originally contributed by Catherine Holloway. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division + +from builtins import range +from enum import Enum + +import quantities as pq +from instruments.abstract_instruments import FunctionGenerator +from instruments.util_fns import ProxyList, assume_units + +# CLASSES ##################################################################### + + +class MHS5200(FunctionGenerator): + """ + The MHS5200 is a low-cost, 2 channel function generator. + + There is no user manual, but Al Williams has reverse-engineered the + communications protocol: + https://github.com/wd5gnr/mhs5200a/blob/master/MHS5200AProtocol.pdf + """ + def __init__(self, filelike): + super(MHS5200, self).__init__(filelike) + self._channel_count = 2 + self.terminator = "\r\n" + + def _ack_expected(self, msg=""): + if msg.find(":r") == 0: + return None + # most commands res + return "ok" + + # INNER CLASSES # + + class Channel(FunctionGenerator.Channel): + """ + Class representing a channel on the MHS52000. + """ + # pylint: disable=protected-access + + __CHANNEL_NAMES = { + 1: '1', + 2: '2' + } + + def __init__(self, mhs, idx): + self._mhs = mhs + super(MHS5200.Channel, self).__init__(parent=mhs, name=idx) + # Use zero-based indexing for the external API, but one-based + # for talking to the instrument. + self._idx = idx + 1 + self._chan = self.__CHANNEL_NAMES[self._idx] + self._count = 0 + + def _get_amplitude_(self): + query = ":r{0}a".format(self._chan) + response = self._mhs.query(query) + return float(response.replace(query, ""))/100.0, self._mhs.VoltageMode.rms + + def _set_amplitude_(self, magnitude, units): + if units == self._mhs.VoltageMode.peak_to_peak or \ + units == self._mhs.VoltageMode.rms: + magnitude = assume_units(magnitude, "V").rescale(pq.V).magnitude + elif units == self._mhs.VoltageMode.dBm: + raise NotImplementedError("Decibel units are not supported.") + magnitude *= 100 + query = ":s{0}a{1}".format(self._chan, int(magnitude)) + self._mhs.sendcmd(query) + + @property + def duty_cycle(self): + """ + Gets/Sets the duty cycle of this channel. + + :units: A fraction + :type: `~quantities.Quantity` + """ + query = ":r{0}d".format(self._chan) + response = self._mhs.query(query) + duty = float(response.replace(query, ""))/10.0 + return duty + + @duty_cycle.setter + def duty_cycle(self, new_val): + query = ":s{0}d{1}".format(self._chan, int(100.0*new_val)) + self._mhs.sendcmd(query) + + @property + def enable(self): + """ + Gets/Sets the enable state of this channel. + + :param newval: the enable state + :type: `bool` + """ + query = ":r{0}b".format(self._chan) + return int(self._mhs.query(query).replace(query, ""). + replace("\r", "")) + + @enable.setter + def enable(self, newval): + query = ":s{0}b{1}".format(self._chan, int(newval)) + self._mhs.sendcmd(query) + + @property + def frequency(self): + """ + Gets/Sets the frequency of this channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units hertz. + :type: `~quantities.Quantity` + """ + query = ":r{0}f".format(self._chan) + response = self._mhs.query(query) + freq = float(response.replace(query, ""))*pq.Hz + return freq/100.0 + + @frequency.setter + def frequency(self, new_val): + new_val = assume_units(new_val, pq.Hz).rescale(pq.Hz).\ + magnitude*100.0 + query = ":s{0}f{1}".format(self._chan, int(new_val)) + self._mhs.sendcmd(query) + + @property + def offset(self): + """ + Gets/Sets the offset of this channel. + + :param new_val: The fraction of the duty cycle to offset the + function by. + :type: `float` + """ + # need to convert + query = ":r{0}o".format(self._chan) + response = self._mhs.query(query) + return int(response.replace(query, ""))/100.0-1.20 + + @offset.setter + def offset(self, new_val): + new_val = int(new_val*100)+120 + query = ":s{0}o{1}".format(self._chan, new_val) + self._mhs.sendcmd(query) + + @property + def phase(self): + """ + Gets/Sets the phase of this channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of degrees. + :type: `~quantities.Quantity` + """ + # need to convert + query = ":r{0}p".format(self._chan) + response = self._mhs.query(query) + return int(response.replace(query, ""))*pq.deg + + @phase.setter + def phase(self, new_val): + new_val = assume_units(new_val, pq.deg).rescale("deg").magnitude + query = ":s{0}p{1}".format(self._chan, int(new_val)) + self._mhs.sendcmd(query) + + @property + def function(self): + """ + Gets/Sets the wave type of this channel. + + :type: `MHS5200.Function` + """ + query = ":r{0}w".format(self._chan) + response = self._mhs.query(query).replace(query, "") + return self._mhs.Function(int(response)) + + @function.setter + def function(self, new_val): + query = ":s{0}w{1}".format(self._chan, + self._mhs.Function(new_val).value) + self._mhs.sendcmd(query) + + class Function(Enum): + """ + Enum containing valid wave modes for + """ + sine = 0 + square = 1 + triangular = 2 + sawtooth_up = 3 + sawtooth_down = 4 + + @property + def channel(self): + """ + Gets a specific channel object. The desired channel is specified like + one would access a list. + + For instance, this would print the counts of the first channel:: + + >>> mhs = ik.minghe.MHS5200.open_serial(vid=1027, pid=24577, + baud=19200, timeout=1) + >>> print(mhs.channel[0].frequency) + + :rtype: `list`[`MHS5200.Channel`] + """ + return ProxyList(self, MHS5200.Channel, range(self._channel_count)) + + @property + def serial_number(self): + """ + Get the serial number, as an int + + :rtype: int + """ + query = ":r0c" + response = self.query(query) + response = response.replace(query, "").replace("\r", "") + return response + + def _get_amplitude_(self): + raise NotImplementedError() + + def _set_amplitude_(self, magnitude, units): + raise NotImplementedError() diff --git a/instruments/tests/test_minghe/test_minghe_mhs5200a.py b/instruments/tests/test_minghe/test_minghe_mhs5200a.py new file mode 100644 index 000000000..1f70afdc8 --- /dev/null +++ b/instruments/tests/test_minghe/test_minghe_mhs5200a.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the MingHe MHS52000a +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +import pytest +import quantities as pq + +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +def test_mhs_amplitude(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1a", + ":r2a", + ":s1a660", + ":s2a800" + ], + [ + ":r1a330", + ":r2a500", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].amplitude[0] == 3.3*pq.V + assert mhs.channel[1].amplitude[0] == 5.0*pq.V + mhs.channel[0].amplitude = 6.6*pq.V + mhs.channel[1].amplitude = 8.0*pq.V + + +def test_mhs_amplitude_dbm_notimplemented(): + with expected_protocol( + ik.minghe.MHS5200, + [], + [], + sep="\r\n" + ) as mhs: + with pytest.raises(NotImplementedError): + mhs.channel[0].amplitude = 6.6*ik.units.dBm + + +def test_mhs_duty_cycle(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1d", + ":r2d", + ":s1d6", + ":s2d80" + + ], + [ + ":r1d010", + ":r2d100", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].duty_cycle == 1.0 + assert mhs.channel[1].duty_cycle == 10.0 + mhs.channel[0].duty_cycle = 0.06 + mhs.channel[1].duty_cycle = 0.8 + + +def test_mhs_enable(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1b", + ":r2b", + ":s1b0", + ":s2b1" + ], + [ + ":r1b1", + ":r2b0", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].enable + assert not mhs.channel[1].enable + mhs.channel[0].enable = False + mhs.channel[1].enable = True + + +def test_mhs_frequency(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1f", + ":r2f", + ":s1f600000", + ":s2f800000" + + ], + [ + ":r1f3300000", + ":r2f50000000", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].frequency == 33.0*pq.kHz + assert mhs.channel[1].frequency == 500.0*pq.kHz + mhs.channel[0].frequency = 6*pq.kHz + mhs.channel[1].frequency = 8*pq.kHz + + +def test_mhs_offset(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1o", + ":r2o", + ":s1o60", + ":s2o180" + + ], + [ + ":r1o120", + ":r2o0", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].offset == 0 + assert mhs.channel[1].offset == -1.2 + mhs.channel[0].offset = -0.6 + mhs.channel[1].offset = 0.6 + + +def test_mhs_phase(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1p", + ":r2p", + ":s1p60", + ":s2p180" + + ], + [ + ":r1p120", + ":r2p0", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].phase == 120 + assert mhs.channel[1].phase == 0 + mhs.channel[0].phase = 60 + mhs.channel[1].phase = 180 + + +def test_mhs_wave_type(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1w", + ":r2w", + ":s1w2", + ":s2w3" + + ], + [ + ":r1w0", + ":r2w1", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].function == mhs.Function.sine + assert mhs.channel[1].function == mhs.Function.square + mhs.channel[0].function = mhs.Function.triangular + mhs.channel[1].function = mhs.Function.sawtooth_up + + +def test_mhs_serial_number(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r0c" + + ], + [ + ":r0c5225A1", + ], + sep="\r\n" + ) as mhs: + assert mhs.serial_number == "5225A1" From b59a45aa755e8d9dbb8163ea872cb8a709dc9d16 Mon Sep 17 00:00:00 2001 From: Steven Casagrande Date: Thu, 14 Feb 2019 23:34:50 -0500 Subject: [PATCH 08/86] Docstring modifications (#207) --- instruments/minghe/mhs5200a.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/instruments/minghe/mhs5200a.py b/instruments/minghe/mhs5200a.py index 97e661f3f..cfe04b2ab 100644 --- a/instruments/minghe/mhs5200a.py +++ b/instruments/minghe/mhs5200a.py @@ -83,7 +83,7 @@ def duty_cycle(self): Gets/Sets the duty cycle of this channel. :units: A fraction - :type: `~quantities.Quantity` + :type: `float` """ query = ":r{0}d".format(self._chan) response = self._mhs.query(query) @@ -100,7 +100,6 @@ def enable(self): """ Gets/Sets the enable state of this channel. - :param newval: the enable state :type: `bool` """ query = ":r{0}b".format(self._chan) @@ -138,8 +137,8 @@ def offset(self): """ Gets/Sets the offset of this channel. - :param new_val: The fraction of the duty cycle to offset the - function by. + The fraction of the duty cycle to offset the function by. + :type: `float` """ # need to convert @@ -208,6 +207,7 @@ def channel(self): For instance, this would print the counts of the first channel:: + >>> import instruments as ik >>> mhs = ik.minghe.MHS5200.open_serial(vid=1027, pid=24577, baud=19200, timeout=1) >>> print(mhs.channel[0].frequency) From 84b0a85631ef8be6aaaf745bebce62491fd42a53 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 31 May 2019 16:17:38 -0700 Subject: [PATCH 09/86] Add Fluke 3000 support --- instruments/__init__.py | 1 + instruments/fluke/__init__.py | 9 + instruments/fluke/fluke3000.py | 340 +++++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 instruments/fluke/__init__.py create mode 100644 instruments/fluke/fluke3000.py diff --git a/instruments/__init__.py b/instruments/__init__.py index 9289f052d..276ca3a75 100644 --- a/instruments/__init__.py +++ b/instruments/__init__.py @@ -13,6 +13,7 @@ from . import agilent from . import generic_scpi +from . import fluke from . import holzworth from . import hp from . import keithley diff --git a/instruments/fluke/__init__.py b/instruments/fluke/__init__.py new file mode 100644 index 000000000..ac860c293 --- /dev/null +++ b/instruments/fluke/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing Fluke instruments +""" + +from __future__ import absolute_import + +from .fluke3000 import Fluke3000 diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py new file mode 100644 index 000000000..7cddfaddc --- /dev/null +++ b/instruments/fluke/fluke3000.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# fluke3000.py: Driver for the Fluke 3000 FC Industrial System +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# 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 . +# +""" +Driver for the Fluke 3000 FC Industrial System + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com) + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +import time +from builtins import range + +from enum import Enum, IntEnum + +import quantities as pq + +from instruments.abstract_instruments import Multimeter +from instruments.util_fns import assume_units, bool_property, enum_property + +# CLASSES ##################################################################### + + +class Fluke3000(Multimeter): + + """The `Fluke3000` is an ecosystem of devices produced by Fluke that may be + connected simultaneously to a Fluke PC3000 wireless adapter which exposes + a serial port to the computer to send and receive commands. + + The `Fluke3000` ecosystem supports the following instruments: + - Fluke 3000 FC Series Wireless Multimeter + - Fluke v3000 FC Wireless AC Voltage Module + - Fluke v3001 FC Wireless DC Voltage Module + - Fluke t3000 FC Wireless Temperature Module + + `Fluke3000` is a USB instrument that communicates through a serial port + via the PC3000 dongle. The commands used to communicate to the dongle + do not follow the SCPI standard. + + When the device is reset, it searches for available wireless modules + and binds them to the PC3000 dongle. At each initialization, this class + checks what device has been bound and saves their module number. + + This class is a work in progress, it currently only supports the t3000 + FC Wireless Temperature Module as it is the only instrument available + to the author. It also only supports single readout. + """ + + def __init__(self, filelike): + """ + Initialise the instrument, and set the required eos, eoi needed for + communication. + """ + super(Fluke3000, self).__init__(filelike) + self.timeout = 1 * pq.second + self.terminator = "\r" + self._null = False + self.positions = {} + self.connect() + + # ENUMS ## + + class Module(Enum): + + """ + Enum containing the supported mode codes + """ + #: Temperature module + t3000 = "t3000" + + class Mode(Enum): + + """ + Enum containing the supported mode codes + """ + #: Temperature + temperature = "rfemd" + + class TriggerMode(IntEnum): + + """ + Enum with valid trigger modes. + """ + internal = 1 + external = 2 + single = 3 + hold = 4 + + # PROPERTIES ## + + mode = enum_property( + "", + Mode, + doc="""Set the measurement mode. + + :type: `Fluke3000.Mode` + """, + writeonly=True, + set_fmt="{}{}") + + module = enum_property( + "", + Module, + doc="""Set the measurement module. + + :type: `Fluke3000.Module` + """, + writeonly=True, + set_fmt="{}{}") + + trigger_mode = enum_property( + "T", + TriggerMode, + doc="""Set the trigger mode. + + Note that using `HP3456a.measure()` will override the `trigger_mode` to + `HP3456a.TriggerMode.single`. + + :type: `HP3456a.TriggerMode` + + """, + writeonly=True, + set_fmt="{}{}") + + @property + def input_range(self): + """Set the input range to be used. + + The `HP3456a` has separate ranges for `~quantities.ohm` and for + `~quantities.volt`. The range value sent to the instrument depends on + the unit set on the input range value. `auto` selects auto ranging. + + :type: `~quantities.Quantity` + """ + raise NotImplementedError + + @input_range.setter + def input_range(self, value): + if isinstance(value, str): + if value.lower() == "auto": + self.sendcmd("R1W") + else: + raise ValueError("Only 'auto' is acceptable when specifying " + "the input range as a string.") + + elif isinstance(value, pq.quantity.Quantity): + if value.units == pq.volt: + valid = HP3456a.ValidRange.voltage.value + value = value.rescale(pq.volt) + elif value.units == pq.ohm: + valid = HP3456a.ValidRange.resistance.value + value = value.rescale(pq.ohm) + else: + raise ValueError("Value {} not quantity.volt or quantity.ohm" + "".format(value)) + + value = float(value) + if value not in valid: + raise ValueError("Value {} outside valid ranges " + "{}".format(value, valid)) + value = valid.index(value) + 2 + self.sendcmd("R{}W".format(value)) + else: + raise TypeError("Range setting must be specified as a float, int, " + "or the string 'auto', got {}".format(type(value))) + + @property + def relative(self): + """ + Enable or disable `HP3456a.MathMode.Null` on the instrument. + + :type: `bool` + """ + return self._null + + @relative.setter + def relative(self, value): + if value is True: + self._null = True + self.sendcmd("M{}".format(HP3456a.MathMode.null.value)) + elif value is False: + self._null = False + self.sendcmd("M{}".format(HP3456a.MathMode.off.value)) + else: + raise TypeError("Relative setting must be specified as a bool, " + "got {}".format(type(value))) + + # METHODS ## + + def connect(self): + """ + Connect to available modules and returns + a dictionary of the modules found and their location + """ + self.scan() + if not self.positions: + self.reset() # Resets the PC3000 dongle + self.query("rfdis") # Discovers connected modules + time.sleep(10) # Wait for modules to connect + self.scan() + + if not self.positions: + raise ValueError("No Fluke3000 modules available") + + def reset(self): + """ + Resets the device and unbinds all modules + """ + self.query("ri") # Resets the device + self.query("rfsm 1") # Turns comms on + + def scan(self): + """ + Search for available modules and returns + a dictionary of the modules found and their location + """ + # Loop over possible channels, store device locations + positions = {} + for port_id in range(1, 7): + # Check if a device is connected to port port_id + output = self.query("rfebd 0{} 1".format(port_id)) + if "PH" not in output: + break + + # If it is, identify the device + module_id = int(output.split("PH=")[-1]) + if module_id == 64: + positions[self.Module.t3000] = port_id + else: + error = "Module ID {} not implemented".format(module_id) + raise NotImplementedError(error) + + self.positions = positions + + def query(self, cmd): + """ + Function used to send a command to the instrument while allowing + for multiline output (multiple termination characters) + + :param str cmd: Command that will be sent to the instrument + :return: The result from the query + :rtype: `str` + """ + # First send the command + self.sendcmd(cmd) + time.sleep(0.1) + + # While there is something to readout, keep going + result = "" + while True: + try: + result += self.read() + except: + break + + return result + + def measure(self, mode): + """Instruct the Fluke3000 to perform a one time measurement. + + Example usage: + + >>> dmm = ik.fluke.Fluke3000.open_serial("/dev/ttyUSB0") + >>> print dmm.measure(dmm.Mode.temperature) + + :param mode: Desired measurement mode. + + :type mode: `Fluke3000.Mode` + + :return: A measurement from the multimeter. + :rtype: `~quantities.quantity.Quantity` + + """ + modevalue = mode.value + units = UNITS[mode] + + value = self.query("rfemd 01 0") # TODO must format module ID + value = self._parse(value, mode) + return value * units + + def _parse(self, result, mode): + """Parses the module output depending on the measurement made + + :param result: Output of the query. + :param mode: Desired measurement mode. + + :type result: `string` + :type mode: `Fluke3000.Mode` + + :return: A measurement from the multimeter. + :rtype: `float` + + """ + # Loop over possible channels, store device locations + value = 0. + if mode == self.Mode.temperature: + data = result.split('PH=')[-1] + least = int(data[:2], 16) + most = int(data[2:4], 16) + sign = 1 if data[6:8] == '02' else -1 + return sign*float(most*255+least)/10 + else: + error = "Mode {} not implemented".format(mode) + raise NotImplementedError(error) + + return value + +# UNITS ####################################################################### + +UNITS = { + None: 1, + Fluke3000.Mode.temperature: pq.celsius +} From e4c22ef71945b406b035632636d19b694736ed8d Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 31 May 2019 17:00:16 -0700 Subject: [PATCH 10/86] Slight tweak --- instruments/fluke/fluke3000.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 7cddfaddc..2f0055d55 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -321,6 +321,9 @@ def _parse(self, result, mode): # Loop over possible channels, store device locations value = 0. if mode == self.Mode.temperature: + if "PH" not in result: + return value + data = result.split('PH=')[-1] least = int(data[:2], 16) most = int(data[2:4], 16) From 53d5847123c47d19827ff5edfd9af60e9411764d Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 28 Jun 2019 09:28:40 -0700 Subject: [PATCH 11/86] More robust measure function for fluke3000 --- instruments/fluke/fluke3000.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 2f0055d55..20796730f 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -90,7 +90,7 @@ def __init__(self, filelike): class Module(Enum): """ - Enum containing the supported mode codes + Enum containing the supported module codes """ #: Temperature module t3000 = "t3000" @@ -222,7 +222,7 @@ def connect(self): self.scan() if not self.positions: self.reset() # Resets the PC3000 dongle - self.query("rfdis") # Discovers connected modules + self.query("rfdis") # Discovers connected modules time.sleep(10) # Wait for modules to connect self.scan() @@ -298,11 +298,21 @@ def measure(self, mode): :rtype: `~quantities.quantity.Quantity` """ - modevalue = mode.value - units = UNITS[mode] + module = self._get_module(mode) + if module not in self.positions.keys(): + raise ValueError("Device necessary to measure {} is not available".format(mode)) + + port_id = self.positions[module] + value = None + init_time = time.time() + while not value and time.time() - init_time < float(self.timeout): + value = self.query("{} 0{} 0".format(mode.value, port_id)) + value = self._parse(value, mode) + + if not value: + raise ValueError("Failed to read out Fluke3000 with mode {}".format(mode)) - value = self.query("rfemd 01 0") # TODO must format module ID - value = self._parse(value, mode) + units = UNITS[mode] return value * units def _parse(self, result, mode): @@ -319,7 +329,7 @@ def _parse(self, result, mode): """ # Loop over possible channels, store device locations - value = 0. + value = None if mode == self.Mode.temperature: if "PH" not in result: return value @@ -330,10 +340,15 @@ def _parse(self, result, mode): sign = 1 if data[6:8] == '02' else -1 return sign*float(most*255+least)/10 else: - error = "Mode {} not implemented".format(mode) - raise NotImplementedError(error) + raise NotImplementedError("Mode {} not implemented".format(mode)) return value + + def _get_module(self, mode): + if mode == self.Mode.temperature: + return self.Module.t3000 + else: + raise ValueError("No module associated with mode {}".format(mode)) # UNITS ####################################################################### From 56cd49e5077e4c9b83521b3ca8e0b5b6c66b8e54 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 28 Jun 2019 09:37:25 -0700 Subject: [PATCH 12/86] Extend fluke3000 timeout --- instruments/fluke/fluke3000.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 20796730f..3d55df893 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -79,7 +79,7 @@ def __init__(self, filelike): communication. """ super(Fluke3000, self).__init__(filelike) - self.timeout = 1 * pq.second + self.timeout = 5 * pq.second self.terminator = "\r" self._null = False self.positions = {} From b52713eb765f44f52d56cb310de84deecf1b59dd Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 2 Jul 2019 18:03:33 -0700 Subject: [PATCH 13/86] Much faster temp readouts --- instruments/fluke/fluke3000.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 3d55df893..517b8a878 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -79,7 +79,7 @@ def __init__(self, filelike): communication. """ super(Fluke3000, self).__init__(filelike) - self.timeout = 5 * pq.second + self.timeout = 0.5 * pq.second self.terminator = "\r" self._null = False self.positions = {} @@ -227,7 +227,9 @@ def connect(self): self.scan() if not self.positions: - raise ValueError("No Fluke3000 modules available") + raise ValueError("No Fluke3000 modules available") + + self.timeout = 0.1 * pq.second def reset(self): """ @@ -305,7 +307,7 @@ def measure(self, mode): port_id = self.positions[module] value = None init_time = time.time() - while not value and time.time() - init_time < float(self.timeout): + while not value and time.time() - init_time < 1: value = self.query("{} 0{} 0".format(mode.value, port_id)) value = self._parse(value, mode) From 468d45c05d08ac58c5b154aac69ccc1bfd348710 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 5 Jul 2019 18:34:26 -0700 Subject: [PATCH 14/86] Allow 0 measurements in fluke3000.py --- instruments/fluke/fluke3000.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 517b8a878..95a842603 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -307,11 +307,11 @@ def measure(self, mode): port_id = self.positions[module] value = None init_time = time.time() - while not value and time.time() - init_time < 1: + while value is None and time.time() - init_time < 1: value = self.query("{} 0{} 0".format(mode.value, port_id)) value = self._parse(value, mode) - if not value: + if value is None: raise ValueError("Failed to read out Fluke3000 with mode {}".format(mode)) units = UNITS[mode] From 2732f2d02e275281600514656bf7701d46487e9e Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 5 Jul 2019 22:10:23 -0700 Subject: [PATCH 15/86] Restructured Fluke3000 read out around multiline reads --- instruments/fluke/fluke3000.py | 72 ++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 95a842603..d3127b732 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -79,7 +79,7 @@ def __init__(self, filelike): communication. """ super(Fluke3000, self).__init__(filelike) - self.timeout = 0.5 * pq.second + self.timeout = 10 * pq.second self.terminator = "\r" self._null = False self.positions = {} @@ -219,24 +219,23 @@ def connect(self): Connect to available modules and returns a dictionary of the modules found and their location """ - self.scan() + self.scan() # Look for connected devices if not self.positions: self.reset() # Resets the PC3000 dongle - self.query("rfdis") # Discovers connected modules - time.sleep(10) # Wait for modules to connect - self.scan() + self.query("rfdis", 3) # Discovers connected modules + self.scan() # Look for connected devices if not self.positions: raise ValueError("No Fluke3000 modules available") - self.timeout = 0.1 * pq.second - + self.timeout = 3 * pq.second + def reset(self): """ Resets the device and unbinds all modules """ - self.query("ri") # Resets the device - self.query("rfsm 1") # Turns comms on + self.query("ri", 3) # Resets the device + self.query("rfsm 1", 2) # Turns comms on def scan(self): """ @@ -247,11 +246,13 @@ def scan(self): positions = {} for port_id in range(1, 7): # Check if a device is connected to port port_id - output = self.query("rfebd 0{} 1".format(port_id)) - if "PH" not in output: + self.sendcmd("rfebd 0{} 1".format(port_id)) + output = self.read() + if "RFEBD" not in output: break - # If it is, identify the device + # If it is, read the next line and identify the device + output = self.read() module_id = int(output.split("PH=")[-1]) if module_id == 64: positions[self.Module.t3000] = port_id @@ -260,30 +261,41 @@ def scan(self): raise NotImplementedError(error) self.positions = positions - - def query(self, cmd): + + def read_lines(self, nlines=1): + """ + Function that keeps reading until reaches a termination + character a set amount of times. This is implemented + to handle the mutiline output of the PC3000 + + :param int nlines: Number of termination characters to reach + :return: Array of lines read out + :rtype: Array of `str` + """ + lines = [] + i = 0 + while i < nlines: + try: + lines.append(self.read()) + i += 1 + except : + continue + + return lines + + def query(self, cmd, nlines=1): """ Function used to send a command to the instrument while allowing for multiline output (multiple termination characters) :param str cmd: Command that will be sent to the instrument - :return: The result from the query - :rtype: `str` + :param int nlines: Number of termination characters to reach + :return: The multiline result from the query + :rtype: Array of `str` """ - # First send the command self.sendcmd(cmd) - time.sleep(0.1) + return self.read_lines(nlines) - # While there is something to readout, keep going - result = "" - while True: - try: - result += self.read() - except: - break - - return result - def measure(self, mode): """Instruct the Fluke3000 to perform a one time measurement. @@ -308,8 +320,8 @@ def measure(self, mode): value = None init_time = time.time() while value is None and time.time() - init_time < 1: - value = self.query("{} 0{} 0".format(mode.value, port_id)) - value = self._parse(value, mode) + value = self.query("{} 0{} 0".format(mode.value, port_id), 2) + value = self._parse(value[1], mode) if value is None: raise ValueError("Failed to read out Fluke3000 with mode {}".format(mode)) From 4d7a00e9d9205f7c740f1e604bba046b125ff6e5 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 6 Jul 2019 18:20:39 -0700 Subject: [PATCH 16/86] Added support for Fluke 3000FC wireless multimeter --- instruments/fluke/fluke3000.py | 187 +++++++++++++++++++++++++-------- 1 file changed, 146 insertions(+), 41 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index d3127b732..b9d2aab73 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -79,7 +79,7 @@ def __init__(self, filelike): communication. """ super(Fluke3000, self).__init__(filelike) - self.timeout = 10 * pq.second + self.timeout = 15 * pq.second self.terminator = "\r" self._null = False self.positions = {} @@ -92,16 +92,32 @@ class Module(Enum): """ Enum containing the supported module codes """ + #: Multimeter + m3000 = 46333030304643 #: Temperature module - t3000 = "t3000" + t3000 = 54333030304643 class Mode(Enum): """ Enum containing the supported mode codes """ + #: AC Voltage + voltage_ac = "01" + #: DC Voltage + voltage_dc = "02" + #: AC Current + current_ac = "03" + #: DC Current + current_dc = "04" + #: Frequency + frequency = "05" #: Temperature - temperature = "rfemd" + temperature = "07" + #: Resistance + resistance = "0B" + #: Capacitance + capacitance = "0F" class TriggerMode(IntEnum): @@ -246,15 +262,17 @@ def scan(self): positions = {} for port_id in range(1, 7): # Check if a device is connected to port port_id - self.sendcmd("rfebd 0{} 1".format(port_id)) - output = self.read() + output = self.query("rfebd 0{} 0".format(port_id))[0] if "RFEBD" not in output: break - # If it is, read the next line and identify the device - output = self.read() + # If it is, identify the device + self.read() + output = self.query('rfgus 0{}'.format(port_id), 2)[-1] module_id = int(output.split("PH=")[-1]) - if module_id == 64: + if module_id == self.Module.m3000.value: + positions[self.Module.m3000] = port_id + elif module_id == self.Module.t3000.value: positions[self.Module.t3000] = port_id else: error = "Module ID {} not implemented".format(module_id) @@ -268,9 +286,13 @@ def read_lines(self, nlines=1): character a set amount of times. This is implemented to handle the mutiline output of the PC3000 - :param int nlines: Number of termination characters to reach + :param nlines: Number of termination characters to reach + + :type: 'int' + :return: Array of lines read out :rtype: Array of `str` + """ lines = [] i = 0 @@ -288,10 +310,15 @@ def query(self, cmd, nlines=1): Function used to send a command to the instrument while allowing for multiline output (multiple termination characters) - :param str cmd: Command that will be sent to the instrument - :param int nlines: Number of termination characters to reach + :param cmd: Command that will be sent to the instrument + :param nlines: Number of termination characters to reach + + :type cmd: 'str' + :type nlines: 'int' + :return: The multiline result from the query :rtype: Array of `str` + """ self.sendcmd(cmd) return self.read_lines(nlines) @@ -304,33 +331,66 @@ def measure(self, mode): >>> dmm = ik.fluke.Fluke3000.open_serial("/dev/ttyUSB0") >>> print dmm.measure(dmm.Mode.temperature) - :param mode: Desired measurement mode. - + :param mode: Desired measurement mode. + :type mode: `Fluke3000.Mode` :return: A measurement from the multimeter. :rtype: `~quantities.quantity.Quantity` """ + # Check that the mode is supported + if not isinstance(mode, self.Mode): + raise ValueError("Mode {} is not supported".format(mode)) + + # Check that the module associated with this mode is available module = self._get_module(mode) if module not in self.positions.keys(): raise ValueError("Device necessary to measure {} is not available".format(mode)) + # Query the module port_id = self.positions[module] - value = None + value = "" init_time = time.time() - while value is None and time.time() - init_time < 1: - value = self.query("{} 0{} 0".format(mode.value, port_id), 2) - value = self._parse(value[1], mode) - - if value is None: + while "PH" not in value and time.time() - init_time < self.timeout: + # Read out + if mode == self.Mode.temperature: + # The temperature module supports single readout + value = self.query("rfemd 0{} 0".format(port_id), 2)[1] + else: + # The multimeter does not support single readout, + # have to open continuous readout, read, then close it + value = self.query("rfemd 0{} 1".format(port_id), 2)[1] + self.query("rfemd 0{} 2".format(port_id)) + + # Parse the output + try: + value = self._parse(value, mode) + except: raise ValueError("Failed to read out Fluke3000 with mode {}".format(mode)) + # Return with the appropriate units units = UNITS[mode] return value * units + def _get_module(self, mode): + """Gets the module associated with this measurement mode. + + :param mode: Desired measurement mode. + + :type mode: `Fluke3000.Mode` + + :return: A Fluke3000 module. + :rtype: `Fluke3000.Module` + + """ + if mode == self.Mode.temperature: + return self.Module.t3000 + else: + return self.Module.m3000 + def _parse(self, result, mode): - """Parses the module output depending on the measurement made + """Parses the module output. :param result: Output of the query. :param mode: Desired measurement mode. @@ -339,34 +399,79 @@ def _parse(self, result, mode): :type mode: `Fluke3000.Mode` :return: A measurement from the multimeter. + :rtype: `Quantity` + + """ + # Check that a value is contained + if "PH" not in result: + raise ValueError("Cannot parse a string that does not contain a return value") + + # Check that the multimeter is in the right mode (fifth byte) + data = result.split('PH=')[-1] + if data[8:10] != mode.value: + raise ValueError("The 3000FC Multimeter is not in the right mode: {}".format(mode)) + + # The first two bytes encode the value + value = int(data[2:4]+data[:2], 16) + + # The fourth byte encodes a prefactor + byte = '{0:08b}'.format(int(data[6:8], 16)) + try: + scale = self._parse_factor(byte) + except: + raise ValueError("Could not parse the prefactor byte: {}".format(byte)) + + # Combine and return + return scale*value + + def _parse_factor(self, byte): + """Parses the measurement prefactor. + + :param byte: Binary encoding of the byte. + + :type result: `str` + + :return: A prefactor. :rtype: `float` """ - # Loop over possible channels, store device locations - value = None - if mode == self.Mode.temperature: - if "PH" not in result: - return value - - data = result.split('PH=')[-1] - least = int(data[:2], 16) - most = int(data[2:4], 16) - sign = 1 if data[6:8] == '02' else -1 - return sign*float(most*255+least)/10 - else: - raise NotImplementedError("Mode {} not implemented".format(mode)) + # The first bit encodes the sign (0 positive, 1 negative) + sign = 1 if byte[0] == '0' else -1 - return value + # The second to fourth bits encode the metric prefix + code = int(byte[1:4], 2) + if code not in PREFIXES.keys(): + raise ValueError("Metric prefix not recognized: {}".format(code)) + prefix = PREFIXES[code] + + # The sixth and seventh bit encode the decimal place + scale = 10**(-int(byte[5:7], 2)) + + # Return the combination + return sign*prefix*scale - def _get_module(self, mode): - if mode == self.Mode.temperature: - return self.Module.t3000 - else: - raise ValueError("No module associated with mode {}".format(mode)) - # UNITS ####################################################################### UNITS = { None: 1, - Fluke3000.Mode.temperature: pq.celsius + Fluke3000.Mode.voltage_ac: pq.volt, + Fluke3000.Mode.voltage_dc: pq.volt, + Fluke3000.Mode.current_ac: pq.amp, + Fluke3000.Mode.current_dc: pq.amp, + Fluke3000.Mode.frequency: pq.hertz, + Fluke3000.Mode.temperature: pq.celsius, + Fluke3000.Mode.resistance: pq.ohm, + Fluke3000.Mode.capacitance: pq.farad +} + +# METRIC PREFIXES ############################################################# + +PREFIXES = { + 0: 1e0, + 2: 1e6, + 3: 1e3, + 4: 1e-3, + 5: 1e-6, + 6: 1e-9 } + From 10861253e2f755d1961ffd718d7e13cb542b4a79 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 7 Jul 2019 22:07:36 -0700 Subject: [PATCH 17/86] Made the readout more robusts, implemented flush --- instruments/fluke/fluke3000.py | 102 ++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 22 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index b9d2aab73..ea01d8b6f 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -235,6 +235,7 @@ def connect(self): Connect to available modules and returns a dictionary of the modules found and their location """ + self.flush() # Flush serial self.scan() # Look for connected devices if not self.positions: self.reset() # Resets the PC3000 dongle @@ -245,6 +246,17 @@ def connect(self): raise ValueError("No Fluke3000 modules available") self.timeout = 3 * pq.second + + def flush(self): + timeout = self.timeout + self.timeout = 0.1 * pq.second + init_time = time.time() + while time.time() - init_time < timeout: + try: + self.read() + except: + break + self.timeout = timeout def reset(self): """ @@ -264,7 +276,7 @@ def scan(self): # Check if a device is connected to port port_id output = self.query("rfebd 0{} 0".format(port_id))[0] if "RFEBD" not in output: - break + continue # If it is, identify the device self.read() @@ -277,7 +289,11 @@ def scan(self): else: error = "Module ID {} not implemented".format(module_id) raise NotImplementedError(error) - + + # Reset device readout + self.query('rfemd 0{} 2'.format(port_id)) + + self.flush() self.positions = positions def read_lines(self, nlines=1): @@ -349,10 +365,10 @@ def measure(self, mode): raise ValueError("Device necessary to measure {} is not available".format(mode)) # Query the module + value = '' port_id = self.positions[module] - value = "" init_time = time.time() - while "PH" not in value and time.time() - init_time < self.timeout: + while time.time() - init_time < self.timeout: # Read out if mode == self.Mode.temperature: # The temperature module supports single readout @@ -363,6 +379,14 @@ def measure(self, mode): value = self.query("rfemd 0{} 1".format(port_id), 2)[1] self.query("rfemd 0{} 2".format(port_id)) + # Check that value is consistent with the request, break + if "PH" in value: + data = value.split("PH=")[-1] + if self._parse_mode(data) != mode.value: + self.flush() + else: + break + # Parse the output try: value = self._parse(value, mode) @@ -406,35 +430,69 @@ def _parse(self, result, mode): if "PH" not in result: raise ValueError("Cannot parse a string that does not contain a return value") - # Check that the multimeter is in the right mode (fifth byte) + # Isolate the data string from the output data = result.split('PH=')[-1] - if data[8:10] != mode.value: - raise ValueError("The 3000FC Multimeter is not in the right mode: {}".format(mode)) - # The first two bytes encode the value - value = int(data[2:4]+data[:2], 16) + # Check that the multimeter is in the right mode (fifth byte) + if self._parse_mode(data) != mode.value: + error = "Mode {} was requested but the Fluke 3000FC Multimeter is in ".format(mode.name) + error += "mode {} instead, could not read the requested quantity.".format(self.Mode(data[8:10]).name) + raise ValueError(error) - # The fourth byte encodes a prefactor - byte = '{0:08b}'.format(int(data[6:8], 16)) + # Extract the value from the first two bytes + value = self._parse_factor(data) + + # Extract the prefactor from the fourth byte try: - scale = self._parse_factor(byte) + scale = self._parse_factor(data) except: - raise ValueError("Could not parse the prefactor byte: {}".format(byte)) + raise ValueError("Could not parse the prefactor byte") # Combine and return return scale*value - def _parse_factor(self, byte): + def _parse_mode(self, data): + """Parses the measurement mode. + + :param data: Measurement output. + + :type data: `str` + + :return: A Mode string. + :rtype: `str` + + """ + # The fixth dual hex byte encodes the measurement mode + return data[8:10] + + def _parse_value(self, data): + """Parses the measurement value. + + :param data: Measurement output. + + :type data: `str` + + :return: A value. + :rtype: `float` + + """ + # The second dual hex byte is the most significant byte + return int(data[2:4]+data[:2], 16) + + def _parse_factor(self, data): """Parses the measurement prefactor. - :param byte: Binary encoding of the byte. + :param data: Measurement output. - :type result: `str` + :type data: `str` :return: A prefactor. :rtype: `float` """ + # Convert the fourth dual hex byte to an 8 bits string + byte = '{0:08b}'.format(int(data[6:8], 16)) + # The first bit encodes the sign (0 positive, 1 negative) sign = 1 if byte[0] == '0' else -1 @@ -467,11 +525,11 @@ def _parse_factor(self, byte): # METRIC PREFIXES ############################################################# PREFIXES = { - 0: 1e0, - 2: 1e6, - 3: 1e3, - 4: 1e-3, - 5: 1e-6, - 6: 1e-9 + 0: 1e0, # None + 2: 1e6, # Mega + 3: 1e3, # Kilo + 4: 1e-3, # milli + 5: 1e-6, # micro + 6: 1e-9 # nano } From 7db9f54057fa8ee3f6c2af74c7ade4f49607f75c Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 7 Jul 2019 22:12:22 -0700 Subject: [PATCH 18/86] Bug fix --- instruments/fluke/fluke3000.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index ea01d8b6f..7d16dff15 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -440,7 +440,7 @@ def _parse(self, result, mode): raise ValueError(error) # Extract the value from the first two bytes - value = self._parse_factor(data) + value = self._parse_value(data) # Extract the prefactor from the fourth byte try: From 71bb1b9c61de0465f34ef026acc698d8a594047f Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 16 Jul 2019 18:53:40 -0700 Subject: [PATCH 19/86] Added Prologix GPIB controller suppport --- .../abstract_instruments/comm/__init__.py | 1 + .../comm/pl_gpib_communicator.py | 286 ++++++++++++++++++ .../abstract_instruments/instrument.py | 19 +- 3 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 instruments/abstract_instruments/comm/pl_gpib_communicator.py diff --git a/instruments/abstract_instruments/comm/__init__.py b/instruments/abstract_instruments/comm/__init__.py index 0cf070538..a941732ac 100644 --- a/instruments/abstract_instruments/comm/__init__.py +++ b/instruments/abstract_instruments/comm/__init__.py @@ -14,6 +14,7 @@ from .visa_communicator import VisaCommunicator from .loopback_communicator import LoopbackCommunicator from .gi_gpib_communicator import GPIBCommunicator +from .pl_gpib_communicator import PLGPIBCommunicator from .file_communicator import FileCommunicator from .usbtmc_communicator import USBTMCCommunicator from .vxi11_communicator import VXI11Communicator diff --git a/instruments/abstract_instruments/comm/pl_gpib_communicator.py b/instruments/abstract_instruments/comm/pl_gpib_communicator.py new file mode 100644 index 000000000..003a2bb38 --- /dev/null +++ b/instruments/abstract_instruments/comm/pl_gpib_communicator.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Provides a communication layer for an instrument connected via a Prologix +GPIB adapter. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + +import io +import time + +from builtins import chr, str, bytes +import quantities as pq + +from instruments.abstract_instruments.comm import AbstractCommunicator +from instruments.util_fns import assume_units + +# CLASSES ##################################################################### + + +class PLGPIBCommunicator(io.IOBase, AbstractCommunicator): + + """ + Communicates with a SocketCommunicator or SerialCommunicator object for + use with Prologix GPIBUSB or GPIBETHERNET adapters. + + It essentially wraps those physical communication layers with the extra + overhead required by the Prologix GPIB adapters. + """ + + # pylint: disable=too-many-instance-attributes + def __init__(self, filelike, gpib_address): + super(PLGPIBCommunicator, self).__init__(self) + + self._file = filelike + self._gpib_address = gpib_address + self._file.terminator = "\r" + self._version = float(self._file.query("++ver").split(' ')[-1]) + self._terminator = None + self.terminator = "\n" + self._eoi = True + self._timeout = 1000 * pq.millisecond + self._eos = "\n" + + # PROPERTIES # + + @property + def address(self): + """ + Gets/sets the GPIB address and downstream address associated with + the instrument. + + When setting, if specified as an integer, only changes the GPIB + address. If specified as a list, the first element changes the GPIB + address, while the second is passed downstream. + + Example: [gpib_address, downstream_address] + + Where downstream_address needs to be formatted as appropriate for the + connection (eg SerialCommunicator, SocketCommunicator, etc). + """ + return self._gpib_address, self._file.address + + @address.setter + def address(self, newval): + if isinstance(newval, int): + if (newval < 1) or (newval > 30): + raise ValueError("GPIB address must be between 1 and 30.") + self._gpib_address = newval + elif isinstance(newval, list): + self.address = newval[0] # Set GPIB address + self._file.address = newval[1] # Send downstream address + else: + raise TypeError("Not a valid input type for Instrument address.") + + @property + def timeout(self): + """ + Gets/sets the timeeout of both the GPIB bus and the connection + channel between the PC and the GPIB adapter. + + :type: `~quantities.Quantity` + :units: As specified, or assumed to be of units ``seconds`` + """ + return self._timeout + + @timeout.setter + def timeout(self, newval): + newval = assume_units(newval, pq.second) + newval = newval.rescale(pq.millisecond) + self._file.sendcmd("++read_tmo_ms {}".format(newval.magnitude)) + self._file.timeout = newval.rescale(pq.second) + self._timeout = newval.rescale(pq.second) + + @property + def terminator(self): + """ + Gets/sets the GPIB termination character. This can be set to + ``\n``, ``\r``, ``\r\n``, or ``eoi``. + + .. seealso:: `eos` and `eoi` for direct manipulation of these + parameters. + + :type: `str` + """ + if not self._eoi: + return self._terminator + + return 'eoi' + + @terminator.setter + def terminator(self, newval): + if isinstance(newval, bytes): + newval = newval.decode("utf-8") + if isinstance(newval, str): + newval = newval.lower() + + if newval != "eoi": + self.eos = newval + self.eoi = False + self._terminator = self.eos + elif newval == "eoi": + self.eos = None + self._terminator = "eoi" + self.eoi = True + + @property + def eoi(self): + """ + Gets/sets the EOI usage status. + + EOI is a dedicated line on the GPIB bus. When used, it is used by + instruments to signal that the current byte being transmitted is the + last in the message. This avoids the need to use a dedicated + termination character such as ``\n``. Frequently, instruments will + use both EOI-signalling and append an end-of-string (EOS) character. + Some will only use one or the other. + + .. seealso:: `terminator`, `eos` for more communication termination + related properties. + + :type: `bool` + """ + return self._eoi + + @eoi.setter + def eoi(self, newval): + if not isinstance(newval, bool): + raise TypeError("EOI status must be specified as a boolean") + self._eoi = newval + self._file.sendcmd("++eoi {}".format('1' if newval else '0')) + + @property + def eos(self): + """ + Gets/sets the end-of-string (EOS) character. + + Valid EOS settings are ``\n``, ``\r``, ``\r\n`` and `None`. + + .. seealso:: `terminator`, `eoi` for more communication termination + related properties. + + :type: `str` or `None` + """ + return self._eos + + @eos.setter + def eos(self, newval): + if isinstance(newval, int): + newval = str(chr(newval)) + if newval == "\r\n": + self._eos = newval + newval = 0 + elif newval == "\r": + self._eos = newval + newval = 1 + elif newval == "\n": + self._eos = newval + newval = 2 + elif newval is None: + self._eos = newval + newval = 3 + else: + raise ValueError("EOS must be CRLF, CR, LF, or None") + self._file.sendcmd("++eos {}".format(newval)) + + # FILE-LIKE METHODS # + + def close(self): + """ + Close connection to the underlying physical connection channel + of the GPIB connection. This is typically a serial connection that + is then closed. + """ + self._file.close() + + def read_raw(self, size=-1): + """ + Read bytes in from the gpibusb connection. + + :param int size: The number of bytes to read in from the + connection. + + :return: The read bytes from the connection + :rtype: `bytes` + """ + return self._file.read_raw(size) + + def read(self, size=-1, encoding="utf-8"): + """ + Read characters from wrapped class (ie SocketCommunicator or + SerialCommunicator). + + If size = -1, characters will be read until termination character + is found. + + GI GPIB adapters always terminate serial connections with a CR. + Function will read until a CR is found. + + :param int size: Number of bytes to read + :param str encoding: Encoding that will be applied to the read bytes + + :return: Data read from the GPIB adapter + :rtype: `str` + """ + return self._file.read(size, encoding) + + def write_raw(self, msg): + """ + Write bytes to the gpibusb connection. + + :param bytes msg: Bytes to be sent to the instrument over the + connection. + """ + self._file.write_raw(msg) + + def write(self, msg, encoding="utf-8"): + """ + Write data string to GPIB connected instrument. + + :param str msg: String to write to the instrument + :param str encoding: Encoding to apply on msg to convert the message + into bytes + """ + self._file.write(msg, encoding) + + def flush_input(self): + """ + Instruct the communicator to flush the input buffer, discarding the + entirety of its contents. + """ + self._file.flush_input() + + # METHODS # + + def _sendcmd(self, msg): + """ + This is the implementation of ``sendcmd`` for communicating with + the Prologix GPIB adapter. This function is in turn wrapped by + the concrete method `AbstractCommunicator.sendcmd` to provide consistent + logging functionality across all communication layers. + + :param str msg: The command message to send to the instrument + """ + self._file.sendcmd(msg) + + def _query(self, msg, size=-1): + """ + This is the implementation of ``query`` for communicating with + the Prologix GPIB adapter. This function is in turn wrapped by + the concrete method `AbstractCommunicator.query` to provide consistent + logging functionality across all communication layers. + + :param str msg: The query message to send to the instrument + :param int size: The number of bytes to read back from the instrument + response. + :return: The instrument response to the query + :rtype: `str` + """ + self._sendcmd(msg) + return self._file.read(size) diff --git a/instruments/abstract_instruments/instrument.py b/instruments/abstract_instruments/instrument.py index 2b294e96c..cc34d0a22 100644 --- a/instruments/abstract_instruments/instrument.py +++ b/instruments/abstract_instruments/instrument.py @@ -40,8 +40,9 @@ class WindowsError(OSError): from instruments.abstract_instruments.comm import ( SocketCommunicator, USBCommunicator, VisaCommunicator, FileCommunicator, - LoopbackCommunicator, GPIBCommunicator, AbstractCommunicator, - USBTMCCommunicator, VXI11Communicator, serial_manager + LoopbackCommunicator, GPIBCommunicator, PLGPIBCommunicator, + AbstractCommunicator, USBTMCCommunicator, VXI11Communicator, + serial_manager ) from instruments.errors import AcknowledgementError, PromptError @@ -529,7 +530,7 @@ def open_serial(cls, port=None, baud=9600, vid=None, pid=None, return cls(ser) @classmethod - def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3): + def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, prologix=False): """ Opens an instrument, connecting via a `Galvant Industries GPIB-USB adapter`_. @@ -559,10 +560,13 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3): timeout=timeout, write_timeout=write_timeout ) - return cls(GPIBCommunicator(ser, gpib_address)) + if not prologix: + return cls(GPIBCommunicator(ser, gpib_address)) + else: + return cls(PLGPIBCommunicator(ser, gpib_address)) @classmethod - def open_gpibethernet(cls, host, port, gpib_address): + def open_gpibethernet(cls, host, port, gpib_address, prologix=False): """ .. warning:: The GPIB-Ethernet adapter that this connection would use does not actually exist, and thus this class method should @@ -570,7 +574,10 @@ def open_gpibethernet(cls, host, port, gpib_address): """ conn = socket.socket() conn.connect((host, port)) - return cls(GPIBCommunicator(conn, gpib_address)) + if not prologix: + return cls(GPIBCommunicator(conn, gpib_address)) + else: + return cls(PLGPIBCommunicator(conn, gpib_address)) @classmethod def open_visa(cls, resource_name): From 45d001241ad1d5bda0854ce7ccd3aab34e9d60b0 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 16 Jul 2019 20:34:34 -0700 Subject: [PATCH 20/86] Added Keithley 485 pico-ampmeter support --- .../comm/pl_gpib_communicator.py | 2 +- instruments/keithley/__init__.py | 1 + instruments/keithley/keithley485.py | 466 ++++++++++++++++++ 3 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 instruments/keithley/keithley485.py diff --git a/instruments/abstract_instruments/comm/pl_gpib_communicator.py b/instruments/abstract_instruments/comm/pl_gpib_communicator.py index 003a2bb38..70b44bff1 100644 --- a/instruments/abstract_instruments/comm/pl_gpib_communicator.py +++ b/instruments/abstract_instruments/comm/pl_gpib_communicator.py @@ -283,4 +283,4 @@ def _query(self, msg, size=-1): :rtype: `str` """ self._sendcmd(msg) - return self._file.read(size) + return self._file.read(size).replace(self._eos, '') diff --git a/instruments/keithley/__init__.py b/instruments/keithley/__init__.py index 0c8c34e4f..36b8eaf48 100644 --- a/instruments/keithley/__init__.py +++ b/instruments/keithley/__init__.py @@ -7,6 +7,7 @@ from __future__ import absolute_import from .keithley195 import Keithley195 +from .keithley485 import Keithley485 from .keithley580 import Keithley580 from .keithley2182 import Keithley2182 from .keithley6220 import Keithley6220 diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py new file mode 100644 index 000000000..3170b5bfc --- /dev/null +++ b/instruments/keithley/keithley485.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# keithley485.py: Driver for the Keithley 485 pico-ampmeter. +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# 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 . +# +""" +Driver for the Keithley 485 pico-ampmeter. + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com). + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +import time +import struct + +from enum import IntEnum + +import quantities as pq + +from instruments.abstract_instruments import Instrument + +# CLASSES ##################################################################### + + +class Keithley485(Instrument): + + """ + The Keithley Model 485 is a 3 1/2 digit resolution autoranging + pico-ampmeter with a +- 2000 count LCD. It is designed for low + current measurement requirements from 0.1pA to 2mA. + + The device needs some processing time (manual reports 300-500ms) after a + command has been transmitted. + """ + + def __init__(self, filelike): + """ + Initialise the instrument + """ + super(Keithley485, self).__init__(filelike) + + # ENUMS # + + class Mode(IntEnum): + """ + Enum containing the supported mode codes + """ + #: DC Current + current_dc = 0 + + class Polarity(IntEnum): + """ + Enum containing valid polarity modes for the Keithley 485 + """ + positive = 0 + negative = 1 + + class Drive(IntEnum): + """ + Enum containing valid drive modes for the Keithley 485 + """ + pulsed = 0 + dc = 1 + + class TriggerMode(IntEnum): + """ + Enum containing valid trigger modes for the Keithley 485 + """ + talk_continuous = 0 + talk_one_shot = 1 + get_continuous = 2 + get_one_shot = 3 + trigger_continuous = 4 + trigger_one_shot = 5 + + # PROPERTIES # + + @property + def polarity(self): + """ + Gets/sets instrument polarity. + + Example use: + + >>> import instruments as ik + >>> keithley = ik.keithley.Keithley485.open_gpibusb('/dev/ttyUSB0', 1) + >>> keithley.polarity = keithley.Polarity.positive + + :type: `Keithley485.Polarity` + """ + value = self.parse_status_word(self.get_status_word())['polarity'] + if value == '+': + return Keithley485.Polarity.positive + elif value == '-': + return Keithley485.Polarity.negative + else: + raise ValueError('Not a valid polarity returned from ' + 'instrument, got {}'.format(value)) + + @polarity.setter + def polarity(self, newval): + if isinstance(newval, str): + newval = Keithley485.Polarity[newval] + if not isinstance(newval, Keithley485.Polarity): + raise TypeError('Polarity must be specified as a ' + 'Keithley485.Polarity, got {} ' + 'instead.'.format(newval)) + + self.sendcmd('P{}X'.format(newval.value)) + + @property + def drive(self): + """ + Gets/sets the instrument drive to either pulsed or DC. + + Example use: + + >>> import instruments as ik + >>> keithley = ik.keithley.Keithley485.open_gpibusb('/dev/ttyUSB0', 1) + >>> keithley.drive = keithley.Drive.pulsed + + :type: `Keithley485.Drive` + """ + value = self.parse_status_word(self.get_status_word())['drive'] + return Keithley485.Drive[value] + + @drive.setter + def drive(self, newval): + if isinstance(newval, str): + newval = Keithley485.Drive[newval] + if not isinstance(newval, Keithley485.Drive): + raise TypeError('Drive must be specified as a ' + 'Keithley485.Drive, got {} ' + 'instead.'.format(newval)) + + self.sendcmd('D{}X'.format(newval.value)) + + @property + def dry_circuit_test(self): + """ + Gets/sets the 'dry circuit test' mode of the Keithley 485. + + This mode is used to minimize any physical and electrical changes in + the contact junction by limiting the maximum source voltage to 20mV. + By limiting the voltage, the measuring circuit will leave the resistive + surface films built up on the contacts undisturbed. This allows for + measurement of the resistance of these films. + + See the Keithley 485 manual for more information. + + :type: `bool` + """ + return self.parse_status_word(self.get_status_word())['drycircuit'] + + @dry_circuit_test.setter + def dry_circuit_test(self, newval): + if not isinstance(newval, bool): + raise TypeError('DryCircuitTest mode must be a boolean.') + self.sendcmd('C{}X'.format(int(newval))) + + @property + def operate(self): + """ + Gets/sets the operating mode of the Keithley 485. If set to true, the + instrument will be in operate mode, while false sets the instruments + into standby mode. + + :type: `bool` + """ + return self.parse_status_word(self.get_status_word())['operate'] + + @operate.setter + def operate(self, newval): + if not isinstance(newval, bool): + raise TypeError('Operate mode must be a boolean.') + self.sendcmd('O{}X'.format(int(newval))) + + @property + def relative(self): + """ + Gets/sets the relative measurement mode of the Keithley 485. + + As stated in the manual: The relative function is used to establish a + baseline reading. This reading is subtracted from all subsequent + readings. The purpose of making relative measurements is to cancel test + lead and offset currents or to store an input as a reference level. + + Once a relative level is established, it remains in effect until another + relative level is set. The relative value is only good for the range the + value was taken on and higher ranges. If a lower range is selected than + that on which the relative was taken, inaccurate results may occur. + Relative cannot be activated when "OL" is displayed. + + See the manual for more information. + + :type: `bool` + """ + return self.parse_status_word(self.get_status_word())['relative'] + + @relative.setter + def relative(self, newval): + if not isinstance(newval, bool): + raise TypeError('Relative mode must be a boolean.') + self.sendcmd('Z{}X'.format(int(newval))) + + @property + def trigger_mode(self): + """ + Gets/sets the trigger mode of the Keithley 485. + + There are two different trigger settings for three different sources. + This means there are six different settings for the trigger mode. + + The two types are continuous and one-shot. Continuous has the instrument + continuously sample the current. One-shot performs a single + current measurement. + + The three trigger sources are on talk, on GET, and on "X". On talk + refers to addressing the instrument to talk over GPIB. On GET is when + the instrument receives the GPIB command byte for "group execute + trigger". Last, on "X" is when one sends the ASCII character "X" to the + instrument. This character is used as a general execute to confirm + commands send to the instrument. In InstrumentKit, "X" is sent after + each command so it is not suggested that one uses on "X" triggering. + + :type: `Keithley485.TriggerMode` + """ + raise NotImplementedError + + @trigger_mode.setter + def trigger_mode(self, newval): + if isinstance(newval, str): + newval = Keithley485.TriggerMode[newval] + if newval not in Keithley485.TriggerMode: + raise TypeError('Drive must be specified as a ' + 'Keithley485.TriggerMode, got {} ' + 'instead.'.format(newval)) + self.sendcmd('T{}X'.format(newval.value)) + + @property + def input_range(self): + """ + Gets/sets the range of the Keithley 485 input terminals. The valid + ranges are one of ``{AUTO|2e-9|2e-8|2e-7|2e-6|2e-5|2e-4|2e-3}`` + + :type: `~quantities.quantity.Quantity` or `str` + """ + value = float(self.parse_status_word(self.get_status_word())['range']) + return value * pq.ohm + + @input_range.setter + def input_range(self, newval): + valid = ('auto', 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) + if isinstance(newval, str): + newval = newval.lower() + if newval == 'auto': + self.sendcmd('R0X') + return + else: + raise ValueError('Only "auto" is acceptable when specifying ' + 'the input range as a string.') + if isinstance(newval, pq.quantity.Quantity): + newval = float(newval) + + if isinstance(newval, (float, int)): + if newval in valid: + newval = valid.index(newval) + else: + raise ValueError('Valid range settings are: {}'.format(valid)) + else: + raise TypeError('Range setting must be specified as a float, int, ' + 'or the string "auto", got {}'.format(type(newval))) + self.sendcmd('R{}X'.format(newval)) + + # METHODS # + + def trigger(self): + """ + Tell the Keithley 485 to execute all commands that it has received. + + Do note that this is different from the standard SCPI ``*TRG`` command + (which is not supported by the 485 anyways). + """ + self.sendcmd('X') + + def auto_range(self): + """ + Turn on auto range for the Keithley 485. + + This is the same as calling the `Keithley485.set_current_range` + method and setting the parameter to "AUTO". + """ + self.sendcmd('R0X') + + def set_calibration_value(self, value): + """ + Sets the calibration value. This is not currently implemented. + + :param value: Calibration value to write + """ + # self.write('V+n.nnnnE+nn') + raise NotImplementedError('setCalibrationValue not implemented') + + def store_calibration_constants(self): + """ + Instructs the instrument to store the calibration constants. This is + not currently implemented. + """ + # self.write('L0X') + raise NotImplementedError('setCalibrationConstants not implemented') + + def get_status_word(self): + """ + The keithley will not always respond with the statusword when asked. We + use a simple heuristic here: request it up to 5 times, using a 1s + delay to allow the keithley some thinking time. + + :rtype: `str` + """ + tries = 5 + statusword = '' + while statusword[:3] != '485' and tries != 0: + tries -= 1 + self.sendcmd('U0X') + time.sleep(1) + statusword = self.query('') + + if statusword is None: + raise IOError('could not retrieve status word') + + return statusword[:-1] + + def parse_status_word(self, statusword): + """ + Parse the status word returned by the function + `~Keithley485.get_status_word`. + + Returns a `dict` with the following keys: + ``{drive,polarity,drycircuit,operate,range,relative,eoi,trigger, + sqrondata,sqronerror,linefreq,terminator}`` + + :param statusword: Byte string to be unpacked and parsed + :type: `str` + + :rtype: `dict` + """ + if statusword[:3] != '485': + raise ValueError('Status word starts with wrong ' + 'prefix: {}'.format(statusword)) + + (drive, polarity, drycircuit, operate, rng, + relative, eoi, trigger, sqrondata, sqronerror, + linefreq) = struct.unpack('@8c2s2s2', statusword[3:]) + + valid = {'drive': {'0': 'pulsed', + '1': 'dc'}, + 'polarity': {'0': '+', + '1': '-'}, + 'range': {'0': 'auto', + '1': 2e-9, + '2': 2e-8, + '3': 2e-7, + '4': 2e-6, + '5': 2e-5, + '6': 2e-4, + '7': 2e-4}, + 'linefreq': {'0': '60Hz', + '1': '50Hz'}} + + try: + drive = valid['drive'][drive] + polarity = valid['polarity'][polarity] + rng = valid['range'][rng] + linefreq = valid['linefreq'][linefreq] + except: + raise RuntimeError('Cannot parse status ' + 'word: {}'.format(statusword)) + + return {'drive': drive, + 'polarity': polarity, + 'drycircuit': (drycircuit == '1'), + 'operate': (operate == '1'), + 'range': rng, + 'relative': (relative == '1'), + 'eoi': eoi, + 'trigger': (trigger == '1'), + 'sqrondata': sqrondata, + 'sqronerror': sqronerror, + 'linefreq': linefreq, + 'terminator': self.terminator} + + def measure(self, mode=0): + """ + Perform a measurement with the Keithley 485. + + The usual mode parameter is ignored for the Keithley 485 as the only + valid mode is current. + + :rtype: `~quantities.quantity.Quantity` + """ + if not isinstance(mode, self.Mode): + raise ValueError('This mode is not supported: {}'.format(mode.name)) + + self.trigger() + return self.parse_measurement(self.query(''))['current'] + + @staticmethod + def parse_measurement(measurement): + """ + Parse the measurement string returned by the instrument. + + Returns a dict with the following keys: + ``{status,polarity,drycircuit,drive,current}`` + + :param measurement: String to be unpacked and parsed + :type: `str` + + :rtype: `dict` + """ + (status, function, base, current) = \ + struct.unpack('@1c2s1c10s', bytes(measurement, 'utf-8')) + + valid = {'status': {b'N': 'normal', + b'C': 'zerocheck', + b'O': 'overflow', + b'Z': 'relative'}, + 'function': {b'DC': 'dc-current'}, + 'base': {b'A': '10', + b'L': 'log10'}} + try: + status = valid['status'][status] + function = valid['function'][function] + base = valid['base'][base] + current = float(current) * pq.ohm + except: + raise Exception('Cannot parse measurement: {}'.format(measurement)) + + return {'status': status, + 'function': function, + 'base': base, + 'current': current} From 44c96152107de9aab0d9a369cfe8750b560cf8e2 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 16 Jul 2019 20:53:41 -0700 Subject: [PATCH 21/86] Bug fix --- instruments/keithley/keithley485.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 3170b5bfc..d69c7154d 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -50,8 +50,8 @@ class Keithley485(Instrument): """ - The Keithley Model 485 is a 3 1/2 digit resolution autoranging - pico-ampmeter with a +- 2000 count LCD. It is designed for low + The Keithley Model 485 is a 4 1/2 digit resolution autoranging + pico-ampmeter with a +- 20000 count LCD. It is designed for low current measurement requirements from 0.1pA to 2mA. The device needs some processing time (manual reports 300-500ms) after a @@ -414,7 +414,7 @@ def parse_status_word(self, statusword): 'linefreq': linefreq, 'terminator': self.terminator} - def measure(self, mode=0): + def measure(self, mode=Mode.current_dc): """ Perform a measurement with the Keithley 485. @@ -423,7 +423,7 @@ def measure(self, mode=0): :rtype: `~quantities.quantity.Quantity` """ - if not isinstance(mode, self.Mode): + if not isinstance(mode, Keithley485.Mode): raise ValueError('This mode is not supported: {}'.format(mode.name)) self.trigger() @@ -445,13 +445,13 @@ def parse_measurement(measurement): (status, function, base, current) = \ struct.unpack('@1c2s1c10s', bytes(measurement, 'utf-8')) - valid = {'status': {b'N': 'normal', - b'C': 'zerocheck', - b'O': 'overflow', - b'Z': 'relative'}, + valid = {'status': {b'N': 'normal', + b'C': 'zerocheck', + b'O': 'overflow', + b'Z': 'relative'}, 'function': {b'DC': 'dc-current'}, - 'base': {b'A': '10', - b'L': 'log10'}} + 'base': {b'A': '10', + b'L': 'log10'}} try: status = valid['status'][status] function = valid['function'][function] From 89df0b3e738d1d3c33f2dd5083952c078c97fd00 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 16 Jul 2019 21:27:30 -0700 Subject: [PATCH 22/86] Added status word parser for Keithley 485 --- instruments/keithley/keithley485.py | 97 +++++++++++++++++------------ 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index d69c7154d..ecb3cc7b3 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -373,46 +373,63 @@ def parse_status_word(self, statusword): raise ValueError('Status word starts with wrong ' 'prefix: {}'.format(statusword)) - (drive, polarity, drycircuit, operate, rng, - relative, eoi, trigger, sqrondata, sqronerror, - linefreq) = struct.unpack('@8c2s2s2', statusword[3:]) - - valid = {'drive': {'0': 'pulsed', - '1': 'dc'}, - 'polarity': {'0': '+', - '1': '-'}, - 'range': {'0': 'auto', - '1': 2e-9, - '2': 2e-8, - '3': 2e-7, - '4': 2e-6, - '5': 2e-5, - '6': 2e-4, - '7': 2e-4}, - 'linefreq': {'0': '60Hz', - '1': '50Hz'}} + (zerocheck, log, range, relative, eoi, + trigger, datamask, errormask) = \ + struct.unpack('@6c2s2s', bytes(statusword[3:], 'utf-8')) + + valid = {'range': {b'0': 'auto', + b'1': 2e-9, + b'2': 2e-8, + b'3': 2e-7, + b'4': 2e-6, + b'5': 2e-5, + b'6': 2e-4, + b'7': 2e-3}, + 'eoi': {b'0': 'send', + b'1': 'omit'}, + 'trigger': {b'0': 'continuous_ontalk', + b'1': 'oneshot_ontalk', + b'2': 'continuous_onget', + b'3': 'oneshot_onget', + b'4': 'continuous_onx', + b'1': 'oneshot_onx'}, + 'datamask': {b'00': 'srq_disabled', + b'01': 'read_of', + b'08': 'read_done', + b'09': 'read_done_of', + b'16': 'busy', + b'17': 'busy_read_of', + b'24': 'busy_read_done', + b'25': 'busy_read_done_of'}, + 'errormask': {b'00': 'srq_disabled', + b'01': 'idcc0', + b'02': 'idcc', + b'03': 'idcc0_idcc', + b'04': 'not_remote', + b'05': 'not_remote_iddc0', + b'06': 'not_remote_idcc', + b'07': 'not_remote_idcc0_idcc'} + } try: - drive = valid['drive'][drive] - polarity = valid['polarity'][polarity] - rng = valid['range'][rng] - linefreq = valid['linefreq'][linefreq] + range = valid['range'][range] + eoi = valid['eoi'][eoi] + trigger = valid['trigger'][trigger] + datamask = valid['datamask'][datamask] + errormask = valid['errormask'][errormask] except: raise RuntimeError('Cannot parse status ' 'word: {}'.format(statusword)) - return {'drive': drive, - 'polarity': polarity, - 'drycircuit': (drycircuit == '1'), - 'operate': (operate == '1'), - 'range': rng, - 'relative': (relative == '1'), + return {'zerocheck': zerocheck == 1, + 'log': log == 1, + 'range': range, + 'relative': relative == 1, 'eoi': eoi, - 'trigger': (trigger == '1'), - 'sqrondata': sqrondata, - 'sqronerror': sqronerror, - 'linefreq': linefreq, - 'terminator': self.terminator} + 'trigger': trigger, + 'datamask': datamask, + 'errormask': errormask, + 'terminator':self.terminator} def measure(self, mode=Mode.current_dc): """ @@ -445,13 +462,13 @@ def parse_measurement(measurement): (status, function, base, current) = \ struct.unpack('@1c2s1c10s', bytes(measurement, 'utf-8')) - valid = {'status': {b'N': 'normal', - b'C': 'zerocheck', - b'O': 'overflow', - b'Z': 'relative'}, - 'function': {b'DC': 'dc-current'}, - 'base': {b'A': '10', - b'L': 'log10'}} + valid = {'status': {b'N': 'normal', + b'C': 'zerocheck', + b'O': 'overflow', + b'Z': 'relative'}, + 'function': {b'DC': 'dc-current'}, + 'base': {b'A': '10', + b'L': 'log10'}} try: status = valid['status'][status] function = valid['function'][function] From d7da2a54df0783cddf202dc65bacf909a94e55f7 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 13:00:44 -0700 Subject: [PATCH 23/86] Combined GPIB communicators into one --- .../abstract_instruments/comm/__init__.py | 3 +- ...b_communicator.py => gpib_communicator.py} | 49 +-- .../comm/pl_gpib_communicator.py | 286 ------------------ .../abstract_instruments/instrument.py | 15 +- 4 files changed, 35 insertions(+), 318 deletions(-) rename instruments/abstract_instruments/comm/{gi_gpib_communicator.py => gpib_communicator.py} (91%) delete mode 100644 instruments/abstract_instruments/comm/pl_gpib_communicator.py diff --git a/instruments/abstract_instruments/comm/__init__.py b/instruments/abstract_instruments/comm/__init__.py index a941732ac..772b31fb9 100644 --- a/instruments/abstract_instruments/comm/__init__.py +++ b/instruments/abstract_instruments/comm/__init__.py @@ -13,8 +13,7 @@ from .serial_communicator import SerialCommunicator from .visa_communicator import VisaCommunicator from .loopback_communicator import LoopbackCommunicator -from .gi_gpib_communicator import GPIBCommunicator -from .pl_gpib_communicator import PLGPIBCommunicator +from .gpib_communicator import GPIBCommunicator from .file_communicator import FileCommunicator from .usbtmc_communicator import USBTMCCommunicator from .vxi11_communicator import VXI11Communicator diff --git a/instruments/abstract_instruments/comm/gi_gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py similarity index 91% rename from instruments/abstract_instruments/comm/gi_gpib_communicator.py rename to instruments/abstract_instruments/comm/gpib_communicator.py index 3123059d7..e88f8e0be 100644 --- a/instruments/abstract_instruments/comm/gi_gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -34,18 +34,20 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ # pylint: disable=too-many-instance-attributes - def __init__(self, filelike, gpib_address): + def __init__(self, filelike, gpib_address, prologix=False): super(GPIBCommunicator, self).__init__(self) self._file = filelike self._gpib_address = gpib_address + self._prologix = prologix self._file.terminator = "\r" - self._version = int(self._file.query("+ver")) + if not prologix: + self._version = int(self._file.query("+ver")) self._terminator = None self.terminator = "\n" self._eoi = True self._timeout = 1000 * pq.millisecond - if self._version <= 4: + if not self._prologix and self._version <= 4: self._eos = 10 else: self._eos = "\n" @@ -95,10 +97,10 @@ def timeout(self): @timeout.setter def timeout(self, newval): newval = assume_units(newval, pq.second) - if self._version <= 4: + if not self._prologix and self._version <= 4: newval = newval.rescale(pq.second) self._file.sendcmd('+t:{}'.format(newval.magnitude)) - elif self._version >= 5: + else: newval = newval.rescale(pq.millisecond) self._file.sendcmd("++read_tmo_ms {}".format(newval.magnitude)) self._file.timeout = newval.rescale(pq.second) @@ -127,7 +129,7 @@ def terminator(self, newval): if isinstance(newval, str): newval = newval.lower() - if self._version <= 4: + if not self._prologix and self._version <= 4: if newval == 'eoi': self.eoi = True elif not isinstance(newval, int): @@ -148,7 +150,7 @@ def terminator(self, newval): self.eoi = False self.eos = newval self._terminator = chr(newval) - elif self._version >= 5: + else: if newval != "eoi": self.eos = newval self.eoi = False @@ -182,10 +184,11 @@ def eoi(self, newval): if not isinstance(newval, bool): raise TypeError("EOI status must be specified as a boolean") self._eoi = newval - if self._version >= 5: - self._file.sendcmd("++eoi {}".format('1' if newval else '0')) - else: + if not self._prologix and self._version <= 4: self._file.sendcmd("+eoi:{}".format('1' if newval else '0')) + else: + self._file.sendcmd("++eoi {}".format('1' if newval else '0')) + @property def eos(self): @@ -203,12 +206,12 @@ def eos(self): @eos.setter def eos(self, newval): - if self._version <= 4: + if not self._prologix and self._version <= 4: if isinstance(newval, (str, bytes)): newval = ord(newval) self._file.sendcmd("+eos:{}".format(newval)) self._eos = newval - elif self._version >= 5: + else: if isinstance(newval, int): newval = str(chr(newval)) if newval == "\r\n": @@ -309,12 +312,20 @@ def _sendcmd(self, msg): if msg == '': return - self._file.sendcmd('+a:' + str(self._gpib_address)) - time.sleep(sleep_time) - self.eoi = self.eoi - time.sleep(sleep_time) - self.timeout = self.timeout - time.sleep(sleep_time) + + if not self._prologix: + self._file.sendcmd('+a:' + str(self._gpib_address)) + time.sleep(sleep_time) + self.eoi = self.eoi + time.sleep(sleep_time) + self.timeout = self.timeout + time.sleep(sleep_time) + else: + # TODO need to set the address + #self._file.sendcmd('++addr ' + str(self._gpib_address)) + #time.sleep(sleep_time) + pass + self.eos = self.eos time.sleep(sleep_time) self._file.sendcmd(msg) @@ -338,6 +349,6 @@ def _query(self, msg, size=-1): :rtype: `str` """ self.sendcmd(msg) - if '?' not in msg: + if not self._prologix and '?' not in msg: self._file.sendcmd('+read') return self._file.read(size).strip() diff --git a/instruments/abstract_instruments/comm/pl_gpib_communicator.py b/instruments/abstract_instruments/comm/pl_gpib_communicator.py deleted file mode 100644 index 70b44bff1..000000000 --- a/instruments/abstract_instruments/comm/pl_gpib_communicator.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Provides a communication layer for an instrument connected via a Prologix -GPIB adapter. -""" - -# IMPORTS ##################################################################### - -from __future__ import absolute_import -from __future__ import division -from __future__ import unicode_literals - -import io -import time - -from builtins import chr, str, bytes -import quantities as pq - -from instruments.abstract_instruments.comm import AbstractCommunicator -from instruments.util_fns import assume_units - -# CLASSES ##################################################################### - - -class PLGPIBCommunicator(io.IOBase, AbstractCommunicator): - - """ - Communicates with a SocketCommunicator or SerialCommunicator object for - use with Prologix GPIBUSB or GPIBETHERNET adapters. - - It essentially wraps those physical communication layers with the extra - overhead required by the Prologix GPIB adapters. - """ - - # pylint: disable=too-many-instance-attributes - def __init__(self, filelike, gpib_address): - super(PLGPIBCommunicator, self).__init__(self) - - self._file = filelike - self._gpib_address = gpib_address - self._file.terminator = "\r" - self._version = float(self._file.query("++ver").split(' ')[-1]) - self._terminator = None - self.terminator = "\n" - self._eoi = True - self._timeout = 1000 * pq.millisecond - self._eos = "\n" - - # PROPERTIES # - - @property - def address(self): - """ - Gets/sets the GPIB address and downstream address associated with - the instrument. - - When setting, if specified as an integer, only changes the GPIB - address. If specified as a list, the first element changes the GPIB - address, while the second is passed downstream. - - Example: [gpib_address, downstream_address] - - Where downstream_address needs to be formatted as appropriate for the - connection (eg SerialCommunicator, SocketCommunicator, etc). - """ - return self._gpib_address, self._file.address - - @address.setter - def address(self, newval): - if isinstance(newval, int): - if (newval < 1) or (newval > 30): - raise ValueError("GPIB address must be between 1 and 30.") - self._gpib_address = newval - elif isinstance(newval, list): - self.address = newval[0] # Set GPIB address - self._file.address = newval[1] # Send downstream address - else: - raise TypeError("Not a valid input type for Instrument address.") - - @property - def timeout(self): - """ - Gets/sets the timeeout of both the GPIB bus and the connection - channel between the PC and the GPIB adapter. - - :type: `~quantities.Quantity` - :units: As specified, or assumed to be of units ``seconds`` - """ - return self._timeout - - @timeout.setter - def timeout(self, newval): - newval = assume_units(newval, pq.second) - newval = newval.rescale(pq.millisecond) - self._file.sendcmd("++read_tmo_ms {}".format(newval.magnitude)) - self._file.timeout = newval.rescale(pq.second) - self._timeout = newval.rescale(pq.second) - - @property - def terminator(self): - """ - Gets/sets the GPIB termination character. This can be set to - ``\n``, ``\r``, ``\r\n``, or ``eoi``. - - .. seealso:: `eos` and `eoi` for direct manipulation of these - parameters. - - :type: `str` - """ - if not self._eoi: - return self._terminator - - return 'eoi' - - @terminator.setter - def terminator(self, newval): - if isinstance(newval, bytes): - newval = newval.decode("utf-8") - if isinstance(newval, str): - newval = newval.lower() - - if newval != "eoi": - self.eos = newval - self.eoi = False - self._terminator = self.eos - elif newval == "eoi": - self.eos = None - self._terminator = "eoi" - self.eoi = True - - @property - def eoi(self): - """ - Gets/sets the EOI usage status. - - EOI is a dedicated line on the GPIB bus. When used, it is used by - instruments to signal that the current byte being transmitted is the - last in the message. This avoids the need to use a dedicated - termination character such as ``\n``. Frequently, instruments will - use both EOI-signalling and append an end-of-string (EOS) character. - Some will only use one or the other. - - .. seealso:: `terminator`, `eos` for more communication termination - related properties. - - :type: `bool` - """ - return self._eoi - - @eoi.setter - def eoi(self, newval): - if not isinstance(newval, bool): - raise TypeError("EOI status must be specified as a boolean") - self._eoi = newval - self._file.sendcmd("++eoi {}".format('1' if newval else '0')) - - @property - def eos(self): - """ - Gets/sets the end-of-string (EOS) character. - - Valid EOS settings are ``\n``, ``\r``, ``\r\n`` and `None`. - - .. seealso:: `terminator`, `eoi` for more communication termination - related properties. - - :type: `str` or `None` - """ - return self._eos - - @eos.setter - def eos(self, newval): - if isinstance(newval, int): - newval = str(chr(newval)) - if newval == "\r\n": - self._eos = newval - newval = 0 - elif newval == "\r": - self._eos = newval - newval = 1 - elif newval == "\n": - self._eos = newval - newval = 2 - elif newval is None: - self._eos = newval - newval = 3 - else: - raise ValueError("EOS must be CRLF, CR, LF, or None") - self._file.sendcmd("++eos {}".format(newval)) - - # FILE-LIKE METHODS # - - def close(self): - """ - Close connection to the underlying physical connection channel - of the GPIB connection. This is typically a serial connection that - is then closed. - """ - self._file.close() - - def read_raw(self, size=-1): - """ - Read bytes in from the gpibusb connection. - - :param int size: The number of bytes to read in from the - connection. - - :return: The read bytes from the connection - :rtype: `bytes` - """ - return self._file.read_raw(size) - - def read(self, size=-1, encoding="utf-8"): - """ - Read characters from wrapped class (ie SocketCommunicator or - SerialCommunicator). - - If size = -1, characters will be read until termination character - is found. - - GI GPIB adapters always terminate serial connections with a CR. - Function will read until a CR is found. - - :param int size: Number of bytes to read - :param str encoding: Encoding that will be applied to the read bytes - - :return: Data read from the GPIB adapter - :rtype: `str` - """ - return self._file.read(size, encoding) - - def write_raw(self, msg): - """ - Write bytes to the gpibusb connection. - - :param bytes msg: Bytes to be sent to the instrument over the - connection. - """ - self._file.write_raw(msg) - - def write(self, msg, encoding="utf-8"): - """ - Write data string to GPIB connected instrument. - - :param str msg: String to write to the instrument - :param str encoding: Encoding to apply on msg to convert the message - into bytes - """ - self._file.write(msg, encoding) - - def flush_input(self): - """ - Instruct the communicator to flush the input buffer, discarding the - entirety of its contents. - """ - self._file.flush_input() - - # METHODS # - - def _sendcmd(self, msg): - """ - This is the implementation of ``sendcmd`` for communicating with - the Prologix GPIB adapter. This function is in turn wrapped by - the concrete method `AbstractCommunicator.sendcmd` to provide consistent - logging functionality across all communication layers. - - :param str msg: The command message to send to the instrument - """ - self._file.sendcmd(msg) - - def _query(self, msg, size=-1): - """ - This is the implementation of ``query`` for communicating with - the Prologix GPIB adapter. This function is in turn wrapped by - the concrete method `AbstractCommunicator.query` to provide consistent - logging functionality across all communication layers. - - :param str msg: The query message to send to the instrument - :param int size: The number of bytes to read back from the instrument - response. - :return: The instrument response to the query - :rtype: `str` - """ - self._sendcmd(msg) - return self._file.read(size).replace(self._eos, '') diff --git a/instruments/abstract_instruments/instrument.py b/instruments/abstract_instruments/instrument.py index cc34d0a22..97aaf80cf 100644 --- a/instruments/abstract_instruments/instrument.py +++ b/instruments/abstract_instruments/instrument.py @@ -40,9 +40,8 @@ class WindowsError(OSError): from instruments.abstract_instruments.comm import ( SocketCommunicator, USBCommunicator, VisaCommunicator, FileCommunicator, - LoopbackCommunicator, GPIBCommunicator, PLGPIBCommunicator, - AbstractCommunicator, USBTMCCommunicator, VXI11Communicator, - serial_manager + LoopbackCommunicator, GPIBCommunicator, AbstractCommunicator, + USBTMCCommunicator, VXI11Communicator, serial_manager ) from instruments.errors import AcknowledgementError, PromptError @@ -560,10 +559,7 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, prologix=F timeout=timeout, write_timeout=write_timeout ) - if not prologix: - return cls(GPIBCommunicator(ser, gpib_address)) - else: - return cls(PLGPIBCommunicator(ser, gpib_address)) + return cls(GPIBCommunicator(ser, gpib_address, prologix)) @classmethod def open_gpibethernet(cls, host, port, gpib_address, prologix=False): @@ -574,10 +570,7 @@ def open_gpibethernet(cls, host, port, gpib_address, prologix=False): """ conn = socket.socket() conn.connect((host, port)) - if not prologix: - return cls(GPIBCommunicator(conn, gpib_address)) - else: - return cls(PLGPIBCommunicator(conn, gpib_address)) + return cls(GPIBCommunicator(conn, gpib_address, prologix)) @classmethod def open_visa(cls, resource_name): From f80ac0e903b1ba6308c68dde19ae5dc18727aa5b Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 13:01:53 -0700 Subject: [PATCH 24/86] Corrected unit returned by Keithley 485 --- instruments/keithley/keithley485.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index ecb3cc7b3..b74f4c805 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -271,7 +271,7 @@ def input_range(self): :type: `~quantities.quantity.Quantity` or `str` """ value = float(self.parse_status_word(self.get_status_word())['range']) - return value * pq.ohm + return value * pq.amp @input_range.setter def input_range(self, newval): @@ -435,7 +435,7 @@ def measure(self, mode=Mode.current_dc): """ Perform a measurement with the Keithley 485. - The usual mode parameter is ignored for the Keithley 485 as the only + The usual mode parameter is defaulted for the Keithley 485 as the only valid mode is current. :rtype: `~quantities.quantity.Quantity` @@ -473,7 +473,7 @@ def parse_measurement(measurement): status = valid['status'][status] function = valid['function'][function] base = valid['base'][base] - current = float(current) * pq.ohm + current = float(current) * pq.amp except: raise Exception('Cannot parse measurement: {}'.format(measurement)) From 48b94b7ceeb611d7f971ca6886ecb24417dbce41 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 20:44:22 -0700 Subject: [PATCH 25/86] Attempt to fix build for python 3.7 --- instruments/tests/test_base_instrument.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/instruments/tests/test_base_instrument.py b/instruments/tests/test_base_instrument.py index f369f3d95..c8e750de2 100644 --- a/instruments/tests/test_base_instrument.py +++ b/instruments/tests/test_base_instrument.py @@ -270,7 +270,8 @@ def test_instrument_open_gpibusb(mock_serial_manager, mock_gpib_comm): mock_gpib_comm.assert_called_with( mock_serial_manager.new_serial_connection.return_value, - 1 + 1, + 0 ) From a64d712307d7c665c3e3d654322746fd6a1f76ac Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 20:49:53 -0700 Subject: [PATCH 26/86] Reformated GPIB controller selection as a model selection --- .../comm/gpib_communicator.py | 17 ++++++++++++++++- instruments/abstract_instruments/instrument.py | 8 ++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/instruments/abstract_instruments/comm/gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py index e88f8e0be..f5cfc9ab7 100644 --- a/instruments/abstract_instruments/comm/gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -14,6 +14,8 @@ import io import time +from enum import Enum + from builtins import chr, str, bytes import quantities as pq @@ -34,8 +36,10 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ # pylint: disable=too-many-instance-attributes - def __init__(self, filelike, gpib_address, prologix=False): + def __init__(self, filelike, gpib_address, model=0): super(GPIBCommunicator, self).__init__(self) + if not isinstance(model, GPIBCommunicator.Model): + raise ValueError('GPIB Controller not supported:'.format(model.value)) self._file = filelike self._gpib_address = gpib_address @@ -52,6 +56,17 @@ def __init__(self, filelike, gpib_address, prologix=False): else: self._eos = "\n" + # ENUMS # + + class Model(Enum): + """ + Enum containing the supported GPIB controller models + """ + #: Galvant Industries + gi = "Galvant Industries" + #: Prologix, LLC + pl = "Prologix" + # PROPERTIES # @property diff --git a/instruments/abstract_instruments/instrument.py b/instruments/abstract_instruments/instrument.py index 97aaf80cf..fca15b7de 100644 --- a/instruments/abstract_instruments/instrument.py +++ b/instruments/abstract_instruments/instrument.py @@ -529,7 +529,7 @@ def open_serial(cls, port=None, baud=9600, vid=None, pid=None, return cls(ser) @classmethod - def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, prologix=False): + def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, model=GPIBCommunicator.Model.gi): """ Opens an instrument, connecting via a `Galvant Industries GPIB-USB adapter`_. @@ -559,10 +559,10 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, prologix=F timeout=timeout, write_timeout=write_timeout ) - return cls(GPIBCommunicator(ser, gpib_address, prologix)) + return cls(GPIBCommunicator(ser, gpib_address, model)) @classmethod - def open_gpibethernet(cls, host, port, gpib_address, prologix=False): + def open_gpibethernet(cls, host, port, gpib_address, model=GPIBCommunicator.Model.gi): """ .. warning:: The GPIB-Ethernet adapter that this connection would use does not actually exist, and thus this class method should @@ -570,7 +570,7 @@ def open_gpibethernet(cls, host, port, gpib_address, prologix=False): """ conn = socket.socket() conn.connect((host, port)) - return cls(GPIBCommunicator(conn, gpib_address, prologix)) + return cls(GPIBCommunicator(conn, gpib_address, model)) @classmethod def open_visa(cls, resource_name): From 5b619904ffcd35667e1570b0a4f2add279ce1575 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 22:17:54 -0700 Subject: [PATCH 27/86] Multiple style fixes --- instruments/fluke/fluke3000.py | 173 +++++++++++----------------- instruments/keithley/keithley485.py | 5 +- 2 files changed, 72 insertions(+), 106 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 7d16dff15..0e53a92c7 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -43,7 +43,7 @@ import quantities as pq from instruments.abstract_instruments import Multimeter -from instruments.util_fns import assume_units, bool_property, enum_property +from instruments.util_fns import enum_property # CLASSES ##################################################################### @@ -63,11 +63,11 @@ class Fluke3000(Multimeter): `Fluke3000` is a USB instrument that communicates through a serial port via the PC3000 dongle. The commands used to communicate to the dongle do not follow the SCPI standard. - + When the device is reset, it searches for available wireless modules and binds them to the PC3000 dongle. At each initialization, this class checks what device has been bound and saves their module number. - + This class is a work in progress, it currently only supports the t3000 FC Wireless Temperature Module as it is the only instrument available to the author. It also only supports single readout. @@ -86,7 +86,7 @@ def __init__(self, filelike): self.connect() # ENUMS ## - + class Module(Enum): """ @@ -96,38 +96,35 @@ class Module(Enum): m3000 = 46333030304643 #: Temperature module t3000 = 54333030304643 - + class Mode(Enum): """ Enum containing the supported mode codes """ #: AC Voltage - voltage_ac = "01" + voltage_ac = "01" #: DC Voltage - voltage_dc = "02" + voltage_dc = "02" #: AC Current - current_ac = "03" + current_ac = "03" #: DC Current - current_dc = "04" + current_dc = "04" #: Frequency - frequency = "05" + frequency = "05" #: Temperature temperature = "07" #: Resistance - resistance = "0B" + resistance = "0B" #: Capacitance capacitance = "0F" - + class TriggerMode(IntEnum): """ Enum with valid trigger modes. """ - internal = 1 - external = 2 - single = 3 - hold = 4 + single = 1 # PROPERTIES ## @@ -140,7 +137,7 @@ class TriggerMode(IntEnum): """, writeonly=True, set_fmt="{}{}") - + module = enum_property( "", Module, @@ -149,99 +146,64 @@ class TriggerMode(IntEnum): :type: `Fluke3000.Module` """, writeonly=True, - set_fmt="{}{}") - + set_fmt="{}{}") + trigger_mode = enum_property( "T", TriggerMode, doc="""Set the trigger mode. - Note that using `HP3456a.measure()` will override the `trigger_mode` to - `HP3456a.TriggerMode.single`. - - :type: `HP3456a.TriggerMode` + Note there is only one trigger mode (single). + :type: `Fluke3000.TriggerMode` """, writeonly=True, set_fmt="{}{}") - + @property def input_range(self): - """Set the input range to be used. - - The `HP3456a` has separate ranges for `~quantities.ohm` and for - `~quantities.volt`. The range value sent to the instrument depends on - the unit set on the input range value. `auto` selects auto ranging. - - :type: `~quantities.Quantity` + """ + The `Fluke3000` FC is autoranging only """ raise NotImplementedError @input_range.setter def input_range(self, value): - if isinstance(value, str): - if value.lower() == "auto": - self.sendcmd("R1W") - else: - raise ValueError("Only 'auto' is acceptable when specifying " - "the input range as a string.") - - elif isinstance(value, pq.quantity.Quantity): - if value.units == pq.volt: - valid = HP3456a.ValidRange.voltage.value - value = value.rescale(pq.volt) - elif value.units == pq.ohm: - valid = HP3456a.ValidRange.resistance.value - value = value.rescale(pq.ohm) - else: - raise ValueError("Value {} not quantity.volt or quantity.ohm" - "".format(value)) - - value = float(value) - if value not in valid: - raise ValueError("Value {} outside valid ranges " - "{}".format(value, valid)) - value = valid.index(value) + 2 - self.sendcmd("R{}W".format(value)) - else: - raise TypeError("Range setting must be specified as a float, int, " - "or the string 'auto', got {}".format(type(value))) + """ + The `Fluke3000` FC is autoranging only + """ + return NotImplementedError @property def relative(self): """ - Enable or disable `HP3456a.MathMode.Null` on the instrument. + The `Fluke3000` FC does not support relative measurements - :type: `bool` + :rtype: `bool` """ - return self._null + return False @relative.setter def relative(self, value): - if value is True: - self._null = True - self.sendcmd("M{}".format(HP3456a.MathMode.null.value)) - elif value is False: - self._null = False - self.sendcmd("M{}".format(HP3456a.MathMode.off.value)) - else: - raise TypeError("Relative setting must be specified as a bool, " - "got {}".format(type(value))) + """ + The `Fluke3000` FC does not support relative measurements + """ + return NotImplementedError # METHODS ## - + def connect(self): """ Connect to available modules and returns a dictionary of the modules found and their location """ - self.flush() # Flush serial - self.scan() # Look for connected devices + self.flush() # Flush serial + self.scan() # Look for connected devices if not self.positions: - self.reset() # Resets the PC3000 dongle - self.query("rfdis", 3) # Discovers connected modules - self.scan() # Look for connected devices - + self.reset() # Resets the PC3000 dongle + self.query_lines("rfdis", 3) # Discovers connected modules + self.scan() # Look for connected devices + if not self.positions: raise ValueError("No Fluke3000 modules available") @@ -257,14 +219,14 @@ def flush(self): except: break self.timeout = timeout - + def reset(self): """ Resets the device and unbinds all modules """ - self.query("ri", 3) # Resets the device - self.query("rfsm 1", 2) # Turns comms on - + self.query_lines("ri", 3) # Resets the device + self.query_lines("rfsm 1", 2) # Turns comms on + def scan(self): """ Search for available modules and returns @@ -274,13 +236,13 @@ def scan(self): positions = {} for port_id in range(1, 7): # Check if a device is connected to port port_id - output = self.query("rfebd 0{} 0".format(port_id))[0] + output = self.query("rfebd 0{} 0".format(port_id)) if "RFEBD" not in output: continue - + # If it is, identify the device self.read() - output = self.query('rfgus 0{}'.format(port_id), 2)[-1] + output = self.query_lines('rfgus 0{}'.format(port_id), 2)[-1] module_id = int(output.split("PH=")[-1]) if module_id == self.Module.m3000.value: positions[self.Module.m3000] = port_id @@ -290,7 +252,7 @@ def scan(self): error = "Module ID {} not implemented".format(module_id) raise NotImplementedError(error) - # Reset device readout + # Reset device readout self.query('rfemd 0{} 2'.format(port_id)) self.flush() @@ -316,12 +278,12 @@ def read_lines(self, nlines=1): try: lines.append(self.read()) i += 1 - except : + except: continue return lines - def query(self, cmd, nlines=1): + def query_lines(self, cmd, nlines=1): """ Function used to send a command to the instrument while allowing for multiline output (multiple termination characters) @@ -338,9 +300,9 @@ def query(self, cmd, nlines=1): """ self.sendcmd(cmd) return self.read_lines(nlines) - + def measure(self, mode): - """Instruct the Fluke3000 to perform a one time measurement. + """Instruct the Fluke3000 to perform a one time measurement. Example usage: @@ -372,11 +334,11 @@ def measure(self, mode): # Read out if mode == self.Mode.temperature: # The temperature module supports single readout - value = self.query("rfemd 0{} 0".format(port_id), 2)[1] + value = self.query_lines("rfemd 0{} 0".format(port_id), 2)[-1] else: # The multimeter does not support single readout, # have to open continuous readout, read, then close it - value = self.query("rfemd 0{} 1".format(port_id), 2)[1] + value = self.query_lines("rfemd 0{} 1".format(port_id), 2)[-1] self.query("rfemd 0{} 2".format(port_id)) # Check that value is consistent with the request, break @@ -396,7 +358,7 @@ def measure(self, mode): # Return with the appropriate units units = UNITS[mode] return value * units - + def _get_module(self, mode): """Gets the module associated with this measurement mode. @@ -410,16 +372,16 @@ def _get_module(self, mode): """ if mode == self.Mode.temperature: return self.Module.t3000 - else: - return self.Module.m3000 + + return self.Module.m3000 def _parse(self, result, mode): """Parses the module output. - :param result: Output of the query. - :param mode: Desired measurement mode. - - :type result: `string` + :param result: Output of the query. + :param mode: Desired measurement mode. + + :type result: `string` :type mode: `Fluke3000.Mode` :return: A measurement from the multimeter. @@ -435,8 +397,9 @@ def _parse(self, result, mode): # Check that the multimeter is in the right mode (fifth byte) if self._parse_mode(data) != mode.value: - error = "Mode {} was requested but the Fluke 3000FC Multimeter is in ".format(mode.name) - error += "mode {} instead, could not read the requested quantity.".format(self.Mode(data[8:10]).name) + error = ('Mode {} was requested but the Fluke 3000FC Multimeter ' + 'is in mode {} instead, could not read the requested' + 'quantity.').format(mode.name, self.Mode(data[8:10]).name) raise ValueError(error) # Extract the value from the first two bytes @@ -451,7 +414,8 @@ def _parse(self, result, mode): # Combine and return return scale*value - def _parse_mode(self, data): + @staticmethod + def _parse_mode(data): """Parses the measurement mode. :param data: Measurement output. @@ -465,7 +429,8 @@ def _parse_mode(self, data): # The fixth dual hex byte encodes the measurement mode return data[8:10] - def _parse_value(self, data): + @staticmethod + def _parse_value(data): """Parses the measurement value. :param data: Measurement output. @@ -479,7 +444,8 @@ def _parse_value(self, data): # The second dual hex byte is the most significant byte return int(data[2:4]+data[:2], 16) - def _parse_factor(self, data): + @staticmethod + def _parse_factor(data): """Parses the measurement prefactor. :param data: Measurement output. @@ -532,4 +498,3 @@ def _parse_factor(self, data): 5: 1e-6, # micro 6: 1e-9 # nano } - diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index b74f4c805..45e5b334f 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -63,6 +63,7 @@ def __init__(self, filelike): Initialise the instrument """ super(Keithley485, self).__init__(filelike) + pass # ENUMS # @@ -71,7 +72,7 @@ class Mode(IntEnum): Enum containing the supported mode codes """ #: DC Current - current_dc = 0 + current_dc = 0 class Polarity(IntEnum): """ @@ -392,7 +393,7 @@ def parse_status_word(self, statusword): b'2': 'continuous_onget', b'3': 'oneshot_onget', b'4': 'continuous_onx', - b'1': 'oneshot_onx'}, + b'5': 'oneshot_onx'}, 'datamask': {b'00': 'srq_disabled', b'01': 'read_of', b'08': 'read_done', From 7785acff6517fcdb41f6e23fcd13ee70dced3a0b Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 22:33:17 -0700 Subject: [PATCH 28/86] More style corrections --- .../comm/gpib_communicator.py | 20 +++++++++---------- .../abstract_instruments/instrument.py | 6 ++++-- instruments/fluke/fluke3000.py | 11 ++++++---- instruments/keithley/keithley485.py | 7 ------- instruments/tests/test_base_instrument.py | 2 +- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/instruments/abstract_instruments/comm/gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py index f5cfc9ab7..4430c90cd 100644 --- a/instruments/abstract_instruments/comm/gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -39,19 +39,19 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): def __init__(self, filelike, gpib_address, model=0): super(GPIBCommunicator, self).__init__(self) if not isinstance(model, GPIBCommunicator.Model): - raise ValueError('GPIB Controller not supported:'.format(model.value)) + raise ValueError('GPIB Controller not supported: {}'.format(model.value)) self._file = filelike self._gpib_address = gpib_address - self._prologix = prologix + self._model = model self._file.terminator = "\r" - if not prologix: + if self._model == GPIBCommunicator.Model.gi: self._version = int(self._file.query("+ver")) self._terminator = None self.terminator = "\n" self._eoi = True self._timeout = 1000 * pq.millisecond - if not self._prologix and self._version <= 4: + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: self._eos = 10 else: self._eos = "\n" @@ -112,7 +112,7 @@ def timeout(self): @timeout.setter def timeout(self, newval): newval = assume_units(newval, pq.second) - if not self._prologix and self._version <= 4: + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: newval = newval.rescale(pq.second) self._file.sendcmd('+t:{}'.format(newval.magnitude)) else: @@ -144,7 +144,7 @@ def terminator(self, newval): if isinstance(newval, str): newval = newval.lower() - if not self._prologix and self._version <= 4: + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: if newval == 'eoi': self.eoi = True elif not isinstance(newval, int): @@ -199,7 +199,7 @@ def eoi(self, newval): if not isinstance(newval, bool): raise TypeError("EOI status must be specified as a boolean") self._eoi = newval - if not self._prologix and self._version <= 4: + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: self._file.sendcmd("+eoi:{}".format('1' if newval else '0')) else: self._file.sendcmd("++eoi {}".format('1' if newval else '0')) @@ -221,7 +221,7 @@ def eos(self): @eos.setter def eos(self, newval): - if not self._prologix and self._version <= 4: + if self._model == GPIBCommunicator.Model.gi and self._version <= 4: if isinstance(newval, (str, bytes)): newval = ord(newval) self._file.sendcmd("+eos:{}".format(newval)) @@ -328,7 +328,7 @@ def _sendcmd(self, msg): if msg == '': return - if not self._prologix: + if self._model == GPIBCommunicator.Model.gi: self._file.sendcmd('+a:' + str(self._gpib_address)) time.sleep(sleep_time) self.eoi = self.eoi @@ -364,6 +364,6 @@ def _query(self, msg, size=-1): :rtype: `str` """ self.sendcmd(msg) - if not self._prologix and '?' not in msg: + if self._model == GPIBCommunicator.Model.gi and '?' not in msg: self._file.sendcmd('+read') return self._file.read(size).strip() diff --git a/instruments/abstract_instruments/instrument.py b/instruments/abstract_instruments/instrument.py index fca15b7de..890ab9d6a 100644 --- a/instruments/abstract_instruments/instrument.py +++ b/instruments/abstract_instruments/instrument.py @@ -529,7 +529,8 @@ def open_serial(cls, port=None, baud=9600, vid=None, pid=None, return cls(ser) @classmethod - def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, model=GPIBCommunicator.Model.gi): + def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, + model=GPIBCommunicator.Model.gi): """ Opens an instrument, connecting via a `Galvant Industries GPIB-USB adapter`_. @@ -562,7 +563,8 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, model=GPIB return cls(GPIBCommunicator(ser, gpib_address, model)) @classmethod - def open_gpibethernet(cls, host, port, gpib_address, model=GPIBCommunicator.Model.gi): + def open_gpibethernet(cls, host, port, gpib_address, + model=GPIBCommunicator.Model.gi): """ .. warning:: The GPIB-Ethernet adapter that this connection would use does not actually exist, and thus this class method should diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 0e53a92c7..1f4fa2f68 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -172,7 +172,7 @@ def input_range(self, value): """ The `Fluke3000` FC is autoranging only """ - return NotImplementedError + raise NotImplementedError @property def relative(self): @@ -188,7 +188,7 @@ def relative(self, value): """ The `Fluke3000` FC does not support relative measurements """ - return NotImplementedError + raise NotImplementedError # METHODS ## @@ -210,13 +210,16 @@ def connect(self): self.timeout = 3 * pq.second def flush(self): + """ + Flushes the serial output cache + """ timeout = self.timeout self.timeout = 0.1 * pq.second init_time = time.time() while time.time() - init_time < timeout: try: self.read() - except: + except OSError: break self.timeout = timeout @@ -278,7 +281,7 @@ def read_lines(self, nlines=1): try: lines.append(self.read()) i += 1 - except: + except OSError: continue return lines diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 45e5b334f..1aaeef7a9 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -58,13 +58,6 @@ class Keithley485(Instrument): command has been transmitted. """ - def __init__(self, filelike): - """ - Initialise the instrument - """ - super(Keithley485, self).__init__(filelike) - pass - # ENUMS # class Mode(IntEnum): diff --git a/instruments/tests/test_base_instrument.py b/instruments/tests/test_base_instrument.py index c8e750de2..2dacc1d2f 100644 --- a/instruments/tests/test_base_instrument.py +++ b/instruments/tests/test_base_instrument.py @@ -271,7 +271,7 @@ def test_instrument_open_gpibusb(mock_serial_manager, mock_gpib_comm): mock_gpib_comm.assert_called_with( mock_serial_manager.new_serial_connection.return_value, 1, - 0 + GPIBCommunicator.Model.gi ) From fb0931d0f3829c0c9dec6275ec088ee1e68a9bc4 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 22:38:57 -0700 Subject: [PATCH 29/86] One more bug fix... --- instruments/abstract_instruments/comm/gpib_communicator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/abstract_instruments/comm/gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py index 4430c90cd..837054af8 100644 --- a/instruments/abstract_instruments/comm/gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -36,7 +36,7 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ # pylint: disable=too-many-instance-attributes - def __init__(self, filelike, gpib_address, model=0): + def __init__(self, filelike, gpib_address, model=Model.gi): super(GPIBCommunicator, self).__init__(self) if not isinstance(model, GPIBCommunicator.Model): raise ValueError('GPIB Controller not supported: {}'.format(model.value)) From ac2c810299c194414066d982347cd5c8f0901353 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 17 Jul 2019 22:48:23 -0700 Subject: [PATCH 30/86] One more --- instruments/abstract_instruments/comm/gpib_communicator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/abstract_instruments/comm/gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py index 837054af8..2b9bd3e70 100644 --- a/instruments/abstract_instruments/comm/gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -36,7 +36,7 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ # pylint: disable=too-many-instance-attributes - def __init__(self, filelike, gpib_address, model=Model.gi): + def __init__(self, filelike, gpib_address, model=GPIBCommunicator.Model.gi): super(GPIBCommunicator, self).__init__(self) if not isinstance(model, GPIBCommunicator.Model): raise ValueError('GPIB Controller not supported: {}'.format(model.value)) From d503f4d336db7ad09afb6f95ae64d3468cf32769 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 18 Jul 2019 11:59:16 -0700 Subject: [PATCH 31/86] GPIB model now passed as str --- .../abstract_instruments/comm/gpib_communicator.py | 9 +++++---- instruments/abstract_instruments/instrument.py | 6 ++---- instruments/tests/test_base_instrument.py | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/instruments/abstract_instruments/comm/gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py index 2b9bd3e70..e982bd94f 100644 --- a/instruments/abstract_instruments/comm/gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -36,14 +36,15 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ # pylint: disable=too-many-instance-attributes - def __init__(self, filelike, gpib_address, model=GPIBCommunicator.Model.gi): + def __init__(self, filelike, gpib_address, model="gi"): super(GPIBCommunicator, self).__init__(self) - if not isinstance(model, GPIBCommunicator.Model): - raise ValueError('GPIB Controller not supported: {}'.format(model.value)) + try: + self._model = self.Model(model) + except ValueError: + raise ValueError('GPIB Controller not supported: \'{}\''.format(model)) self._file = filelike self._gpib_address = gpib_address - self._model = model self._file.terminator = "\r" if self._model == GPIBCommunicator.Model.gi: self._version = int(self._file.query("+ver")) diff --git a/instruments/abstract_instruments/instrument.py b/instruments/abstract_instruments/instrument.py index 890ab9d6a..0c407f1ce 100644 --- a/instruments/abstract_instruments/instrument.py +++ b/instruments/abstract_instruments/instrument.py @@ -529,8 +529,7 @@ def open_serial(cls, port=None, baud=9600, vid=None, pid=None, return cls(ser) @classmethod - def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, - model=GPIBCommunicator.Model.gi): + def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, model="gi"): """ Opens an instrument, connecting via a `Galvant Industries GPIB-USB adapter`_. @@ -563,8 +562,7 @@ def open_gpibusb(cls, port, gpib_address, timeout=3, write_timeout=3, return cls(GPIBCommunicator(ser, gpib_address, model)) @classmethod - def open_gpibethernet(cls, host, port, gpib_address, - model=GPIBCommunicator.Model.gi): + def open_gpibethernet(cls, host, port, gpib_address, model="gi"): """ .. warning:: The GPIB-Ethernet adapter that this connection would use does not actually exist, and thus this class method should diff --git a/instruments/tests/test_base_instrument.py b/instruments/tests/test_base_instrument.py index 2dacc1d2f..e97c36b1b 100644 --- a/instruments/tests/test_base_instrument.py +++ b/instruments/tests/test_base_instrument.py @@ -257,7 +257,7 @@ def test_instrument_open_gpibusb(mock_serial_manager, mock_gpib_comm): mock_serial_manager.new_serial_connection.return_value.__class__ = SerialCommunicator mock_gpib_comm.return_value.__class__ = GPIBCommunicator - inst = ik.Instrument.open_gpibusb("/dev/port", gpib_address=1) + inst = ik.Instrument.open_gpibusb("/dev/port", gpib_address=1, model="gi") assert isinstance(inst._file, GPIBCommunicator) is True @@ -271,7 +271,7 @@ def test_instrument_open_gpibusb(mock_serial_manager, mock_gpib_comm): mock_gpib_comm.assert_called_with( mock_serial_manager.new_serial_connection.return_value, 1, - GPIBCommunicator.Model.gi + "gi" ) From e4207d21e843754a0e1faa612c56e45d76995c41 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 18 Jul 2019 12:01:15 -0700 Subject: [PATCH 32/86] Bug fix --- instruments/abstract_instruments/comm/gpib_communicator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instruments/abstract_instruments/comm/gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py index e982bd94f..443adfd81 100644 --- a/instruments/abstract_instruments/comm/gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -64,9 +64,9 @@ class Model(Enum): Enum containing the supported GPIB controller models """ #: Galvant Industries - gi = "Galvant Industries" + gi = "gi" #: Prologix, LLC - pl = "Prologix" + pl = "pl" # PROPERTIES # From 28f0541951984ccf51f7fe209ff69a77f5bad586 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 31 Jul 2019 13:32:34 -0700 Subject: [PATCH 33/86] Added support for HP E3631A power supply --- instruments/hp/__init__.py | 1 + instruments/hp/hpe3631a.py | 310 +++++++++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 instruments/hp/hpe3631a.py diff --git a/instruments/hp/__init__.py b/instruments/hp/__init__.py index f8864cea6..0f7893fe2 100644 --- a/instruments/hp/__init__.py +++ b/instruments/hp/__init__.py @@ -10,3 +10,4 @@ from .hp6624a import HP6624a from .hp6632b import HP6632b from .hp6652a import HP6652a +from .hpe3631a import HPe3631a diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py new file mode 100644 index 000000000..ca556d336 --- /dev/null +++ b/instruments/hp/hpe3631a.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# hpe3631a.py: Driver for the HP E3631A Power Supply +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# 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 . +# +""" +Driver for the HP E3631A Power Supply + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com) + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +import time + +import quantities as pq + +from instruments.abstract_instruments import ( + PowerSupply, + PowerSupplyChannel +) +from instruments.util_fns import ( + ProxyList, + unitful_property, + bounded_unitful_property, + bool_property +) + +# CLASSES ##################################################################### + + +class HPe3631a(PowerSupply): + + """ + The HPe3631a is three channels voltage/current supply. + - Channel 1 is a positive +6V/5A channel (P6V) + - Channel 2 is a positive +25V/1A channel (P25V) + - Channel 3 is a negative -25V/1A channel (N25V) + + Example usage: + + >>> import instruments as ik + >>> psu = ik.hp.HPe3631a.open_gpibusb("/dev/ttyUSB0", 10) + >>> psu.channelid = 2 # Sets channel to P25V + >>> psu.voltage = 12.5 # Sets voltage to 12.5V + >>> print(psu.voltage) # Reads back set current + + This module is designed for the power supply to be set to + a specific channel and remain set afterwards as this device + does not offer commands to set or read multiple channels + without calling the channel set command each time. It is + possible to call a specific channel through psu.channel[idx], + which will automatically reset the channel id, if necessary. + + This module is likely to work as is for the Agilent E3631 and + Keysight E3631 which seem to be rebranded but identical devices. + """ + + def __init__(self, filelike): + super(HPe3631a, self).__init__(filelike) + self.channel_count = 3 # Total number of channels + self.channelid = 1 # Current channel set on the device + self.sendcmd('SYST:REM') # Puts the device in remote operation + time.sleep(0.1) + + # INNER CLASSES # + + class Channel(PowerSupplyChannel): + """ + Class representing a power output channel on the HPe3631a. + + .. warning:: This class should NOT be manually created by the user. It is + designed to be initialized by the `HPe3631a` class. + """ + + def __init__(self, hp, idx): + print('init channel', idx) + self._hp = hp + self._idx = idx + + def sendcmd(self, cmd): + """ + Function used to send a command to the instrument after + checking that it is set to the right channel. + + :param str cmd: Command that will be sent to the instrument + """ + if self._idx != self._hp._idx: + self._hp.channelid = self._idx+1 + self._hp.sendcmd(cmd) + + def query(self, cmd): + """ + Function used to send a command to the instrument after + checking that it is set to the right channel. + + :param str cmd: Command that will be sent to the instrument + :return: The result from the query + :rtype: `str` + """ + if self._idx != self._hp._idx: + print('modifying channel') + self._hp.channelid = self._idx+1 + return self._hp.query(cmd) + + # PROPERTIES # + + @property + def mode(self): + """ + Gets/sets the mode for the specified channel. + """ + raise NotImplementedError + + @mode.setter + def mode(self, newval): + raise NotImplementedError + + voltage, voltage_min, voltage_max = bounded_unitful_property( + "SOUR:VOLT", + pq.volt, + min_fmt_str="{}? MIN", + max_fmt_str="{}? MAX", + doc=""" + Gets/sets the output voltage of the source. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + ) + + current, current_min, current_max = bounded_unitful_property( + "SOUR:CURR", + pq.amp, + min_fmt_str="{}? MIN", + max_fmt_str="{}? MAX", + doc=""" + Gets/sets the output current of the source. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + ) + + voltage_sense = unitful_property( + "MEAS:VOLT", + pq.volt, + readonly=True, + doc=""" + Gets the actual output voltage as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + ) + + current_sense = unitful_property( + "MEAS:CURR", + pq.amp, + readonly=True, + doc=""" + Gets the actual output current as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + ) + + output = bool_property( + "OUTP", + inst_true="1", + inst_false="0s", + doc=""" + Gets/sets the outputting status of the specified channel. + + This is a toggle setting. ON will turn on the channel output + while OFF will turn it off. + + :type: `bool` + """ + ) + + # PROPERTIES ## + + @property + def channel(self): + """ + Gets a specific channel object. The desired channel is specified like + one would access a list. + + :rtype: `HPe3631a.Channel` + + .. seealso:: + `HPe3631a` for example using this property. + """ + return ProxyList(self, HPe3631a.Channel, range(self.channel_count)) + + @property + def channelid(self, idx): + """ + Select the active channel (0=P6V, 1=P25V, 2=N25V) + """ + return self._idx+1 + + @channelid.setter + def channelid(self, newval): + if newval not in [1, 2, 3]: + raise ValueError('Channel {idx} not available, choose 1, 2 or 3'.format(idx=newval)) + self._idx = newval-1 + self.sendcmd('INST:NSEL {idx}'.format(idx=newval)) + time.sleep(0.5) + + @property + def voltage(self): + """ + Gets/sets the voltage for the current channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units Volts. + :type: `list` of `~quantities.quantity.Quantity` with units Volt + """ + return self.channel[self._idx].voltage + + @voltage.setter + def voltage(self, newval): + self.channel[self._idx].voltage = newval + + @property + def current(self): + """ + Gets/sets the current for the current channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units Amps. + :type: `list` of `~quantities.quantity.Quantity` with units Amp + """ + return self.channel[self._idx].current + + @current.setter + def current(self, newval): + self.channel[self._idx].current = newval + + @property + def voltage_sense(self): + """ + Gets/sets the voltage from the sense wires for the current channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units Volts. + :type: `list` of `~quantities.quantity.Quantity` with units Volt + """ + return self.channel[self._idx].voltage_sense + + @voltage_sense.setter + def voltage_sense(self, newval): + self.channel[self._idx].voltage_sense = newval + + @property + def current_sense(self): + """ + Gets/sets the current from the sense wires for the current channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units Amps. + :type: `list` of `~quantities.quantity.Quantity` with units Amp + """ + return self.channel[self._idx].current_sense + + @current_sense.setter + def current_sense(self, newval): + self.channel[self._idx].current_sense = newval + + @property + def output(self): + """ + Gets/sets the voltage for the current channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units Volts. + :type: `list` of `~quantities.quantity.Quantity` with units Volt + """ + return self.channel[self._idx].output + + @voltage.setter + def output(self, newval): + self.channel[self._idx].output = newval From d46d69173613fbbec45e65d8b1900110e2253a2a Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 31 Jul 2019 13:35:29 -0700 Subject: [PATCH 34/86] Removed print functions inside HPe3631a class --- instruments/hp/hpe3631a.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index ca556d336..7c4107c44 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -98,7 +98,6 @@ class Channel(PowerSupplyChannel): """ def __init__(self, hp, idx): - print('init channel', idx) self._hp = hp self._idx = idx @@ -123,7 +122,6 @@ def query(self, cmd): :rtype: `str` """ if self._idx != self._hp._idx: - print('modifying channel') self._hp.channelid = self._idx+1 return self._hp.query(cmd) From da2f6792331d9727528c53b45e3a0fdf0d4468aa Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 31 Jul 2019 14:43:15 -0700 Subject: [PATCH 35/86] Style fixes in class HPe3631a --- instruments/hp/hpe3631a.py | 39 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index 7c4107c44..d456c7c96 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -72,9 +72,9 @@ class HPe3631a(PowerSupply): This module is designed for the power supply to be set to a specific channel and remain set afterwards as this device does not offer commands to set or read multiple channels - without calling the channel set command each time. It is + without calling the channel set command each time (0.5s). It is possible to call a specific channel through psu.channel[idx], - which will automatically reset the channel id, if necessary. + which will automatically reset the channel id, when necessary. This module is likely to work as is for the Agilent E3631 and Keysight E3631 which seem to be rebranded but identical devices. @@ -83,7 +83,8 @@ class HPe3631a(PowerSupply): def __init__(self, filelike): super(HPe3631a, self).__init__(filelike) self.channel_count = 3 # Total number of channels - self.channelid = 1 # Current channel set on the device + self.idx = 0 # Current channel to be set on the device + self.channelid = 1 # Set the channel self.sendcmd('SYST:REM') # Puts the device in remote operation time.sleep(0.1) @@ -108,7 +109,7 @@ def sendcmd(self, cmd): :param str cmd: Command that will be sent to the instrument """ - if self._idx != self._hp._idx: + if self._idx != self._hp.idx: self._hp.channelid = self._idx+1 self._hp.sendcmd(cmd) @@ -121,7 +122,7 @@ def query(self, cmd): :return: The result from the query :rtype: `str` """ - if self._idx != self._hp._idx: + if self._idx != self._hp.idx: self._hp.channelid = self._idx+1 return self._hp.query(cmd) @@ -218,17 +219,17 @@ def channel(self): return ProxyList(self, HPe3631a.Channel, range(self.channel_count)) @property - def channelid(self, idx): + def channelid(self): """ Select the active channel (0=P6V, 1=P25V, 2=N25V) """ - return self._idx+1 + return self.idx+1 @channelid.setter def channelid(self, newval): if newval not in [1, 2, 3]: raise ValueError('Channel {idx} not available, choose 1, 2 or 3'.format(idx=newval)) - self._idx = newval-1 + self.idx = newval-1 self.sendcmd('INST:NSEL {idx}'.format(idx=newval)) time.sleep(0.5) @@ -241,11 +242,11 @@ def voltage(self): of units Volts. :type: `list` of `~quantities.quantity.Quantity` with units Volt """ - return self.channel[self._idx].voltage + return self.channel[self.idx].voltage @voltage.setter def voltage(self, newval): - self.channel[self._idx].voltage = newval + self.channel[self.idx].voltage = newval @property def current(self): @@ -256,11 +257,11 @@ def current(self): of units Amps. :type: `list` of `~quantities.quantity.Quantity` with units Amp """ - return self.channel[self._idx].current + return self.channel[self.idx].current @current.setter def current(self, newval): - self.channel[self._idx].current = newval + self.channel[self.idx].current = newval @property def voltage_sense(self): @@ -271,11 +272,11 @@ def voltage_sense(self): of units Volts. :type: `list` of `~quantities.quantity.Quantity` with units Volt """ - return self.channel[self._idx].voltage_sense + return self.channel[self.idx].voltage_sense @voltage_sense.setter def voltage_sense(self, newval): - self.channel[self._idx].voltage_sense = newval + self.channel[self.idx].voltage_sense = newval @property def current_sense(self): @@ -286,11 +287,11 @@ def current_sense(self): of units Amps. :type: `list` of `~quantities.quantity.Quantity` with units Amp """ - return self.channel[self._idx].current_sense + return self.channel[self.idx].current_sense @current_sense.setter def current_sense(self, newval): - self.channel[self._idx].current_sense = newval + self.channel[self.idx].current_sense = newval @property def output(self): @@ -301,8 +302,4 @@ def output(self): of units Volts. :type: `list` of `~quantities.quantity.Quantity` with units Volt """ - return self.channel[self._idx].output - - @voltage.setter - def output(self, newval): - self.channel[self._idx].output = newval + return self.channel[self.idx].output From 6f57ed84f07ece5303fcd46ca9785f0410e6d0d6 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 31 Jul 2019 14:57:55 -0700 Subject: [PATCH 36/86] Range use style fix --- instruments/hp/hpe3631a.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index d456c7c96..ef795ce14 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -82,7 +82,7 @@ class HPe3631a(PowerSupply): def __init__(self, filelike): super(HPe3631a, self).__init__(filelike) - self.channel_count = 3 # Total number of channels + self.channels = [0,1,2] # List of channels self.idx = 0 # Current channel to be set on the device self.channelid = 1 # Set the channel self.sendcmd('SYST:REM') # Puts the device in remote operation @@ -216,7 +216,7 @@ def channel(self): .. seealso:: `HPe3631a` for example using this property. """ - return ProxyList(self, HPe3631a.Channel, range(self.channel_count)) + return ProxyList(self, HPe3631a.Channel, self.channels) @property def channelid(self): From 4c7f7fd06da5bfcdd6e1821a6790d5c554a24e6b Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 31 Jul 2019 15:09:43 -0700 Subject: [PATCH 37/86] Last style fix... --- instruments/hp/hpe3631a.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index ef795ce14..301608b5d 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -82,10 +82,10 @@ class HPe3631a(PowerSupply): def __init__(self, filelike): super(HPe3631a, self).__init__(filelike) - self.channels = [0,1,2] # List of channels - self.idx = 0 # Current channel to be set on the device - self.channelid = 1 # Set the channel - self.sendcmd('SYST:REM') # Puts the device in remote operation + self.channels = [0, 1, 2] # List of channels + self.idx = 0 # Current channel to be set on the device + self.channelid = 1 # Set the channel + self.sendcmd('SYST:REM') # Puts the device in remote operation time.sleep(0.1) # INNER CLASSES # From 4f7523e4e97df1e26ec8b948a508198f80850216 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 2 Aug 2019 09:47:42 -0700 Subject: [PATCH 38/86] Completed support for Keithley 485 --- instruments/keithley/keithley485.py | 388 +++++++++++----------------- 1 file changed, 147 insertions(+), 241 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 1aaeef7a9..5ebce7499 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -38,7 +38,7 @@ import time import struct -from enum import IntEnum +from enum import IntEnum, Enum import quantities as pq @@ -60,144 +60,133 @@ class Keithley485(Instrument): # ENUMS # - class Mode(IntEnum): + class Trigger(IntEnum): """ - Enum containing the supported mode codes + Enum containing valid trigger modes for the Keithley 485 """ - #: DC Current - current_dc = 0 + continuous_ontalk = 0 + oneshot_ontalk = 1 + continuous_onget = 2 + oneshot_onget = 3 + continuous_onx = 4 + oneshot_onx = 5 - class Polarity(IntEnum): + class SRQDataMask(IntEnum): """ - Enum containing valid polarity modes for the Keithley 485 + Enum containing valid SRQ data masks for the Keithley 485 """ - positive = 0 - negative = 1 + srq_disabled = 0 + read_ovf = 1 + read_done = 8 + read_done_ovf = 9 + busy = 16 + busy_read_ovf = 17 + busy_read_done = 24 + busy_read_done_ovf = 25 - class Drive(IntEnum): + class SRQErrorMask(IntEnum): """ - Enum containing valid drive modes for the Keithley 485 + Enum containing valid SRQ error masks for the Keithley 485 """ - pulsed = 0 - dc = 1 + srq_disabled = 0 + idcco = 1 + idcc = 2 + idcco_idcc = 3 + not_remote = 4 + not_remote_idcco = 5 + not_remote_idcc = 6 + not_remote_idcco_idcc = 7 - class TriggerMode(IntEnum): + class Status(Enum): """ - Enum containing valid trigger modes for the Keithley 485 + Enum containing valid status keys in the measurement string """ - talk_continuous = 0 - talk_one_shot = 1 - get_continuous = 2 - get_one_shot = 3 - trigger_continuous = 4 - trigger_one_shot = 5 + normal = b'N' + zerocheck = b'C' + overflow = b'O' + relative = b'Z' # PROPERTIES # @property - def polarity(self): - """ - Gets/sets instrument polarity. - - Example use: - - >>> import instruments as ik - >>> keithley = ik.keithley.Keithley485.open_gpibusb('/dev/ttyUSB0', 1) - >>> keithley.polarity = keithley.Polarity.positive - - :type: `Keithley485.Polarity` + def zero_check(self): """ - value = self.parse_status_word(self.get_status_word())['polarity'] - if value == '+': - return Keithley485.Polarity.positive - elif value == '-': - return Keithley485.Polarity.negative - else: - raise ValueError('Not a valid polarity returned from ' - 'instrument, got {}'.format(value)) - - @polarity.setter - def polarity(self, newval): - if isinstance(newval, str): - newval = Keithley485.Polarity[newval] - if not isinstance(newval, Keithley485.Polarity): - raise TypeError('Polarity must be specified as a ' - 'Keithley485.Polarity, got {} ' - 'instead.'.format(newval)) + Gets/sets the 'zero check' mode (C) of the Keithley 485. - self.sendcmd('P{}X'.format(newval.value)) + Once zero check is enabled (C1 sent), the display can be + zeroed with the REL feature or the front panel pot. - @property - def drive(self): - """ - Gets/sets the instrument drive to either pulsed or DC. - - Example use: - - >>> import instruments as ik - >>> keithley = ik.keithley.Keithley485.open_gpibusb('/dev/ttyUSB0', 1) - >>> keithley.drive = keithley.Drive.pulsed + See the Keithley 485 manual for more information. - :type: `Keithley485.Drive` + :type: `bool` """ - value = self.parse_status_word(self.get_status_word())['drive'] - return Keithley485.Drive[value] + return self.parse_status_word(self.get_status_word())['zerocheck'] - @drive.setter - def drive(self, newval): - if isinstance(newval, str): - newval = Keithley485.Drive[newval] - if not isinstance(newval, Keithley485.Drive): - raise TypeError('Drive must be specified as a ' - 'Keithley485.Drive, got {} ' - 'instead.'.format(newval)) - - self.sendcmd('D{}X'.format(newval.value)) + @zero_check.setter + def zero_check(self, newval): + if not isinstance(newval, bool): + raise TypeError('Zero Check mode must be a boolean.') + self.query('C{}X'.format(int(newval))) @property - def dry_circuit_test(self): + def log(self): """ - Gets/sets the 'dry circuit test' mode of the Keithley 485. + Gets/sets the 'log' mode (D) of the Keithley 485. - This mode is used to minimize any physical and electrical changes in - the contact junction by limiting the maximum source voltage to 20mV. - By limiting the voltage, the measuring circuit will leave the resistive - surface films built up on the contacts undisturbed. This allows for - measurement of the resistance of these films. + Once log is enabled (D1 sent), the device will return + the logarithm of the current readings. See the Keithley 485 manual for more information. :type: `bool` """ - return self.parse_status_word(self.get_status_word())['drycircuit'] + return self.parse_status_word(self.get_status_word())['log'] - @dry_circuit_test.setter - def dry_circuit_test(self, newval): + @log.setter + def log(self, newval): if not isinstance(newval, bool): - raise TypeError('DryCircuitTest mode must be a boolean.') - self.sendcmd('C{}X'.format(int(newval))) + raise TypeError('Log mode must be a boolean.') + self.query('D{}X'.format(int(newval))) @property - def operate(self): + def range(self): """ - Gets/sets the operating mode of the Keithley 485. If set to true, the - instrument will be in operate mode, while false sets the instruments - into standby mode. + Gets/sets the range (R) of the Keithley 485 input terminals. The valid + ranges are one of ``{AUTO|2e-9|2e-8|2e-7|2e-6|2e-5|2e-4|2e-3}`` - :type: `bool` + :type: `~quantities.quantity.Quantity` or `str` """ - return self.parse_status_word(self.get_status_word())['operate'] + value = float(self.parse_status_word(self.get_status_word())['range']) + return value * pq.amp - @operate.setter - def operate(self, newval): - if not isinstance(newval, bool): - raise TypeError('Operate mode must be a boolean.') - self.sendcmd('O{}X'.format(int(newval))) + @range.setter + def range(self, newval): + valid = ('auto', 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) + if isinstance(newval, str): + newval = newval.lower() + if newval == 'auto': + self.sendcmd('R0X') + return + else: + raise ValueError('Only "auto" is acceptable when specifying ' + 'the range as a string.') + if isinstance(newval, pq.quantity.Quantity): + newval = float(newval) + + if isinstance(newval, (float, int)): + if newval in valid: + newval = valid.index(newval) + else: + raise ValueError('Valid range settings are: {}'.format(valid)) + else: + raise TypeError('Range setting must be specified as a float, int, ' + 'or the string "auto", got {}'.format(type(newval))) + self.query('R{}X'.format(newval)) @property def relative(self): """ - Gets/sets the relative measurement mode of the Keithley 485. + Gets/sets the relative measurement mode (Z) of the Keithley 485. As stated in the manual: The relative function is used to establish a baseline reading. This reading is subtracted from all subsequent @@ -220,12 +209,12 @@ def relative(self): def relative(self, newval): if not isinstance(newval, bool): raise TypeError('Relative mode must be a boolean.') - self.sendcmd('Z{}X'.format(int(newval))) + self.query('Z{}X'.format(int(newval))) @property - def trigger_mode(self): + def trigger(self): """ - Gets/sets the trigger mode of the Keithley 485. + Gets/sets the trigger mode (T) of the Keithley 485. There are two different trigger settings for three different sources. This means there are six different settings for the trigger mode. @@ -242,65 +231,46 @@ def trigger_mode(self): commands send to the instrument. In InstrumentKit, "X" is sent after each command so it is not suggested that one uses on "X" triggering. - :type: `Keithley485.TriggerMode` + It is recommended to leave it in the default mode (T0, continuous on talk), + and simply ignore the output when other commands are called. + + :type: `Keithley485.Trigger` """ - raise NotImplementedError + return self.parse_status_word(self.get_status_word())['trigger'] - @trigger_mode.setter - def trigger_mode(self, newval): + @trigger.setter + def trigger(self, newval): if isinstance(newval, str): - newval = Keithley485.TriggerMode[newval] - if newval not in Keithley485.TriggerMode: + newval = Keithley485.Trigger[newval] + if newval not in Keithley485.Trigger: raise TypeError('Drive must be specified as a ' - 'Keithley485.TriggerMode, got {} ' + 'Keithley485.Trigger, got {} ' 'instead.'.format(newval)) - self.sendcmd('T{}X'.format(newval.value)) + self.query('T{}X'.format(newval.value)) @property - def input_range(self): + def eoi(self): """ - Gets/sets the range of the Keithley 485 input terminals. The valid - ranges are one of ``{AUTO|2e-9|2e-8|2e-7|2e-6|2e-5|2e-4|2e-3}`` + Gets/sets the 'eoi' mode (K) of the Keithley 485. - :type: `~quantities.quantity.Quantity` or `str` - """ - value = float(self.parse_status_word(self.get_status_word())['range']) - return value * pq.amp + The model 485 will normally send an end of interrupt (EOI) + during the last byte of its data string or status word. + The EOI reponse of the instrument may be included or omitted. + Warning: the default setting (K0) includes it. - @input_range.setter - def input_range(self, newval): - valid = ('auto', 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) - if isinstance(newval, str): - newval = newval.lower() - if newval == 'auto': - self.sendcmd('R0X') - return - else: - raise ValueError('Only "auto" is acceptable when specifying ' - 'the input range as a string.') - if isinstance(newval, pq.quantity.Quantity): - newval = float(newval) - - if isinstance(newval, (float, int)): - if newval in valid: - newval = valid.index(newval) - else: - raise ValueError('Valid range settings are: {}'.format(valid)) - else: - raise TypeError('Range setting must be specified as a float, int, ' - 'or the string "auto", got {}'.format(type(newval))) - self.sendcmd('R{}X'.format(newval)) - - # METHODS # + See the Keithley 485 manual for more information. - def trigger(self): + :type: `bool` """ - Tell the Keithley 485 to execute all commands that it has received. + return not self.parse_status_word(self.get_status_word())['eoi'] - Do note that this is different from the standard SCPI ``*TRG`` command - (which is not supported by the 485 anyways). - """ - self.sendcmd('X') + @eoi.setter + def eoi(self, newval): + if not isinstance(newval, bool): + raise TypeError('EOI mode must be a boolean.') + self.query('K{}X'.format(1-int(newval))) + + # METHODS # def auto_range(self): """ @@ -311,26 +281,9 @@ def auto_range(self): """ self.sendcmd('R0X') - def set_calibration_value(self, value): - """ - Sets the calibration value. This is not currently implemented. - - :param value: Calibration value to write - """ - # self.write('V+n.nnnnE+nn') - raise NotImplementedError('setCalibrationValue not implemented') - - def store_calibration_constants(self): - """ - Instructs the instrument to store the calibration constants. This is - not currently implemented. - """ - # self.write('L0X') - raise NotImplementedError('setCalibrationConstants not implemented') - def get_status_word(self): """ - The keithley will not always respond with the statusword when asked. We + The device will not always respond with the statusword when asked. We use a simple heuristic here: request it up to 5 times, using a 1s delay to allow the keithley some thinking time. @@ -339,10 +292,8 @@ def get_status_word(self): tries = 5 statusword = '' while statusword[:3] != '485' and tries != 0: + statusword = self.query('U0X') tries -= 1 - self.sendcmd('U0X') - time.sleep(1) - statusword = self.query('') if statusword is None: raise IOError('could not retrieve status word') @@ -355,8 +306,8 @@ def parse_status_word(self, statusword): `~Keithley485.get_status_word`. Returns a `dict` with the following keys: - ``{drive,polarity,drycircuit,operate,range,relative,eoi,trigger, - sqrondata,sqronerror,linefreq,terminator}`` + ``{zerocheck,log,range,relative,eoi,relative, + trigger,datamask,errormask,terminator}`` :param statusword: Byte string to be unpacked and parsed :type: `str` @@ -371,82 +322,47 @@ def parse_status_word(self, statusword): trigger, datamask, errormask) = \ struct.unpack('@6c2s2s', bytes(statusword[3:], 'utf-8')) - valid = {'range': {b'0': 'auto', - b'1': 2e-9, - b'2': 2e-8, - b'3': 2e-7, - b'4': 2e-6, - b'5': 2e-5, - b'6': 2e-4, - b'7': 2e-3}, - 'eoi': {b'0': 'send', - b'1': 'omit'}, - 'trigger': {b'0': 'continuous_ontalk', - b'1': 'oneshot_ontalk', - b'2': 'continuous_onget', - b'3': 'oneshot_onget', - b'4': 'continuous_onx', - b'5': 'oneshot_onx'}, - 'datamask': {b'00': 'srq_disabled', - b'01': 'read_of', - b'08': 'read_done', - b'09': 'read_done_of', - b'16': 'busy', - b'17': 'busy_read_of', - b'24': 'busy_read_done', - b'25': 'busy_read_done_of'}, - 'errormask': {b'00': 'srq_disabled', - b'01': 'idcc0', - b'02': 'idcc', - b'03': 'idcc0_idcc', - b'04': 'not_remote', - b'05': 'not_remote_iddc0', - b'06': 'not_remote_idcc', - b'07': 'not_remote_idcc0_idcc'} - } + valid_range = {b'0': 'auto', + b'1': 2e-9, + b'2': 2e-8, + b'3': 2e-7, + b'4': 2e-6, + b'5': 2e-5, + b'6': 2e-4, + b'7': 2e-3} try: - range = valid['range'][range] - eoi = valid['eoi'][eoi] - trigger = valid['trigger'][trigger] - datamask = valid['datamask'][datamask] - errormask = valid['errormask'][errormask] + range = valid_range[range] + trigger = self.Trigger(int(trigger)).name + datamask = self.SRQDataMask(int(datamask)).name + errormask = self.SRQErrorMask(int(errormask)).name except: raise RuntimeError('Cannot parse status ' 'word: {}'.format(statusword)) - return {'zerocheck': zerocheck == 1, - 'log': log == 1, + return {'zerocheck': zerocheck == b'1', + 'log': log == b'1', 'range': range, - 'relative': relative == 1, - 'eoi': eoi, + 'relative': relative == b'1', + 'eoi': eoi == b'0', 'trigger': trigger, 'datamask': datamask, 'errormask': errormask, - 'terminator':self.terminator} + 'terminator': self.terminator} - def measure(self, mode=Mode.current_dc): + def measure(self): """ - Perform a measurement with the Keithley 485. - - The usual mode parameter is defaulted for the Keithley 485 as the only - valid mode is current. + Perform a current measurement with the Keithley 485. :rtype: `~quantities.quantity.Quantity` """ - if not isinstance(mode, Keithley485.Mode): - raise ValueError('This mode is not supported: {}'.format(mode.name)) - - self.trigger() - return self.parse_measurement(self.query(''))['current'] + return self.parse_measurement(self.query('X')) - @staticmethod - def parse_measurement(measurement): + def parse_measurement(self, measurement): """ Parse the measurement string returned by the instrument. - Returns a dict with the following keys: - ``{status,polarity,drycircuit,drive,current}`` + Returns the current formatted as a Quantity. :param measurement: String to be unpacked and parsed :type: `str` @@ -456,22 +372,12 @@ def parse_measurement(measurement): (status, function, base, current) = \ struct.unpack('@1c2s1c10s', bytes(measurement, 'utf-8')) - valid = {'status': {b'N': 'normal', - b'C': 'zerocheck', - b'O': 'overflow', - b'Z': 'relative'}, - 'function': {b'DC': 'dc-current'}, - 'base': {b'A': '10', - b'L': 'log10'}} try: - status = valid['status'][status] - function = valid['function'][function] - base = valid['base'][base] - current = float(current) * pq.amp + status = self.Status(status) + if status != self.Status.normal: + raise ValueError('Instrument not in normal mode: {}'.format(status.name)) + current = float(current) * pq.amp if base == b'A' else 10**(float(current)) * pq.amp except: raise Exception('Cannot parse measurement: {}'.format(measurement)) - return {'status': status, - 'function': function, - 'base': base, - 'current': current} + return current From 2b566dc15606597ed4cff87243b231ae87b690f1 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 2 Aug 2019 09:58:30 -0700 Subject: [PATCH 39/86] Style fixes for Keithley 485 --- instruments/keithley/keithley485.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 5ebce7499..e3687e9ca 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -35,7 +35,6 @@ from __future__ import absolute_import from __future__ import division -import time import struct from enum import IntEnum, Enum @@ -376,6 +375,8 @@ def parse_measurement(self, measurement): status = self.Status(status) if status != self.Status.normal: raise ValueError('Instrument not in normal mode: {}'.format(status.name)) + if function != b'DC': + raise ValueError('Instrument not returning DC function: {}'.format(function)) current = float(current) * pq.amp if base == b'A' else 10**(float(current)) * pq.amp except: raise Exception('Cannot parse measurement: {}'.format(measurement)) From 49c40926c78c591960e2f381adb90e618a916cd0 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 2 Aug 2019 11:01:59 -0700 Subject: [PATCH 40/86] Python 2.7 style corrections for Keithley 485 --- instruments/keithley/keithley485.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index e3687e9ca..0f856bce3 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -37,7 +37,7 @@ from __future__ import division import struct -from enum import IntEnum, Enum +from enum import Enum import quantities as pq @@ -59,7 +59,7 @@ class Keithley485(Instrument): # ENUMS # - class Trigger(IntEnum): + class Trigger(Enum): """ Enum containing valid trigger modes for the Keithley 485 """ @@ -70,7 +70,7 @@ class Trigger(IntEnum): continuous_onx = 4 oneshot_onx = 5 - class SRQDataMask(IntEnum): + class SRQDataMask(Enum): """ Enum containing valid SRQ data masks for the Keithley 485 """ @@ -83,7 +83,7 @@ class SRQDataMask(IntEnum): busy_read_done = 24 busy_read_done_ovf = 25 - class SRQErrorMask(IntEnum): + class SRQErrorMask(Enum): """ Enum containing valid SRQ error masks for the Keithley 485 """ @@ -241,7 +241,7 @@ def trigger(self): def trigger(self, newval): if isinstance(newval, str): newval = Keithley485.Trigger[newval] - if newval not in Keithley485.Trigger: + if not isinstance(newval, Keithley485.Trigger): raise TypeError('Drive must be specified as a ' 'Keithley485.Trigger, got {} ' 'instead.'.format(newval)) From 27bea6af84ed3c91d841fe9d6307a252a1d637c5 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 3 Aug 2019 14:16:30 -0700 Subject: [PATCH 41/86] Completed support for Prologix GPIB controllers --- .../comm/gpib_communicator.py | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/instruments/abstract_instruments/comm/gpib_communicator.py b/instruments/abstract_instruments/comm/gpib_communicator.py index 443adfd81..9c5b37b5c 100644 --- a/instruments/abstract_instruments/comm/gpib_communicator.py +++ b/instruments/abstract_instruments/comm/gpib_communicator.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ Provides a communication layer for an instrument connected via a Galvant -Industries GPIB adapter. +Industries or Prologix GPIB adapter. """ # IMPORTS ##################################################################### @@ -29,25 +29,23 @@ class GPIBCommunicator(io.IOBase, AbstractCommunicator): """ Communicates with a SocketCommunicator or SerialCommunicator object for - use with Galvant Industries GPIBUSB or GPIBETHERNET adapters. + use with Galvant Industries or Prologix GPIBUSB or GPIBETHERNET adapters. It essentially wraps those physical communication layers with the extra - overhead required by the Galvant GPIB adapters. + overhead required by the GPIB adapters. """ # pylint: disable=too-many-instance-attributes def __init__(self, filelike, gpib_address, model="gi"): super(GPIBCommunicator, self).__init__(self) - try: - self._model = self.Model(model) - except ValueError: - raise ValueError('GPIB Controller not supported: \'{}\''.format(model)) - + self._model = self.Model(model) self._file = filelike self._gpib_address = gpib_address self._file.terminator = "\r" if self._model == GPIBCommunicator.Model.gi: self._version = int(self._file.query("+ver")) + if self._model == GPIBCommunicator.Model.pl: + self._file.sendcmd("++auto 0") self._terminator = None self.terminator = "\n" self._eoi = True @@ -115,10 +113,10 @@ def timeout(self, newval): newval = assume_units(newval, pq.second) if self._model == GPIBCommunicator.Model.gi and self._version <= 4: newval = newval.rescale(pq.second) - self._file.sendcmd('+t:{}'.format(newval.magnitude)) + self._file.sendcmd('+t:{}'.format(int(newval.magnitude))) else: newval = newval.rescale(pq.millisecond) - self._file.sendcmd("++read_tmo_ms {}".format(newval.magnitude)) + self._file.sendcmd("++read_tmo_ms {}".format(int(newval.magnitude))) self._file.timeout = newval.rescale(pq.second) self._timeout = newval.rescale(pq.second) @@ -205,7 +203,6 @@ def eoi(self, newval): else: self._file.sendcmd("++eoi {}".format('1' if newval else '0')) - @property def eos(self): """ @@ -318,8 +315,8 @@ def flush_input(self): def _sendcmd(self, msg): """ This is the implementation of ``sendcmd`` for communicating with - the Galvant Industries GPIB adapter. This function is in turn wrapped by - the concrete method `AbstractCommunicator.sendcmd` to provide consistent + the GPIB adapters. This function is in turn wrapped by the concrete + method `AbstractCommunicator.sendcmd` to provide consistent logging functionality across all communication layers. :param str msg: The command message to send to the instrument @@ -328,20 +325,15 @@ def _sendcmd(self, msg): if msg == '': return - if self._model == GPIBCommunicator.Model.gi: self._file.sendcmd('+a:' + str(self._gpib_address)) - time.sleep(sleep_time) - self.eoi = self.eoi - time.sleep(sleep_time) - self.timeout = self.timeout - time.sleep(sleep_time) else: - # TODO need to set the address - #self._file.sendcmd('++addr ' + str(self._gpib_address)) - #time.sleep(sleep_time) - pass - + self._file.sendcmd('++addr ' + str(self._gpib_address)) + time.sleep(sleep_time) + self.eoi = self.eoi + time.sleep(sleep_time) + self.timeout = self.timeout + time.sleep(sleep_time) self.eos = self.eos time.sleep(sleep_time) self._file.sendcmd(msg) @@ -350,13 +342,18 @@ def _sendcmd(self, msg): def _query(self, msg, size=-1): """ This is the implementation of ``query`` for communicating with - the Galvant Industries GPIB adapter. This function is in turn wrapped by - the concrete method `AbstractCommunicator.query` to provide consistent + the GPIB adapters. This function is in turn wrapped by the concrete + method `AbstractCommunicator.query` to provide consistent logging functionality across all communication layers. - If a ``?`` is not present in ``msg`` then the adapter will be - instructed to get the response from the instrument via the ``+read`` - command. + The Galvant Industries adaptor is set to automatically get a + response if a ``?`` is present in ``msg``. If it is not present, + then the adapter will be instructed to get the response from the + instrument via the ``+read`` command. + + The Prologix adapter is set to not get a response unless told to do + so. It is instructed to get a response from the instrument via the + ``++read`` command. :param str msg: The query message to send to the instrument :param int size: The number of bytes to read back from the instrument @@ -367,4 +364,6 @@ def _query(self, msg, size=-1): self.sendcmd(msg) if self._model == GPIBCommunicator.Model.gi and '?' not in msg: self._file.sendcmd('+read') + if self._model == GPIBCommunicator.Model.pl: + self._file.sendcmd('++read') return self._file.read(size).strip() From 20bbae8f39fd0a7d7c708d20e816177174eecd44 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 3 Aug 2019 14:33:49 -0700 Subject: [PATCH 42/86] Fixed and renamed GPIB communicator pytest --- .../tests/test_comm/{test_gi_gpibusb.py => test_gpibusb.py} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename instruments/tests/test_comm/{test_gi_gpibusb.py => test_gpibusb.py} (97%) diff --git a/instruments/tests/test_comm/test_gi_gpibusb.py b/instruments/tests/test_comm/test_gpibusb.py similarity index 97% rename from instruments/tests/test_comm/test_gi_gpibusb.py rename to instruments/tests/test_comm/test_gpibusb.py index b6b75f428..e68c360d3 100644 --- a/instruments/tests/test_comm/test_gi_gpibusb.py +++ b/instruments/tests/test_comm/test_gpibusb.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Unit tests for the GI GPIBUSB communication layer +Unit tests for the GPIBUSB communication layer """ # IMPORTS #################################################################### @@ -199,7 +199,7 @@ def test_gpibusbcomm_timeout(): unit_eq(comm.timeout, 1000 * pq.millisecond) comm.timeout = 5000 * pq.millisecond - comm._file.sendcmd.assert_called_with("++read_tmo_ms 5000.0") + comm._file.sendcmd.assert_called_with("++read_tmo_ms 5000") def test_gpibusbcomm_close(): @@ -235,7 +235,7 @@ def test_gpibusbcomm_sendcmd(): comm._file.sendcmd.assert_has_calls([ mock.call("+a:1"), mock.call("++eoi 1"), - mock.call("++read_tmo_ms 1000.0"), + mock.call("++read_tmo_ms 1000"), mock.call("++eos 2"), mock.call("mock") ]) From 901926a3c481296e0876b51f2205125f68abbb81 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 3 Aug 2019 14:41:49 -0700 Subject: [PATCH 43/86] Keythley 485 no longer queries when setting properties (GPIB comm fixed) --- instruments/keithley/keithley485.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 0f856bce3..47400ab03 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -125,7 +125,7 @@ def zero_check(self): def zero_check(self, newval): if not isinstance(newval, bool): raise TypeError('Zero Check mode must be a boolean.') - self.query('C{}X'.format(int(newval))) + self.sendcmd('C{}X'.format(int(newval))) @property def log(self): @@ -145,7 +145,7 @@ def log(self): def log(self, newval): if not isinstance(newval, bool): raise TypeError('Log mode must be a boolean.') - self.query('D{}X'.format(int(newval))) + self.sendcmd('D{}X'.format(int(newval))) @property def range(self): @@ -180,7 +180,7 @@ def range(self, newval): else: raise TypeError('Range setting must be specified as a float, int, ' 'or the string "auto", got {}'.format(type(newval))) - self.query('R{}X'.format(newval)) + self.sendcmd('R{}X'.format(newval)) @property def relative(self): @@ -208,7 +208,7 @@ def relative(self): def relative(self, newval): if not isinstance(newval, bool): raise TypeError('Relative mode must be a boolean.') - self.query('Z{}X'.format(int(newval))) + self.sendcmd('Z{}X'.format(int(newval))) @property def trigger(self): @@ -220,15 +220,13 @@ def trigger(self): The two types are continuous and one-shot. Continuous has the instrument continuously sample the current. One-shot performs a single - current measurement. + current measurement when requested to do so. The three trigger sources are on talk, on GET, and on "X". On talk refers to addressing the instrument to talk over GPIB. On GET is when the instrument receives the GPIB command byte for "group execute trigger". Last, on "X" is when one sends the ASCII character "X" to the - instrument. This character is used as a general execute to confirm - commands send to the instrument. In InstrumentKit, "X" is sent after - each command so it is not suggested that one uses on "X" triggering. + instrument. It is recommended to leave it in the default mode (T0, continuous on talk), and simply ignore the output when other commands are called. @@ -245,7 +243,7 @@ def trigger(self, newval): raise TypeError('Drive must be specified as a ' 'Keithley485.Trigger, got {} ' 'instead.'.format(newval)) - self.query('T{}X'.format(newval.value)) + self.sendcmd('T{}X'.format(newval.value)) @property def eoi(self): @@ -267,7 +265,7 @@ def eoi(self): def eoi(self, newval): if not isinstance(newval, bool): raise TypeError('EOI mode must be a boolean.') - self.query('K{}X'.format(1-int(newval))) + self.sendcmd('K{}X'.format(1-int(newval))) # METHODS # From 8660adc55f3ec4e25ea7e7d55d31d7420ccea6fa Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 3 Aug 2019 15:34:10 -0700 Subject: [PATCH 44/86] Privatized obscure functions of Keithley 485, added get_status() function --- instruments/keithley/keithley485.py | 35 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 47400ab03..958e223cf 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -119,7 +119,7 @@ def zero_check(self): :type: `bool` """ - return self.parse_status_word(self.get_status_word())['zerocheck'] + return self.get_status()['zerocheck'] @zero_check.setter def zero_check(self, newval): @@ -139,7 +139,7 @@ def log(self): :type: `bool` """ - return self.parse_status_word(self.get_status_word())['log'] + return self.get_status()['log'] @log.setter def log(self, newval): @@ -155,7 +155,7 @@ def range(self): :type: `~quantities.quantity.Quantity` or `str` """ - value = float(self.parse_status_word(self.get_status_word())['range']) + value = float(self.get_status()['range']) return value * pq.amp @range.setter @@ -202,7 +202,7 @@ def relative(self): :type: `bool` """ - return self.parse_status_word(self.get_status_word())['relative'] + return self.get_status()['relative'] @relative.setter def relative(self, newval): @@ -233,7 +233,7 @@ def trigger(self): :type: `Keithley485.Trigger` """ - return self.parse_status_word(self.get_status_word())['trigger'] + return self.get_status()['trigger'] @trigger.setter def trigger(self, newval): @@ -259,7 +259,7 @@ def eoi(self): :type: `bool` """ - return not self.parse_status_word(self.get_status_word())['eoi'] + return not self.get_status()['eoi'] @eoi.setter def eoi(self, newval): @@ -278,11 +278,22 @@ def auto_range(self): """ self.sendcmd('R0X') - def get_status_word(self): + def get_status(self): + """ + Gets and parses the status word. + + Returns a `dict` with the following keys: + ``{zerocheck,log,range,relative,eoi,relative, + trigger,datamask,errormask,terminator}`` + + :rtype: `dict` + """ + return self._parse_status_word(self._get_status_word()) + + def _get_status_word(self): """ The device will not always respond with the statusword when asked. We - use a simple heuristic here: request it up to 5 times, using a 1s - delay to allow the keithley some thinking time. + use a simple heuristic here: request it up to 5 times. :rtype: `str` """ @@ -297,7 +308,7 @@ def get_status_word(self): return statusword[:-1] - def parse_status_word(self, statusword): + def _parse_status_word(self, statusword): """ Parse the status word returned by the function `~Keithley485.get_status_word`. @@ -353,9 +364,9 @@ def measure(self): :rtype: `~quantities.quantity.Quantity` """ - return self.parse_measurement(self.query('X')) + return self._parse_measurement(self.query('X')) - def parse_measurement(self, measurement): + def _parse_measurement(self, measurement): """ Parse the measurement string returned by the instrument. From 1d5f17b0f3e5d94d29b24f106ea350d65b11fd21 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 3 Aug 2019 15:45:31 -0700 Subject: [PATCH 45/86] Standardized range and trigger function names of Keithley 485 --- instruments/keithley/keithley485.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 958e223cf..44b31d577 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -59,7 +59,7 @@ class Keithley485(Instrument): # ENUMS # - class Trigger(Enum): + class TriggerMode(Enum): """ Enum containing valid trigger modes for the Keithley 485 """ @@ -148,7 +148,7 @@ def log(self, newval): self.sendcmd('D{}X'.format(int(newval))) @property - def range(self): + def input_range(self): """ Gets/sets the range (R) of the Keithley 485 input terminals. The valid ranges are one of ``{AUTO|2e-9|2e-8|2e-7|2e-6|2e-5|2e-4|2e-3}`` @@ -158,8 +158,8 @@ def range(self): value = float(self.get_status()['range']) return value * pq.amp - @range.setter - def range(self, newval): + @input_range.setter + def input_range(self, newval): valid = ('auto', 2e-9, 2e-8, 2e-7, 2e-6, 2e-5, 2e-4, 2e-3) if isinstance(newval, str): newval = newval.lower() @@ -211,7 +211,7 @@ def relative(self, newval): self.sendcmd('Z{}X'.format(int(newval))) @property - def trigger(self): + def trigger_mode(self): """ Gets/sets the trigger mode (T) of the Keithley 485. @@ -235,8 +235,8 @@ def trigger(self): """ return self.get_status()['trigger'] - @trigger.setter - def trigger(self, newval): + @trigger_mode.setter + def trigger_mode(self, newval): if isinstance(newval, str): newval = Keithley485.Trigger[newval] if not isinstance(newval, Keithley485.Trigger): @@ -341,7 +341,7 @@ def _parse_status_word(self, statusword): try: range = valid_range[range] - trigger = self.Trigger(int(trigger)).name + trigger = self.TriggerMode(int(trigger)).name datamask = self.SRQDataMask(int(datamask)).name errormask = self.SRQErrorMask(int(errormask)).name except: From 7b038b71b7ec9a0cec57a0339d76fb8d359a5d4c Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 3 Aug 2019 15:51:15 -0700 Subject: [PATCH 46/86] Trigger name change fix --- instruments/keithley/keithley485.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 44b31d577..4354dfed1 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -231,17 +231,17 @@ def trigger_mode(self): It is recommended to leave it in the default mode (T0, continuous on talk), and simply ignore the output when other commands are called. - :type: `Keithley485.Trigger` + :type: `Keithley485.TriggerMode` """ return self.get_status()['trigger'] @trigger_mode.setter def trigger_mode(self, newval): if isinstance(newval, str): - newval = Keithley485.Trigger[newval] - if not isinstance(newval, Keithley485.Trigger): + newval = Keithley485.TriggerMode[newval] + if not isinstance(newval, Keithley485.TriggerMode): raise TypeError('Drive must be specified as a ' - 'Keithley485.Trigger, got {} ' + 'Keithley485.TriggerMode, got {} ' 'instead.'.format(newval)) self.sendcmd('T{}X'.format(newval.value)) From 59a73bc02d2eab92fd51e1dc6a3fa89469f6adad Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 6 Aug 2019 13:02:21 -0700 Subject: [PATCH 47/86] Added support for Glassman FR (EJ, ET, EY, FJ) power supplies --- instruments/__init__.py | 1 + instruments/glassman/__init__.py | 9 + instruments/glassman/glassmanfr.py | 485 +++++++++++++++++++++++++++++ 3 files changed, 495 insertions(+) create mode 100644 instruments/glassman/__init__.py create mode 100644 instruments/glassman/glassmanfr.py diff --git a/instruments/__init__.py b/instruments/__init__.py index 276ca3a75..0d3efa551 100644 --- a/instruments/__init__.py +++ b/instruments/__init__.py @@ -14,6 +14,7 @@ from . import agilent from . import generic_scpi from . import fluke +from . import glassman from . import holzworth from . import hp from . import keithley diff --git a/instruments/glassman/__init__.py b/instruments/glassman/__init__.py new file mode 100644 index 000000000..18daacfef --- /dev/null +++ b/instruments/glassman/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing Glassman power supplies +""" + +from __future__ import absolute_import + +from .glassmanfr import GlassmanFR diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py new file mode 100644 index 000000000..4f831eac2 --- /dev/null +++ b/instruments/glassman/glassmanfr.py @@ -0,0 +1,485 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# hpe3631a.py: Driver for the Glassman FR Series Power Supplies +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# 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 . +# +""" +Driver for the Glassman FR Series Power Supplies + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com) + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +import struct + +from enum import Enum + +import quantities as pq + +from instruments.abstract_instruments import ( + PowerSupply, + PowerSupplyChannel +) +from instruments.util_fns import assume_units + +# CLASSES ##################################################################### + + +class GlassmanFR(PowerSupply, PowerSupplyChannel): + + """ + The GlassmanFR is a single output power supply. + + Because it is a single channel output, this object inherits from both + PowerSupply and PowerSupplyChannel. + + This class should work for any of the Glassman FR Series power supplies + and is also likely to work for the EJ, ET, EY and FJ Series which seem + to share their communication protocols. The code has only been tested + by the author with an Glassman FR50R6 power supply. + + Before this power supply can be remotely operated, remote communication + must be enabled and the HV must be on. Please refer to the manual. + + Example usage: + + >>> import instruments as ik + >>> psu = ik.glassman.GlassmanFR.open_serial('/dev/ttyUSB0', 9600) + >>> psu.voltage = 100 # Sets output voltage to 100V. + >>> psu.voltage + array(100.0) * V + >>> psu.output = True # Turns on the power supply + >>> psu.voltage_sense < 200 + True + + This code uses default values of `voltage_max`, `current_max` and + `polarity` that are only valid of the FR50R6 in its positive setting. + If your power supply differs, reset those values by calling: + + >>> import quantities as pq + >>> psu.voltage_max = 40.0 * pq.kilovolt + >>> psu.current_max = 6.0 * pq.milliamp + >>> psu.polarity = -1 + """ + + def __init__(self, filelike): + """ + Initialize the instrument, and set the properties needed for communication. + """ + super(GlassmanFR, self).__init__(filelike) + self.terminator = "\r" + self.voltage_max = 50.0 * pq.kilovolt + self.current_max = 6.0 * pq.milliamp + self.polarity = +1 + self._device_timeout = True + self._voltage = self.voltage_sense + self._current = self.current_sense + + # ENUMS # + + class Mode(Enum): + """ + Enum containing the possible modes of operations of the instrument + """ + #: Constant voltage mode + voltage = '0' + #: Constant current mode + current = '1' + + class ResponseCode(Enum): + """ + Enum containing the possible reponse codes returned by the instrument. + """ + #: A set command expects an acknoledge response (`A`) + S = 'A' + #: A query command expects a response packet (`R`) + Q = 'R' + #: A version query expects a different response packet (`B`) + V = 'B' + #: A configure command expects an acknoledge response (`A`) + C = 'A' + + class ErrorCode(Enum): + """ + Enum containing the possible error codes returned by the instrument. + """ + #: Undefined command received (not S, Q, V or C) + undefined_command = '1' + #: The checksum calculated by the instrument does not correspond to the one received + checksum_error = '2' + #: The command was longer than expected + extra_bytes = '3' + #: The digital control byte set was not one of HV On, HV Off or Power supply Reset + illegal_control = '4' + #: A send command was sent without a reset byte while the power supply is faulted + illegal_while_fault = '5' + #: Command valid, error while executing it + processing_error = '6' + + # PROPERTIES ## + + @property + def channel(self): + """ + Return the channel (which in this case is the entire instrument, since + there is only 1 channel on the GlassmanFR.) + + :rtype: 'tuple' of length 1 containing a reference back to the parent + GlassmanFR object. + """ + return self, + + @property + def voltage(self): + """ + Gets/sets the output voltage setting. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + return self.polarity*self._voltage + + @voltage.setter + def voltage(self, newval): + self.set_status(voltage=assume_units(newval, pq.volt)) + + @property + def voltage_sense(self): + """ + Gets the output voltage as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + return self.get_status()['voltage'] + + @property + def current(self): + """ + Gets/sets the output current setting. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + return self.polarity*self._current + + @current.setter + def current(self, newval): + self.set_status(current=assume_units(newval, pq.amp)) + + @property + def current_sense(self): + """ + Gets/sets the output current as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~quantities.Quantity` + """ + return self.get_status()['current'] + + @property + def mode(self): + """ + Gets/sets the mode for the specified channel. + + The constant-voltage/constant-current modes of the power supply + are selected automatically depending on the load (resistance) + connected to the power supply. If the load greater than the set + V/I is connected, a voltage V is applied and the current flowing + is lower than I. If the load is smaller than V/I, the set current + I acts as a current limiter and the voltage is lower than V. + """ + return self.get_status()['mode'] + + @mode.setter + def mode(self, newval): + raise NotImplementedError('The `GlassmanFR` sets its mode automatically') + + @property + def output(self): + """ + Gets/sets the output status. + + This is a toggle setting. True will turn on the instrument output + while False will turn it off. + + :type: `bool` + """ + return self.get_status()['output'] + + @output.setter + def output(self, newval): + if not isinstance(newval, bool): + raise TypeError('Ouput status mode must be a boolean.') + self.set_status(output=newval) + + @property + def fault(self): + """ + Gets/sets the output status. + + Returns True if the instrument has a fault. + + :type: `bool` + """ + return self.get_status()['fault'] + + @property + def version(self): + """ + The software revision level of the power supply's + data intereface via the `V` command + + :rtype: `str` + """ + return self.query("V") + + @property + def device_timeout(self): + """ + Gets/sets the timeout instrument side. + + This is a toggle setting. ON will set the timeout to 1.5 + seconds while OFF will disable it. + + :type: `bool` + """ + return self._device_timeout + + @device_timeout.setter + def device_timeout(self, newval): + if not isinstance(newval, bool): + raise TypeError('Device timeout mode must be a boolean.') + self._device_timeout = newval + self.query('C{}'.format(int(not newval))) # Device acknowledges + + # METHODS ## + + def sendcmd(self, msg): + """ + Overrides the default `setcmd` by padding the front of each + command sent to the instrument with an SOH character and the + back of it with a checksum. + + :param str msg: The command message to send to the instrument + """ + checksum = self._get_checksum(msg) + self._file.sendcmd('\x01' + msg + checksum) # Add SOH and checksum + + def query(self, msg, size=-1): + """ + Overrides the default `query` by padding the front of each + command sent to the instrument with an SOH character and the + back of it with a checksum. + + This implementation also automatically check that the checksum + returned by the instrument is consistent with the message. If + the message returned is an error, it parses it and raises. + + :param str msg: The query message to send to the instrument + :param int size: The number of bytes to read back from the instrument + response. + :return: The instrument response to the query + :rtype: `str` + """ + self.sendcmd(msg) + result = self._file.read(size) + if result[0] != getattr(self.ResponseCode, msg[0]).value and result[0] != 'E': + raise ValueError('Invalid response code: {}'.format(result)) + if result[0] == 'A': + return "Acknowledged" + if not self._verify_checksum(result): + raise ValueError('Invalid checksum: {}'.format(result)) + if result[0] == 'E': + error_name = self.ErrorCode(result[1]).name + raise ValueError('Instrument responded with error: {}'.format(error_name)) + + return result[1:-2] # Remove SOH and checksum + + def reset(self): + """ + Reset device to default status (HV Off, V=0.0, A=0.0) + """ + self.set_status(reset=True) + + def set_status(self, voltage=None, current=None, output=None, reset=False): + """ + Sets the requested variables on the instrument. + + This instrument can only set all of its variables simultaneously, + if some of them are omitted in this function, they will simply be + kept as what they were set to previously. + """ + if reset: + self._voltage = 0. * pq.volt + self._current = 0. * pq.amp + cmd = format(4, '013d') + else: + cmd = '' + value_max = int(0xfff)-1 + + # If the voltage is not specified, keep it as is + self._voltage = assume_units(voltage, pq.volt) if voltage is not None else self.voltage + assert self._voltage >= 0. * pq.volt and self._voltage <= self.voltage_max + ratio = float(self._voltage.rescale(pq.volt)/self.voltage_max.rescale(pq.volt)) + cmd += format(int(value_max*ratio), '03X') + + # If the current is not specified, keep it as is + self._current = assume_units(current, pq.amp) if current is not None else self.current + assert self._current >= 0. * pq.amp and self._current <= self.current_max + ratio = float(self._current.rescale(pq.amp)/self.current_max.rescale(pq.amp)) + cmd += format(int(value_max*ratio), '03X') + + # If the output status is not specified, keep it as is + output = output if output is not None else self.output + control = '00{}{}'.format(int(output), int(not output)) + cmd += format(int(control, 2), '07X') + + self.query('S' + cmd) # Device acknoledges + + def get_status(self): + """ + Gets and parses the response packet. + + Returns a `dict` with the following keys: + ``{voltage,current,mode,fault,output}`` + + :rtype: `dict` + """ + return self._parse_response(self.query('Q')) + + def _parse_response(self, response): + """ + Parse the response packet returned by the power supply. + + Returns a `dict` with the following keys: + ``{voltage,current,mode,fault,output}`` + + :param response: Byte string to be unpacked and parsed + :type: `str` + + :rtype: `dict` + """ + (voltage, current, monitors) = \ + struct.unpack('@3s3s3x1c2x', bytes(response, 'utf-8')) + + try: + voltage = self._parse_voltage(voltage) + current = self._parse_current(current) + mode, fault, output = self._parse_monitors(monitors) + except: + raise RuntimeError('Cannot parse response ' + 'packet: {}'.format(response)) + + return {'voltage': voltage, + 'current': current, + 'mode': mode, + 'fault': fault, + 'output': output} + + def _parse_voltage(self, word): + ''' + Converts the three-bytes voltage word returned in the + response packet to a single voltage quantity. + + :param string: Byte string to be parsed + :type: `bytes` + + :rtype: `~quantities.quantity.Quantity` + ''' + value = int(word.decode('utf-8'), 16) + value_max = int(0x3ff) + return self.polarity*self.voltage_max*float(value)/value_max + + def _parse_current(self, word): + ''' + Converts the three-bytes current word returned in the + response packet to a single current quantity. + + :param string: Byte string to be parsed + :type: `bytes` + + :rtype: `~quantities.quantity.Quantity` + ''' + value = int(word.decode('utf-8'), 16) + value_max = int(0x3ff) + return self.polarity*self.current_max*float(value)/value_max + + def _parse_monitors(self, word): + ''' + Converts the monitors byte returned in the response packet + to a mode, a fault boolean and an output boolean. + + :param string: Byte to be parsed + :type: `byte` + + :rtype: `str, bool, bool` + ''' + bits = format(int(word, 16), '04b') + mode = self.Mode(bits[-1]).name + fault = bits[-2] == '1' + output = bits[-3] == '1' + return mode, fault, output + + def _verify_checksum(self, word): + ''' + Calculates the modulo 256 checksum of a string of characters + and compares it to the one returned by the instrument. + + Returns True if they agree, False otherwise. + + :param string: Byte string to be checked + :type: `str` + + :rtype: `bool` + ''' + data = word[1:-2] + inst_checksum = word[-2:] + calc_checksum = self._get_checksum(data) + return inst_checksum == calc_checksum + + @staticmethod + def _get_checksum(data): + ''' + Calculates the modulo 256 checksum of a string of characters. + This checksum, expressed in hexadecimal, is used in every + communication of this instrument, as a sanity check. + + Returns a string corresponding to the hexademical value + of the checksum, without the `0x` prefix. + + :param string: Byte string to be checksummed + :type: `str` + + :rtype: `str` + ''' + chrs = list(data) + sum = 0 + for c in chrs: + sum += ord(c) + + return format(sum % 256, '02X') From b4154bb5b26f5e9c60923a67cec6a2b323583e23 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 6 Aug 2019 13:13:09 -0700 Subject: [PATCH 48/86] Style fix --- instruments/glassman/glassmanfr.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index 4f831eac2..e84cb544a 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -279,18 +279,18 @@ def device_timeout(self, newval): # METHODS ## - def sendcmd(self, msg): + def sendcmd(self, cmd): """ Overrides the default `setcmd` by padding the front of each command sent to the instrument with an SOH character and the back of it with a checksum. - :param str msg: The command message to send to the instrument + :param str cmd: The command message to send to the instrument """ - checksum = self._get_checksum(msg) - self._file.sendcmd('\x01' + msg + checksum) # Add SOH and checksum + checksum = self._get_checksum(cmd) + self._file.sendcmd('\x01' + cmd + checksum) # Add SOH and checksum - def query(self, msg, size=-1): + def query(self, cmd, size=-1): """ Overrides the default `query` by padding the front of each command sent to the instrument with an SOH character and the @@ -300,15 +300,15 @@ def query(self, msg, size=-1): returned by the instrument is consistent with the message. If the message returned is an error, it parses it and raises. - :param str msg: The query message to send to the instrument + :param str cmd: The query message to send to the instrument :param int size: The number of bytes to read back from the instrument response. :return: The instrument response to the query :rtype: `str` """ - self.sendcmd(msg) + self.sendcmd(cmd) result = self._file.read(size) - if result[0] != getattr(self.ResponseCode, msg[0]).value and result[0] != 'E': + if result[0] != getattr(self.ResponseCode, cmd[0]).value and result[0] != 'E': raise ValueError('Invalid response code: {}'.format(result)) if result[0] == 'A': return "Acknowledged" From 656578d89ba5bd0e621d897c104942de057cc739 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 6 Aug 2019 16:50:02 -0700 Subject: [PATCH 49/86] Completed documentation for Keithley 485 --- instruments/keithley/keithley485.py | 35 ++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 4354dfed1..de3393bd4 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -55,6 +55,13 @@ class Keithley485(Instrument): The device needs some processing time (manual reports 300-500ms) after a command has been transmitted. + + Example usage: + + >>> import instruments as ik + >>> inst = ik.keithley.keithley485.open_gpibusb("/dev/ttyUSB0", 22) + >>> inst.measure() # Measures the current + array(-1.278e-10) * A """ # ENUMS # @@ -63,46 +70,72 @@ class TriggerMode(Enum): """ Enum containing valid trigger modes for the Keithley 485 """ + #: Continuously measures current, returns on talk continuous_ontalk = 0 + #: Measures current once and returns on talk oneshot_ontalk = 1 + #: Continuously measures current, returns on `GET` continuous_onget = 2 + #: Measures current once and returns on `GET` oneshot_onget = 3 + #: Continuously measures current, returns on `X` continuous_onx = 4 + #: Measures current once and returns on `X` oneshot_onx = 5 class SRQDataMask(Enum): """ Enum containing valid SRQ data masks for the Keithley 485 """ + #: Service request (SRQ) disabled srq_disabled = 0 + #: Read overflow read_ovf = 1 + #: Read done read_done = 8 + #: Read done or read overflow read_done_ovf = 9 + #: Device busy busy = 16 + #: Device busy or read overflow busy_read_ovf = 17 + #: Device busy or read overflow busy_read_done = 24 + #: Device busy, read done or read overflow busy_read_done_ovf = 25 class SRQErrorMask(Enum): """ Enum containing valid SRQ error masks for the Keithley 485 """ + #: Service request (SRQ) disabled srq_disabled = 0 + #: Illegal Device-Dependent Command Option (IDDCO) idcco = 1 + #: Illegal Device-Dependent Command (IDDC) idcc = 2 + #: IDDCO or IDDC idcco_idcc = 3 + #: Device not in remote not_remote = 4 + #: Device not in remote or IDDCO not_remote_idcco = 5 + #: Device not in remote or IDDC not_remote_idcc = 6 + #: Device not in remote, IDDCO or IDDC not_remote_idcco_idcc = 7 class Status(Enum): """ Enum containing valid status keys in the measurement string """ + #: Measurement normal normal = b'N' + #: Measurement zero-check zerocheck = b'C' + #: Measurement overflow overflow = b'O' + #: Measurement relative relative = b'Z' # PROPERTIES # @@ -375,7 +408,7 @@ def _parse_measurement(self, measurement): :param measurement: String to be unpacked and parsed :type: `str` - :rtype: `dict` + :rtype: `~quantities.quantity.Quantity` """ (status, function, base, current) = \ struct.unpack('@1c2s1c10s', bytes(measurement, 'utf-8')) From ed3ae94570d313c4568445c2bac90386f9653e79 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 6 Aug 2019 16:53:36 -0700 Subject: [PATCH 50/86] Completed documentation for HP E3631A --- instruments/hp/hpe3631a.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index 301608b5d..d41f24c9b 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -61,14 +61,6 @@ class HPe3631a(PowerSupply): - Channel 2 is a positive +25V/1A channel (P25V) - Channel 3 is a negative -25V/1A channel (N25V) - Example usage: - - >>> import instruments as ik - >>> psu = ik.hp.HPe3631a.open_gpibusb("/dev/ttyUSB0", 10) - >>> psu.channelid = 2 # Sets channel to P25V - >>> psu.voltage = 12.5 # Sets voltage to 12.5V - >>> print(psu.voltage) # Reads back set current - This module is designed for the power supply to be set to a specific channel and remain set afterwards as this device does not offer commands to set or read multiple channels @@ -78,6 +70,17 @@ class HPe3631a(PowerSupply): This module is likely to work as is for the Agilent E3631 and Keysight E3631 which seem to be rebranded but identical devices. + + Example usage: + + >>> import instruments as ik + >>> psu = ik.hp.HPe3631a.open_gpibusb("/dev/ttyUSB0", 10) + >>> psu.channelid = 2 # Sets channel to P25V + >>> psu.voltage = 12.5 # Sets voltage to 12.5V + >>> psu.voltage # Reads back set voltage + array(12.5) * V + >>> psu.voltage_sense # Reads back sensed voltage + array(12.501) * V """ def __init__(self, filelike): @@ -132,12 +135,19 @@ def query(self, cmd): def mode(self): """ Gets/sets the mode for the specified channel. + + The constant-voltage/constant-current modes of the power supply + are selected automatically depending on the load (resistance) + connected to the power supply. If the load greater than the set + V/I is connected, a voltage V is applied and the current flowing + is lower than I. If the load is smaller than V/I, the set current + I acts as a current limiter and the voltage is lower than V. """ - raise NotImplementedError + return 'auto' @mode.setter def mode(self, newval): - raise NotImplementedError + raise NotImplementedError('The `HPe3631a` sets its mode automatically') voltage, voltage_min, voltage_max = bounded_unitful_property( "SOUR:VOLT", From b9abfee8a11fffc83ec4c23a773e9dd2f968fec2 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 9 Aug 2019 10:41:41 -0700 Subject: [PATCH 51/86] Completed Fluke 3000 FC support --- instruments/fluke/fluke3000.py | 167 +++++++++++++++++---------------- 1 file changed, 87 insertions(+), 80 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 1f4fa2f68..266687d3f 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -68,20 +68,45 @@ class Fluke3000(Multimeter): and binds them to the PC3000 dongle. At each initialization, this class checks what device has been bound and saves their module number. - This class is a work in progress, it currently only supports the t3000 - FC Wireless Temperature Module as it is the only instrument available - to the author. It also only supports single readout. + This class has been tested with the 3000 FC Wireless Multimeter and + the t3000 FC Wireless Temperature Module. They have been operated + separately and simultaneously. It does not support the Wireless AC/DC + Voltage Modules as the author did not have them on hand. + + It is important to note that the mode of the multimeter cannot be set + remotely. If must be set on the device prior to the measurement. If + the measurement read back from the multimeter is not expressed in the + expected units, this module will raise an error. + + Example usage: + + >>> import instruments as ik + >>> mult = ik.fluke.fluke3000.open_serial("/dev/ttyUSB0", 115200) + >>> mult.measure(mult.Mode.voltage_dc) # Measures the DC voltage + array(12.345) * V + + It is crucial not to kill this program in the process of making a measurement, + as for the Fluke 3000 FC Wireless Multimeter, one has to open continuous + readout, make a read and close it. If the process is killed, the read out + may not be closed and the serial cache will be constantly filled with measurements + that will interfere with any status query. If the multimeter is stuck in + continuous trigger after a bad kill, simply do: + + >>> mult.reset() + >>> mult.connect() + + Follow the same procedure if you want to add/remove an instrument to/from + the readout chain as the code will not look for new instruments if some + have already been connected to the PC3000 dongle. """ def __init__(self, filelike): """ - Initialise the instrument, and set the required eos, eoi needed for - communication. + Initialize the instrument, and set the properties needed for communication. """ super(Fluke3000, self).__init__(filelike) - self.timeout = 15 * pq.second + self.timeout = 3 * pq.second self.terminator = "\r" - self._null = False self.positions = {} self.connect() @@ -90,7 +115,7 @@ def __init__(self, filelike): class Module(Enum): """ - Enum containing the supported module codes + Enum containing the supported modules serial numbers. """ #: Multimeter m3000 = 46333030304643 @@ -100,7 +125,7 @@ class Module(Enum): class Mode(Enum): """ - Enum containing the supported mode codes + Enum containing the supported mode codes. """ #: AC Voltage voltage_ac = "01" @@ -123,27 +148,33 @@ class TriggerMode(IntEnum): """ Enum with valid trigger modes. + + The only supported mode is to trigger the device once when a + measurement is queried. This device does support continuous + triggering but it would quickly flood the serial input cache as + readouts do not overwrite each other and are accumulated. """ + #: Single trigger on query single = 1 # PROPERTIES ## - mode = enum_property( + module = enum_property( "", - Mode, - doc="""Set the measurement mode. + Module, + doc="""Set the measurement module. - :type: `Fluke3000.Mode` + :type: `Fluke3000.Module` """, writeonly=True, set_fmt="{}{}") - module = enum_property( + mode = enum_property( "", - Module, - doc="""Set the measurement module. + Mode, + doc="""Set the measurement mode. - :type: `Fluke3000.Module` + :type: `Fluke3000.Mode` """, writeonly=True, set_fmt="{}{}") @@ -163,21 +194,20 @@ class TriggerMode(IntEnum): @property def input_range(self): """ - The `Fluke3000` FC is autoranging only + The `Fluke3000` FC is an autoranging only multimeter. + + :rtype: `str` """ - raise NotImplementedError + return 'auto' @input_range.setter def input_range(self, value): - """ - The `Fluke3000` FC is autoranging only - """ - raise NotImplementedError + raise NotImplementedError('The `Fluke3000` FC is an autoranging only multimeter') @property def relative(self): """ - The `Fluke3000` FC does not support relative measurements + The `Fluke3000` FC does not support relative measurements. :rtype: `bool` """ @@ -185,33 +215,35 @@ def relative(self): @relative.setter def relative(self, value): - """ - The `Fluke3000` FC does not support relative measurements - """ - raise NotImplementedError + raise NotImplementedError("The `Fluke3000` FC does not support relative measurements") # METHODS ## def connect(self): """ - Connect to available modules and returns - a dictionary of the modules found and their location + Connect to available modules and returns a dictionary + of the modules found and their port ID. """ - self.flush() # Flush serial - self.scan() # Look for connected devices + self.flush() # Flush serial + self.scan() # Look for connected devices if not self.positions: - self.reset() # Resets the PC3000 dongle - self.query_lines("rfdis", 3) # Discovers connected modules - self.scan() # Look for connected devices + self.reset() # Reset the PC3000 dongle + timeout = self.timeout # Store default timeout + self.timeout = 30 * pq.second # PC 3000 can take a while to bind with wireless devices + self.query_lines("rfdis", 3) # Discover available modules and bind them + self.timeout = timeout # Restore default timeout + self.scan() # Look for connected devices if not self.positions: - raise ValueError("No Fluke3000 modules available") - - self.timeout = 3 * pq.second + raise ValueError("No `Fluke3000` modules available") def flush(self): """ - Flushes the serial output cache + Flushes the serial input cache. + + This device outputs a terminator after each output line. + The serial input cache is flushed by repeatedly reading + until a terminator is not found. """ timeout = self.timeout self.timeout = 0.1 * pq.second @@ -225,15 +257,15 @@ def flush(self): def reset(self): """ - Resets the device and unbinds all modules + Resets the device and unbinds all modules. """ self.query_lines("ri", 3) # Resets the device self.query_lines("rfsm 1", 2) # Turns comms on def scan(self): """ - Search for available modules and returns - a dictionary of the modules found and their location + Search for available modules and reformatturns a dictionary + of the modules found and their port ID. """ # Loop over possible channels, store device locations positions = {} @@ -252,44 +284,30 @@ def scan(self): elif module_id == self.Module.t3000.value: positions[self.Module.t3000] = port_id else: - error = "Module ID {} not implemented".format(module_id) - raise NotImplementedError(error) + raise NotImplementedError("Module ID {} not implemented".format(module_id)) - # Reset device readout - self.query('rfemd 0{} 2'.format(port_id)) - - self.flush() self.positions = positions def read_lines(self, nlines=1): """ Function that keeps reading until reaches a termination character a set amount of times. This is implemented - to handle the mutiline output of the PC3000 + to handle the mutiline output of the PC3000. :param nlines: Number of termination characters to reach - :type: 'int' + :type nlines: 'int' :return: Array of lines read out :rtype: Array of `str` """ - lines = [] - i = 0 - while i < nlines: - try: - lines.append(self.read()) - i += 1 - except OSError: - continue - - return lines + return [self.read() for i in range(nlines)] def query_lines(self, cmd, nlines=1): """ - Function used to send a command to the instrument while allowing - for multiline output (multiple termination characters) + Function used to send a query to the instrument while allowing + for the multiline output of the PC3000. :param cmd: Command that will be sent to the instrument :param nlines: Number of termination characters to reach @@ -307,11 +325,6 @@ def query_lines(self, cmd, nlines=1): def measure(self, mode): """Instruct the Fluke3000 to perform a one time measurement. - Example usage: - - >>> dmm = ik.fluke.Fluke3000.open_serial("/dev/ttyUSB0") - >>> print dmm.measure(dmm.Mode.temperature) - :param mode: Desired measurement mode. :type mode: `Fluke3000.Mode` @@ -348,15 +361,12 @@ def measure(self, mode): if "PH" in value: data = value.split("PH=")[-1] if self._parse_mode(data) != mode.value: - self.flush() + self.query("rfemd 0{} 2".format(port_id)) else: break # Parse the output - try: - value = self._parse(value, mode) - except: - raise ValueError("Failed to read out Fluke3000 with mode {}".format(mode)) + value = self._parse(value, mode) # Return with the appropriate units units = UNITS[mode] @@ -400,19 +410,16 @@ def _parse(self, result, mode): # Check that the multimeter is in the right mode (fifth byte) if self._parse_mode(data) != mode.value: - error = ('Mode {} was requested but the Fluke 3000FC Multimeter ' - 'is in mode {} instead, could not read the requested' - 'quantity.').format(mode.name, self.Mode(data[8:10]).name) + error = ("Mode {} was requested but the Fluke 3000FC Multimeter " + "is in mode {} instead. Could not read the requested " + "quantity.").format(mode.name, self.Mode(data[8:10]).name) raise ValueError(error) # Extract the value from the first two bytes value = self._parse_value(data) # Extract the prefactor from the fourth byte - try: - scale = self._parse_factor(data) - except: - raise ValueError("Could not parse the prefactor byte") + scale = self._parse_factor(data) # Combine and return return scale*value @@ -460,7 +467,7 @@ def _parse_factor(data): """ # Convert the fourth dual hex byte to an 8 bits string - byte = '{0:08b}'.format(int(data[6:8], 16)) + byte = format(int(data[6:8], 16), '08b') # The first bit encodes the sign (0 positive, 1 negative) sign = 1 if byte[0] == '0' else -1 From b7f5a5cb6e73fb2c4c3e62dffc36e37b3a23acb7 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 9 Aug 2019 11:00:43 -0700 Subject: [PATCH 52/86] Fixed ignored variable name --- instruments/fluke/fluke3000.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 266687d3f..460e981fe 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -302,7 +302,7 @@ def read_lines(self, nlines=1): :rtype: Array of `str` """ - return [self.read() for i in range(nlines)] + return [self.read() for _ in range(nlines)] def query_lines(self, cmd, nlines=1): """ From 1d167357ef804e6c7c39053acb9ec08cf4c3f742 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 10 Aug 2019 22:12:17 -0700 Subject: [PATCH 53/86] Added tests for the Keithley 485 --- instruments/keithley/keithley485.py | 61 ++++---- .../tests/test_keithley/test_keithley485.py | 144 ++++++++++++++++++ 2 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 instruments/tests/test_keithley/test_keithley485.py diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index de3393bd4..4cea11682 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# keithley485.py: Driver for the Keithley 485 pico-ampmeter. +# keithley485.py: Driver for the Keithley 485 picoammeter. # # © 2019 Francois Drielsma (francois.drielsma@gmail.com). # @@ -22,7 +22,7 @@ # along with this program. If not, see . # """ -Driver for the Keithley 485 pico-ampmeter. +Driver for the Keithley 485 picoammeter. Originally contributed and copyright held by Francois Drielsma (francois.drielsma@gmail.com). @@ -50,7 +50,7 @@ class Keithley485(Instrument): """ The Keithley Model 485 is a 4 1/2 digit resolution autoranging - pico-ampmeter with a +- 20000 count LCD. It is designed for low + picoammeter with a +- 20000 count LCD. It is designed for low current measurement requirements from 0.1pA to 2mA. The device needs some processing time (manual reports 300-500ms) after a @@ -184,11 +184,13 @@ def log(self, newval): def input_range(self): """ Gets/sets the range (R) of the Keithley 485 input terminals. The valid - ranges are one of ``{AUTO|2e-9|2e-8|2e-7|2e-6|2e-5|2e-4|2e-3}`` + ranges are one of ``{auto|2e-9|2e-8|2e-7|2e-6|2e-5|2e-4|2e-3}`` :type: `~quantities.quantity.Quantity` or `str` """ - value = float(self.get_status()['range']) + value = self.get_status()['range'] + if isinstance(value, str): + return value return value * pq.amp @input_range.setter @@ -243,6 +245,28 @@ def relative(self, newval): raise TypeError('Relative mode must be a boolean.') self.sendcmd('Z{}X'.format(int(newval))) + @property + def eoi_mode(self): + """ + Gets/sets the 'eoi' mode (K) of the Keithley 485. + + The model 485 will normally send an end of interrupt (EOI) + during the last byte of its data string or status word. + The EOI reponse of the instrument may be included or omitted. + Warning: the default setting (K0) includes it. + + See the Keithley 485 manual for more information. + + :type: `bool` + """ + return self.get_status()['eoi_mode'] + + @eoi_mode.setter + def eoi_mode(self, newval): + if not isinstance(newval, bool): + raise TypeError('EOI mode must be a boolean.') + self.sendcmd('K{}X'.format(1-int(newval))) + @property def trigger_mode(self): """ @@ -278,28 +302,6 @@ def trigger_mode(self, newval): 'instead.'.format(newval)) self.sendcmd('T{}X'.format(newval.value)) - @property - def eoi(self): - """ - Gets/sets the 'eoi' mode (K) of the Keithley 485. - - The model 485 will normally send an end of interrupt (EOI) - during the last byte of its data string or status word. - The EOI reponse of the instrument may be included or omitted. - Warning: the default setting (K0) includes it. - - See the Keithley 485 manual for more information. - - :type: `bool` - """ - return not self.get_status()['eoi'] - - @eoi.setter - def eoi(self, newval): - if not isinstance(newval, bool): - raise TypeError('EOI mode must be a boolean.') - self.sendcmd('K{}X'.format(1-int(newval))) - # METHODS # def auto_range(self): @@ -359,7 +361,7 @@ def _parse_status_word(self, statusword): raise ValueError('Status word starts with wrong ' 'prefix: {}'.format(statusword)) - (zerocheck, log, range, relative, eoi, + (zerocheck, log, range, relative, eoi_mode, trigger, datamask, errormask) = \ struct.unpack('@6c2s2s', bytes(statusword[3:], 'utf-8')) @@ -381,11 +383,12 @@ def _parse_status_word(self, statusword): raise RuntimeError('Cannot parse status ' 'word: {}'.format(statusword)) + print(eoi_mode, eoi_mode == b'0') return {'zerocheck': zerocheck == b'1', 'log': log == b'1', 'range': range, 'relative': relative == b'1', - 'eoi': eoi == b'0', + 'eoi_mode': eoi_mode == b'0', 'trigger': trigger, 'datamask': datamask, 'errormask': errormask, diff --git a/instruments/tests/test_keithley/test_keithley485.py b/instruments/tests/test_keithley/test_keithley485.py new file mode 100644 index 000000000..893baadf6 --- /dev/null +++ b/instruments/tests/test_keithley/test_keithley485.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the Keithley 485 picoammeter +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +import quantities as pq + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + +def test_zero_check(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "C0X", + "C1X", + "U0X" + ], [ + "4851000000000:" + ] + ) as inst: + inst.zero_check = False + inst.zero_check = True + assert inst.zero_check == True + +def test_log(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "D0X", + "D1X", + "U0X" + ], [ + "4850100000000:" + ] + ) as inst: + inst.log = False + inst.log = True + assert inst.log == True + +def test_input_range(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "R0X", + "R7X", + "U0X" + ], [ + "4850070000000:" + ] + ) as inst: + inst.input_range = 'auto' + inst.input_range = 2e-3 + assert inst.input_range == 2. * pq.milliamp + +def test_relative(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "Z0X", + "Z1X", + "U0X" + ], [ + "4850001000000:" + ] + ) as inst: + inst.relative = False + inst.relative = True + assert inst.relative == True + +def test_eoi_mode(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "K0X", + "K1X", + "U0X" + ], [ + "4850000100000:" + ] + ) as inst: + inst.eoi_mode = True + inst.eoi_mode = False + assert inst.eoi_mode == False + +def test_trigger_mode(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "T0X", + "T5X", + "U0X" + ], [ + "4850000050000:" + ] + ) as inst: + inst.trigger_mode = 'continuous_ontalk' + inst.trigger_mode = 'oneshot_onx' + assert inst.trigger_mode == 'oneshot_onx' + +def test_auto_range(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "R0X", + "U0X" + ], [ + "4850000000000:" + ] + ) as inst: + inst.auto_range() + assert inst.input_range == 'auto' + +def test_get_status(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "U0X" + ], [ + "4850000000000:" + ] + ) as inst: + inst.get_status() + +def test_measure(): + with expected_protocol( + ik.keithley.Keithley485, + [ + "X", + "X" + ], [ + "NDCA+1.2345E-9", + "NDCL-9.0000E+0" + ] + ) as inst: + assert inst.measure() == 1.2345 * pq.nanoamp + assert inst.measure() == 1. * pq.nanoamp From 0013b1e817e59313dbebcd197472c4bcaf39fba3 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 09:08:38 -0700 Subject: [PATCH 54/86] Style fixes, python 2.7 compatibility --- instruments/keithley/keithley485.py | 7 ++--- .../tests/test_keithley/test_keithley485.py | 26 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 4cea11682..60a220e0f 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -35,7 +35,8 @@ from __future__ import absolute_import from __future__ import division -import struct +from builtins import bytes +from struct import unpack from enum import Enum @@ -363,7 +364,7 @@ def _parse_status_word(self, statusword): (zerocheck, log, range, relative, eoi_mode, trigger, datamask, errormask) = \ - struct.unpack('@6c2s2s', bytes(statusword[3:], 'utf-8')) + unpack('@6c2s2s', bytes(statusword[3:], 'utf-8')) valid_range = {b'0': 'auto', b'1': 2e-9, @@ -414,7 +415,7 @@ def _parse_measurement(self, measurement): :rtype: `~quantities.quantity.Quantity` """ (status, function, base, current) = \ - struct.unpack('@1c2s1c10s', bytes(measurement, 'utf-8')) + unpack('@1c2s1c10s', bytes(measurement, 'utf-8')) try: status = self.Status(status) diff --git a/instruments/tests/test_keithley/test_keithley485.py b/instruments/tests/test_keithley/test_keithley485.py index 893baadf6..c8cb6c7db 100644 --- a/instruments/tests/test_keithley/test_keithley485.py +++ b/instruments/tests/test_keithley/test_keithley485.py @@ -17,7 +17,7 @@ def test_zero_check(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "C0X", "C1X", @@ -28,11 +28,11 @@ def test_zero_check(): ) as inst: inst.zero_check = False inst.zero_check = True - assert inst.zero_check == True + assert inst.zero_check def test_log(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "D0X", "D1X", @@ -43,11 +43,11 @@ def test_log(): ) as inst: inst.log = False inst.log = True - assert inst.log == True + assert inst.log def test_input_range(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "R0X", "R7X", @@ -62,7 +62,7 @@ def test_input_range(): def test_relative(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "Z0X", "Z1X", @@ -73,11 +73,11 @@ def test_relative(): ) as inst: inst.relative = False inst.relative = True - assert inst.relative == True + assert inst.relative def test_eoi_mode(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "K0X", "K1X", @@ -88,11 +88,11 @@ def test_eoi_mode(): ) as inst: inst.eoi_mode = True inst.eoi_mode = False - assert inst.eoi_mode == False + assert not inst.eoi_mode def test_trigger_mode(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "T0X", "T5X", @@ -107,7 +107,7 @@ def test_trigger_mode(): def test_auto_range(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "R0X", "U0X" @@ -120,7 +120,7 @@ def test_auto_range(): def test_get_status(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "U0X" ], [ @@ -131,7 +131,7 @@ def test_get_status(): def test_measure(): with expected_protocol( - ik.keithley.Keithley485, + ik.keithley.Keithley485, [ "X", "X" From 35355a3002df9a066476a4dcda3a682085ea9e4d Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 09:15:17 -0700 Subject: [PATCH 55/86] Stray print function removed --- instruments/keithley/keithley485.py | 1 - 1 file changed, 1 deletion(-) diff --git a/instruments/keithley/keithley485.py b/instruments/keithley/keithley485.py index 60a220e0f..ff9fe0dfa 100644 --- a/instruments/keithley/keithley485.py +++ b/instruments/keithley/keithley485.py @@ -384,7 +384,6 @@ def _parse_status_word(self, statusword): raise RuntimeError('Cannot parse status ' 'word: {}'.format(statusword)) - print(eoi_mode, eoi_mode == b'0') return {'zerocheck': zerocheck == b'1', 'log': log == b'1', 'range': range, From 9657ffc21cbeed78ee7b1c28aafc7b9ce67fe98d Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 11:03:46 -0700 Subject: [PATCH 56/86] Added tests for the Fluke 3000 --- instruments/fluke/fluke3000.py | 160 +++++++-------- instruments/tests/test_fluke/__init__.py | 0 .../tests/test_fluke/test_fluke3000.py | 186 ++++++++++++++++++ 3 files changed, 266 insertions(+), 80 deletions(-) create mode 100644 instruments/tests/test_fluke/__init__.py create mode 100644 instruments/tests/test_fluke/test_fluke3000.py diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 460e981fe..00bbb4b72 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -38,12 +38,11 @@ import time from builtins import range -from enum import Enum, IntEnum +from enum import Enum import quantities as pq from instruments.abstract_instruments import Multimeter -from instruments.util_fns import enum_property # CLASSES ##################################################################### @@ -93,6 +92,7 @@ class Fluke3000(Multimeter): continuous trigger after a bad kill, simply do: >>> mult.reset() + >>> mult.flush() >>> mult.connect() Follow the same procedure if you want to add/remove an instrument to/from @@ -113,7 +113,6 @@ def __init__(self, filelike): # ENUMS ## class Module(Enum): - """ Enum containing the supported modules serial numbers. """ @@ -123,7 +122,6 @@ class Module(Enum): t3000 = 54333030304643 class Mode(Enum): - """ Enum containing the supported mode codes. """ @@ -144,56 +142,70 @@ class Mode(Enum): #: Capacitance capacitance = "0F" - class TriggerMode(IntEnum): + # PROPERTIES ## + + @property + def mode(self): + """ + Gets/sets the measurement mode for the multimeter. + + The measurement mode of the multimeter must be set on the + device manually and cannot be set remotely. If a multimeter + is bound to the PC3000, returns its measurement mode by + making a measurement and checking the units bytes in response. + :rtype: `Fluke3000.Mode` + """ + if self.Module.m3000 not in self.positions.keys(): + raise KeyError("No `Fluke3000` FC multimeter is connected") + port_id = self.positions[self.Module.m3000] + value = self.query_lines("rfemd 0{} 1".format(port_id), 2)[-1] + self.query("rfemd 0{} 2".format(port_id)) + data = value.split("PH=")[-1] + return self.Mode(self._parse_mode(data)) + + @mode.setter + def mode(self, newval): + raise NotImplementedError("The `Fluke3000` measurement mode can only be set on the device") + + @property + def trigger_mode(self): """ - Enum with valid trigger modes. + Gets/sets the trigger mode for the multimeter. The only supported mode is to trigger the device once when a measurement is queried. This device does support continuous triggering but it would quickly flood the serial input cache as readouts do not overwrite each other and are accumulated. - """ - #: Single trigger on query - single = 1 - - # PROPERTIES ## - module = enum_property( - "", - Module, - doc="""Set the measurement module. - - :type: `Fluke3000.Module` - """, - writeonly=True, - set_fmt="{}{}") + :rtype: `str` + """ + return 'single' - mode = enum_property( - "", - Mode, - doc="""Set the measurement mode. + @trigger_mode.setter + def trigger_mode(self, newval): + raise ValueError("The `Fluke3000` only supports single trigger when queried") - :type: `Fluke3000.Mode` - """, - writeonly=True, - set_fmt="{}{}") + @property + def relative(self): + """ + Gets/sets the status of relative measuring mode for the multimeter. - trigger_mode = enum_property( - "T", - TriggerMode, - doc="""Set the trigger mode. + The `Fluke3000` FC does not support relative measurements. - Note there is only one trigger mode (single). + :rtype: `bool` + """ + return False - :type: `Fluke3000.TriggerMode` - """, - writeonly=True, - set_fmt="{}{}") + @relative.setter + def relative(self, newval): + raise ValueError("The `Fluke3000` FC does not support relative measurements") @property def input_range(self): """ + Gets/sets the current input range setting of the multimeter. + The `Fluke3000` FC is an autoranging only multimeter. :rtype: `str` @@ -201,21 +213,8 @@ def input_range(self): return 'auto' @input_range.setter - def input_range(self, value): - raise NotImplementedError('The `Fluke3000` FC is an autoranging only multimeter') - - @property - def relative(self): - """ - The `Fluke3000` FC does not support relative measurements. - - :rtype: `bool` - """ - return False - - @relative.setter - def relative(self, value): - raise NotImplementedError("The `Fluke3000` FC does not support relative measurements") + def input_range(self, newval): + raise ValueError('The `Fluke3000` FC is an autoranging only multimeter') # METHODS ## @@ -224,7 +223,6 @@ def connect(self): Connect to available modules and returns a dictionary of the modules found and their port ID. """ - self.flush() # Flush serial self.scan() # Look for connected devices if not self.positions: self.reset() # Reset the PC3000 dongle @@ -237,31 +235,6 @@ def connect(self): if not self.positions: raise ValueError("No `Fluke3000` modules available") - def flush(self): - """ - Flushes the serial input cache. - - This device outputs a terminator after each output line. - The serial input cache is flushed by repeatedly reading - until a terminator is not found. - """ - timeout = self.timeout - self.timeout = 0.1 * pq.second - init_time = time.time() - while time.time() - init_time < timeout: - try: - self.read() - except OSError: - break - self.timeout = timeout - - def reset(self): - """ - Resets the device and unbinds all modules. - """ - self.query_lines("ri", 3) # Resets the device - self.query_lines("rfsm 1", 2) # Turns comms on - def scan(self): """ Search for available modules and reformatturns a dictionary @@ -288,6 +261,13 @@ def scan(self): self.positions = positions + def reset(self): + """ + Resets the device and unbinds all modules. + """ + self.query_lines("ri", 3) # Resets the device + self.query_lines("rfsm 1", 2) # Turns comms on + def read_lines(self, nlines=1): """ Function that keeps reading until reaches a termination @@ -322,6 +302,24 @@ def query_lines(self, cmd, nlines=1): self.sendcmd(cmd) return self.read_lines(nlines) + def flush(self): + """ + Flushes the serial input cache. + + This device outputs a terminator after each output line. + The serial input cache is flushed by repeatedly reading + until a terminator is not found. + """ + timeout = self.timeout + self.timeout = 0.1 * pq.second + init_time = time.time() + while time.time() - init_time < 1.: + try: + self.read() + except OSError: + break + self.timeout = timeout + def measure(self, mode): """Instruct the Fluke3000 to perform a one time measurement. @@ -346,7 +344,7 @@ def measure(self, mode): value = '' port_id = self.positions[module] init_time = time.time() - while time.time() - init_time < self.timeout: + while time.time() - init_time < 3.: # Read out if mode == self.Mode.temperature: # The temperature module supports single readout @@ -361,7 +359,9 @@ def measure(self, mode): if "PH" in value: data = value.split("PH=")[-1] if self._parse_mode(data) != mode.value: - self.query("rfemd 0{} 2".format(port_id)) + if self.Module.m3000 in self.positions.keys(): + self.query("rfemd 0{} 2".format(self.positions[self.Module.m3000])) + self.flush() else: break diff --git a/instruments/tests/test_fluke/__init__.py b/instruments/tests/test_fluke/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instruments/tests/test_fluke/test_fluke3000.py b/instruments/tests/test_fluke/test_fluke3000.py new file mode 100644 index 000000000..d55b8baed --- /dev/null +++ b/instruments/tests/test_fluke/test_fluke3000.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the Fluke 3000 FC +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +import quantities as pq + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + +# Empty initialization sequence (scan function) that does not uncover +# any available Fluke 3000 FC device. +none_sequence = [ + "rfebd 01 0", + "rfebd 02 0", + "rfebd 03 0", + "rfebd 04 0", + "rfebd 05 0", + "rfebd 06 0" + ] +none_response = [ + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2" + ] + +# Default initialization sequence (scan function) that binds a multimeter +# to port 1 and a temperature module to port 2. +init_sequence = [ + "rfebd 01 0", # 1 + "rfgus 01", # 2 + "rfebd 02 0", # 3 + "rfgus 02", # 4 + "rfebd 03 0", # 5 + "rfebd 04 0", # 6 + "rfebd 05 0", # 7 + "rfebd 06 0" # 8 + ] +init_response = [ + "CR:Ack=0:RFEBD", # 1.1 + "ME:R:S#=01:DCC=012:PH=64", # 1.2 + "CR:Ack=0:RFGUS", # 2.1 + "ME:R:S#=01:DCC=004:PH=46333030304643", # 2.2 + "CR:Ack=0:RFEBD", # 3.1 + "ME:R:S#=01:DCC=012:PH=64", # 3.2 + "CR:Ack=0:RFGUS", # 4.1 + "ME:R:S#=02:DCC=004:PH=54333030304643", # 4.2 + "CR:Ack=2", # 5 + "CR:Ack=2", # 6 + "CR:Ack=2", # 7 + "CR:Ack=2" # 8 + ] + +def test_mode(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence + + [ + "rfemd 01 1", # 1 + "rfemd 01 2" # 2 + ], + init_response + + [ + "CR:Ack=0:RFEMD", # 1.1 + "ME:R:S#=01:DCC=010:PH=00000006020C0600", # 1.2 + "CR:Ack=0:RFEMD" # 2 + ], + '\r' + ) as inst: + assert inst.mode == inst.Mode.voltage_dc + +def test_trigger_mode(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence, + init_response, + '\r' + ) as inst: + assert inst.trigger_mode == 'single' + +def test_relative(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence, + init_response, + '\r' + ) as inst: + assert not inst.relative + +def test_input_range(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence, + init_response, + '\r' + ) as inst: + assert inst.input_range == 'auto' + +def test_connect(): + with expected_protocol( + ik.fluke.Fluke3000, + none_sequence + + [ + "ri", # 1 + "rfsm 1", # 2 + "rfdis", # 3 + ] + + init_sequence, + none_response + + [ + "CR:Ack=0:RI", # 1.1 + "SI:PON=Power On", # 1.2 + "RE:O", # 1.3 + "CR:Ack=0:RFSM:Radio On Master", # 2.1 + "RE:M", # 2.2 + "CR:Ack=0:RFDIS", # 3.1 + "ME:S", # 3.2 + "ME:D:010200000000", # 3.3 + ] + + init_response, + '\r' + ) as inst: + assert inst.positions[ik.fluke.Fluke3000.Module.m3000] == 1 + assert inst.positions[ik.fluke.Fluke3000.Module.t3000] == 2 + +def test_scan(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence, + init_response, + '\r' + ) as inst: + assert inst.positions[ik.fluke.Fluke3000.Module.m3000] == 1 + assert inst.positions[ik.fluke.Fluke3000.Module.t3000] == 2 + +def test_reset(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence + + [ + "ri", # 1 + "rfsm 1" # 2 + ], + init_response + + [ + "CR:Ack=0:RI", # 1.1 + "SI:PON=Power On", # 1.2 + "RE:O", # 1.3 + "CR:Ack=0:RFSM:Radio On Master", # 2.1 + "RE:M" # 2.2 + ], + '\r' + ) as inst: + inst.reset() + +def test_measure(): + with expected_protocol( + ik.fluke.Fluke3000, + init_sequence + + [ + "rfemd 01 1", # 1 + "rfemd 01 2", # 2 + "rfemd 02 0" # 3 + ], + init_response + + [ + "CR:Ack=0:RFEMD", # 1.1 + "ME:R:S#=01:DCC=010:PH=FD010006020C0600", # 1.2 + "CR:Ack=0:RFEMD", # 2 + "CR:Ack=0:RFEMD", # 3.1 + "ME:R:S#=02:DCC=010:PH=FD00C08207220000" # 3.2 + ], + '\r' + ) as inst: + assert inst.measure(inst.Mode.voltage_dc) == 0.509 * pq.volt + assert inst.measure(inst.Mode.temperature) == -25.3 * pq.celsius From 8b156e769b29f46ab81ed8ede1bb6c35af7b82e0 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 11:10:57 -0700 Subject: [PATCH 57/86] Indentation style fix in Fluke 3000 tests --- .../tests/test_fluke/test_fluke3000.py | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/instruments/tests/test_fluke/test_fluke3000.py b/instruments/tests/test_fluke/test_fluke3000.py index d55b8baed..fd02da04b 100644 --- a/instruments/tests/test_fluke/test_fluke3000.py +++ b/instruments/tests/test_fluke/test_fluke3000.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Module containing tests for the Fluke 3000 FC +Module containing tests for the Fluke 3000 FC multimeter """ # IMPORTS #################################################################### @@ -18,48 +18,48 @@ # Empty initialization sequence (scan function) that does not uncover # any available Fluke 3000 FC device. none_sequence = [ - "rfebd 01 0", - "rfebd 02 0", - "rfebd 03 0", - "rfebd 04 0", - "rfebd 05 0", - "rfebd 06 0" - ] + "rfebd 01 0", + "rfebd 02 0", + "rfebd 03 0", + "rfebd 04 0", + "rfebd 05 0", + "rfebd 06 0" +] none_response = [ - "CR:Ack=2", - "CR:Ack=2", - "CR:Ack=2", - "CR:Ack=2", - "CR:Ack=2", - "CR:Ack=2" - ] + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2", + "CR:Ack=2" +] # Default initialization sequence (scan function) that binds a multimeter # to port 1 and a temperature module to port 2. init_sequence = [ - "rfebd 01 0", # 1 - "rfgus 01", # 2 - "rfebd 02 0", # 3 - "rfgus 02", # 4 - "rfebd 03 0", # 5 - "rfebd 04 0", # 6 - "rfebd 05 0", # 7 - "rfebd 06 0" # 8 - ] + "rfebd 01 0", # 1 + "rfgus 01", # 2 + "rfebd 02 0", # 3 + "rfgus 02", # 4 + "rfebd 03 0", # 5 + "rfebd 04 0", # 6 + "rfebd 05 0", # 7 + "rfebd 06 0" # 8 +] init_response = [ - "CR:Ack=0:RFEBD", # 1.1 - "ME:R:S#=01:DCC=012:PH=64", # 1.2 - "CR:Ack=0:RFGUS", # 2.1 - "ME:R:S#=01:DCC=004:PH=46333030304643", # 2.2 - "CR:Ack=0:RFEBD", # 3.1 - "ME:R:S#=01:DCC=012:PH=64", # 3.2 - "CR:Ack=0:RFGUS", # 4.1 - "ME:R:S#=02:DCC=004:PH=54333030304643", # 4.2 - "CR:Ack=2", # 5 - "CR:Ack=2", # 6 - "CR:Ack=2", # 7 - "CR:Ack=2" # 8 - ] + "CR:Ack=0:RFEBD", # 1.1 + "ME:R:S#=01:DCC=012:PH=64", # 1.2 + "CR:Ack=0:RFGUS", # 2.1 + "ME:R:S#=01:DCC=004:PH=46333030304643", # 2.2 + "CR:Ack=0:RFEBD", # 3.1 + "ME:R:S#=01:DCC=012:PH=64", # 3.2 + "CR:Ack=0:RFGUS", # 4.1 + "ME:R:S#=02:DCC=004:PH=54333030304643", # 4.2 + "CR:Ack=2", # 5 + "CR:Ack=2", # 6 + "CR:Ack=2", # 7 + "CR:Ack=2" # 8 +] def test_mode(): with expected_protocol( From c4ffe336b26194ba3a36347dd538f4847028fa2b Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 17:15:48 -0700 Subject: [PATCH 58/86] Convert Fluke 3000 unsettable attributed to read-only --- instruments/fluke/fluke3000.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/instruments/fluke/fluke3000.py b/instruments/fluke/fluke3000.py index 00bbb4b72..77472f489 100644 --- a/instruments/fluke/fluke3000.py +++ b/instruments/fluke/fluke3000.py @@ -157,17 +157,13 @@ def mode(self): :rtype: `Fluke3000.Mode` """ if self.Module.m3000 not in self.positions.keys(): - raise KeyError("No `Fluke3000` FC multimeter is connected") + raise KeyError("No `Fluke3000` FC multimeter is bound") port_id = self.positions[self.Module.m3000] value = self.query_lines("rfemd 0{} 1".format(port_id), 2)[-1] self.query("rfemd 0{} 2".format(port_id)) data = value.split("PH=")[-1] return self.Mode(self._parse_mode(data)) - @mode.setter - def mode(self, newval): - raise NotImplementedError("The `Fluke3000` measurement mode can only be set on the device") - @property def trigger_mode(self): """ @@ -180,11 +176,7 @@ def trigger_mode(self): :rtype: `str` """ - return 'single' - - @trigger_mode.setter - def trigger_mode(self, newval): - raise ValueError("The `Fluke3000` only supports single trigger when queried") + raise AttributeError("The `Fluke3000` only supports single trigger when queried") @property def relative(self): @@ -195,11 +187,7 @@ def relative(self): :rtype: `bool` """ - return False - - @relative.setter - def relative(self, newval): - raise ValueError("The `Fluke3000` FC does not support relative measurements") + raise AttributeError("The `Fluke3000` FC does not support relative measurements") @property def input_range(self): @@ -210,11 +198,7 @@ def input_range(self): :rtype: `str` """ - return 'auto' - - @input_range.setter - def input_range(self, newval): - raise ValueError('The `Fluke3000` FC is an autoranging only multimeter') + return AttributeError('The `Fluke3000` FC is an autoranging only multimeter') # METHODS ## From 7d80db606fa8c5aed7feb3acf2254b50687cd42c Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 17:17:37 -0700 Subject: [PATCH 59/86] Removed superfluous tests for Fluke 3000 --- .../tests/test_fluke/test_fluke3000.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/instruments/tests/test_fluke/test_fluke3000.py b/instruments/tests/test_fluke/test_fluke3000.py index fd02da04b..f91ec38f0 100644 --- a/instruments/tests/test_fluke/test_fluke3000.py +++ b/instruments/tests/test_fluke/test_fluke3000.py @@ -79,33 +79,6 @@ def test_mode(): ) as inst: assert inst.mode == inst.Mode.voltage_dc -def test_trigger_mode(): - with expected_protocol( - ik.fluke.Fluke3000, - init_sequence, - init_response, - '\r' - ) as inst: - assert inst.trigger_mode == 'single' - -def test_relative(): - with expected_protocol( - ik.fluke.Fluke3000, - init_sequence, - init_response, - '\r' - ) as inst: - assert not inst.relative - -def test_input_range(): - with expected_protocol( - ik.fluke.Fluke3000, - init_sequence, - init_response, - '\r' - ) as inst: - assert inst.input_range == 'auto' - def test_connect(): with expected_protocol( ik.fluke.Fluke3000, From c074946501073876ae5c34c8e316137034f163d8 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 21:47:50 -0700 Subject: [PATCH 60/86] Added tests for the Glassman FR-series power supplies --- instruments/glassman/glassmanfr.py | 66 +++--- instruments/tests/test_glassman/__init__.py | 0 .../tests/test_glassman/test_glassmanfr.py | 222 ++++++++++++++++++ 3 files changed, 257 insertions(+), 31 deletions(-) create mode 100644 instruments/tests/test_glassman/__init__.py create mode 100644 instruments/tests/test_glassman/test_glassmanfr.py diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index e84cb544a..f144ba3c5 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -34,7 +34,8 @@ from __future__ import absolute_import from __future__ import division -import struct +from builtins import bytes +from struct import unpack from enum import Enum @@ -73,7 +74,7 @@ class GlassmanFR(PowerSupply, PowerSupplyChannel): >>> psu.voltage array(100.0) * V >>> psu.output = True # Turns on the power supply - >>> psu.voltage_sense < 200 + >>> psu.voltage_sense < 200 * pq.volt True This code uses default values of `voltage_max`, `current_max` and @@ -82,7 +83,7 @@ class GlassmanFR(PowerSupply, PowerSupplyChannel): >>> import quantities as pq >>> psu.voltage_max = 40.0 * pq.kilovolt - >>> psu.current_max = 6.0 * pq.milliamp + >>> psu.current_max = 7.5 * pq.milliamp >>> psu.polarity = -1 """ @@ -96,8 +97,8 @@ def __init__(self, filelike): self.current_max = 6.0 * pq.milliamp self.polarity = +1 self._device_timeout = True - self._voltage = self.voltage_sense - self._current = self.current_sense + self._voltage = 0. * pq.volt + self._current = 0. * pq.amp # ENUMS # @@ -151,7 +152,7 @@ def channel(self): :rtype: 'tuple' of length 1 containing a reference back to the parent GlassmanFR object. """ - return self, + return [self] @property def voltage(self): @@ -167,16 +168,6 @@ def voltage(self): def voltage(self, newval): self.set_status(voltage=assume_units(newval, pq.volt)) - @property - def voltage_sense(self): - """ - Gets the output voltage as measured by the sense wires. - - :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.Quantity` - """ - return self.get_status()['voltage'] - @property def current(self): """ @@ -191,13 +182,23 @@ def current(self): def current(self, newval): self.set_status(current=assume_units(newval, pq.amp)) + @property + def voltage_sense(self): + """ + Gets the output voltage as measured by the sense wires. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `~quantities.Quantity` + """ + return self.get_status()['voltage'] + @property def current_sense(self): """ Gets/sets the output current as measured by the sense wires. :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.Quantity` + :type: `~quantities.Quantity` """ return self.get_status()['current'] @@ -212,13 +213,11 @@ def mode(self): V/I is connected, a voltage V is applied and the current flowing is lower than I. If the load is smaller than V/I, the set current I acts as a current limiter and the voltage is lower than V. + + :type: `GlassmanFR.Mode` """ return self.get_status()['mode'] - @mode.setter - def mode(self, newval): - raise NotImplementedError('The `GlassmanFR` sets its mode automatically') - @property def output(self): """ @@ -339,27 +338,32 @@ def set_status(self, voltage=None, current=None, output=None, reset=False): self._current = 0. * pq.amp cmd = format(4, '013d') else: + # The maximum value is encoded as the maximum of three hex characters (4095) cmd = '' - value_max = int(0xfff)-1 + value_max = int(0xfff) # If the voltage is not specified, keep it as is - self._voltage = assume_units(voltage, pq.volt) if voltage is not None else self.voltage + voltage = assume_units(voltage, pq.volt) if voltage is not None else self.voltage + ratio = float(voltage.rescale(pq.volt)/self.voltage_max.rescale(pq.volt)) + voltage_int = int(round(value_max*ratio)) + self._voltage = self.voltage_max*float(voltage_int)/value_max assert self._voltage >= 0. * pq.volt and self._voltage <= self.voltage_max - ratio = float(self._voltage.rescale(pq.volt)/self.voltage_max.rescale(pq.volt)) - cmd += format(int(value_max*ratio), '03X') + cmd += format(voltage_int, '03X') # If the current is not specified, keep it as is - self._current = assume_units(current, pq.amp) if current is not None else self.current + current = assume_units(current, pq.amp) if current is not None else self.current + ratio = float(current.rescale(pq.amp)/self.current_max.rescale(pq.amp)) + current_int = int(round(value_max*ratio)) + self._current = self.current_max*float(current_int)/value_max assert self._current >= 0. * pq.amp and self._current <= self.current_max - ratio = float(self._current.rescale(pq.amp)/self.current_max.rescale(pq.amp)) - cmd += format(int(value_max*ratio), '03X') + cmd += format(current_int, '03X') # If the output status is not specified, keep it as is output = output if output is not None else self.output control = '00{}{}'.format(int(output), int(not output)) cmd += format(int(control, 2), '07X') - self.query('S' + cmd) # Device acknoledges + self.query('S' + cmd) # Device acknowledges def get_status(self): """ @@ -385,7 +389,7 @@ def _parse_response(self, response): :rtype: `dict` """ (voltage, current, monitors) = \ - struct.unpack('@3s3s3x1c2x', bytes(response, 'utf-8')) + unpack('@3s3s3x1c2x', bytes(response, 'utf-8')) try: voltage = self._parse_voltage(voltage) @@ -440,7 +444,7 @@ def _parse_monitors(self, word): :rtype: `str, bool, bool` ''' bits = format(int(word, 16), '04b') - mode = self.Mode(bits[-1]).name + mode = self.Mode(bits[-1]) fault = bits[-2] == '1' output = bits[-3] == '1' return mode, fault, output diff --git a/instruments/tests/test_glassman/__init__.py b/instruments/tests/test_glassman/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/instruments/tests/test_glassman/test_glassmanfr.py b/instruments/tests/test_glassman/test_glassmanfr.py new file mode 100644 index 000000000..90e525555 --- /dev/null +++ b/instruments/tests/test_glassman/test_glassmanfr.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the Glassman FR power supply +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +import quantities as pq + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ###################################################################### + +def set_defaults(inst): + """ + Sets default values for the voltage and current range of the Glassman FR + to be used to test the voltage and current property getters/setters. + """ + inst.voltage_max = 50.0 * pq.kilovolt + inst.current_max = 6.0 * pq.milliamp + inst.polarity = +1 + +def test_channel(): + with expected_protocol( + ik.glassman.GlassmanFR, + [], + [], + '\r' + ) as inst: + assert len(inst.channel) == 1 + assert inst.channel[0] == inst + +def test_voltage(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01Q51", + "\x01S3330000000001CD" + ], + [ + "R00000000000040", + "A" + ], + '\r' + ) as inst: + set_defaults(inst) + inst.voltage = 10.0 * pq.kilovolt + assert inst.voltage == 10.0 * pq.kilovolt + +def test_current(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01Q51", + "\x01S0003330000001CD" + ], + [ + "R00000000000040", + "A" + ], + '\r' + ) as inst: + set_defaults(inst) + inst.current = 1.2 * pq.milliamp + assert inst.current == 1.2 * pq.milliamp + +def test_voltage_sense(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01Q51" + ], + [ + "R10A00000010053" + ], + '\r' + ) as inst: + set_defaults(inst) + assert round(inst.voltage_sense) == 13.0 * pq.kilovolt + +def test_current_sense(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01Q51" + ], + [ + "R0001550001004C" + ], + '\r' + ) as inst: + set_defaults(inst) + assert inst.current_sense == 2.0 * pq.milliamp + +def test_mode(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01Q51", + "\x01Q51" + ], + [ + "R00000000000040", + "R00000000010041" + ], + '\r' + ) as inst: + assert inst.mode == inst.Mode.voltage + assert inst.mode == inst.Mode.current + +def test_output(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01S0000000000001C4", + "\x01Q51", + "\x01S0000000000002C5", + "\x01Q51" + ], + [ + "A", + "R00000000000040", + "A", + "R00000000040044" + ], + '\r' + ) as inst: + inst.output = False + assert not inst.output + inst.output = True + assert inst.output + +def test_version(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01V56" + ], + [ + "B1465" + ], + '\r' + ) as inst: + assert inst.version == '14' + +def test_device_timeout(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01C073", + "\x01C174" + ], + [ + "A", + "A" + ], + '\r' + ) as inst: + inst.device_timeout = True + assert inst.device_timeout + inst.device_timeout = False + assert not inst.device_timeout + +def test_sendcmd(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01123ABC5C" + ], + [], + '\r' + ) as inst: + inst.sendcmd('123ABC') + +def test_query(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01Q123ABCAD" + ], + [ + "R123ABC5C" + ], + '\r' + ) as inst: + inst.query('Q123ABC') + +def test_reset(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01S0000000000004C7" + ], + [ + "A" + ], + '\r' + ) as inst: + inst.reset() + +def test_set_status(): + with expected_protocol( + ik.glassman.GlassmanFR, + [ + "\x01S3333330000002D7", + "\x01Q51" + ], + [ + "A", + "R00000000040044" + ], + '\r' + ) as inst: + set_defaults(inst) + inst.set_status(voltage=10*pq.kilovolt, current=1.2*pq.milliamp, output=True) + assert inst.output + assert inst.voltage == 10 * pq.kilovolt + assert inst.current == 1.2 * pq.milliamp From 4395bb26fb9e141fbae9db7631b62071584bdd86 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sun, 11 Aug 2019 21:54:27 -0700 Subject: [PATCH 61/86] Explicit builtin round function import --- instruments/glassman/glassmanfr.py | 2 +- instruments/tests/test_glassman/test_glassmanfr.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index f144ba3c5..bddde0751 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -34,7 +34,7 @@ from __future__ import absolute_import from __future__ import division -from builtins import bytes +from builtins import bytes, round from struct import unpack from enum import Enum diff --git a/instruments/tests/test_glassman/test_glassmanfr.py b/instruments/tests/test_glassman/test_glassmanfr.py index 90e525555..1f99045d7 100644 --- a/instruments/tests/test_glassman/test_glassmanfr.py +++ b/instruments/tests/test_glassman/test_glassmanfr.py @@ -7,6 +7,7 @@ # IMPORTS #################################################################### from __future__ import absolute_import +from builtins import round import quantities as pq From 24c8e3451f8d74cbd61dfaa96a5caa5e56e11995 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Mon, 12 Aug 2019 22:12:46 -0700 Subject: [PATCH 62/86] Greatly simplified implementation of the HP E3631A power supply --- instruments/glassman/glassmanfr.py | 2 +- instruments/hp/hpe3631a.py | 272 +++++++++-------------------- 2 files changed, 88 insertions(+), 186 deletions(-) diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index bddde0751..561144e06 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -100,7 +100,7 @@ def __init__(self, filelike): self._voltage = 0. * pq.volt self._current = 0. * pq.amp - # ENUMS # + # ENUMS ## class Mode(Enum): """ diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index d41f24c9b..5ae09cfc9 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python + #!/usr/bin/env python # -*- coding: utf-8 -*- # # hpe3631a.py: Driver for the HP E3631A Power Supply @@ -44,7 +44,7 @@ PowerSupplyChannel ) from instruments.util_fns import ( - ProxyList, + int_property, unitful_property, bounded_unitful_property, bool_property @@ -53,10 +53,10 @@ # CLASSES ##################################################################### -class HPe3631a(PowerSupply): +class HPe3631a(PowerSupply, PowerSupplyChannel): """ - The HPe3631a is three channels voltage/current supply. + The HPe3631a is a three channels voltage/current supply. - Channel 1 is a positive +6V/5A channel (P6V) - Channel 2 is a positive +25V/1A channel (P25V) - Channel 3 is a negative -25V/1A channel (N25V) @@ -85,15 +85,12 @@ class HPe3631a(PowerSupply): def __init__(self, filelike): super(HPe3631a, self).__init__(filelike) - self.channels = [0, 1, 2] # List of channels - self.idx = 0 # Current channel to be set on the device - self.channelid = 1 # Set the channel self.sendcmd('SYST:REM') # Puts the device in remote operation time.sleep(0.1) - # INNER CLASSES # + # INNER CLASSES ## - class Channel(PowerSupplyChannel): + class Channel(object): """ Class representing a power output channel on the HPe3631a. @@ -101,117 +98,20 @@ class Channel(PowerSupplyChannel): designed to be initialized by the `HPe3631a` class. """ - def __init__(self, hp, idx): - self._hp = hp - self._idx = idx - - def sendcmd(self, cmd): - """ - Function used to send a command to the instrument after - checking that it is set to the right channel. - - :param str cmd: Command that will be sent to the instrument - """ - if self._idx != self._hp.idx: - self._hp.channelid = self._idx+1 - self._hp.sendcmd(cmd) - - def query(self, cmd): - """ - Function used to send a command to the instrument after - checking that it is set to the right channel. - - :param str cmd: Command that will be sent to the instrument - :return: The result from the query - :rtype: `str` - """ - if self._idx != self._hp.idx: - self._hp.channelid = self._idx+1 - return self._hp.query(cmd) - - # PROPERTIES # - - @property - def mode(self): - """ - Gets/sets the mode for the specified channel. - - The constant-voltage/constant-current modes of the power supply - are selected automatically depending on the load (resistance) - connected to the power supply. If the load greater than the set - V/I is connected, a voltage V is applied and the current flowing - is lower than I. If the load is smaller than V/I, the set current - I acts as a current limiter and the voltage is lower than V. - """ - return 'auto' - - @mode.setter - def mode(self, newval): - raise NotImplementedError('The `HPe3631a` sets its mode automatically') - - voltage, voltage_min, voltage_max = bounded_unitful_property( - "SOUR:VOLT", - pq.volt, - min_fmt_str="{}? MIN", - max_fmt_str="{}? MAX", - doc=""" - Gets/sets the output voltage of the source. - - :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.Quantity` - """ - ) - - current, current_min, current_max = bounded_unitful_property( - "SOUR:CURR", - pq.amp, - min_fmt_str="{}? MIN", - max_fmt_str="{}? MAX", - doc=""" - Gets/sets the output current of the source. - - :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.Quantity` - """ - ) - - voltage_sense = unitful_property( - "MEAS:VOLT", - pq.volt, - readonly=True, - doc=""" - Gets the actual output voltage as measured by the sense wires. - - :units: As specified, or assumed to be :math:`\\text{V}` otherwise. - :type: `float` or `~quantities.Quantity` - """ - ) - - current_sense = unitful_property( - "MEAS:CURR", - pq.amp, - readonly=True, - doc=""" - Gets the actual output current as measured by the sense wires. - - :units: As specified, or assumed to be :math:`\\text{A}` otherwise. - :type: `float` or `~quantities.Quantity` - """ - ) - - output = bool_property( - "OUTP", - inst_true="1", - inst_false="0s", - doc=""" - Gets/sets the outputting status of the specified channel. - - This is a toggle setting. ON will turn on the channel output - while OFF will turn it off. - - :type: `bool` - """ - ) + def __init__(self, parent, valid_set): + self._parent = parent + self._valid_set = valid_set + + def __getitem__(self, idx): + # Check that the channel is available. If it is, set the + # channelid of the device and return the device object. + if self._parent.channelid != idx: + self._parent.channelid = idx + time.sleep(0.5) + return self._parent + + def __len__(self): + return len(self._valid_set) # PROPERTIES ## @@ -226,90 +126,92 @@ def channel(self): .. seealso:: `HPe3631a` for example using this property. """ - return ProxyList(self, HPe3631a.Channel, self.channels) - - @property - def channelid(self): - """ - Select the active channel (0=P6V, 1=P25V, 2=N25V) - """ - return self.idx+1 - - @channelid.setter - def channelid(self, newval): - if newval not in [1, 2, 3]: - raise ValueError('Channel {idx} not available, choose 1, 2 or 3'.format(idx=newval)) - self.idx = newval-1 - self.sendcmd('INST:NSEL {idx}'.format(idx=newval)) - time.sleep(0.5) + return self.Channel(self, [1, 2, 3]) @property - def voltage(self): + def mode(self): """ - Gets/sets the voltage for the current channel. + Gets/sets the mode for the specified channel. - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `list` of `~quantities.quantity.Quantity` with units Volt + The constant-voltage/constant-current modes of the power supply + are selected automatically depending on the load (resistance) + connected to the power supply. If the load greater than the set + V/I is connected, a voltage V is applied and the current flowing + is lower than I. If the load is smaller than V/I, the set current + I acts as a current limiter and the voltage is lower than V. """ - return self.channel[self.idx].voltage - - @voltage.setter - def voltage(self, newval): - self.channel[self.idx].voltage = newval + return AttributeError('The `HPe3631a` sets its mode automatically') - @property - def current(self): - """ - Gets/sets the current for the current channel. + channelid = int_property( + "INST:NSEL", + valid_set=[1, 2, 3], + doc=""" + Gets/Sets the active channel ID. - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Amps. - :type: `list` of `~quantities.quantity.Quantity` with units Amp + :type: `HPe3631a.ChannelType` """ - return self.channel[self.idx].current + ) - @current.setter - def current(self, newval): - self.channel[self.idx].current = newval + voltage, voltage_min, voltage_max = bounded_unitful_property( + "SOUR:VOLT", + pq.volt, + min_fmt_str="{}? MIN", + max_fmt_str="{}? MAX", + doc=""" + Gets/sets the output voltage of the source. - @property - def voltage_sense(self): + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~quantities.Quantity` """ - Gets/sets the voltage from the sense wires for the current channel. + ) + + current, current_min, current_max = bounded_unitful_property( + "SOUR:CURR", + pq.amp, + min_fmt_str="{}? MIN", + max_fmt_str="{}? MAX", + doc=""" + Gets/sets the output current of the source. - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `list` of `~quantities.quantity.Quantity` with units Volt + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `float` or `~quantities.Quantity` """ - return self.channel[self.idx].voltage_sense + ) - @voltage_sense.setter - def voltage_sense(self, newval): - self.channel[self.idx].voltage_sense = newval + voltage_sense = unitful_property( + "MEAS:VOLT", + pq.volt, + readonly=True, + doc=""" + Gets the actual output voltage as measured by the sense wires. - @property - def current_sense(self): + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `~quantities.Quantity` """ - Gets/sets the current from the sense wires for the current channel. + ) + + current_sense = unitful_property( + "MEAS:CURR", + pq.amp, + readonly=True, + doc=""" + Gets the actual output current as measured by the sense wires. - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Amps. - :type: `list` of `~quantities.quantity.Quantity` with units Amp + :units: As specified, or assumed to be :math:`\\text{A}` otherwise. + :type: `~quantities.Quantity` """ - return self.channel[self.idx].current_sense + ) - @current_sense.setter - def current_sense(self, newval): - self.channel[self.idx].current_sense = newval + output = bool_property( + "OUTP", + inst_true="1", + inst_false="0", + doc=""" + Gets/sets the outputting status of the specified channel. - @property - def output(self): - """ - Gets/sets the voltage for the current channel. + This is a toggle setting. ON will turn on the channel output + while OFF will turn it off. - :units: As specified (if a `~quantities.Quantity`) or assumed to be - of units Volts. - :type: `list` of `~quantities.quantity.Quantity` with units Volt + :type: `bool` """ - return self.channel[self.idx].output + ) From 64a93906a57d5bbf04c69a75f3ab451e6361107a Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Mon, 12 Aug 2019 22:18:02 -0700 Subject: [PATCH 63/86] Added tests for the HP E3631A power supply --- instruments/tests/test_hp/test_hpe3631a.py | 158 +++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 instruments/tests/test_hp/test_hpe3631a.py diff --git a/instruments/tests/test_hp/test_hpe3631a.py b/instruments/tests/test_hp/test_hpe3631a.py new file mode 100644 index 000000000..c1ec5ac4d --- /dev/null +++ b/instruments/tests/test_hp/test_hpe3631a.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the HP E3631A power supply +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import + +import quantities as pq + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ####################################################################### + +def test_channel(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", + "INST:NSEL?", + "INST:NSEL?", + "INST:NSEL 2", + "INST:NSEL?" + ], + [ + "1", + "1", + "2" + ] + ) as inst: + assert inst.channelid == 1 + assert inst.channel[2] == inst + assert inst.channelid == 2 + +def test_channelid(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", # 0 + "INST:NSEL?", # 1 + "INST:NSEL 2", # 2 + "INST:NSEL?" # 3 + ], + [ + "1", # 1 + "2" # 3 + ] + ) as inst: + assert inst.channelid == 1 + inst.channelid = 2 + assert inst.channelid == 2 + +def test_voltage(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", # 0 + "SOUR:VOLT? MIN", # 1.1 + "SOUR:VOLT? MAX", # 1.2 + "SOUR:VOLT? MIN", # 2.1 + "SOUR:VOLT? MAX", # 2.2 + "SOUR:VOLT 3.000000e+00", # 3 + "SOUR:VOLT?", # 4 + "SOUR:VOLT? MIN", # 5 + "SOUR:VOLT? MIN", # 6.1 + "SOUR:VOLT? MAX" # 6.2 + ], + [ + "0.0", # 1.1 + "6.0", # 1.2 + "0.0", # 2.1 + "6.0", # 2.2 + "3.0", # 4 + "0.0", # 5 + "0.0", # 6.1 + "6.0" # 6.2 + ] + ) as inst: + assert inst.voltage_min == 0.0 * pq.volt + assert inst.voltage_max == 6.0 * pq.volt + inst.voltage = 3.0 * pq.volt + assert inst.voltage == 3.0 * pq.volt + try: + inst.voltage = -1.0 * pq.volt + except ValueError: + pass + try: + inst.voltage = 7.0 * pq.volt + except ValueError: + pass + +def test_current(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", # 0 + "SOUR:CURR? MIN", # 1.1 + "SOUR:CURR? MAX", # 1.2 + "SOUR:CURR? MIN", # 2.1 + "SOUR:CURR? MAX", # 2.2 + "SOUR:CURR 2.000000e+00", # 3 + "SOUR:CURR?", # 4 + "SOUR:CURR? MIN", # 5 + "SOUR:CURR? MIN", # 6.1 + "SOUR:CURR? MAX" # 6.2 + ], + [ + "0.0", # 1.1 + "5.0", # 1.2 + "0.0", # 2.1 + "5.0", # 2.2 + "2.0", # 4 + "0.0", # 5 + "0.0", # 6.1 + "5.0" # 6.2 + ] + ) as inst: + assert inst.current_min == 0.0 * pq.amp + assert inst.current_max == 5.0 * pq.amp + inst.current = 2.0 * pq.amp + assert inst.current == 2.0 * pq.amp + try: + inst.current = -1.0 * pq.amp + except ValueError: + pass + try: + inst.current = 6.0 * pq.amp + except ValueError: + pass + +def test_voltage_sense(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", # 0 + "MEAS:VOLT?" # 1 + ], + [ + "1.234" # 1 + ] + ) as inst: + assert inst.voltage_sense == 1.234 * pq.volt + +def test_current_sense(): + with expected_protocol( + ik.hp.HPe3631a, + [ + "SYST:REM", # 0 + "MEAS:CURR?" # 1 + ], + [ + "1.234" # 1 + ] + ) as inst: + assert inst.current_sense == 1.234 * pq.amp From add3f15526169e99546ea8a64867f0e5da4ad9aa Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 16 Aug 2019 15:21:09 -0700 Subject: [PATCH 64/86] Added generic SCPI commands to the HP E3631A power supply --- instruments/hp/hpe3631a.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index 5ae09cfc9..8addd8d69 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -43,6 +43,7 @@ PowerSupply, PowerSupplyChannel ) +from instruments.generic_scpi import SCPIInstrument from instruments.util_fns import ( int_property, unitful_property, @@ -53,7 +54,7 @@ # CLASSES ##################################################################### -class HPe3631a(PowerSupply, PowerSupplyChannel): +class HPe3631a(PowerSupply, PowerSupplyChannel, SCPIInstrument): """ The HPe3631a is a three channels voltage/current supply. From d690cb3f0119e4a2dac289ccd55c0895ff2dec3e Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 22 Aug 2019 17:56:41 -0700 Subject: [PATCH 65/86] Fix voltage property of HP E3631A to handle negative channel --- instruments/hp/hpe3631a.py | 76 +++++++++++++++++++--- instruments/tests/test_hp/test_hpe3631a.py | 26 ++++---- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index 8addd8d69..0e7206131 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -48,7 +48,9 @@ int_property, unitful_property, bounded_unitful_property, - bool_property + bool_property, + split_unit_str, + assume_units ) # CLASSES ##################################################################### @@ -153,18 +155,76 @@ def mode(self): """ ) - voltage, voltage_min, voltage_max = bounded_unitful_property( - "SOUR:VOLT", - pq.volt, - min_fmt_str="{}? MIN", - max_fmt_str="{}? MAX", - doc=""" + @property + def voltage(self): + ''' Gets/sets the output voltage of the source. :units: As specified, or assumed to be :math:`\\text{V}` otherwise. :type: `float` or `~quantities.Quantity` + ''' + raw = self.query('SOUR:VOLT?') + return pq.Quantity(*split_unit_str(raw, pq.volt)).rescale(pq.volt) + + @voltage.setter + def voltage(self, newval): + ''' + Gets/sets the output voltage of the source. + + :units: As specified, or assumed to be :math:`\\text{V}` otherwise. + :type: `float` or `~quantities.Quantity` + ''' + min_value, max_value = self.voltage_range + if newval < min_value: + raise ValueError("Voltage quantity is too low. Got {}, minimum " + "value is {}".format(newval, min_value)) + + if newval > max_value: + raise ValueError("Voltage quantity is too high. Got {}, maximum " + "value is {}".format(newval, max_value)) + + # Rescale to the correct unit before printing. This will also + # catch bad units. + strval = '{:e}'.format(assume_units(newval, pq.volt).rescale(pq.volt).item()) + self.sendcmd('SOUR:VOLT {}'.format(strval)) + + @property + def voltage_min(self): """ - ) + Gets the minimum voltage for the current channel. + + :units: :math:`\\text{V}`. + :type: `~quantities.Quantity` + """ + return self.voltage_range[0] + + @property + def voltage_max(self): + """ + Gets the maximum voltage for the current channel. + + :units: :math:`\\text{V}`. + :type: `~quantities.Quantity` + """ + return self.voltage_range[1] + + @property + def voltage_range(self): + """ + Gets the voltage range for the current channel. + + The MAX function SCPI command is designed in such a way + on this device that it always returns the largest absolute value. + There is no need to query MIN, as it is always 0., but one has to + order the values as MAX can be negative. + + :units: :math:`\\text{V}`. + :type: array of `~quantities.Quantity` + """ + value = pq.Quantity(*split_unit_str(self.query('SOUR:VOLT? MAX'), pq.volt)) + if value < 0.: + return (value, 0.) + return (0., value) current, current_min, current_max = bounded_unitful_property( "SOUR:CURR", diff --git a/instruments/tests/test_hp/test_hpe3631a.py b/instruments/tests/test_hp/test_hpe3631a.py index c1ec5ac4d..b640a926b 100644 --- a/instruments/tests/test_hp/test_hpe3631a.py +++ b/instruments/tests/test_hp/test_hpe3631a.py @@ -58,25 +58,21 @@ def test_voltage(): ik.hp.HPe3631a, [ "SYST:REM", # 0 - "SOUR:VOLT? MIN", # 1.1 - "SOUR:VOLT? MAX", # 1.2 - "SOUR:VOLT? MIN", # 2.1 - "SOUR:VOLT? MAX", # 2.2 - "SOUR:VOLT 3.000000e+00", # 3 + "SOUR:VOLT? MAX", # 1 + "SOUR:VOLT? MAX", # 2 + "SOUR:VOLT? MAX", # 3.1 + "SOUR:VOLT 3.000000e+00", # 3.2 "SOUR:VOLT?", # 4 - "SOUR:VOLT? MIN", # 5 - "SOUR:VOLT? MIN", # 6.1 - "SOUR:VOLT? MAX" # 6.2 + "SOUR:VOLT? MAX", # 5 + "SOUR:VOLT? MAX" # 6 ], [ - "0.0", # 1.1 - "6.0", # 1.2 - "0.0", # 2.1 - "6.0", # 2.2 + "6.0", # 1 + "6.0", # 2 + "6.0", # 3.1 "3.0", # 4 - "0.0", # 5 - "0.0", # 6.1 - "6.0" # 6.2 + "6.0", # 5 + "6.0" # 6 ] ) as inst: assert inst.voltage_min == 0.0 * pq.volt From f25b7f3ca64a1240d649a267017a6c806c4b11e0 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 26 Nov 2019 15:18:28 -0800 Subject: [PATCH 66/86] Added initial support for Keithley 6517b electrometer --- instruments/keithley/__init__.py | 1 + instruments/keithley/keithley6517b.py | 243 ++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 instruments/keithley/keithley6517b.py diff --git a/instruments/keithley/__init__.py b/instruments/keithley/__init__.py index 36b8eaf48..05e6607c2 100644 --- a/instruments/keithley/__init__.py +++ b/instruments/keithley/__init__.py @@ -12,3 +12,4 @@ from .keithley2182 import Keithley2182 from .keithley6220 import Keithley6220 from .keithley6514 import Keithley6514 +from .keithley6517b import Keithley6517b diff --git a/instruments/keithley/keithley6517b.py b/instruments/keithley/keithley6517b.py new file mode 100644 index 000000000..0a1f1d892 --- /dev/null +++ b/instruments/keithley/keithley6517b.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Provides support for the Keithley 6517b electrometer +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division +from builtins import map + +from enum import Enum + +import quantities as pq + +from instruments.abstract_instruments import Electrometer +from instruments.generic_scpi import SCPIInstrument +from instruments.util_fns import bool_property, enum_property + +# CLASSES ##################################################################### + + +class Keithley6517b(SCPIInstrument, Electrometer): + + """ + The `Keithley 6517b` is an electrometer capable of doing sensitive current, + charge, voltage and resistance measurements. + + Example usage: + + >>> import instruments as ik + >>> import quantities as pq + >>> dmm = ik.keithley.Keithley6517b.open_serial('/dev/ttyUSB0', baud=115200) + >>> dmm.measure(dmm.Mode.current) + array(0.123) * pq.amp + """ + + # ENUMS ## + + class Mode(Enum): + """ + Enum containing valid measurement modes for the Keithley 6517b + """ + voltage_dc = 'VOLT:DC' + current_dc = 'CURR:DC' + resistance = 'RES' + charge = 'CHAR' + + class TriggerMode(Enum): + """ + Enum containing valid trigger modes for the Keithley 6517b + """ + immediate = 'IMM' + tlink = 'TLINK' + + class ArmSource(Enum): + """ + Enum containing valid trigger arming sources for the Keithley 6517b + """ + immediate = 'IMM' + timer = 'TIM' + bus = 'BUS' + tlink = 'TLIN' + stest = 'STES' + pstest = 'PST' + nstest = 'NST' + manual = 'MAN' + + class ValidRange(Enum): + """ + Enum containing valid measurement ranges for the Keithley 6517b + """ + voltage_dc = (2, 20, 200) + current_dc = (20e-12, 200e-12, 2e-9, 20e-9, + 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) + resistance = (2e3, 20e3, 200e3, 2e6, 20e6, 200e6, 2e9, 20e9, 200e9) + charge = (20e-9, 200e-9, 2e-6, 20e-6) + + # PROPERTIES ## + + mode = enum_property( + 'FUNCTION', + Mode, + input_decoration=lambda val: val[1:-1], + # output_decoration=lambda val: '"{}"'.format(val), + set_fmt='{} "{}"', + doc=""" + Gets/sets the measurement mode of the Keithley 6517b. + """ + ) + + trigger_mode = enum_property( + 'TRIGGER:SOURCE', + TriggerMode, + doc=""" + Gets/sets the trigger mode of the Keithley 6517b. + """ + ) + + arm_source = enum_property( + 'ARM:SOURCE', + ArmSource, + doc=""" + Gets/sets the arm source of the Keithley 6517b. + """ + ) + + zero_check = bool_property( + 'SYST:ZCH', + inst_true='ON', + inst_false='OFF', + doc=""" + Gets/sets the zero checking status of the Keithley 6517b. + """ + ) + + zero_correct = bool_property( + 'SYST:ZCOR', + inst_true='ON', + inst_false='OFF', + doc=""" + Gets/sets the zero correcting status of the Keithley 6517b. + """ + ) + + @property + def unit(self): + return UNITS[self.mode] + + @property + def auto_range(self): + """ + Gets/sets the auto range setting + + :type: `bool` + """ + # pylint: disable=no-member + out = self.query('{}:RANGE:AUTO?'.format(self.mode.value)) + return True if out == '1' else False + + @auto_range.setter + def auto_range(self, newval): + # pylint: disable=no-member + self.sendcmd('{}:RANGE:AUTO {}'.format( + self.mode.value, '1' if newval else '0')) + + @property + def input_range(self): + """ + Gets/sets the upper limit of the current range. + + :type: `~quantities.Quantity` + """ + # pylint: disable=no-member + mode = self.mode + out = self.query('{}:RANGE:UPPER?'.format(mode.value)) + return float(out) * UNITS[mode] + + @input_range.setter + def input_range(self, newval): + # pylint: disable=no-member + mode = self.mode + val = newval.rescale(UNITS[mode]).item() + if val not in self._valid_range(mode).value: + raise ValueError( + 'Unexpected range limit for currently selected mode.') + self.sendcmd('{}:RANGE:UPPER {:e}'.format(mode.value, val)) + + # METHODS ## + + def auto_config(self, mode): + """ + This command causes the device to do the following: + - Switch to the specified mode + - Reset all related controls to default values + - Set trigger and arm to the 'immediate' setting + - Set arm and trigger counts to 1 + - Set trigger delays to 0 + - Place unit in idle state + - Disable all math calculations + - Disable buffer operation + - Enable autozero + """ + self.sendcmd('CONF:{}'.format(mode.value)) + + def fetch(self): + """ + Request the latest post-processed readings using the current mode. + (So does not issue a trigger) + Returns a tuple of the form (reading, timestamp, trigger_count) + """ + return self._parse_measurement(self.query('FETC?')) + + def read_measurements(self): + """ + Trigger and acquire readings using the current mode. + Returns a tuple of the form (reading, timestamp, trigger_count) + """ + return self._parse_measurement(self.query('READ?')) + + def measure(self, mode=None): + """ + Trigger and acquire readings using the requested mode. + Returns the measurement reading only. + """ + # Check the current mode, change if necessary + if mode is not None: + if mode != self.mode: + self.auto_config(mode) + + return self.read_measurements()[0] + + # PRIVATE METHODS ## + + def _valid_range(self, mode): + if mode == self.Mode.voltage: + return self.ValidRange.voltage + elif mode == self.Mode.current: + return self.ValidRange.current + elif mode == self.Mode.resistance: + return self.ValidRange.resistance + elif mode == self.Mode.charge: + return self.ValidRange.charge + else: + raise ValueError('Invalid mode.') + + def _parse_measurement(self, ascii): + # Split the string in three comma-separated parts (value, time, number of triggers) + vals = ascii.split(',') + reading = float(vals[0].split('N')[0]) * self.unit + timestamp = float(vals[1].split('s')[0]) * pq.second + trigger_count = int(vals[2][:-5].split('R')[0]) + return reading, timestamp, trigger_count + +# UNITS ####################################################################### + +UNITS = { + Keithley6517b.Mode.voltage_dc: pq.volt, + Keithley6517b.Mode.current_dc: pq.amp, + Keithley6517b.Mode.resistance: pq.ohm, + Keithley6517b.Mode.charge: pq.coulomb +} From 998d312477814bcc2869b22f8bc5b2bc92207d7f Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 26 Nov 2019 16:55:50 -0800 Subject: [PATCH 67/86] Style fixes for Keithley 6517b --- instruments/keithley/keithley6517b.py | 54 ++++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/instruments/keithley/keithley6517b.py b/instruments/keithley/keithley6517b.py index 0a1f1d892..1269789dd 100644 --- a/instruments/keithley/keithley6517b.py +++ b/instruments/keithley/keithley6517b.py @@ -1,5 +1,26 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# +# keithley6517b.py: Driver for the Keithley 6517b Electrometer +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# 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 . +# """ Provides support for the Keithley 6517b electrometer """ @@ -8,7 +29,6 @@ from __future__ import absolute_import from __future__ import division -from builtins import map from enum import Enum @@ -36,6 +56,14 @@ class Keithley6517b(SCPIInstrument, Electrometer): array(0.123) * pq.amp """ + def __init__(self, filelike): + """ + Auto configs the instrument in to the current readout mode + (sets the trigger and communication types rights) + """ + super(Keithley6517b, self).__init__(filelike) + self.auto_config(self.mode) + # ENUMS ## class Mode(Enum): @@ -73,9 +101,9 @@ class ValidRange(Enum): """ voltage_dc = (2, 20, 200) current_dc = (20e-12, 200e-12, 2e-9, 20e-9, - 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) - resistance = (2e3, 20e3, 200e3, 2e6, 20e6, 200e6, 2e9, 20e9, 200e9) - charge = (20e-9, 200e-9, 2e-6, 20e-6) + 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) + resistance = (2e6, 20e6, 200e6, 2e9, 20e9, 200e9, 2e12, 20e12, 200e12) + charge = (2e-9, 20e-9, 200e-9, 2e-6) # PROPERTIES ## @@ -137,7 +165,7 @@ def auto_range(self): """ # pylint: disable=no-member out = self.query('{}:RANGE:AUTO?'.format(self.mode.value)) - return True if out == '1' else False + return out == '1' @auto_range.setter def auto_range(self, newval): @@ -214,16 +242,16 @@ def measure(self, mode=None): # PRIVATE METHODS ## def _valid_range(self, mode): - if mode == self.Mode.voltage: - return self.ValidRange.voltage - elif mode == self.Mode.current: - return self.ValidRange.current - elif mode == self.Mode.resistance: + if mode == self.Mode.voltage_dc: + return self.ValidRange.voltage_dc + if mode == self.Mode.current_dc: + return self.ValidRange.current_dc + if mode == self.Mode.resistance: return self.ValidRange.resistance - elif mode == self.Mode.charge: + if mode == self.Mode.charge: return self.ValidRange.charge - else: - raise ValueError('Invalid mode.') + + raise ValueError('Invalid mode.') def _parse_measurement(self, ascii): # Split the string in three comma-separated parts (value, time, number of triggers) From 2a0a39afe6793e1ea875f532031bb977483a4b9f Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 26 Nov 2019 16:56:45 -0800 Subject: [PATCH 68/86] Statement ordering fix in GlassmanFR driver --- instruments/glassman/glassmanfr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index 561144e06..526a64c7d 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -273,8 +273,8 @@ def device_timeout(self): def device_timeout(self, newval): if not isinstance(newval, bool): raise TypeError('Device timeout mode must be a boolean.') - self._device_timeout = newval self.query('C{}'.format(int(not newval))) # Device acknowledges + self._device_timeout = newval # METHODS ## From 3d904e571817803f3df9eb9e8aaec2029c1af478 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 26 Nov 2019 17:04:07 -0800 Subject: [PATCH 69/86] Attempt at fixing Travis issue --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index b4082436b..a1e90fe77 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ +attrs==19.2.0 mock pytest pytest-mock From 870b62db15b00e4fb6b263f096d5b8c250dca48c Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 26 Nov 2019 17:13:33 -0800 Subject: [PATCH 70/86] Attempt at fixing Travis issue 2 --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index a1e90fe77..d0c5c10ae 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ attrs==19.2.0 mock -pytest +pytest==5.1.1 pytest-mock hypothesis pylint==1.7.1 From 30e9ecd3f5e24a8f3407d0b888c2a4e1c867781b Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 26 Nov 2019 17:28:47 -0800 Subject: [PATCH 71/86] Attempt at fixing Travis issue 3 --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index d0c5c10ae..dd98603a7 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ attrs==19.2.0 mock -pytest==5.1.1 +pytest==4.6.6 pytest-mock hypothesis pylint==1.7.1 From bcfc8d5fd49f1c5c4c0f28c7b0fe19a2a86c6e16 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 3 Dec 2019 09:57:38 -0800 Subject: [PATCH 72/86] Remove device-side timeout on GlassmanFR by default --- dev-requirements.txt | 4 ++-- instruments/glassman/glassmanfr.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index dd98603a7..766be9404 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,6 @@ -attrs==19.2.0 +attrs>=19.2.0 mock -pytest==4.6.6 +pytest>=4.6.6 pytest-mock hypothesis pylint==1.7.1 diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index 526a64c7d..8392a24b6 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -96,7 +96,7 @@ def __init__(self, filelike): self.voltage_max = 50.0 * pq.kilovolt self.current_max = 6.0 * pq.milliamp self.polarity = +1 - self._device_timeout = True + self._device_timeout = False self._voltage = 0. * pq.volt self._current = 0. * pq.amp From c813956c75ad22706faa6560d8dc922eb13cb02e Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Sat, 22 Feb 2020 14:54:32 -0800 Subject: [PATCH 73/86] Fixed voltage/current setters of Glassman FR power supply --- instruments/glassman/glassmanfr.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index 8392a24b6..6996ca176 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -96,9 +96,10 @@ def __init__(self, filelike): self.voltage_max = 50.0 * pq.kilovolt self.current_max = 6.0 * pq.milliamp self.polarity = +1 - self._device_timeout = False self._voltage = 0. * pq.volt self._current = 0. * pq.amp + self._device_timeout = False + self.device_timeout = False # ENUMS ## @@ -166,7 +167,9 @@ def voltage(self): @voltage.setter def voltage(self, newval): - self.set_status(voltage=assume_units(newval, pq.volt)) + voltage = assume_units(newval, pq.volt) + self.set_status(voltage=voltage) + self._voltage = voltage @property def current(self): @@ -180,7 +183,9 @@ def current(self): @current.setter def current(self, newval): - self.set_status(current=assume_units(newval, pq.amp)) + current = assume_units(newval, pq.amp) + self.set_status(current=current) + self._current = current @property def voltage_sense(self): From 7cbb163b46e1fa61771eee9be53a60aa64536faf Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 26 Feb 2020 15:57:14 -0800 Subject: [PATCH 74/86] Added dead time to standalone sendcmd of HPE3631A --- instruments/hp/hpe3631a.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/instruments/hp/hpe3631a.py b/instruments/hp/hpe3631a.py index 0e7206131..ead760bec 100644 --- a/instruments/hp/hpe3631a.py +++ b/instruments/hp/hpe3631a.py @@ -88,8 +88,8 @@ class HPe3631a(PowerSupply, PowerSupplyChannel, SCPIInstrument): def __init__(self, filelike): super(HPe3631a, self).__init__(filelike) - self.sendcmd('SYST:REM') # Puts the device in remote operation - time.sleep(0.1) + self._device_timeout = 0.1 # [s], device timeout at each set command + self.sendcmd('SYST:REM') # Puts the device in remote operation # INNER CLASSES ## @@ -276,3 +276,27 @@ def voltage_range(self): :type: `bool` """ ) + + # METHODS ## + + def sendcmd(self, cmd): + """ + Overrides the default `setcmd` by adding some dead time after + each set, to accomodate the device timeout. + + :param str cmd: The command message to send to the instrument + """ + self._file.sendcmd(cmd) + time.sleep(self._device_timeout) + + def query(self, cmd, size=-1): + """ + Overrides the default `query` by directly using the underlying + file system setcmd, to bypass the deadtime for queries.. + + :param str cmd: The query message to send to the instrument + :return: The instrument response to the query + :rtype: `str` + """ + self._file.sendcmd(cmd) + return self._file.read(size) From b91d08a38a0cb995e3f29d9ec62bbe1b2c39a9f1 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Tue, 8 Sep 2020 20:45:29 -0700 Subject: [PATCH 75/86] Added support for the Keithley 6485 picoammeter --- instruments/keithley/__init__.py | 1 + instruments/keithley/keithley6485.py | 205 ++++++++++++++++++++++++++ instruments/keithley/keithley6517b.py | 8 +- 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 instruments/keithley/keithley6485.py diff --git a/instruments/keithley/__init__.py b/instruments/keithley/__init__.py index 05e6607c2..b0daf58e5 100644 --- a/instruments/keithley/__init__.py +++ b/instruments/keithley/__init__.py @@ -11,5 +11,6 @@ from .keithley580 import Keithley580 from .keithley2182 import Keithley2182 from .keithley6220 import Keithley6220 +from .keithley6485 import Keithley6485 from .keithley6514 import Keithley6514 from .keithley6517b import Keithley6517b diff --git a/instruments/keithley/keithley6485.py b/instruments/keithley/keithley6485.py new file mode 100644 index 000000000..48000ac17 --- /dev/null +++ b/instruments/keithley/keithley6485.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# keithley6485.py: Driver for the Keithley 6485 picoammeter +# +# © 2019 Francois Drielsma (francois.drielsma@gmail.com). +# +# 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 . +# +""" +Driver for the Keithley 6485 picoammeter. + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com). + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division + +from enum import Enum + +import quantities as pq + +from instruments.generic_scpi import SCPIInstrument +from instruments.util_fns import bool_property, enum_property + +# CLASSES ##################################################################### + + +class Keithley6485(SCPIInstrument): + + """ + The `Keithley 6485` is an electrometer capable of doing sensitive current, + charge, voltage and resistance measurements. + + WARNING: Must set the terminator to `LF` on the define for this to work. + + Example usage: + + >>> import instruments as ik + >>> import quantities as pq + >>> dmm = ik.keithley.Keithley6485.open_serial('/dev/ttyUSB0', baud=9600) + >>> dmm.measure() + array(0.000123) * pq.amp + """ + + def __init__(self, filelike): + """ + Resets device to be read. + """ + super(Keithley6485, self).__init__(filelike) + + # ENUMS ## + + class TriggerMode(Enum): + """ + Enum containing valid trigger modes for the Keithley 6485 + """ + immediate = 'IMM' + tlink = 'TLINK' + + class ArmSource(Enum): + """ + Enum containing valid trigger arming sources for the Keithley 6485 + """ + immediate = 'IMM' + timer = 'TIM' + bus = 'BUS' + tlink = 'TLIN' + stest = 'STES' + pstest = 'PST' + nstest = 'NST' + manual = 'MAN' + + # PROPERTIES ## + + trigger_mode = enum_property( + 'TRIGGER:SOURCE', + TriggerMode, + doc=""" + Gets/sets the trigger mode of the Keithley 6485. + """ + ) + + arm_source = enum_property( + 'ARM:SOURCE', + ArmSource, + doc=""" + Gets/sets the arm source of the Keithley 6485. + """ + ) + + zero_check = bool_property( + 'SYST:ZCH', + inst_true='ON', + inst_false='OFF', + doc=""" + Gets/sets the zero checking status of the Keithley 6485. + """ + ) + + zero_correct = bool_property( + 'SYST:ZCOR', + inst_true='ON', + inst_false='OFF', + doc=""" + Gets/sets the zero correcting status of the Keithley 6485. + """ + ) + + @property + def unit(self): + return pq.amp + + @property + def auto_range(self): + """ + Gets/sets the auto range setting + + :type: `bool` + """ + # pylint: disable=no-member + out = self.query('{CURR:RANGE:AUTO?') + return out == '1' + + @auto_range.setter + def auto_range(self, newval): + # pylint: disable=no-member + self.sendcmd('CUUR:RANGE:AUTO {}'.format('1' if newval else '0')) + + @property + def input_range(self): + """ + Gets/sets the upper limit of the current range. + + :type: `~quantities.Quantity` + """ + # pylint: disable=no-member + out = self.query('CUUR:RANGE:UPPER?') + return float(out) * pq.amp + + @input_range.setter + def input_range(self, newval): + # pylint: disable=no-member + val = newval.rescale(pq.amp).item() + if val not in self._valid_range().value: + raise ValueError( + 'Unexpected range limit for currently selected mode.') + self.sendcmd('{}:RANGE:UPPER {:e}'.format(mode.value, val)) + + # METHODS ## + + def fetch(self): + """ + Request the latest post-processed readings using the current mode. + (So does not issue a trigger) + Returns a tuple of the form (reading, timestamp, trigger_count) + """ + return self._parse_measurement(self.query('FETC?')) + + def read_measurements(self): + """ + Trigger and acquire readings using the current mode. + Returns a tuple of the form (reading, timestamp, trigger_count) + """ + return self._parse_measurement(self.query('READ?')) + + def measure(self, mode=None): + """ + Trigger and acquire readings. + Returns the measurement reading only. + """ + return self.read_measurements()[0] + + # PRIVATE METHODS ## + + def _valid_range(self): + return (2e-9, 20e-9, 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) + + def _parse_measurement(self, ascii): + # Split the string in three comma-separated parts (value, time, number of triggers) + vals = ascii.split(',') + reading = float(vals[0][:-1]) * self.unit + timestamp = float(vals[1]) * pq.second + trigger_count = int(float(vals[2])) + return reading, timestamp, trigger_count diff --git a/instruments/keithley/keithley6517b.py b/instruments/keithley/keithley6517b.py index 1269789dd..3d069c5e4 100644 --- a/instruments/keithley/keithley6517b.py +++ b/instruments/keithley/keithley6517b.py @@ -22,7 +22,13 @@ # along with this program. If not, see . # """ -Provides support for the Keithley 6517b electrometer +Provides support for the Keithley 6517b electrometer. + +Originally contributed and copyright held by Francois Drielsma +(francois.drielsma@gmail.com). + +An unrestricted license has been provided to the maintainers of the Instrument +Kit project. """ # IMPORTS ##################################################################### From df26fc01e072d142b49f0b319f853de1dbadaae2 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 10 Sep 2020 16:34:54 -0700 Subject: [PATCH 76/86] Set Keithley 6485 in the right mode in __init__ --- instruments/keithley/keithley6485.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/instruments/keithley/keithley6485.py b/instruments/keithley/keithley6485.py index 48000ac17..f19e012f5 100644 --- a/instruments/keithley/keithley6485.py +++ b/instruments/keithley/keithley6485.py @@ -65,9 +65,11 @@ class Keithley6485(SCPIInstrument): def __init__(self, filelike): """ - Resets device to be read. + Resets device to be read, disables zero check. """ super(Keithley6485, self).__init__(filelike) + self.reset() + self.zero_check = False # ENUMS ## From 2ca8792e2094a4e60b3c25f6d1b47befe0f22c17 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Mon, 26 Apr 2021 10:43:08 -0700 Subject: [PATCH 77/86] Fixed typo --- instruments/glassman/glassmanfr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instruments/glassman/glassmanfr.py b/instruments/glassman/glassmanfr.py index 6996ca176..e90304826 100644 --- a/instruments/glassman/glassmanfr.py +++ b/instruments/glassman/glassmanfr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# hpe3631a.py: Driver for the Glassman FR Series Power Supplies +# glassmanfr.py: Driver for the Glassman FR Series Power Supplies # # © 2019 Francois Drielsma (francois.drielsma@gmail.com). # From 8ad3cdf5eb9061408d3e9700570d582bb6b56149 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 1 Dec 2021 17:36:19 -0800 Subject: [PATCH 78/86] Fixed Glassman FR tests --- .../tests/test_glassman/test_glassmanfr.py | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/instruments/tests/test_glassman/test_glassmanfr.py b/instruments/tests/test_glassman/test_glassmanfr.py index 1f99045d7..ba23a3f6b 100644 --- a/instruments/tests/test_glassman/test_glassmanfr.py +++ b/instruments/tests/test_glassman/test_glassmanfr.py @@ -28,8 +28,12 @@ def set_defaults(inst): def test_channel(): with expected_protocol( ik.glassman.GlassmanFR, - [], - [], + [ + "\x01C174" + ], + [ + "A" + ], '\r' ) as inst: assert len(inst.channel) == 1 @@ -39,10 +43,12 @@ def test_voltage(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01Q51", "\x01S3330000000001CD" ], [ + "A", "R00000000000040", "A" ], @@ -56,10 +62,12 @@ def test_current(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01Q51", "\x01S0003330000001CD" ], [ + "A", "R00000000000040", "A" ], @@ -73,9 +81,11 @@ def test_voltage_sense(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01Q51" ], [ + "A", "R10A00000010053" ], '\r' @@ -87,9 +97,11 @@ def test_current_sense(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01Q51" ], [ + "A", "R0001550001004C" ], '\r' @@ -101,10 +113,12 @@ def test_mode(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01Q51", "\x01Q51" ], [ + "A", "R00000000000040", "R00000000010041" ], @@ -117,12 +131,14 @@ def test_output(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01S0000000000001C4", "\x01Q51", "\x01S0000000000002C5", "\x01Q51" ], [ + "A", "A", "R00000000000040", "A", @@ -139,9 +155,11 @@ def test_version(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01V56" ], [ + "A", "B1465" ], '\r' @@ -152,10 +170,12 @@ def test_device_timeout(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01C073", "\x01C174" ], [ + "A", "A", "A" ], @@ -170,9 +190,12 @@ def test_sendcmd(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01123ABC5C" ], - [], + [ + "A" + ], '\r' ) as inst: inst.sendcmd('123ABC') @@ -181,9 +204,11 @@ def test_query(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01Q123ABCAD" ], [ + "A", "R123ABC5C" ], '\r' @@ -194,9 +219,11 @@ def test_reset(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01S0000000000004C7" ], [ + "A", "A" ], '\r' @@ -207,10 +234,12 @@ def test_set_status(): with expected_protocol( ik.glassman.GlassmanFR, [ + "\x01C174", "\x01S3333330000002D7", "\x01Q51" ], [ + "A", "A", "R00000000040044" ], From 411799e52c56320622db7bc6cc8dc6c1fa8c6154 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Wed, 1 Dec 2021 22:19:02 -0800 Subject: [PATCH 79/86] Fixed Keithley 6517b tests --- .../tests/test_keithley/test_keithley6517b.py | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 instruments/tests/test_keithley/test_keithley6517b.py diff --git a/instruments/tests/test_keithley/test_keithley6517b.py b/instruments/tests/test_keithley/test_keithley6517b.py new file mode 100644 index 000000000..9495931dd --- /dev/null +++ b/instruments/tests/test_keithley/test_keithley6517b.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Unit tests for the Keithley 6517b electrometer +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import + +import quantities as pq +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ####################################################################### + +# pylint: disable=protected-access + + +def test_parse_measurement(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "FUNCTION?", + ], + [ + "\"VOLT:DC\"", + "\"VOLT:DC\"" + ] + ) as inst: + reading, timestamp, status = inst._parse_measurement("1.0N,1234s,5678R00000") + assert reading == 1.0 * pq.volt + assert timestamp == 1234 + assert status == 5678 + + +def test_mode(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "FUNCTION?", + "FUNCTION \"VOLT:DC\"" + ], + [ + "\"VOLT:DC\"", + "\"VOLT:DC\"" + ] + ) as inst: + assert inst.mode == inst.Mode.voltage_dc + inst.mode = inst.Mode.voltage_dc + + +def test_trigger_source(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "TRIGGER:SOURCE?", + "TRIGGER:SOURCE IMM" + ], + [ + "\"VOLT:DC\"", + "TLINK" + ] + ) as inst: + assert inst.trigger_mode == inst.TriggerMode.tlink + inst.trigger_mode = inst.TriggerMode.immediate + + +def test_arm_source(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "ARM:SOURCE?", + "ARM:SOURCE IMM" + ], + [ + "\"VOLT:DC\"", + "TIM" + ] + ) as inst: + assert inst.arm_source == inst.ArmSource.timer + inst.arm_source = inst.ArmSource.immediate + + +def test_zero_check(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "SYST:ZCH?", + "SYST:ZCH ON" + ], + [ + "\"VOLT:DC\"", + "OFF" + ] + ) as inst: + assert inst.zero_check is False + inst.zero_check = True + + +def test_zero_correct(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "SYST:ZCOR?", + "SYST:ZCOR ON" + ], + [ + "\"VOLT:DC\"", + "OFF" + ] + ) as inst: + assert inst.zero_correct is False + inst.zero_correct = True + + +def test_unit(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "FUNCTION?", + ], + [ + "\"VOLT:DC\"", + "\"VOLT:DC\"" + ] + ) as inst: + assert inst.unit == pq.volt + + +def test_auto_range(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "FUNCTION?", + "VOLT:DC:RANGE:AUTO?", + "FUNCTION?", + "VOLT:DC:RANGE:AUTO 1" + ], + [ + "\"VOLT:DC\"", + "\"VOLT:DC\"", + "0", + "\"VOLT:DC\"" + ] + ) as inst: + assert inst.auto_range is False + inst.auto_range = True + + +def test_input_range(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "FUNCTION?", + "VOLT:DC:RANGE:UPPER?", + "FUNCTION?", + "VOLT:DC:RANGE:UPPER {:e}".format(20) + ], + [ + "\"VOLT:DC\"", + "\"VOLT:DC\"", + "10", + "\"VOLT:DC\"" + ] + ) as inst: + assert inst.input_range == 10 * pq.volt + inst.input_range = 20 * pq.volt + + +def test_input_range_invalid(): + with pytest.raises(ValueError): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + ], + [ + "\"VOLT:DC\"" + ] + ) as inst: + inst.input_range = 10 * pq.volt + + +def test_auto_config(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "CONF:VOLT:DC" + ], + [ + "\"VOLT:DC\"", + "\"VOLT:DC\"" + ] + ) as inst: + inst.auto_config(inst.Mode.voltage_dc) + + +def test_fetch(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "FETC?", + "FUNCTION?", + ], + [ + "\"VOLT:DC\"", + "1.0N,1234s,5678R00000", + "\"VOLT:DC\"" + ] + ) as inst: + reading, timestamp, trigger_count = inst.fetch() + assert reading == 1.0 * pq.volt + assert timestamp == 1234 + assert trigger_count == 5678 + + +def test_read(): + with expected_protocol( + ik.keithley.Keithley6517b, + [ + "FUNCTION?", + "CONF:VOLT:DC", + "READ?", + "FUNCTION?" + ], + [ + "\"VOLT:DC\"", + "1.0N,1234s,5678R00000", + "\"VOLT:DC\"" + ] + ) as inst: + reading, timestamp, trigger_count = inst.read_measurements() + assert reading == 1.0 * pq.volt + assert timestamp == 1234 + assert trigger_count == 5678 From fd5bf33374a1196831788805f7abc7db65c9cff6 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 2 Dec 2021 17:22:23 -0800 Subject: [PATCH 80/86] Fixed bugs in Keithley 6485, added tests --- instruments/keithley/keithley6485.py | 48 +---- .../tests/test_keithley/test_keithley6485.py | 164 ++++++++++++++++++ .../tests/test_keithley/test_keithley6517b.py | 4 +- 3 files changed, 171 insertions(+), 45 deletions(-) create mode 100644 instruments/tests/test_keithley/test_keithley6485.py diff --git a/instruments/keithley/keithley6485.py b/instruments/keithley/keithley6485.py index f19e012f5..6905c4502 100644 --- a/instruments/keithley/keithley6485.py +++ b/instruments/keithley/keithley6485.py @@ -71,46 +71,8 @@ def __init__(self, filelike): self.reset() self.zero_check = False - # ENUMS ## - - class TriggerMode(Enum): - """ - Enum containing valid trigger modes for the Keithley 6485 - """ - immediate = 'IMM' - tlink = 'TLINK' - - class ArmSource(Enum): - """ - Enum containing valid trigger arming sources for the Keithley 6485 - """ - immediate = 'IMM' - timer = 'TIM' - bus = 'BUS' - tlink = 'TLIN' - stest = 'STES' - pstest = 'PST' - nstest = 'NST' - manual = 'MAN' - # PROPERTIES ## - trigger_mode = enum_property( - 'TRIGGER:SOURCE', - TriggerMode, - doc=""" - Gets/sets the trigger mode of the Keithley 6485. - """ - ) - - arm_source = enum_property( - 'ARM:SOURCE', - ArmSource, - doc=""" - Gets/sets the arm source of the Keithley 6485. - """ - ) - zero_check = bool_property( 'SYST:ZCH', inst_true='ON', @@ -141,13 +103,13 @@ def auto_range(self): :type: `bool` """ # pylint: disable=no-member - out = self.query('{CURR:RANGE:AUTO?') + out = self.query('RANG:AUTO?') return out == '1' @auto_range.setter def auto_range(self, newval): # pylint: disable=no-member - self.sendcmd('CUUR:RANGE:AUTO {}'.format('1' if newval else '0')) + self.sendcmd('RANG:AUTO {}'.format('1' if newval else '0')) @property def input_range(self): @@ -157,17 +119,17 @@ def input_range(self): :type: `~quantities.Quantity` """ # pylint: disable=no-member - out = self.query('CUUR:RANGE:UPPER?') + out = self.query('RANG?') return float(out) * pq.amp @input_range.setter def input_range(self, newval): # pylint: disable=no-member val = newval.rescale(pq.amp).item() - if val not in self._valid_range().value: + if val not in self._valid_range(): raise ValueError( 'Unexpected range limit for currently selected mode.') - self.sendcmd('{}:RANGE:UPPER {:e}'.format(mode.value, val)) + self.sendcmd('RANG {:e}'.format(val)) # METHODS ## diff --git a/instruments/tests/test_keithley/test_keithley6485.py b/instruments/tests/test_keithley/test_keithley6485.py new file mode 100644 index 000000000..7a4daf5f3 --- /dev/null +++ b/instruments/tests/test_keithley/test_keithley6485.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Unit tests for the Keithley 6485 electrometer +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import + +import quantities as pq +import pytest + +import instruments as ik +from instruments.tests import expected_protocol + +# TESTS ####################################################################### + +# pylint: disable=protected-access + + +def test_parse_measurement(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF" + ], + [] + ) as inst: + reading, timestamp, trigger_count = inst._parse_measurement("1.234E-3A,567,89") + assert reading == 1.234 * pq.milliamp + assert timestamp == 567 + assert trigger_count == 89 + + +def test_zero_check(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF", + "SYST:ZCH?", + "SYST:ZCH ON" + ], + [ + "OFF" + ] + ) as inst: + assert inst.zero_check is False + inst.zero_check = True + + +def test_zero_correct(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF", + "SYST:ZCOR?", + "SYST:ZCOR ON" + ], + [ + "OFF" + ] + ) as inst: + assert inst.zero_correct is False + inst.zero_correct = True + + +def test_unit(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF" + ], + [] + ) as inst: + assert inst.unit == pq.amp + + +def test_auto_range(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF", + "RANG:AUTO?", + "RANG:AUTO 1" + ], + [ + "0" + ] + ) as inst: + assert inst.auto_range is False + inst.auto_range = True + + +def test_input_range(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF", + "RANG?", + "RANG {:e}".format(2e-3) + ], + [ + "0.002" + ] + ) as inst: + assert inst.input_range == 2 * pq.milliamp + inst.input_range = 2 * pq.milliamp + + +def test_input_range_invalid(): + with pytest.raises(ValueError): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF", + "RANG {:e}".format(10) + ], + [] + ) as inst: + inst.input_range = 10 * pq.amp + + +def test_fetch(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF", + "FETC?" + ], + [ + "1.234E-3A,567,89" + ] + ) as inst: + reading, timestamp, trigger_count = inst.fetch() + assert reading == 1.234 * pq.milliamp + assert timestamp == 567 + assert trigger_count == 89 + + +def test_read(): + with expected_protocol( + ik.keithley.Keithley6485, + [ + "*RST", + "SYST:ZCH OFF", + "READ?" + ], + [ + "1.234E-3A,567,89" + ] + ) as inst: + reading, timestamp, trigger_count = inst.read_measurements() + assert reading == 1.234 * pq.milliamp + assert timestamp == 567 + assert trigger_count == 89 diff --git a/instruments/tests/test_keithley/test_keithley6517b.py b/instruments/tests/test_keithley/test_keithley6517b.py index 9495931dd..bb40b17d5 100644 --- a/instruments/tests/test_keithley/test_keithley6517b.py +++ b/instruments/tests/test_keithley/test_keithley6517b.py @@ -32,10 +32,10 @@ def test_parse_measurement(): "\"VOLT:DC\"" ] ) as inst: - reading, timestamp, status = inst._parse_measurement("1.0N,1234s,5678R00000") + reading, timestamp, trigger_count = inst._parse_measurement("1.0N,1234s,5678R00000") assert reading == 1.0 * pq.volt assert timestamp == 1234 - assert status == 5678 + assert trigger_count == 5678 def test_mode(): From 06bbc05ed09e1ae0cad57019d832bb98de8cc7a6 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 2 Dec 2021 23:16:19 -0800 Subject: [PATCH 81/86] pylint fixes --- instruments/keithley/keithley6485.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/instruments/keithley/keithley6485.py b/instruments/keithley/keithley6485.py index 62e85b9f2..33cda7cfb 100644 --- a/instruments/keithley/keithley6485.py +++ b/instruments/keithley/keithley6485.py @@ -33,11 +33,9 @@ # IMPORTS ##################################################################### -from enum import Enum - from instruments.generic_scpi import SCPIInstrument from instruments.units import ureg as u -from instruments.util_fns import bool_property, enum_property +from instruments.util_fns import bool_property # CLASSES ##################################################################### @@ -144,7 +142,7 @@ def read_measurements(self): """ return self._parse_measurement(self.query('READ?')) - def measure(self, mode=None): + def measure(self): """ Trigger and acquire readings. Returns the measurement reading only. @@ -153,9 +151,6 @@ def measure(self, mode=None): # PRIVATE METHODS ## - def _valid_range(self): - return (2e-9, 20e-9, 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) - def _parse_measurement(self, ascii): # Split the string in three comma-separated parts (value, time, number of triggers) vals = ascii.split(',') @@ -163,3 +158,7 @@ def _parse_measurement(self, ascii): timestamp = float(vals[1]) * u.second trigger_count = int(float(vals[2])) return reading, timestamp, trigger_count + + @staticmethod + def _valid_range(): + return (2e-9, 20e-9, 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) From b28df73c1add649dcd072a88cc81fd99547745ff Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 2 Dec 2021 23:24:53 -0800 Subject: [PATCH 82/86] Solved one more pylint error, removed unit member of Keithley 6485 --- instruments/keithley/keithley6485.py | 17 +++++++---------- .../tests/test_keithley/test_keithley6485.py | 9 --------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/instruments/keithley/keithley6485.py b/instruments/keithley/keithley6485.py index 33cda7cfb..1b7e8f150 100644 --- a/instruments/keithley/keithley6485.py +++ b/instruments/keithley/keithley6485.py @@ -85,10 +85,6 @@ def __init__(self, filelike): """ ) - @property - def unit(self): - return u.amp - @property def auto_range(self): """ @@ -151,14 +147,15 @@ def measure(self): # PRIVATE METHODS ## - def _parse_measurement(self, ascii): + @staticmethod + def _valid_range(): + return (2e-9, 20e-9, 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) + + @staticmethod + def _parse_measurement(ascii): # Split the string in three comma-separated parts (value, time, number of triggers) vals = ascii.split(',') - reading = float(vals[0][:-1]) * self.unit + reading = float(vals[0][:-1]) * u.amp timestamp = float(vals[1]) * u.second trigger_count = int(float(vals[2])) return reading, timestamp, trigger_count - - @staticmethod - def _valid_range(): - return (2e-9, 20e-9, 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) diff --git a/instruments/tests/test_keithley/test_keithley6485.py b/instruments/tests/test_keithley/test_keithley6485.py index 60af2195b..0180a1ee2 100644 --- a/instruments/tests/test_keithley/test_keithley6485.py +++ b/instruments/tests/test_keithley/test_keithley6485.py @@ -68,15 +68,6 @@ def test_zero_correct(): inst.zero_correct = True -def test_unit(): - with expected_protocol( - ik.keithley.Keithley6485, - init_sequence, - [] - ) as inst: - assert inst.unit == u.amp - - def test_auto_range(): with expected_protocol( ik.keithley.Keithley6485, From fa5a2e958806c930d8e24c46ddb337dc0142bbcd Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Thu, 3 Oct 2024 16:12:49 -0700 Subject: [PATCH 83/86] Removed dev-requirements.txt --- dev-requirements.txt | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 928f6db4f..000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -attrs>=19.2.0 -mock -<<<<<<< HEAD -pytest==6.1.1 -pytest-mock -hypothesis==4.28.2 -pylint==2.4.4 -pyvisa-sim -======= -pytest -pytest-mock -hypothesis -pylint==1.7.1 -astroid==1.5.3 ->>>>>>> ik/develop From f4655f0ed76ea949bf8b148825f00f421d0444d5 Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 4 Oct 2024 10:59:33 -0700 Subject: [PATCH 84/86] Harmonized code with current main --- src/instruments/agilent/agilent33220a.py | 18 +++++++----------- src/instruments/fluke/fluke3000.py | 14 ++++++++------ src/instruments/glassman/glassmanfr.py | 4 ++-- src/instruments/minghe/__init__.py | 1 - 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/instruments/agilent/agilent33220a.py b/src/instruments/agilent/agilent33220a.py index b23a9f65d..559011e9f 100644 --- a/src/instruments/agilent/agilent33220a.py +++ b/src/instruments/agilent/agilent33220a.py @@ -5,15 +5,11 @@ # IMPORTS ##################################################################### -from __future__ import absolute_import -from __future__ import division -from builtins import range - from enum import Enum -import quantities as pq from instruments.generic_scpi import SCPIFunctionGenerator +from instruments.units import ureg as u from instruments.util_fns import ( enum_property, int_property, @@ -35,10 +31,10 @@ class Agilent33220a(SCPIFunctionGenerator): Example usage: >>> import instruments as ik - >>> import quantities as pq + >>> import instruments.units as u >>> inst = ik.agilent.Agilent33220a.open_gpibusb('/dev/ttyUSB0', 1) >>> inst.function = inst.Function.sinusoid - >>> inst.frequency = 1 * pq.kHz + >>> inst.frequency = 1 * u.kHz >>> inst.output = True .. _Agilent/Keysight 33220a: http://www.keysight.com/en/pd-127539-pn-33220A @@ -162,13 +158,13 @@ def load_resistance(self): function allows the instrument to compensate of the voltage divider and accurately report the voltage across the attached load. - :units: As specified (if a `~quantities.quantity.Quantity`) or assumed + :units: As specified (if a `~pint.Quantity`) or assumed to be of units :math:`\\Omega` (ohm). - :type: `~quantities.quantity.Quantity` or `Agilent33220a.LoadResistance` + :type: `~pint.Quantity` or `Agilent33220a.LoadResistance` """ value = self.query("OUTP:LOAD?") try: - return int(value) * pq.ohm + return int(value) * u.ohm except ValueError: return self.LoadResistance(value.strip()) @@ -177,7 +173,7 @@ def load_resistance(self, newval): if isinstance(newval, self.LoadResistance): newval = newval.value else: - newval = assume_units(newval, pq.ohm).rescale(pq.ohm).magnitude + newval = assume_units(newval, u.ohm).rescale(u.ohm).magnitude if (newval < 0) or (newval > 10000): raise ValueError("Load resistance must be between 0 and 10,000") self.sendcmd(f"OUTP:LOAD {newval}") diff --git a/src/instruments/fluke/fluke3000.py b/src/instruments/fluke/fluke3000.py index 377788237..511f80a1e 100644 --- a/src/instruments/fluke/fluke3000.py +++ b/src/instruments/fluke/fluke3000.py @@ -208,12 +208,14 @@ def connect(self): """ self.scan() # Look for connected devices if not self.positions: - self.reset() # Reset the PC3000 dongle - timeout = self.timeout # Store default timeout - self.timeout = 30 * u.second # PC 3000 can take a while to bind with wireless devices - self.query_lines("rfdis", 3) # Discover available modules and bind them - self.timeout = timeout # Restore default timeout - self.scan() # Look for connected devices + self.reset() # Reset the PC3000 dongle + timeout = self.timeout # Store default timeout + self.timeout = ( + 30 * u.second + ) # PC 3000 can take a while to bind with wireless devices + self.query_lines("rfdis", 3) # Discover available modules and bind them + self.timeout = timeout # Restore default timeout + self.scan() # Look for connected device if not self.positions: raise ValueError("No `Fluke3000` modules available") diff --git a/src/instruments/glassman/glassmanfr.py b/src/instruments/glassman/glassmanfr.py index 8f84fbf99..73da914af 100644 --- a/src/instruments/glassman/glassmanfr.py +++ b/src/instruments/glassman/glassmanfr.py @@ -86,9 +86,9 @@ def __init__(self, filelike): self.voltage_max = 50.0 * u.kilovolt self.current_max = 6.0 * u.milliamp self.polarity = +1 - self._voltage = 0. * u.volt - self._current = 0. * u.amp self._device_timeout = False + self._voltage = 0.0 * u.volt + self._current = 0.0 * u.amp self.device_timeout = False # ENUMS ## diff --git a/src/instruments/minghe/__init__.py b/src/instruments/minghe/__init__.py index d57846cb6..4c9ec9535 100644 --- a/src/instruments/minghe/__init__.py +++ b/src/instruments/minghe/__init__.py @@ -2,5 +2,4 @@ """ Module containing MingHe instruments """ -from __future__ import absolute_import from .mhs5200a import MHS5200 From 7d5aa0d156a339c92a48d9ce7ddb7092b71edd9a Mon Sep 17 00:00:00 2001 From: Francois Drielsma Date: Fri, 4 Oct 2024 11:01:23 -0700 Subject: [PATCH 85/86] Couple more typo fixes --- src/instruments/agilent/agilent33220a.py | 2 +- src/instruments/fluke/fluke3000.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instruments/agilent/agilent33220a.py b/src/instruments/agilent/agilent33220a.py index 559011e9f..a78de6ba7 100644 --- a/src/instruments/agilent/agilent33220a.py +++ b/src/instruments/agilent/agilent33220a.py @@ -173,7 +173,7 @@ def load_resistance(self, newval): if isinstance(newval, self.LoadResistance): newval = newval.value else: - newval = assume_units(newval, u.ohm).rescale(u.ohm).magnitude + newval = assume_units(newval, u.ohm).to(u.ohm).magnitude if (newval < 0) or (newval > 10000): raise ValueError("Load resistance must be between 0 and 10,000") self.sendcmd(f"OUTP:LOAD {newval}") diff --git a/src/instruments/fluke/fluke3000.py b/src/instruments/fluke/fluke3000.py index 511f80a1e..c968d5be5 100644 --- a/src/instruments/fluke/fluke3000.py +++ b/src/instruments/fluke/fluke3000.py @@ -215,7 +215,7 @@ def connect(self): ) # PC 3000 can take a while to bind with wireless devices self.query_lines("rfdis", 3) # Discover available modules and bind them self.timeout = timeout # Restore default timeout - self.scan() # Look for connected device + self.scan() # Look for connected devices if not self.positions: raise ValueError("No `Fluke3000` modules available") From 491c04302688242670a45ce28f2a98695a6f93ca Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:04:38 +0000 Subject: [PATCH 86/86] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/instruments/keithley/keithley6485.py | 39 ++--- src/instruments/keithley/keithley6517b.py | 117 +++++++------ tests/test_keithley/test_keithley6485.py | 81 ++------- tests/test_keithley/test_keithley6517b.py | 197 +++++++--------------- 4 files changed, 153 insertions(+), 281 deletions(-) diff --git a/src/instruments/keithley/keithley6485.py b/src/instruments/keithley/keithley6485.py index 1b7e8f150..ea43f4a5a 100644 --- a/src/instruments/keithley/keithley6485.py +++ b/src/instruments/keithley/keithley6485.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # keithley6485.py: Driver for the Keithley 6485 picoammeter # @@ -41,7 +40,6 @@ class Keithley6485(SCPIInstrument): - """ The `Keithley 6485` is an electrometer capable of doing sensitive current, charge, voltage and resistance measurements. @@ -61,28 +59,28 @@ def __init__(self, filelike): """ Resets device to be read, disables zero check. """ - super(Keithley6485, self).__init__(filelike) + super().__init__(filelike) self.reset() self.zero_check = False # PROPERTIES ## zero_check = bool_property( - 'SYST:ZCH', - inst_true='ON', - inst_false='OFF', + "SYST:ZCH", + inst_true="ON", + inst_false="OFF", doc=""" Gets/sets the zero checking status of the Keithley 6485. - """ + """, ) zero_correct = bool_property( - 'SYST:ZCOR', - inst_true='ON', - inst_false='OFF', + "SYST:ZCOR", + inst_true="ON", + inst_false="OFF", doc=""" Gets/sets the zero correcting status of the Keithley 6485. - """ + """, ) @property @@ -93,13 +91,13 @@ def auto_range(self): :type: `bool` """ # pylint: disable=no-member - out = self.query('RANG:AUTO?') - return out == '1' + out = self.query("RANG:AUTO?") + return out == "1" @auto_range.setter def auto_range(self, newval): # pylint: disable=no-member - self.sendcmd('RANG:AUTO {}'.format('1' if newval else '0')) + self.sendcmd("RANG:AUTO {}".format("1" if newval else "0")) @property def input_range(self): @@ -109,7 +107,7 @@ def input_range(self): :type: `~pint.Quantity` """ # pylint: disable=no-member - out = self.query('RANG?') + out = self.query("RANG?") return float(out) * u.amp @input_range.setter @@ -117,9 +115,8 @@ def input_range(self, newval): # pylint: disable=no-member val = newval.to(u.amp).magnitude if val not in self._valid_range(): - raise ValueError( - 'Unexpected range limit for currently selected mode.') - self.sendcmd('RANG {:e}'.format(val)) + raise ValueError("Unexpected range limit for currently selected mode.") + self.sendcmd(f"RANG {val:e}") # METHODS ## @@ -129,14 +126,14 @@ def fetch(self): (So does not issue a trigger) Returns a tuple of the form (reading, timestamp, trigger_count) """ - return self._parse_measurement(self.query('FETC?')) + return self._parse_measurement(self.query("FETC?")) def read_measurements(self): """ Trigger and acquire readings using the current mode. Returns a tuple of the form (reading, timestamp, trigger_count) """ - return self._parse_measurement(self.query('READ?')) + return self._parse_measurement(self.query("READ?")) def measure(self): """ @@ -154,7 +151,7 @@ def _valid_range(): @staticmethod def _parse_measurement(ascii): # Split the string in three comma-separated parts (value, time, number of triggers) - vals = ascii.split(',') + vals = ascii.split(",") reading = float(vals[0][:-1]) * u.amp timestamp = float(vals[1]) * u.second trigger_count = int(float(vals[2])) diff --git a/src/instruments/keithley/keithley6517b.py b/src/instruments/keithley/keithley6517b.py index 2e5f24db6..c8d2d21c5 100644 --- a/src/instruments/keithley/keithley6517b.py +++ b/src/instruments/keithley/keithley6517b.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # # keithley6517b.py: Driver for the Keithley 6517b Electrometer # @@ -44,7 +43,6 @@ class Keithley6517b(SCPIInstrument, Electrometer): - """ The `Keithley 6517b` is an electrometer capable of doing sensitive current, charge, voltage and resistance measurements. @@ -63,7 +61,7 @@ def __init__(self, filelike): Auto configs the instrument in to the current readout mode (sets the trigger and communication types rights) """ - super(Keithley6517b, self).__init__(filelike) + super().__init__(filelike) self.auto_config(self.mode) # ENUMS ## @@ -72,86 +70,100 @@ class Mode(Enum): """ Enum containing valid measurement modes for the Keithley 6517b """ - voltage_dc = 'VOLT:DC' - current_dc = 'CURR:DC' - resistance = 'RES' - charge = 'CHAR' + + voltage_dc = "VOLT:DC" + current_dc = "CURR:DC" + resistance = "RES" + charge = "CHAR" class TriggerMode(Enum): """ Enum containing valid trigger modes for the Keithley 6517b """ - immediate = 'IMM' - tlink = 'TLINK' + + immediate = "IMM" + tlink = "TLINK" class ArmSource(Enum): """ Enum containing valid trigger arming sources for the Keithley 6517b """ - immediate = 'IMM' - timer = 'TIM' - bus = 'BUS' - tlink = 'TLIN' - stest = 'STES' - pstest = 'PST' - nstest = 'NST' - manual = 'MAN' + + immediate = "IMM" + timer = "TIM" + bus = "BUS" + tlink = "TLIN" + stest = "STES" + pstest = "PST" + nstest = "NST" + manual = "MAN" class ValidRange(Enum): """ Enum containing valid measurement ranges for the Keithley 6517b """ + voltage_dc = (2, 20, 200) - current_dc = (20e-12, 200e-12, 2e-9, 20e-9, - 200e-9, 2e-6, 20e-6, 200e-6, 2e-3, 20e-3) + current_dc = ( + 20e-12, + 200e-12, + 2e-9, + 20e-9, + 200e-9, + 2e-6, + 20e-6, + 200e-6, + 2e-3, + 20e-3, + ) resistance = (2e6, 20e6, 200e6, 2e9, 20e9, 200e9, 2e12, 20e12, 200e12) charge = (2e-9, 20e-9, 200e-9, 2e-6) # PROPERTIES ## mode = enum_property( - 'FUNCTION', + "FUNCTION", Mode, input_decoration=lambda val: val[1:-1], # output_decoration=lambda val: '"{}"'.format(val), set_fmt='{} "{}"', doc=""" Gets/sets the measurement mode of the Keithley 6517b. - """ + """, ) trigger_mode = enum_property( - 'TRIGGER:SOURCE', + "TRIGGER:SOURCE", TriggerMode, doc=""" Gets/sets the trigger mode of the Keithley 6517b. - """ + """, ) arm_source = enum_property( - 'ARM:SOURCE', + "ARM:SOURCE", ArmSource, doc=""" Gets/sets the arm source of the Keithley 6517b. - """ + """, ) zero_check = bool_property( - 'SYST:ZCH', - inst_true='ON', - inst_false='OFF', + "SYST:ZCH", + inst_true="ON", + inst_false="OFF", doc=""" Gets/sets the zero checking status of the Keithley 6517b. - """ + """, ) zero_correct = bool_property( - 'SYST:ZCOR', - inst_true='ON', - inst_false='OFF', + "SYST:ZCOR", + inst_true="ON", + inst_false="OFF", doc=""" Gets/sets the zero correcting status of the Keithley 6517b. - """ + """, ) @property @@ -166,14 +178,13 @@ def auto_range(self): :type: `bool` """ # pylint: disable=no-member - out = self.query('{}:RANGE:AUTO?'.format(self.mode.value)) - return out == '1' + out = self.query(f"{self.mode.value}:RANGE:AUTO?") + return out == "1" @auto_range.setter def auto_range(self, newval): # pylint: disable=no-member - self.sendcmd('{}:RANGE:AUTO {}'.format( - self.mode.value, '1' if newval else '0')) + self.sendcmd("{}:RANGE:AUTO {}".format(self.mode.value, "1" if newval else "0")) @property def input_range(self): @@ -184,7 +195,7 @@ def input_range(self): """ # pylint: disable=no-member mode = self.mode - out = self.query('{}:RANGE:UPPER?'.format(mode.value)) + out = self.query(f"{mode.value}:RANGE:UPPER?") return float(out) * UNITS[mode] @input_range.setter @@ -193,9 +204,8 @@ def input_range(self, newval): mode = self.mode val = newval.to(UNITS[mode]).magnitude if val not in self._valid_range(mode).value: - raise ValueError( - 'Unexpected range limit for currently selected mode.') - self.sendcmd('{}:RANGE:UPPER {:e}'.format(mode.value, val)) + raise ValueError("Unexpected range limit for currently selected mode.") + self.sendcmd(f"{mode.value}:RANGE:UPPER {val:e}") # METHODS ## @@ -212,7 +222,7 @@ def auto_config(self, mode): - Disable buffer operation - Enable autozero """ - self.sendcmd('CONF:{}'.format(mode.value)) + self.sendcmd(f"CONF:{mode.value}") def fetch(self): """ @@ -220,14 +230,14 @@ def fetch(self): (So does not issue a trigger) Returns a tuple of the form (reading, timestamp, trigger_count) """ - return self._parse_measurement(self.query('FETC?')) + return self._parse_measurement(self.query("FETC?")) def read_measurements(self): """ Trigger and acquire readings using the current mode. Returns a tuple of the form (reading, timestamp, trigger_count) """ - return self._parse_measurement(self.query('READ?')) + return self._parse_measurement(self.query("READ?")) def measure(self, mode=None): """ @@ -253,21 +263,22 @@ def _valid_range(self, mode): if mode == self.Mode.charge: return self.ValidRange.charge - raise ValueError('Invalid mode.') + raise ValueError("Invalid mode.") def _parse_measurement(self, ascii): # Split the string in three comma-separated parts (value, time, number of triggers) - vals = ascii.split(',') - reading = float(vals[0].split('N')[0]) * self.unit - timestamp = float(vals[1].split('s')[0]) * u.second - trigger_count = int(vals[2][:-5].split('R')[0]) + vals = ascii.split(",") + reading = float(vals[0].split("N")[0]) * self.unit + timestamp = float(vals[1].split("s")[0]) * u.second + trigger_count = int(vals[2][:-5].split("R")[0]) return reading, timestamp, trigger_count + # UNITS ####################################################################### UNITS = { - Keithley6517b.Mode.voltage_dc: u.volt, - Keithley6517b.Mode.current_dc: u.amp, - Keithley6517b.Mode.resistance: u.ohm, - Keithley6517b.Mode.charge: u.coulomb + Keithley6517b.Mode.voltage_dc: u.volt, + Keithley6517b.Mode.current_dc: u.amp, + Keithley6517b.Mode.resistance: u.ohm, + Keithley6517b.Mode.charge: u.coulomb, } diff --git a/tests/test_keithley/test_keithley6485.py b/tests/test_keithley/test_keithley6485.py index 0180a1ee2..031212478 100644 --- a/tests/test_keithley/test_keithley6485.py +++ b/tests/test_keithley/test_keithley6485.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Keithley 6485 picoammeter """ @@ -18,18 +17,11 @@ # pylint: disable=protected-access -init_sequence = [ - "*RST", - "SYST:ZCH OFF" -] +init_sequence = ["*RST", "SYST:ZCH OFF"] def test_parse_measurement(): - with expected_protocol( - ik.keithley.Keithley6485, - init_sequence, - [] - ) as inst: + with expected_protocol(ik.keithley.Keithley6485, init_sequence, []) as inst: reading, timestamp, trigger_count = inst._parse_measurement("1.234E-3A,567,89") assert reading == 1.234 * u.milliamp assert timestamp == 567 * u.second @@ -38,15 +30,7 @@ def test_parse_measurement(): def test_zero_check(): with expected_protocol( - ik.keithley.Keithley6485, - init_sequence+ - [ - "SYST:ZCH?", - "SYST:ZCH ON" - ], - [ - "OFF" - ] + ik.keithley.Keithley6485, init_sequence + ["SYST:ZCH?", "SYST:ZCH ON"], ["OFF"] ) as inst: assert inst.zero_check is False inst.zero_check = True @@ -54,15 +38,9 @@ def test_zero_check(): def test_zero_correct(): with expected_protocol( - ik.keithley.Keithley6485, - init_sequence+ - [ - "SYST:ZCOR?", - "SYST:ZCOR ON" - ], - [ - "OFF" - ] + ik.keithley.Keithley6485, + init_sequence + ["SYST:ZCOR?", "SYST:ZCOR ON"], + ["OFF"], ) as inst: assert inst.zero_correct is False inst.zero_correct = True @@ -70,15 +48,7 @@ def test_zero_correct(): def test_auto_range(): with expected_protocol( - ik.keithley.Keithley6485, - init_sequence+ - [ - "RANG:AUTO?", - "RANG:AUTO 1" - ], - [ - "0" - ] + ik.keithley.Keithley6485, init_sequence + ["RANG:AUTO?", "RANG:AUTO 1"], ["0"] ) as inst: assert inst.auto_range is False inst.auto_range = True @@ -86,15 +56,9 @@ def test_auto_range(): def test_input_range(): with expected_protocol( - ik.keithley.Keithley6485, - init_sequence+ - [ - "RANG?", - "RANG {:e}".format(2e-3) - ], - [ - "0.002" - ] + ik.keithley.Keithley6485, + init_sequence + ["RANG?", f"RANG {2e-3:e}"], + ["0.002"], ) as inst: assert inst.input_range == 2 * u.milliamp inst.input_range = 2 * u.milliamp @@ -103,26 +67,14 @@ def test_input_range(): def test_input_range_invalid(): with pytest.raises(ValueError): with expected_protocol( - ik.keithley.Keithley6485, - init_sequence+ - [ - "RANG {:e}".format(10) - ], - [] + ik.keithley.Keithley6485, init_sequence + [f"RANG {10:e}"], [] ) as inst: inst.input_range = 10 * u.amp def test_fetch(): with expected_protocol( - ik.keithley.Keithley6485, - init_sequence+ - [ - "FETC?" - ], - [ - "1.234E-3A,567,89" - ] + ik.keithley.Keithley6485, init_sequence + ["FETC?"], ["1.234E-3A,567,89"] ) as inst: reading, timestamp, trigger_count = inst.fetch() assert reading == 1.234 * u.milliamp @@ -132,14 +84,7 @@ def test_fetch(): def test_read(): with expected_protocol( - ik.keithley.Keithley6485, - init_sequence+ - [ - "READ?" - ], - [ - "1.234E-3A,567,89" - ] + ik.keithley.Keithley6485, init_sequence + ["READ?"], ["1.234E-3A,567,89"] ) as inst: reading, timestamp, trigger_count = inst.read_measurements() assert reading == 1.234 * u.milliamp diff --git a/tests/test_keithley/test_keithley6517b.py b/tests/test_keithley/test_keithley6517b.py index b7da1c725..e75aad96d 100644 --- a/tests/test_keithley/test_keithley6517b.py +++ b/tests/test_keithley/test_keithley6517b.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ Unit tests for the Keithley 6517b electrometer """ @@ -18,28 +17,19 @@ # pylint: disable=protected-access -init_sequence = [ - "FUNCTION?", - "CONF:VOLT:DC" -] -init_response = [ - "\"VOLT:DC\"" -] +init_sequence = ["FUNCTION?", "CONF:VOLT:DC"] +init_response = ['"VOLT:DC"'] def test_parse_measurement(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "FUNCTION?" - ], - init_response+ - [ - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + ["FUNCTION?"], + init_response + ['"VOLT:DC"'], ) as inst: - reading, timestamp, trigger_count = inst._parse_measurement("1.0N,1234s,5678R00000") + reading, timestamp, trigger_count = inst._parse_measurement( + "1.0N,1234s,5678R00000" + ) assert reading == 1.0 * u.volt assert timestamp == 1234 * u.second assert trigger_count == 5678 @@ -47,16 +37,9 @@ def test_parse_measurement(): def test_mode(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "FUNCTION?", - "FUNCTION \"VOLT:DC\"" - ], - init_response+ - [ - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + ["FUNCTION?", 'FUNCTION "VOLT:DC"'], + init_response + ['"VOLT:DC"'], ) as inst: assert inst.mode == inst.Mode.voltage_dc inst.mode = inst.Mode.voltage_dc @@ -64,16 +47,9 @@ def test_mode(): def test_trigger_source(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "TRIGGER:SOURCE?", - "TRIGGER:SOURCE IMM" - ], - init_response+ - [ - "TLINK" - ] + ik.keithley.Keithley6517b, + init_sequence + ["TRIGGER:SOURCE?", "TRIGGER:SOURCE IMM"], + init_response + ["TLINK"], ) as inst: assert inst.trigger_mode == inst.TriggerMode.tlink inst.trigger_mode = inst.TriggerMode.immediate @@ -81,16 +57,9 @@ def test_trigger_source(): def test_arm_source(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "ARM:SOURCE?", - "ARM:SOURCE IMM" - ], - init_response+ - [ - "TIM" - ] + ik.keithley.Keithley6517b, + init_sequence + ["ARM:SOURCE?", "ARM:SOURCE IMM"], + init_response + ["TIM"], ) as inst: assert inst.arm_source == inst.ArmSource.timer inst.arm_source = inst.ArmSource.immediate @@ -98,16 +67,9 @@ def test_arm_source(): def test_zero_check(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "SYST:ZCH?", - "SYST:ZCH ON" - ], - init_response+ - [ - "OFF" - ] + ik.keithley.Keithley6517b, + init_sequence + ["SYST:ZCH?", "SYST:ZCH ON"], + init_response + ["OFF"], ) as inst: assert inst.zero_check is False inst.zero_check = True @@ -115,16 +77,9 @@ def test_zero_check(): def test_zero_correct(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "SYST:ZCOR?", - "SYST:ZCOR ON" - ], - init_response+ - [ - "OFF" - ] + ik.keithley.Keithley6517b, + init_sequence + ["SYST:ZCOR?", "SYST:ZCOR ON"], + init_response + ["OFF"], ) as inst: assert inst.zero_correct is False inst.zero_correct = True @@ -132,35 +87,22 @@ def test_zero_correct(): def test_unit(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "FUNCTION?", - ], - init_response+ - [ - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + + [ + "FUNCTION?", + ], + init_response + ['"VOLT:DC"'], ) as inst: assert inst.unit == u.volt def test_auto_range(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "FUNCTION?", - "VOLT:DC:RANGE:AUTO?", - "FUNCTION?", - "VOLT:DC:RANGE:AUTO 1" - ], - init_response+ - [ - "\"VOLT:DC\"", - "0", - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + + ["FUNCTION?", "VOLT:DC:RANGE:AUTO?", "FUNCTION?", "VOLT:DC:RANGE:AUTO 1"], + init_response + ['"VOLT:DC"', "0", '"VOLT:DC"'], ) as inst: assert inst.auto_range is False inst.auto_range = True @@ -168,20 +110,15 @@ def test_auto_range(): def test_input_range(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "FUNCTION?", - "VOLT:DC:RANGE:UPPER?", - "FUNCTION?", - "VOLT:DC:RANGE:UPPER {:e}".format(20) - ], - init_response+ - [ - "\"VOLT:DC\"", - "10", - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + + [ + "FUNCTION?", + "VOLT:DC:RANGE:UPPER?", + "FUNCTION?", + f"VOLT:DC:RANGE:UPPER {20:e}", + ], + init_response + ['"VOLT:DC"', "10", '"VOLT:DC"'], ) as inst: assert inst.input_range == 10 * u.volt inst.input_range = 20 * u.volt @@ -190,41 +127,31 @@ def test_input_range(): def test_input_range_invalid(): with pytest.raises(ValueError): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence, - init_response, + ik.keithley.Keithley6517b, + init_sequence, + init_response, ) as inst: inst.input_range = 10 * u.volt def test_auto_config(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "CONF:VOLT:DC" - ], - init_response+ - [ - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + ["CONF:VOLT:DC"], + init_response + ['"VOLT:DC"'], ) as inst: inst.auto_config(inst.Mode.voltage_dc) def test_fetch(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "FETC?", - "FUNCTION?", - ], - init_response+ - [ - "1.0N,1234s,5678R00000", - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + + [ + "FETC?", + "FUNCTION?", + ], + init_response + ["1.0N,1234s,5678R00000", '"VOLT:DC"'], ) as inst: reading, timestamp, trigger_count = inst.fetch() assert reading == 1.0 * u.volt @@ -234,17 +161,9 @@ def test_fetch(): def test_read(): with expected_protocol( - ik.keithley.Keithley6517b, - init_sequence+ - [ - "READ?", - "FUNCTION?" - ], - init_response+ - [ - "1.0N,1234s,5678R00000", - "\"VOLT:DC\"" - ] + ik.keithley.Keithley6517b, + init_sequence + ["READ?", "FUNCTION?"], + init_response + ["1.0N,1234s,5678R00000", '"VOLT:DC"'], ) as inst: reading, timestamp, trigger_count = inst.read_measurements() assert reading == 1.0 * u.volt