Skip to content

Commit 72a7e2f

Browse files
committed
6430 -> initial commit
1 parent c2e4005 commit 72a7e2f

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

src/SMU-Keithley_6430/license.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
This Device Class is published under the terms of the MIT License.
2+
Required Third Party Libraries, which are included in the Device Class
3+
package for convenience purposes, may have a different license. You can
4+
find those in the corresponding folders or contact the maintainer.
5+
6+
MIT License
7+
8+
Copyright (c) 2025 SweepMe! GmbH (sweep-me.net)
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy
11+
of this software and associated documentation files (the "Software"), to deal
12+
in the Software without restriction, including without limitation the rights
13+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
copies of the Software, and to permit persons to whom the Software is
15+
furnished to do so, subject to the following conditions:
16+
17+
The above copyright notice and this permission notice shall be included in all
18+
copies or substantial portions of the Software.
19+
20+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
SOFTWARE.

src/SMU-Keithley_6430/main.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# This Device Class is published under the terms of the MIT License.
2+
# Required Third Party Libraries, which are included in the Device Class
3+
# package for convenience purposes, may have a different license. You can
4+
# find those in the corresponding folders or contact the maintainer.
5+
#
6+
# MIT License
7+
#
8+
# Copyright (c) 2025 SweepMe! GmbH (sweep-me.net)
9+
#
10+
# Permission is hereby granted, free of charge, to any person obtaining a copy
11+
# of this software and associated documentation files (the "Software"), to deal
12+
# in the Software without restriction, including without limitation the rights
13+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
# copies of the Software, and to permit persons to whom the Software is
15+
# furnished to do so, subject to the following conditions:
16+
#
17+
# The above copyright notice and this permission notice shall be included in all
18+
# copies or substantial portions of the Software.
19+
#
20+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
# SOFTWARE.
27+
#
28+
# SweepMe! driver
29+
# * Module: SMU
30+
# * Instrument: Keithley 6430
31+
32+
from __future__ import annotations
33+
34+
from typing import Any
35+
36+
from pysweepme.EmptyDeviceClass import EmptyDevice
37+
38+
39+
class Device(EmptyDevice):
40+
"""Driver for the Keithley 6430."""
41+
42+
def __init__(self) -> None:
43+
"""Initialize the driver class and the instrument parameters."""
44+
super().__init__()
45+
46+
self.shortname = "6430" # short name will be shown in the sequencer
47+
48+
# SweepMe! parameters
49+
self.variables = ["Voltage", "Current"]
50+
self.units = ["V", "A"]
51+
self.plottype = [True, True]
52+
self.savetype = [True, True]
53+
54+
# Communication Parameters
55+
self.port_string: str = ""
56+
self.port_manager = True
57+
self.port_types = ["GPIB", "COM", "TCPIP"]
58+
59+
# Measurement parameters
60+
self.channel: int = 1
61+
62+
def update_gui_parameters(self, parameters: dict[str, Any]) -> dict[str, Any]:
63+
"""Determine the new GUI parameters of the driver depending on the current parameters."""
64+
del parameters
65+
return {
66+
"Channel": [1, 2, 3],
67+
"SweepMode": ["None", "Voltage in V", "Current in A"],
68+
"Compliance": 0.0,
69+
"Range": ["AUTO", "MIN", "MAX"],
70+
"RangeVoltage": ["AUTO", "MIN", "MAX"],
71+
}
72+
73+
def apply_gui_parameters(self, parameters: dict[str, Any]) -> None:
74+
"""Receive the values of the GUI parameters that were set by the user in the SweepMe! GUI."""
75+
self.port_string = parameters.get("Port", "")
76+
self.channel = parameters.get("Channel", 1)
77+
78+
self.sweep_mode = parameters.get("SweepMode", "")
79+
self.measure_function = parameters.get("MeasureFunction", "")
80+
self.compliance = parameters.get("Compliance", 0.0)
81+
self.range = parameters.get("Range", "")
82+
83+
def connect(self) -> None:
84+
"""Connect to the device. This function is called only once at the start of the measurement."""
85+
86+
def disconnect(self) -> None:
87+
"""Disconnect from the device. This function is called only once at the end of the measurement."""
88+
89+
def initialize(self) -> None:
90+
"""Initialize the device. This function is called only once at the start of the measurement."""
91+
92+
def deinitialize(self) -> None:
93+
"""Deinitialize the device. This function is called only once at the end of the measurement."""
94+
95+
def configure(self) -> None:
96+
"""Configure the device. This function is called every time the device is used in the sequencer."""
97+
# set compliance, range and speed
98+
if self.sweep_mode.startswith("Voltage"):
99+
self.set_compliance_current(self.compliance)
100+
elif self.sweep_mode.startswith("Current"):
101+
self.set_compliance_voltage(self.compliance)
102+
103+
def unconfigure(self) -> None:
104+
"""Unconfigure the device. This function is called when the procedure leaves a branch of the sequencer."""
105+
self.output_off()
106+
107+
def apply(self) -> None:
108+
"""'apply' is used to set the new setvalue that is always available as 'self.value'."""
109+
if self.sweep_mode.startswith("Voltage"):
110+
self.set_voltage(self.value)
111+
elif self.sweep_mode.startswith("Current"):
112+
self.set_current(self.value)
113+
114+
def call(self) -> list:
115+
"""Return the measurement results. Must return as many values as defined in self.variables."""
116+
return [self.voltage, self.current]
117+
118+
# Wrapped Functions
119+
120+
def output_on(self) -> None:
121+
"""Turn the instrument output on (:OUTPut:STATe ON)."""
122+
self.port.write(":OUTPut:STATe ON")
123+
124+
def output_off(self) -> None:
125+
"""Turn the instrument output off (:OUTPut:STATe OFF)."""
126+
self.port.write(":OUTPut:STATe OFF")
127+
128+
def set_output(self, enable: bool) -> None:
129+
"""Convenience wrapper to set output ON/OFF."""
130+
self.port.write(f":OUTPut:STATe {'ON' if enable else 'OFF'}")
131+
132+
# Sourcing
133+
def set_voltage(self, value: float) -> None:
134+
"""Set source voltage level (V-Source). Uses SOURce:VOLTage:LEVel."""
135+
self.port.write(f":SOURce:VOLTage:LEVel {value}")
136+
137+
def set_current(self, value: float) -> None:
138+
"""Set source current level (I-Source). Uses SOURce:CURRent:LEVel."""
139+
self.port.write(f":SOURce:CURRent:LEVel {value}")
140+
141+
def set_compliance_voltage(self, v_limit: float) -> None:
142+
"""When sourcing current, set voltage compliance (SOURce:VOLTage:LEVel:PROTection or SENSe:VOLT:PROT?).
143+
Using SENSe:VOLTage:PROTection is common for SCPI-capable SourceMeters.
144+
"""
145+
# Many Keithley instruments use :SOURce:VOLTage:LEVel and :SENSe:VOLTage:PROTection; try the SENSe form first.
146+
self.port.write(f":SENSe:VOLTage:PROTection {v_limit}")
147+
148+
def set_compliance_current(self, i_limit: float) -> None:
149+
"""When sourcing voltage, set current compliance (SOURce:CURRent:LEVel:PROTection or SENSe:CURR:PROT?)."""
150+
self.port.write(f":SENSe:CURRent:PROTection {i_limit}")
151+
152+
# Measurement wrappers
153+
def measure_voltage(self) -> float:
154+
"""Measure DC voltage using :MEASure:VOLTage?"""
155+
resp = self.query_raw(":MEASure:VOLTage:DC?")
156+
return float(resp)
157+
158+
def measure_current(self) -> float:
159+
"""Measure DC current using :MEASure:CURRent?"""
160+
resp = self.query_raw(":MEASure:CURRent:DC?")
161+
return float(resp)
162+
163+
def measure_resistance(self) -> float:
164+
"""Measure resistance using :MEASure:RESistance?"""
165+
resp = self.query_raw(":MEASure:RESistance?")
166+
return float(resp)
167+
168+
def read_measurement(self) -> float:
169+
"""
170+
Use :READ? to trigger a source/measure and return numeric value.
171+
:READ? typically returns a comma-separated list (value, unit, status...).
172+
Here we read the first numeric value from the response.
173+
"""
174+
raw = self.query_raw(":READ?")
175+
# parse the first token that looks like a float
176+
for token in raw.replace(",", " ").split():
177+
try:
178+
return float(token)
179+
except Exception:
180+
continue
181+
# couldn't parse numeric value; raise error
182+
raise RuntimeError(f"Could not parse numeric measurement from response: '{raw}'")

0 commit comments

Comments
 (0)