diff --git a/src/instruments/glassman/glassmanfr.py b/src/instruments/glassman/glassmanfr.py
index 5de38043..73da914a 100644
--- a/src/instruments/glassman/glassmanfr.py
+++ b/src/instruments/glassman/glassmanfr.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
#
-# 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).
#
@@ -89,6 +89,7 @@ def __init__(self, filelike):
self._device_timeout = False
self._voltage = 0.0 * u.volt
self._current = 0.0 * u.amp
+ self.device_timeout = False
# ENUMS ##
@@ -159,7 +160,9 @@ def voltage(self):
@voltage.setter
def voltage(self, newval):
- self.set_status(voltage=assume_units(newval, u.volt))
+ voltage = assume_units(newval, u.volt)
+ self.set_status(voltage=voltage)
+ self._voltage = voltage
@property
def current(self):
@@ -173,7 +176,9 @@ def current(self):
@current.setter
def current(self, newval):
- self.set_status(current=assume_units(newval, u.amp))
+ current = assume_units(newval, u.amp)
+ self.set_status(current=current)
+ self._current = current
@property
def voltage_sense(self):
diff --git a/src/instruments/keithley/__init__.py b/src/instruments/keithley/__init__.py
index 56f8ef37..bc6cb2fb 100644
--- a/src/instruments/keithley/__init__.py
+++ b/src/instruments/keithley/__init__.py
@@ -9,4 +9,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/src/instruments/keithley/keithley6485.py b/src/instruments/keithley/keithley6485.py
new file mode 100644
index 00000000..ea43f4a5
--- /dev/null
+++ b/src/instruments/keithley/keithley6485.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python
+#
+# 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 instruments.generic_scpi import SCPIInstrument
+from instruments.units import ureg as u
+from instruments.util_fns import bool_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 instruments.units as u
+ >>> dmm = ik.keithley.Keithley6485.open_serial('/dev/ttyUSB0', baud=9600)
+ >>> dmm.measure()
+
+ """
+
+ def __init__(self, filelike):
+ """
+ Resets device to be read, disables zero check.
+ """
+ super().__init__(filelike)
+ self.reset()
+ self.zero_check = False
+
+ # PROPERTIES ##
+
+ 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 auto_range(self):
+ """
+ Gets/sets the auto range setting
+
+ :type: `bool`
+ """
+ # pylint: disable=no-member
+ 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"))
+
+ @property
+ def input_range(self):
+ """
+ Gets/sets the upper limit of the current range.
+
+ :type: `~pint.Quantity`
+ """
+ # pylint: disable=no-member
+ out = self.query("RANG?")
+ return float(out) * u.amp
+
+ @input_range.setter
+ 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(f"RANG {val:e}")
+
+ # 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):
+ """
+ Trigger and acquire readings.
+ Returns the measurement reading only.
+ """
+ return self.read_measurements()[0]
+
+ # PRIVATE METHODS ##
+
+ @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]) * u.amp
+ timestamp = float(vals[1]) * u.second
+ trigger_count = int(float(vals[2]))
+ return reading, timestamp, trigger_count
diff --git a/src/instruments/keithley/keithley6517b.py b/src/instruments/keithley/keithley6517b.py
new file mode 100644
index 00000000..c8d2d21c
--- /dev/null
+++ b/src/instruments/keithley/keithley6517b.py
@@ -0,0 +1,284 @@
+#!/usr/bin/env python
+#
+# 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.
+
+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 enum import Enum
+
+from instruments.abstract_instruments import Electrometer
+from instruments.generic_scpi import SCPIInstrument
+from instruments.units import ureg as u
+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 instruments.units as u
+ >>> dmm = ik.keithley.Keithley6517b.open_serial('/dev/ttyUSB0', baud=115200)
+ >>> dmm.measure(dmm.Mode.current)
+
+ """
+
+ def __init__(self, filelike):
+ """
+ Auto configs the instrument in to the current readout mode
+ (sets the trigger and communication types rights)
+ """
+ super().__init__(filelike)
+ self.auto_config(self.mode)
+
+ # 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 = (2e6, 20e6, 200e6, 2e9, 20e9, 200e9, 2e12, 20e12, 200e12)
+ charge = (2e-9, 20e-9, 200e-9, 2e-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(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"))
+
+ @property
+ def input_range(self):
+ """
+ Gets/sets the upper limit of the current range.
+
+ :type: `~pint.Quantity`
+ """
+ # pylint: disable=no-member
+ mode = self.mode
+ out = self.query(f"{mode.value}:RANGE:UPPER?")
+ return float(out) * UNITS[mode]
+
+ @input_range.setter
+ def input_range(self, newval):
+ # pylint: disable=no-member
+ 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(f"{mode.value}:RANGE:UPPER {val:e}")
+
+ # 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(f"CONF:{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_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
+ if mode == self.Mode.charge:
+ return self.ValidRange.charge
+
+ 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])
+ 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,
+}
diff --git a/tests/test_keithley/test_keithley6485.py b/tests/test_keithley/test_keithley6485.py
new file mode 100644
index 00000000..03121247
--- /dev/null
+++ b/tests/test_keithley/test_keithley6485.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python
+"""
+Unit tests for the Keithley 6485 picoammeter
+"""
+
+# IMPORTS #####################################################################
+
+
+import pytest
+
+import instruments as ik
+from instruments.tests import expected_protocol
+from instruments.units import ureg as u
+
+# TESTS #######################################################################
+
+# pylint: disable=protected-access
+
+
+init_sequence = ["*RST", "SYST:ZCH OFF"]
+
+
+def test_parse_measurement():
+ 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
+ assert trigger_count == 89
+
+
+def test_zero_check():
+ with expected_protocol(
+ ik.keithley.Keithley6485, init_sequence + ["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,
+ init_sequence + ["SYST:ZCOR?", "SYST:ZCOR ON"],
+ ["OFF"],
+ ) as inst:
+ assert inst.zero_correct is False
+ inst.zero_correct = True
+
+
+def test_auto_range():
+ with expected_protocol(
+ ik.keithley.Keithley6485, init_sequence + ["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,
+ 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
+
+
+def test_input_range_invalid():
+ with pytest.raises(ValueError):
+ with expected_protocol(
+ 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"]
+ ) as inst:
+ reading, timestamp, trigger_count = inst.fetch()
+ assert reading == 1.234 * u.milliamp
+ assert timestamp == 567 * u.second
+ assert trigger_count == 89
+
+
+def test_read():
+ with expected_protocol(
+ 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
+ assert timestamp == 567 * u.second
+ assert trigger_count == 89
diff --git a/tests/test_keithley/test_keithley6517b.py b/tests/test_keithley/test_keithley6517b.py
new file mode 100644
index 00000000..e75aad96
--- /dev/null
+++ b/tests/test_keithley/test_keithley6517b.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python
+"""
+Unit tests for the Keithley 6517b electrometer
+"""
+
+# IMPORTS #####################################################################
+
+
+import pytest
+
+import instruments as ik
+from instruments.tests import expected_protocol
+from instruments.units import ureg as u
+
+# TESTS #######################################################################
+
+# pylint: disable=protected-access
+
+
+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"'],
+ ) as inst:
+ 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
+
+
+def test_mode():
+ with expected_protocol(
+ 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
+
+
+def test_trigger_source():
+ with expected_protocol(
+ 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
+
+
+def test_arm_source():
+ with expected_protocol(
+ 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
+
+
+def test_zero_check():
+ with expected_protocol(
+ 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
+
+
+def test_zero_correct():
+ with expected_protocol(
+ 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
+
+
+def test_unit():
+ with expected_protocol(
+ 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"'],
+ ) as inst:
+ assert inst.auto_range is False
+ inst.auto_range = True
+
+
+def test_input_range():
+ with expected_protocol(
+ 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
+
+
+def test_input_range_invalid():
+ with pytest.raises(ValueError):
+ with expected_protocol(
+ 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"'],
+ ) 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"'],
+ ) as inst:
+ reading, timestamp, trigger_count = inst.fetch()
+ assert reading == 1.0 * u.volt
+ assert timestamp == 1234 * u.second
+ assert trigger_count == 5678
+
+
+def test_read():
+ with expected_protocol(
+ 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
+ assert timestamp == 1234 * u.second
+ assert trigger_count == 5678