#!/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,
}