From 2c38d5edff6078766020f505dc3b1fa14008b82b Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 28 Nov 2025 11:20:17 +0100 Subject: [PATCH 1/6] Add Keithley 2600 sourcemeter driver --- .../keithley2600/keithley2600_VISADriver.py | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py diff --git a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py new file mode 100644 index 0000000..939e5d3 --- /dev/null +++ b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py @@ -0,0 +1,179 @@ +import numpy as np +import pyvisa +from pymodaq_plugins_keithley import config +from pymodaq.utils.logger import set_logger, get_module_name +logger = set_logger(get_module_name(__file__)) + + +def table_to_np(table): + """Convert sequence of ASCII-encoded, comma-separated values to NumPy array.""" + split = table.split(", ") + floats = [float(x) for x in split] + array = np.array(floats) + return array + + +class Keithley2600VISADriver: + """VISA class driver for Keithley 2600 sourcemeters. + + Communication with the device is performed in text mode (TSP). Detailed instructions can be found in: + https://download.tek.com/manual/2600BS-901-01_C_Aug_2016_2.pdf + """ + + + def __init__(self, resource_name, channel_name="A", autorange=True, pyvisa_backend="@py"): + """Initialize KeithleyVISADriver class. + + Parameters + ---------- + resource_name: str + VISA resource name. (ex: "USB0::1510::9782::1234567::0::INSTR") + channel_name: str, optional + Channel name. (default: "A") + autorange: bool, optional + Enable I and V autorange. (default: True) + pyvisa_backend: str, optional + pyvisa backend identifier or path to the visa backend dll (ref. to pyvisa) + (default: "@py") + """ + + # Establish connection + resourceman = pyvisa.ResourceManager(pyvisa_backend) + self._instr = resourceman.open_resource(resource_name) + + # Create channel + self.channel = Keithley2600Channel(self, channel_name, autorange) + + + def close(self): + """Terminate connection with the instrument.""" + self._instr.close() + self._instr = None + + + def _write(self, cmd): + """Convenience methode to send a TSP command to the device.""" + self._instr.write(cmd) + + + def _read(self): + """Convenience methode to get response from the device.""" + return self._instr.read() + + +class Keithley2600Channel: + """Class for handling a single channel on a Keithley 2600 sourcemeter.""" + + def __init__(self, parent, channel, autorange): + """Initialize class. + + Parameters + ---------- + parent: Keithley2600 + Parent class. + channel: str + Identifier of the channel. (ex: "A") + autorange: bool + Enable I and V autorange. + """ + + # Initialize variables + self.channel = channel + self.smu = f"smu{channel.lower()}" + self.parent = parent + + # Set autorange if enabled + if autorange: + self.autorange() + + + def _write(self, cmd): + """Convenience methode to send a TSP command to the device.""" + self.parent._write(cmd) + + + def _read(self): + """Convenience methode to get response from the device.""" + return self.parent._read() + + + @property + def current_limit(self): + """Get current limit [A] of the channel. + + Returns + ------- + current_limit: float + Current limit [A] of the selected channel. + """ + self._write(f"print({self.smu}.source.limiti)") + limit = self._read() + return float(limit) + + + @current_limit.setter + def current_limit(self, limit): + """Set current limit [A] of the channel. + + Parameters + ---------- + limit: float + Current limit [A] to set. + """ + limit = f"{limit:.6e}" + self._write(f"{self.smu}.source.limiti = {limit}") + + + def autorange(self): + """Set current and voltage measurements to autorange.""" + self._write(f"{self.smu}.measure.autorangei = {self.smu}.AUTORANGE_ON") + self._write(f"{self.smu}.measure.autorangev = {self.smu}.AUTORANGE_ON") + + + def sweepV_measureI(self, startv=0, stopv=1, stime=1e-3, npoints=100): + """Perform a linear voltage sweep and measure current. This version is called with arguments. + + Parameters + ---------- + startv: float + Starting voltage [V] of the sweep. + stopv: float + Stopping voltage [V] of the sweep. + stime: float + Stabilization time [s]. The device waits for this amount of time at each measurement + step, once voltage has reached the setpoint. In practice, actual step time is longer + than this value because of the time needed to reach the voltage setpoint. + npoints: int + Number of points to be acquired. Must be >2. + + Returns + ------- + x: np.ndarray + Voltage values [V]. + y: np.ndarray + Current (intensity) values [A]. + """ + + # Convert channel and step time + startv = f"{startv:.6e}" + stopv = f"{stopv:.6e}" + stime = f"{stime:.6e}" + npoints = str(npoints) + + # Send request to sweep + self._write(f"SweepVLinMeasureI({self.smu}, {startv}, {stopv}, {stime}, {npoints})") + self._write(f"print(status.measurement.buffer_available.{self.smu.upper()})") + ret = self._read() + if not int(float(ret)) == 2: + raise ValueError(f"Return data {ret} != 2") + + # Retrieve applied voltages + self._write(f"printbuffer(1, {npoints}, {self.smu}.nvbuffer1.sourcevalues)") + x = table_to_np(self._read()) + + # Retrieve measured currents + self._write(f"printbuffer(1, {npoints}, {self.smu}.nvbuffer1.readings)") + y = table_to_np(self._read()) + + # Return x and y vectors + return x, y From 6fa8a1cddf800d3d1d676b2f423599fe97aee534 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 28 Nov 2025 11:21:04 +0100 Subject: [PATCH 2/6] Add pyusb to plugin requirements --- plugin_info.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin_info.toml b/plugin_info.toml index ddbdee2..7505293 100644 --- a/plugin_info.toml +++ b/plugin_info.toml @@ -11,7 +11,7 @@ license = 'MIT' [plugin-install] #packages required for your plugin: -packages-required = ["pyvisa", "pyvisa-py", "pymeasure", "zeroconf",'pymodaq>=4.0'] +packages-required = ["pyvisa", "pyvisa-py", "pymeasure", "pyusb", "zeroconf",'pymodaq>=4.0'] [features] # defines the plugin features contained into this plugin instruments = true # true if plugin contains instrument classes (else false, notice the lowercase for toml files) From 3d01fe1a0f713e9a47d653081786a0ebdd257960 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 28 Nov 2025 11:21:35 +0100 Subject: [PATCH 3/6] Add Keithley2600 DAQ1D viewer --- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/pymodaq_plugins_keithley/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Keithley2600.py diff --git a/src/pymodaq_plugins_keithley/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Keithley2600.py b/src/pymodaq_plugins_keithley/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Keithley2600.py new file mode 100644 index 0000000..293860c --- /dev/null +++ b/src/pymodaq_plugins_keithley/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Keithley2600.py @@ -0,0 +1,189 @@ +import numpy as np + +from pymodaq_utils.utils import ThreadCommand +from pymodaq_data.data import DataToExport, Axis, Q_ +from pymodaq_gui.parameter import Parameter + +from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main +from pymodaq.utils.data import DataFromPlugins + +import pyvisa +from pymodaq_plugins_keithley.hardware.keithley2600.keithley2600_VISADriver import Keithley2600VISADriver + + +# Helper functions +def _get_VISA_resources(pyvisa_backend="@py"): + + # Get list of VISA resources + resourceman = pyvisa.ResourceManager(pyvisa_backend) + resources = list(resourceman.list_resources()) + + # Move the first USB connection to the top + for i, val in enumerate(resources): + if val.startswith("USB0"): + resources.remove(val) + resources.insert(0, val) + break + + # Return list of resources + return resources + + +def _build_param(name, title, type, value, limits=None, unit=None, **kwargs): + params = {} + params["name"] = name + params["title"] = title + params["type"] = type + params["value"] = value + if limits is not None: + params["limits"] = limits + if unit is not None: + params["suffix"] = unit + params["siPrefix"] = True + for argn, argv in kwargs.items(): + params[argn] = argv + return params + + +def _emit_xy_data(self, x, y): + x_axis = Axis(data=x, label="Voltage", units="V", index=0) + self.dte_signal.emit(DataToExport("Keithley2600", + data=[DataFromPlugins(name="Keithley2600", + data=[y], + units="A", + dim="Data1D", labels=["I-V"], + axes=[x_axis])])) + + +class DAQ_1DViewer_Keithley2600(DAQ_Viewer_base): + """ Instrument plugin class for a Keithley 2600 sourcemeter. + + Attributes: + ----------- + controller: object + The particular object that allow the communication with the hardware, in general a python wrapper around the + hardware library. + + # TODO add your particular attributes here if any + + """ + params = comon_parameters+[ + _build_param("resource_name", "VISA resource", "list", "", limits=_get_VISA_resources()), + _build_param("channel", "Channel", "str", "A"), + _build_param("startv", "Sweep start voltage", "float", 0, unit="V"), + _build_param("stopv", "Sweep stop voltage", "float", 1, unit="V"), + _build_param("stime", "Sweep stabilization time", "float", 1e-3, unit="s"), + _build_param("npoints", "Sweep points", "int", 101), + _build_param("ilimit", "Current limit", "float", 0.1, unit="A"), + _build_param("autorange", "Autorange", "bool", True) + ] + + + def ini_attributes(self): + # Type declaration of the controller + self.controller: Keithley2600VISADriver = None + + + def commit_settings(self, param: Parameter): + """Apply the consequences of a change of value in the detector settings + + Parameters + ---------- + param: Parameter + A given parameter (within detector_settings) whose value has been changed by the user + """ + # Dispatch arguments + name = param.name() + val = param.value() + unit = param.opts.get("suffix") + qty = Q_(val, unit) + + # Current limit + if name == "ilimit": + self.controller.channel.current_limit = qty.to("A") + + + def ini_detector(self, controller=None): + """Detector communication initialization + + Parameters + ---------- + controller: (object) + custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller + (Master case) + + Returns + ------- + info: str + initialized: bool + False if initialization failed otherwise True + """ + + # If stand-alone device, initialize controller object + if self.is_master: + + # Get initialization parameters + resource_name = self.settings["resource_name"] + channel = self.settings["channel"] + autorange = self.settings["autorange"] + + # Initialize device + self.controller = Keithley2600VISADriver(resource_name, + channel_name=channel, + autorange=autorange) + initialized = True + + # If slave device, retrieve controller object + else: + self.controller = controller + initialized = True + + # Initialize viewers pannel with the future type of data + mock_x = np.linspace(0, 1, 101) + mock_y = np.zeros(101) + _emit_xy_data(self, mock_x, mock_y) + + # Initialization successful + info = "Keithey 2600 initialization finished." + return info, initialized + + + def close(self): + """Terminate the communication protocol""" + if self.is_master: + self.controller.close() + self.controller = None + + + def grab_data(self, Naverage=1, **kwargs): + """Start a grab from the detector + + Parameters + ---------- + Naverage: int + Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to + True in class preamble and you should code this implementation) + kwargs: dict + others optionals arguments + """ + + # Retrieve parameters + startv = self.settings["startv"] + stopv = self.settings["stopv"] + stime = self.settings["stime"] + npoints = self.settings["npoints"] + + # Sweep and retrieve x and y axes + x, y = self.controller.channel.sweepV_measureI(startv, stopv, stime, npoints) + + # Emit data to PyMoDAQ + _emit_xy_data(self, x, y) + + + def stop(self): + """Stop the current grab hardware wise if necessary""" + pass + + +if __name__ == '__main__': + main(__file__) From 7ad1ab8fdf59944b065840d0678a2733debc9be4 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 28 Nov 2025 13:16:05 +0100 Subject: [PATCH 4/6] Update contributors --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0557657..9cedaa5 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,7 @@ Contributors * Nicolas Bruyant * Loïc Guilmard (loic.guilmard@insa-lyon.fr) * Anthony Buthod (anthony.buthod@insa-lyon.fr) +* Arnaud Meyer (arnaud.meyer@univ-st-etienne.fr) Instruments =========== @@ -46,4 +47,4 @@ Viewer0D * **Keithley2110**: Multimeter Keithley 2110 * **Keithley2700**: Keithley 2700 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules) * **Keithley2701**: Keithley 2701 Ethernet Multimeter/Switch System -- Ethernet/RS-232 -- 2 slots (7700 series modules) -* **Keithley2750**: Keithley 2750 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules) \ No newline at end of file +* **Keithley2750**: Keithley 2750 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules) From c1760aaefe39aa9566060ce0b528e658ae2d9997 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 28 Nov 2025 14:41:01 +0100 Subject: [PATCH 5/6] Update README with Keithley 2600 --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 9cedaa5..c88e786 100644 --- a/README.rst +++ b/README.rst @@ -45,6 +45,7 @@ Viewer0D * **Keithley_Pico**: Pico-Amperemeter Keithley 648X Series, 6430 and 6514 * **Keithley2100**: Multimeter Keithley 2100 * **Keithley2110**: Multimeter Keithley 2110 +* **Keithley2700**: Keithley 2600 series Sourcemeter * **Keithley2700**: Keithley 2700 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules) * **Keithley2701**: Keithley 2701 Ethernet Multimeter/Switch System -- Ethernet/RS-232 -- 2 slots (7700 series modules) * **Keithley2750**: Keithley 2750 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules) From 2ff30dffa5f9e41255d471f8d7a54dc35bf435da Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 28 Nov 2025 16:32:04 +0100 Subject: [PATCH 6/6] Update README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c88e786..0c1594c 100644 --- a/README.rst +++ b/README.rst @@ -45,7 +45,7 @@ Viewer0D * **Keithley_Pico**: Pico-Amperemeter Keithley 648X Series, 6430 and 6514 * **Keithley2100**: Multimeter Keithley 2100 * **Keithley2110**: Multimeter Keithley 2110 -* **Keithley2700**: Keithley 2600 series Sourcemeter +* **Keithley2600**: Keithley 2600 series Sourcemeter * **Keithley2700**: Keithley 2700 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules) * **Keithley2701**: Keithley 2701 Ethernet Multimeter/Switch System -- Ethernet/RS-232 -- 2 slots (7700 series modules) * **Keithley2750**: Keithley 2750 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules)