Source code for instruments.keithley.keithley195

#!/usr/bin/env python
"""
Driver for the Keithley 195 digital multimeter
"""

# IMPORTS #####################################################################


import time
import struct
from enum import Enum, IntEnum

from instruments.abstract_instruments import Multimeter
from instruments.units import ureg as u

# CLASSES #####################################################################


[docs] class Keithley195(Multimeter): """ The Keithley 195 is a 5 1/2 digit auto-ranging digital multimeter. You can find the full specifications list in the `Keithley 195 user's guide`_. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> print dmm.measure(dmm.Mode.resistance) .. _Keithley 195 user's guide: http://www.keithley.com/data?asset=803 """ def __init__(self, filelike): super().__init__(filelike) self.sendcmd("YX") # Removes the termination CRLF self.sendcmd("G1DX") # Disable returning prefix and suffix # ENUMS ##
[docs] class Mode(IntEnum): """ Enum containing valid measurement modes for the Keithley 195 """ voltage_dc = 0 voltage_ac = 1 resistance = 2 current_dc = 3 current_ac = 4
[docs] class TriggerMode(IntEnum): """ Enum containing valid trigger modes for the Keithley 195 """ talk_continuous = 0 talk_one_shot = 1 get_continuous = 2 get_one_shot = 3 x_continuous = 4 x_one_shot = 5 ext_continuous = 6 ext_one_shot = 7
[docs] class ValidRange(Enum): """ Enum containing valid range settings for the Keithley 195 """ voltage_dc = (20e-3, 200e-3, 2, 20, 200, 1000) voltage_ac = (20e-3, 200e-3, 2, 20, 200, 700) current_dc = (20e-6, 200e-6, 2e-3, 20e-3, 200e-3, 2) current_ac = (20e-6, 200e-6, 2e-3, 20e-3, 200e-3, 2, 2) resistance = (20, 200, 2000, 20e3, 200e3, 2e6, 20e6)
# PROPERTIES # @property def mode(self): """ Gets/sets the measurement mode for the Keithley 195. The base model only has DC voltage and resistance measurements. In order to use AC voltage, DC current, and AC current measurements your unit must be equiped with option 1950. Example use: >>> import instruments as ik >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> dmm.mode = dmm.Mode.resistance :type: `Keithley195.Mode` """ return self.parse_status_word(self.get_status_word())["mode"] @mode.setter def mode(self, newval): if isinstance(newval, str): newval = self.Mode[newval] if not isinstance(newval, Keithley195.Mode): raise TypeError( "Mode must be specified as a Keithley195.Mode " "value, got {} instead.".format(newval) ) self.sendcmd(f"F{newval.value}DX") @property def trigger_mode(self): """ Gets/sets the trigger mode of the Keithley 195. There are two different trigger settings for four different sources. This means there are eight different settings for the trigger mode. The two types are continuous and one-shot. Continuous has the instrument continuously sample the resistance. One-shot performs a single resistance 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". 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. Last, is external triggering. This is the port on the rear of the instrument. Refer to the manual for electrical characteristics of this port. :type: `Keithley195.TriggerMode` """ return self.parse_status_word(self.get_status_word())["trigger"] @trigger_mode.setter def trigger_mode(self, newval): if isinstance(newval, str): newval = Keithley195.TriggerMode[newval] if not isinstance(newval, Keithley195.TriggerMode): raise TypeError( "Drive must be specified as a " "Keithley195.TriggerMode, got {} " "instead.".format(newval) ) self.sendcmd(f"T{newval.value}X") @property def relative(self): """ Gets/sets the zero command (relative measurement) mode of the Keithley 195. As stated in the manual: The zero mode serves as a means for a baseline suppression. When the correct zero command is send over the bus, the instrument will enter the zero mode, as indicated by the front panel ZERO indicator light. All reading displayed or send over the bus while zero is enabled are the difference between the stored baseline adn the actual voltage level. For example, if a 100mV baseline is stored, 100mV will be subtracted from all subsequent readings as long as the zero mode is enabled. The value of the stored baseline can be as little as a few microvolts or as large as the selected range will permit. 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(f"Z{int(newval)}DX") @property def input_range(self): """ Gets/sets the range of the Keithley 195 input terminals. The valid range settings depends on the current mode of the instrument. They are listed as follows: #) voltage_dc = ``(20e-3, 200e-3, 2, 20, 200, 1000)`` #) voltage_ac = ``(20e-3, 200e-3, 2, 20, 200, 700)`` #) current_dc = ``(20e-6, 200e-6, 2e-3, 20e-3, 200e-3, 2)`` #) current_ac = ``(20e-6, 200e-6, 2e-3, 20e-3, 200e-3, 2)`` #) resistance = ``(20, 200, 2000, 20e3, 200e3, 2e6, 20e6)`` All modes will also accept the string ``auto`` which will set the 195 into auto ranging mode. :rtype: `~pint.Quantity` or `str` """ index = self.parse_status_word(self.get_status_word())["range"] if index == 0: return "auto" mode = self.mode value = Keithley195.ValidRange[mode.name].value[index - 1] units = UNITS2[mode] return value * units @input_range.setter def input_range(self, newval): if isinstance(newval, str): if newval.lower() == "auto": self.sendcmd("R0DX") return else: raise ValueError( 'Only "auto" is acceptable when specifying ' "the input range as a string." ) if isinstance(newval, u.Quantity): newval = float(newval.magnitude) mode = self.mode valid = Keithley195.ValidRange[mode.name].value if isinstance(newval, (float, int)): if newval in valid: newval = valid.index(newval) + 1 else: raise ValueError( "Valid range settings for mode {} " "are: {}".format(mode, valid) ) else: raise TypeError( "Range setting must be specified as a float, int, " 'or the string "auto", got {}'.format(type(newval)) ) self.sendcmd(f"R{newval}DX") # METHODS #
[docs] def measure(self, mode=None): """ Instruct the Keithley 195 to perform a one time measurement. The instrument will use default parameters for the requested measurement. The measurement will immediately take place, and the results are directly sent to the instrument's output buffer. Method returns a Python quantity consisting of a numpy array with the instrument value and appropriate units. With the 195, it is HIGHLY recommended that you seperately set the mode and let the instrument settle into the new mode. This can sometimes take longer than the 2 second delay added in this method. In our testing the 2 seconds seems to be sufficient but we offer no guarentee. Example usage: >>> import instruments as ik >>> import instruments.units as u >>> dmm = ik.keithley.Keithley195.open_gpibusb('/dev/ttyUSB0', 12) >>> print(dmm.measure(dmm.Mode.resistance)) :param mode: Desired measurement mode. This must always be specified in order to provide the correct return units. :type mode: `Keithley195.Mode` :return: A measurement from the multimeter. :rtype: `~pint.Quantity` """ if mode is not None: current_mode = self.mode if mode != current_mode: self.mode = mode time.sleep(2) # Gives the instrument a moment to settle else: mode = self.mode value = self.query("") return float(value) * UNITS2[mode]
[docs] def get_status_word(self): """ Retreive the status word from the instrument. This contains information regarding the various settings of the instrument. The function `~Keithley195.parse_status_word` is designed to parse the return string from this function. :return: String containing setting information of the instrument :rtype: `str` """ self.sendcmd("U0DX") return self._file.read_raw()
[docs] @staticmethod def parse_status_word(statusword): # pylint: disable=too-many-locals """ Parse the status word returned by the function `~Keithley195.get_status_word`. Returns a `dict` with the following keys: ``{trigger,mode,range,eoi,buffer,rate,srqmode,relative,delay,multiplex, selftest,dataformat,datacontrol,filter,terminator}`` :param statusword: Byte string to be unpacked and parsed :type: `str` :return: A parsed version of the status word as a Python dictionary :rtype: `dict` """ if statusword[:3] != b"195": raise ValueError( "Status word starts with wrong prefix, expected " "195, got {}".format(statusword) ) ( trigger, function, input_range, eoi, buf, rate, srqmode, relative, delay, multiplex, selftest, data_fmt, data_ctrl, filter_mode, terminator, ) = struct.unpack("@4c2s3c2s5c2s", statusword[4:]) return { "trigger": Keithley195.TriggerMode(int(trigger)), "mode": Keithley195.Mode(int(function)), "range": int(input_range), "eoi": (eoi == b"1"), "buffer": buf, "rate": rate, "srqmode": srqmode, "relative": (relative == b"1"), "delay": delay, "multiplex": (multiplex == b"1"), "selftest": selftest, "dataformat": data_fmt, "datacontrol": data_ctrl, "filter": filter_mode, "terminator": terminator, }
[docs] def trigger(self): """ Tell the Keithley 195 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 195 anyways). """ self.sendcmd("X")
[docs] def auto_range(self): """ Turn on auto range for the Keithley 195. This is the same as calling ``Keithley195.input_range = 'auto'`` """ self.input_range = "auto"
# UNITS ####################################################################### UNITS = { "DCV": u.volt, "ACV": u.volt, "ACA": u.amp, "DCA": u.amp, "OHM": u.ohm, } UNITS2 = { Keithley195.Mode.voltage_dc: u.volt, Keithley195.Mode.voltage_ac: u.volt, Keithley195.Mode.current_dc: u.amp, Keithley195.Mode.current_ac: u.amp, Keithley195.Mode.resistance: u.ohm, }