Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========
Expand All @@ -44,6 +45,7 @@ Viewer0D
* **Keithley_Pico**: Pico-Amperemeter Keithley 648X Series, 6430 and 6514
* **Keithley2100**: Multimeter Keithley 2100
* **Keithley2110**: Multimeter Keithley 2110
* **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)
* **Keithley2750**: Keithley 2750 Multimeter/Switch System -- RS-232/GPIB -- 2 slots (7700 series modules)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the 2600 serie you just workoed on?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c1760aa

2 changes: 1 addition & 1 deletion plugin_info.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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__)
Original file line number Diff line number Diff line change
@@ -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