Skip to content

Commit 19ea62f

Browse files
authored
Merge pull request #40 from AnnaGiasson/dev, Added new device drivers
New Devices: - Keithley Data Acquisition system (daq.Keithley_DAQ6510) - Tektronix 5 series MSO oscilloscope (oscilloscope.Tektronix_MSO5xB) - Test Equity Thermal Chamber (temperaturecontroller.TestEquity_1007C) - Test Equity Thermal Chamber (temperaturecontroller.Thermotron_2800) - Keysight Power Supply (source.Keysight_RP7900) - Keysight Multi-output Supply (source.Keysight_EDU36311A)
2 parents aa5b1d7 + 2f04bdd commit 19ea62f

File tree

18 files changed

+1710
-13
lines changed

18 files changed

+1710
-13
lines changed
19.8 MB
Binary file not shown.
8.36 MB
Binary file not shown.
30.2 MB
Binary file not shown.
448 KB
Binary file not shown.
Binary file not shown.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pythonequipmentdrivers"
7-
version = "2.10.0"
7+
version = "2.11.0"
88
authors = [
99
{ name="Anna Giasson", email="[email protected]" },
1010
]
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from ._agilent_34972a import Agilent_34972A
2+
from ._keithley_daq6510 import Keithley_DAQ6510
23

3-
__all__ = ["Agilent_34972A"]
4+
__all__ = ["Agilent_34972A", "Keithley_DAQ6510"]
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
from enum import Enum
2+
from itertools import batched
3+
4+
from ..core import VisaResource
5+
6+
7+
class Keithley_DAQ6510(VisaResource):
8+
9+
def __init__(self, address, clear=False, **kwargs):
10+
super().__init__(address, clear, **kwargs)
11+
12+
# check this is the correct device
13+
is_daq6510 = ("keithley" in self.idn.lower()) and (
14+
"daq6510" in self.idn.lower()
15+
)
16+
if not is_daq6510:
17+
raise ValueError(
18+
f"Instrument at {address} is not a Keithley DAQ6510 data acquision system"
19+
)
20+
21+
self.write_resource("*LANG SCPI")
22+
23+
# Open all relays, start in a safe known state
24+
self.write_resource("ROUT:OPEN:ALL")
25+
26+
def clear_registers(self) -> None:
27+
self.reset()
28+
self.clear()
29+
self.write_resource("STATus:PRESet") # Clears event registers
30+
self.clear_buffer()
31+
32+
def clear_buffer(self) -> None:
33+
self.write_resource("TRAC:CLE")
34+
35+
def get_status(self) -> list[str]:
36+
# TODO: Validate response under different status flags
37+
status_reg = int(self.query_resource("STAT:OPER:COND?"))
38+
39+
bit_decoding = (
40+
"Measurement Summary", # B0
41+
"N/A", # B1
42+
"Error Availible",
43+
"Questionable Summary",
44+
"Message Availible",
45+
"Event Summary",
46+
"Request for Service", # B6
47+
"Operation Summary", # B7
48+
)
49+
50+
status_flags = []
51+
for n in range(len(bit_decoding)):
52+
if (status_reg >> n) & 0b1:
53+
status_flags.append(bit_decoding[n])
54+
55+
return status_flags
56+
57+
# def set_measurement_type(self, channel: int, type: Measurement_Type) -> None:
58+
# self.write_resource(f'SENSe:FUNCtion "{type.value}", (@{channel})')
59+
60+
class Measurement_Type(Enum):
61+
VOLTAGE_DC = "VOLT:DC"
62+
RESISTANCE = "RES"
63+
TEMPERATURE = "TEMP"
64+
VOLTAGE_AC = "VOLT:AC"
65+
CONTINUITY = "CONT"
66+
CURRENT_DC = "CURR:DC"
67+
DIODE = "DIOD"
68+
FREQUENCY = "FREQ:VOLT"
69+
CURRENT_AC = "CURR:AC"
70+
CAPACITANCE = "CAP"
71+
PERIOD = "PER:VOLT"
72+
NONE = "NONE"
73+
74+
class Range_Value:
75+
class VOLTAGE_DC(Enum):
76+
AUTO = ":AUTO ON"
77+
_100mV = " 100e-3"
78+
_1V = " 1"
79+
_10V = " 10"
80+
_100V = " 100"
81+
_1000V = " 1000"
82+
83+
class RESISTANCE(Enum):
84+
AUTO = ":AUTO ON"
85+
_1ohm = " 1"
86+
_10ohm = " 10"
87+
_100ohm = " 100"
88+
_1kohm = " 1e3"
89+
_10kohm = " 10e3"
90+
_100kohm = " 100e3"
91+
_1Mohm = " 1e6"
92+
_10Mohm = " 10e6"
93+
_100Mohm = " 100e6"
94+
95+
class VOLTAGE_AC(Enum):
96+
AUTO = ":AUTO ON"
97+
_100mV = " 100e-3"
98+
_1V = " 1"
99+
_10V = " 10"
100+
_100V = " 100"
101+
_750V = " 750"
102+
103+
class CURRENT_DC(Enum):
104+
AUTO = ":AUTO ON"
105+
_10uA = " 10e-6"
106+
_100uA = " 100e-6"
107+
_1mA = " 1e-3"
108+
_10mA = " 10e-3"
109+
_100mA = " 100e-3"
110+
_1A = " 1"
111+
_3A = " 3"
112+
113+
class CURRENT_AC(Enum):
114+
AUTO = ":AUTO ON"
115+
_100uA = " 100e-6"
116+
_1mA = " 1e-3"
117+
_10mA = " 10e-3"
118+
_100mA = " 100e-3"
119+
_1A = " 1"
120+
_3A = " 3"
121+
122+
class CAPACITANCE(Enum):
123+
AUTO = ":AUTO ON"
124+
_1nF = " 1e-9"
125+
_10nF = " 10e-9"
126+
_100nF = " 100e-9"
127+
_1uF = " 1e-6"
128+
_10uF = " 10e-6"
129+
_100uF = " 100e-6"
130+
131+
def set_function(self, function: Measurement_Type, *channels: int) -> None:
132+
133+
function_str = function.value
134+
135+
self.write_resource(f'FUNC "{function_str}", (@{",".join(map(str, channels))})')
136+
137+
def get_function(self, *channels: int) -> tuple[Measurement_Type]:
138+
139+
response = self.query_resource(
140+
f'Sense:Function? (@{",".join(map(str, channels))})'
141+
)
142+
143+
return tuple(
144+
self.Measurement_Type(channel_func) for channel_func in response.split(";")
145+
)
146+
147+
def set_scan_channels(self, *channels: int) -> None:
148+
self.write_resource(f'ROUT:SCAN (@{",".join(map(str, channels))})')
149+
150+
def get_scan_channels(self) -> tuple[int]:
151+
"""
152+
get_scan_channels()
153+
154+
Returns the channel numbers configured for the current scan
155+
156+
Returns:
157+
tuple[int]: channel numbers
158+
"""
159+
160+
response = self.query_resource("ROUT:SCAN?")
161+
str_channels = response[2:-1] # strip off formatting characters
162+
163+
if len(str_channels) == 0: # no channels
164+
return tuple()
165+
166+
if ":" not in str_channels: # non contiguous channels
167+
return tuple(map(int, str_channels.split(",")))
168+
169+
channel_list: list[int] = []
170+
for grouping in str_channels.split(","):
171+
if ":" not in grouping: # single channel
172+
channel_list.append(int(grouping))
173+
continue
174+
175+
# range of contiguous channels
176+
start_channel, end_channel = map(int, grouping.split(":"))
177+
channel_list.extend(range(start_channel, end_channel + 1, 1))
178+
179+
return tuple(channel_list)
180+
181+
def run_scan(self, count: int | None = None) -> None:
182+
183+
if count is not None:
184+
self.set_scan_count(count)
185+
186+
self.write_resource("INIT")
187+
188+
def set_scan_count(self, count: int) -> None:
189+
self.write_resource(f"Route:scan:count:scan {count}")
190+
191+
def get_scan_count(self) -> int:
192+
response = self.query_resource(f"Route:scan:count:scan?")
193+
return int(response)
194+
195+
def fetch_scan_data(
196+
self, return_sample_time: bool = False
197+
) -> dict[int, float | tuple[float, float] | list[float | tuple[float, float]]]:
198+
199+
n_channels = len(self.get_scan_channels())
200+
n_samples = self.get_scan_count()
201+
202+
START_INDEX = 1 # min value of 1
203+
BUFFER_NAME = "defbuffer1"
204+
read_type = "chan,rel,read" if return_sample_time else "chan,read"
205+
response = self.query_resource(
206+
f'trace:data? {START_INDEX}, {n_samples*n_channels}, "{BUFFER_NAME}", {read_type}'
207+
)
208+
209+
batched_data = batched(response.split(","), 3 if return_sample_time else 2)
210+
211+
data = {}
212+
for meas in batched_data:
213+
214+
chan = int(meas[0])
215+
datum = (
216+
tuple(map(float, meas[1:])) if return_sample_time else float(meas[1])
217+
)
218+
219+
if n_samples == 1:
220+
data[chan] = datum
221+
continue
222+
223+
if chan not in data.keys():
224+
data[chan] = []
225+
226+
data[chan].append(datum)
227+
return data
228+
229+
def get_scan_status(self) -> dict[str, str | int]: # TODO: Validate functionallity
230+
response = self.query_resource("ROUTe:scan:state?")
231+
232+
state, scan_count, step_count = response.split(";")
233+
scan_count = int(scan_count)
234+
step_count = int(step_count)
235+
236+
return {
237+
"state": state,
238+
"scan_count": scan_count,
239+
"step_count": step_count,
240+
}
241+
242+
def set_range(self, rng: Range_Value) -> None:
243+
function_str = (
244+
getattr(self.Measurement_Type, str(rng.__class__.__name__)).value
245+
).split(":")
246+
247+
self.write_resource(f"{function_str[0]}:RANG{rng.value}")
248+
249+
def set_aperture_time(
250+
self,
251+
function: Measurement_Type,
252+
aperature_time: float | None = None,
253+
*channels: int,
254+
) -> None:
255+
function_str = function.value.split(":")
256+
# TODO: If support is ever added for Dititize Voltage/Current then default support needs to be added, AUTO is arguemnt for default
257+
if (
258+
aperature_time is None
259+
): # If user doesn't pass a time value set to proper default value.
260+
if (
261+
function_str[0] == "FREQ" or function_str[0] == "PER"
262+
): # 200ms for Frequency and Period
263+
time = 200e-3
264+
else: # 16.67ms for everything else
265+
time = 16.67e-3
266+
else:
267+
time = aperature_time
268+
269+
self.write_resource(
270+
f"{function_str[0]}:APER {time}, (@{",".join(map(str, channels))})"
271+
)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from ._lecroy_wr8xxx import Lecroy_WR8xxx
22
from ._tektronix_dpo4xxx import Tektronix_DPO4xxx
3+
from ._tektronix_mso5xb import Tektronix_MSO5xB
34
from ._tektronix_mso5xxx import Tektronix_MSO5xxx
45

56
__all__ = [
67
"Lecroy_WR8xxx",
78
"Tektronix_DPO4xxx",
89
"Tektronix_MSO5xxx",
10+
"Tektronix_MSO5xB",
911
]

0 commit comments

Comments
 (0)