From 2c38d5edff6078766020f505dc3b1fa14008b82b Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 28 Nov 2025 11:20:17 +0100 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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 06/18] 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) From 2d3106d425f448c158f83c7396b8434982b8ad64 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Mon, 26 Jan 2026 14:45:39 +0100 Subject: [PATCH 07/18] Pass value instead of Quantity for current limit --- .../daq_viewer_plugins/plugins_1D/daq_1Dviewer_Keithley2600.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 293860c..bceb7b1 100644 --- 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 @@ -100,7 +100,7 @@ def commit_settings(self, param: Parameter): # Current limit if name == "ilimit": - self.controller.channel.current_limit = qty.to("A") + self.controller.channel.current_limit = qty.to("A").m def ini_detector(self, controller=None): From 32b78813da17af81515d53e6837451a06e456988 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Mon, 23 Feb 2026 13:31:16 +0100 Subject: [PATCH 08/18] Add constant I/V source modes --- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 7 +- .../keithley2600/keithley2600_VISADriver.py | 83 +++++++++++++++++-- 2 files changed, 79 insertions(+), 11 deletions(-) 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 index bceb7b1..bbff7d7 100644 --- 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 @@ -100,7 +100,7 @@ def commit_settings(self, param: Parameter): # Current limit if name == "ilimit": - self.controller.channel.current_limit = qty.to("A").m + self.controller.channel.Ilimit = qty.to("A").m def ini_detector(self, controller=None): @@ -138,7 +138,10 @@ def ini_detector(self, controller=None): self.controller = controller initialized = True - # Initialize viewers pannel with the future type of data + # Get current limit applied in the device + self.settings["ilimit"] = self.controller.channel.Ilimit + + # Initialize viewers panel 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) diff --git a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py index 939e5d3..a9d59ee 100644 --- a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py +++ b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py @@ -98,30 +98,57 @@ def _read(self): @property - def current_limit(self): + def Ilimit(self): """Get current limit [A] of the channel. Returns ------- - current_limit: float + ilimit: float Current limit [A] of the selected channel. """ self._write(f"print({self.smu}.source.limiti)") - limit = self._read() - return float(limit) + ilimit = self._read() + return float(ilimit) - @current_limit.setter - def current_limit(self, limit): + @Ilimit.setter + def Ilimit(self, ilimit): """Set current limit [A] of the channel. Parameters ---------- - limit: float + ilimit: float Current limit [A] to set. """ - limit = f"{limit:.6e}" - self._write(f"{self.smu}.source.limiti = {limit}") + ilimit = f"{ilimit:.6e}" + self._write(f"{self.smu}.source.limiti = {ilimit}") + + + @property + def Vlimit(self): + """Get voltage limit [A] of the channel. + + Returns + ------- + vlimit: float + Voltage limit [A] of the selected channel. + """ + self._write(f"print({self.smu}.source.limitv)") + vlimit = self._read() + return float(vlimit) + + + @Vlimit.setter + def Vlimit(self, vlimit): + """Set voltage limit [A] of the channel. + + Parameters + ---------- + vlimit: float + Voltage limit [A] to set. + """ + vlimit = f"{vlimit:.6e}" + self._write(f"{self.smu}.source.limitv = {vlimit}") def autorange(self): @@ -130,6 +157,44 @@ def autorange(self): self._write(f"{self.smu}.measure.autorangev = {self.smu}.AUTORANGE_ON") + def off(self, highz=False): + """Switch off channel output. + + Parameters + ---------- + highz: bool, default: False + Set output to high impedance mode in addition to switching off. + """ + offmode = 2 if highz else 0 + self._write(f"{self.smu}.source.output = {offmode}") + + + def constantI(self, isetpoint): + """Set channel output to constant current with the specified setpoint. + + Parameters + ---------- + isetpoint: float + Current [A] to set. + """ + isetpoint = f"{isetpoint:.6e}" + self._write(f"{self.smu}.source.output = 1") + self._write(f"{self.smu}.source.leveli = {isetpoint}") + + + def constantV(self, vsetpoint): + """Set channel output to constant voltage with the specified setpoint. + + Parameters + ---------- + vsetpoint: float + Voltage [V] to set. + """ + vsetpoint = f"{vsetpoint:.6e}" + self._write(f"{self.smu}.source.output = 1") + self._write(f"{self.smu}.source.levelv = {vsetpoint}") + + 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. From 1d606698ab57826df3bf4d915133567d33e7bc83 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Mon, 23 Feb 2026 15:57:44 +0100 Subject: [PATCH 09/18] Apply I limit at every measurement --- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 index bbff7d7..f3e9052 100644 --- 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 @@ -98,9 +98,7 @@ def commit_settings(self, param: Parameter): unit = param.opts.get("suffix") qty = Q_(val, unit) - # Current limit - if name == "ilimit": - self.controller.channel.Ilimit = qty.to("A").m + # No argument processing for now def ini_detector(self, controller=None): @@ -138,9 +136,6 @@ def ini_detector(self, controller=None): self.controller = controller initialized = True - # Get current limit applied in the device - self.settings["ilimit"] = self.controller.channel.Ilimit - # Initialize viewers panel with the future type of data mock_x = np.linspace(0, 1, 101) mock_y = np.zeros(101) @@ -175,6 +170,10 @@ def grab_data(self, Naverage=1, **kwargs): stopv = self.settings["stopv"] stime = self.settings["stime"] npoints = self.settings["npoints"] + ilimit = self.settings["ilimit"] + + # Apply current limit + self.controller.channel.Ilimit = ilimit # Sweep and retrieve x and y axes x, y = self.controller.channel.sweepV_measureI(startv, stopv, stime, npoints) From 07d5c97bc7f6d065a4cfb3f3ffdd7f860131281f Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Mon, 23 Feb 2026 16:01:43 +0100 Subject: [PATCH 10/18] Split handling of controller and channels --- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 25 +++++++++------- .../keithley2600/keithley2600_VISADriver.py | 29 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) 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 index f3e9052..6ebc65c 100644 --- 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 @@ -8,7 +8,7 @@ from pymodaq.utils.data import DataFromPlugins import pyvisa -from pymodaq_plugins_keithley.hardware.keithley2600.keithley2600_VISADriver import Keithley2600VISADriver +from pymodaq_plugins_keithley.hardware.keithley2600.keithley2600_VISADriver import Keithley2600VISADriver, Keithley2600Channel # Helper functions @@ -82,6 +82,7 @@ class DAQ_1DViewer_Keithley2600(DAQ_Viewer_base): def ini_attributes(self): # Type declaration of the controller self.controller: Keithley2600VISADriver = None + self.channel: Keithley2600Channel = None def commit_settings(self, param: Parameter): @@ -117,18 +118,16 @@ def ini_detector(self, controller=None): False if initialization failed otherwise True """ + # Get initialization parameters + resource_name = self.settings["resource_name"] + channel = self.settings["channel"] + autorange = self.settings["autorange"] + # 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) + self.controller = Keithley2600VISADriver(resource_name) initialized = True # If slave device, retrieve controller object @@ -136,6 +135,10 @@ def ini_detector(self, controller=None): self.controller = controller initialized = True + # Initialize channel + self.channel = self.controller.create_channel(channel_name=channel, + autorange=autorange) + # Initialize viewers panel with the future type of data mock_x = np.linspace(0, 1, 101) mock_y = np.zeros(101) @@ -173,10 +176,10 @@ def grab_data(self, Naverage=1, **kwargs): ilimit = self.settings["ilimit"] # Apply current limit - self.controller.channel.Ilimit = ilimit + self.channel.Ilimit = ilimit # Sweep and retrieve x and y axes - x, y = self.controller.channel.sweepV_measureI(startv, stopv, stime, npoints) + x, y = self.channel.sweepV_measureI(startv, stopv, stime, npoints) # Emit data to PyMoDAQ _emit_xy_data(self, x, y) diff --git a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py index a9d59ee..aad2314 100644 --- a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py +++ b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py @@ -21,29 +21,20 @@ class Keithley2600VISADriver: """ - def __init__(self, resource_name, channel_name="A", autorange=True, pyvisa_backend="@py"): + def __init__(self, resource_name, 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.""" @@ -51,6 +42,19 @@ def close(self): self._instr = None + def create_channel(self, channel_name="A", autorange=True): + """Create an object for driving an SMU channel connected to this device. + + Parameters + ---------- + channel_name: str, optional + Channel name. (default: "A") + autorange: bool, optional + Enable I and V autorange. (default: True) + """ + return Keithley2600Channel(self, channel_name, autorange) + + def _write(self, cmd): """Convenience methode to send a TSP command to the device.""" self._instr.write(cmd) @@ -62,7 +66,7 @@ def _read(self): class Keithley2600Channel: - """Class for handling a single channel on a Keithley 2600 sourcemeter.""" + """Class for handling a single SMU channel on a Keithley 2600 sourcemeter.""" def __init__(self, parent, channel, autorange): """Initialize class. @@ -162,8 +166,9 @@ def off(self, highz=False): Parameters ---------- - highz: bool, default: False + highz: bool, optional Set output to high impedance mode in addition to switching off. + (default: False) """ offmode = 2 if highz else 0 self._write(f"{self.smu}.source.output = {offmode}") From 41fe01630a55479881e90cc94d71f9694f4e87e7 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Mon, 23 Feb 2026 16:35:26 +0100 Subject: [PATCH 11/18] Move get_VISA_resources() to hardware driver --- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 21 ++----------------- .../keithley2600/keithley2600_VISADriver.py | 18 ++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) 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 index 6ebc65c..d4b835b 100644 --- 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 @@ -8,27 +8,10 @@ from pymodaq.utils.data import DataFromPlugins import pyvisa -from pymodaq_plugins_keithley.hardware.keithley2600.keithley2600_VISADriver import Keithley2600VISADriver, Keithley2600Channel +from pymodaq_plugins_keithley.hardware.keithley2600.keithley2600_VISADriver import Keithley2600VISADriver, Keithley2600Channel, get_VISA_resources # 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 @@ -68,7 +51,7 @@ class DAQ_1DViewer_Keithley2600(DAQ_Viewer_base): """ params = comon_parameters+[ - _build_param("resource_name", "VISA resource", "list", "", limits=_get_VISA_resources()), + _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"), diff --git a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py index aad2314..5464dc8 100644 --- a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py +++ b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py @@ -5,6 +5,24 @@ logger = set_logger(get_module_name(__file__)) +# 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 table_to_np(table): """Convert sequence of ASCII-encoded, comma-separated values to NumPy array.""" split = table.split(", ") From 9599a5f685b330d759d34f07216c047056695dd3 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Tue, 24 Feb 2026 09:11:17 +0100 Subject: [PATCH 12/18] Add voltage and current measurement to driver --- .../keithley2600/keithley2600_VISADriver.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py index 5464dc8..da3232f 100644 --- a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py +++ b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py @@ -179,6 +179,28 @@ def autorange(self): self._write(f"{self.smu}.measure.autorangev = {self.smu}.AUTORANGE_ON") + def measureI(self): + """Measure current [A].""" + self._write(f"print({self.smu}.measure.i())") + meas = self._read() + return float(meas) + + + def measureV(self): + """Measure voltage [V].""" + self._write(f"print({self.smu}.measure.v())") + meas = self._read() + return float(meas) + + + def measureIV(self): + """Measure simultaneously current [A] and voltage [V].""" + self._write(f"print({self.smu}.measure.iv())") + ret = self._read() + i, v = ret.split() + return float(i), float(v) + + def off(self, highz=False): """Switch off channel output. From 79d867ffef827cf8cf77777a1a56146285b52776 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Tue, 24 Feb 2026 12:50:03 +0100 Subject: [PATCH 13/18] Create daq_move for constant I/V source --- .../daq_move_plugins/daq_move_Keithley2600.py | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py diff --git a/src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py b/src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py new file mode 100644 index 0000000..ab91bea --- /dev/null +++ b/src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py @@ -0,0 +1,238 @@ +from typing import Union, List, Dict +from pymodaq.control_modules.move_utility_classes import (DAQ_Move_base, comon_parameters_fun, + main, DataActuatorType, DataActuator) + +from pymodaq_utils.utils import ThreadCommand # object used to send info back to the main thread +from pymodaq_gui.parameter import Parameter +from collections.abc import Callable + +import pyvisa +from pymodaq_plugins_keithley.hardware.keithley2600.keithley2600_VISADriver import Keithley2600VISADriver, Keithley2600Channel, get_VISA_resources + + +# Helper functions +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 + + +class DAQ_Move_Keithley2600(DAQ_Move_base): + """ Instrument plugin class for an actuator. + + This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Move module through inheritance via + DAQ_Move_base. It makes a bridge between the DAQ_Move module and the Python wrapper of a particular instrument. + + Compatible devices: Keithley 2600 series sourcemeters + Tested on: Keithley 2636B / PyMoDAQ 5.1.1 / Ubuntu 24.04 LTS + Installation instructions: no special drivers other than PyVISA + + Attributes: + ----------- + controller: object + The particular object that allow the communication with the hardware, in general a python wrapper around the + hardware library. + """ + + # General parameters + is_multiaxes = False + _axis_names: Union[List[str], Dict[str, int]] = ["Source"] + _controller_units: Union[str, List[str]] = "V" + _epsilon: Union[float, List[float]] = 0.01 + data_actuator_type = DataActuatorType.DataActuator + + # Parameter tree + params = comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilon) + [ + _build_param("resource_name", "VISA resource", "list", "", limits=get_VISA_resources()), + _build_param("channel", "Channel", "str", "A"), + _build_param("autorange", "Autorange", "bool", True), + _build_param("type", "Source type", "list", "", limits=["Voltage", "Current"]), + ] + + + def ini_attributes(self): + # For autocompletion + self.controller: Keithley2600VISADriver = None + self.channel: Keithley2600Channel = None + self._meas_function: Callable = None + self._move_function: Callable[float] = None + + + @property + def v_source(self): + """Returns True if source type is set to voltage.""" + return self.settings["type"] == "Voltage" + + + @property + def i_source(self): + """Returns True if source is type set to current.""" + return self.settings["type"] == "Current" + + + def _set_source_type(self): + """Adjust units and control function according to the selected source type.""" + + # Voltage source + if self.v_source: + self.axis_unit = "V" + self._meas_function = self.channel.measureV + self._move_function = self.channel.constantV + + # Current source + elif self.i_source: + self.axis_unit = "A" + self._meas_function = self.channel.measureI + self._move_function = self.channel.constantI + + # Unknown source type + else: + source_type = self.settings["type"] + raise ValueError(f"Unknown source type: {source_type}") + + # Update displayed value + self.current_value = self._meas_function() + self.emit_value(self.current_value) + + + def get_actuator_value(self): + """Get the current value from the hardware with scaling conversion. + + Returns + ------- + float: The position obtained after scaling conversion. + """ + pos = DataActuator(data=self._meas_function(), units=self.axis_unit) + pos = self.get_position_with_scaling(pos) + return pos + + + def user_condition_to_reach_target(self) -> bool: + """ Implement a condition for exiting the polling mechanism and specifying that the + target value has been reached + + Returns + ------- + bool: if True, PyMoDAQ considers the target value has been reached + """ + return True + + + def close(self): + """Terminate the communication protocol""" + if self.is_master: + self.controller.close() + + + 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") + + # Change parameters in function of source type + if name == "type": + self._set_source_type() + + + def ini_stage(self, controller=None): + """Actuator communication initialization + + Parameters + ---------- + controller: (object) + custom object of a PyMoDAQ plugin (Slave case). None if only one actuator by controller (Master case) + + Returns + ------- + info: str + initialized: bool + False if initialization failed otherwise True + """ + # Get initialization parameters + resource_name = self.settings["resource_name"] + channel = self.settings["channel"] + autorange = self.settings["autorange"] + + # If stand-alone device, initialize controller object + if self.is_master: + + # Initialize device + self.controller = Keithley2600VISADriver(resource_name) + initialized = True + + # If slave device, retrieve controller object + else: + self.controller = controller + initialized = True + + # Initialize channel + self.channel = self.controller.create_channel(channel_name=channel, + autorange=autorange) + + # Change parameters in function of source type + self._set_source_type() + + # Initialization successful + info = "Keithey 2600 initialization finished." + return info, initialized + + + def move_abs(self, value: DataActuator): + """ Move the actuator to the absolute target defined by value + + Parameters + ---------- + value: (float) value of the absolute target positioning + """ + value = self.check_bound(value) #if user checked bounds, the defined bounds are applied here + self.target_value = value + value = self.set_position_with_scaling(value) # apply scaling if the user specified one + self._move_function(value.value(self.axis_unit)) + self.emit_status(ThreadCommand('Update_Status', [f"Moving abs to {value}"])) + + + def move_rel(self, value: DataActuator): + """ Move the actuator to the relative target actuator value defined by value + + Parameters + ---------- + value: (float) value of the relative target positioning + """ + value = self.check_bound(self.current_position + value) - self.current_position + self.target_value = value + self.current_position + value = self.set_position_relative_with_scaling(value) + self._move_function(self.target_value.value(self.axis_unit)) + self.emit_status(ThreadCommand('Update_Status', [f"Moving ref by {value}"])) + + + def move_home(self): + """Call the reference method of the controller""" + pass + + + def stop_motion(self): + """Stop the actuator and emits move_done signal""" + self.channel.off() + self.emit_status(ThreadCommand('Update_Status', [f"Stopping movement"])) + + +if __name__ == '__main__': + main(__file__) From 7dde3d6fd6472e22c2d59503f7cff0d0ee99d451 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Tue, 24 Feb 2026 13:55:26 +0100 Subject: [PATCH 14/18] Set source mode properly in constant I/V modes --- .../hardware/keithley2600/keithley2600_VISADriver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py index da3232f..7dfaaf2 100644 --- a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py +++ b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py @@ -223,6 +223,7 @@ def constantI(self, isetpoint): Current [A] to set. """ isetpoint = f"{isetpoint:.6e}" + self._write(f"{self.smu}.source.func = 0") self._write(f"{self.smu}.source.output = 1") self._write(f"{self.smu}.source.leveli = {isetpoint}") @@ -236,6 +237,7 @@ def constantV(self, vsetpoint): Voltage [V] to set. """ vsetpoint = f"{vsetpoint:.6e}" + self._write(f"{self.smu}.source.func = 1") self._write(f"{self.smu}.source.output = 1") self._write(f"{self.smu}.source.levelv = {vsetpoint}") From 491db63ed0c711e0ff3c21f40b9d3e9f969e6074 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Thu, 26 Feb 2026 09:46:32 +0100 Subject: [PATCH 15/18] Update README --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 0c1594c..bb331ca 100644 --- a/README.rst +++ b/README.rst @@ -38,6 +38,7 @@ Actuator ++++++++ * **Keithley2400**: Sourcemeter Keithley 2400 (using pymeasure intermediate package) +* **Keithley2600**: Keithley 2600 series Sourcemeter Viewer0D ++++++++ From 0983fb27f148fc5847f19f705b593c0cb91b3901 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 27 Feb 2026 11:34:20 +0100 Subject: [PATCH 16/18] Add settings to keep DUT polarized after I-V scan --- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 index d4b835b..978961e 100644 --- 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 @@ -58,7 +58,9 @@ class DAQ_1DViewer_Keithley2600(DAQ_Viewer_base): _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) + _build_param("autorange", "Autorange", "bool", True), + _build_param("idle_pol_on", "Keep polarized after scan", "bool", False), + _build_param("idle_pol_v", "Polarization voltage after scan", "float", 0, unit="V"), ] @@ -157,6 +159,8 @@ def grab_data(self, Naverage=1, **kwargs): stime = self.settings["stime"] npoints = self.settings["npoints"] ilimit = self.settings["ilimit"] + idle_pol_on = self.settings["idle_pol_on"] + idle_pol_v = self.settings["idle_pol_v"] # Apply current limit self.channel.Ilimit = ilimit @@ -167,6 +171,10 @@ def grab_data(self, Naverage=1, **kwargs): # Emit data to PyMoDAQ _emit_xy_data(self, x, y) + # If "keep polarized after scan" is selected, apply selected voltage + if idle_pol_on: + self.channel.constantV(idle_pol_v) + def stop(self): """Stop the current grab hardware wise if necessary""" From f8f0c980ebc271c654bd1d32a6410ba15510fe05 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Fri, 27 Feb 2026 13:19:47 +0100 Subject: [PATCH 17/18] More explicit method names for sourcing I/V --- .../daq_move_plugins/daq_move_Keithley2600.py | 4 ++-- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 2 +- .../hardware/keithley2600/keithley2600_VISADriver.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py b/src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py index ab91bea..d2c7c23 100644 --- a/src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py +++ b/src/pymodaq_plugins_keithley/daq_move_plugins/daq_move_Keithley2600.py @@ -87,13 +87,13 @@ def _set_source_type(self): if self.v_source: self.axis_unit = "V" self._meas_function = self.channel.measureV - self._move_function = self.channel.constantV + self._move_function = self.channel.sourceV # Current source elif self.i_source: self.axis_unit = "A" self._meas_function = self.channel.measureI - self._move_function = self.channel.constantI + self._move_function = self.channel.sourceI # Unknown source type else: 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 index 978961e..e618a8f 100644 --- 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 @@ -173,7 +173,7 @@ def grab_data(self, Naverage=1, **kwargs): # If "keep polarized after scan" is selected, apply selected voltage if idle_pol_on: - self.channel.constantV(idle_pol_v) + self.channel.sourceV(idle_pol_v) def stop(self): diff --git a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py index 7dfaaf2..d6b8153 100644 --- a/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py +++ b/src/pymodaq_plugins_keithley/hardware/keithley2600/keithley2600_VISADriver.py @@ -214,7 +214,7 @@ def off(self, highz=False): self._write(f"{self.smu}.source.output = {offmode}") - def constantI(self, isetpoint): + def sourceI(self, isetpoint): """Set channel output to constant current with the specified setpoint. Parameters @@ -228,7 +228,7 @@ def constantI(self, isetpoint): self._write(f"{self.smu}.source.leveli = {isetpoint}") - def constantV(self, vsetpoint): + def sourceV(self, vsetpoint): """Set channel output to constant voltage with the specified setpoint. Parameters From 29e78b213d6862416426e9f16cae20c89762f507 Mon Sep 17 00:00:00 2001 From: Arnaud Meyer Date: Mon, 23 Feb 2026 11:36:09 +0100 Subject: [PATCH 18/18] Add recording of acquisition start and end times --- .../plugins_1D/daq_1Dviewer_Keithley2600.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 index e618a8f..cf72078 100644 --- 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 @@ -10,6 +10,9 @@ import pyvisa from pymodaq_plugins_keithley.hardware.keithley2600.keithley2600_VISADriver import Keithley2600VISADriver, Keithley2600Channel, get_VISA_resources +import datetime +from qtpy.QtCore import QDateTime + # Helper functions def _build_param(name, title, type, value, limits=None, unit=None, **kwargs): @@ -61,6 +64,10 @@ class DAQ_1DViewer_Keithley2600(DAQ_Viewer_base): _build_param("autorange", "Autorange", "bool", True), _build_param("idle_pol_on", "Keep polarized after scan", "bool", False), _build_param("idle_pol_v", "Polarization voltage after scan", "float", 0, unit="V"), + _build_param("meas_start", "Last measurement start time", "date_time", + QDateTime(datetime.datetime.now()), readonly=True), + _build_param("meas_end", "Last measurement end time", "date_time", + QDateTime(datetime.datetime.now()), readonly=True), ] @@ -165,9 +172,17 @@ def grab_data(self, Naverage=1, **kwargs): # Apply current limit self.channel.Ilimit = ilimit + # Get timestamp before acquisition + start_time = datetime.datetime.now() + # Sweep and retrieve x and y axes x, y = self.channel.sweepV_measureI(startv, stopv, stime, npoints) + # Get timestamp after acquisition and update timestamp parameters + end_time = datetime.datetime.now() + self.settings["meas_start"] = QDateTime(start_time) + self.settings["meas_end"] = QDateTime(end_time) + # Emit data to PyMoDAQ _emit_xy_data(self, x, y)