-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathLecroyScope.py
More file actions
337 lines (264 loc) · 11.2 KB
/
LecroyScope.py
File metadata and controls
337 lines (264 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# (c) 2020, ETH Zurich, Power Electronic Systems Laboratory, T. Guillod
import vxi11
import numpy
class LecroyScope():
"""
This Python class remote controls the "Lecroy WaveSurfer 24MXs" over ethernet:
- connect to the device
- set up the channels
- control the trigger
- download a screenshot
- download the waveform data
This class:
- uses the "VXI-11" ethernet instrument control protocol
- was tested on "MS Windows" but should run with Linux
- should also be easy to adapt to other Lecroy scopes
- was tested with Python 2.7 but should run with Python 3.x
This class is meant as a lightweight code to be used as a "code snippet" and not as a full package.
Be careful, the Lecroy instruction are messy and incoherent between the scope models.
"""
def __init__(self, ip):
"""
Constructor of the LecroyScope class.
"""
self.ip = ip
self.channel_name = ["C1", "C2", "C3", "C4"]
self.scope = None
self.time = None
self.channel = None
def open(self):
"""
Make the connection to the device. Reset the device.
"""
self.scope = vxi11.Instrument(self.ip)
self.reset_config()
def close(self):
"""
Close the connection to the device.
"""
self.scope = None
self.time = None
self.channel = None
def set_config(self, config):
"""
Configure the channels, the timebase, and the trigger.
"""
# save the config
self.time = config["time"]
self.channel = config["channel"]
self.trigger = config["trigger"]
# reset old config
self.reset_config()
# set the timebase and the time offset
self.scope.write('VBS "app.Acquisition.Horizontal.HorScale = ""%e"' % (self.time["div"]))
self.scope.write('VBS "app.Acquisition.Horizontal.HorOffsetOrigin = ""%e"' % (self.time["offset_origin"]))
self.scope.write('VBS "app.Acquisition.Horizontal.HorOffset = ""%e"' % (self.time["offset"]))
# set the sample storage
self.scope.write('VBS "app.Acquisition.Horizontal.MaxSamples = ""%e"' % (self.time["sample"]))
self.scope.write('VBS "app.Acquisition.Horizontal.ReferenceClock = ""INT"')
self.scope.write('VBS "app.Acquisition.Horizontal.SampleClock = ""INT"')
# set the channels
for name_tmp in self.channel:
# check channel name
assert name_tmp in self.channel_name, "invalid channel"
data_tmp = self.channel[name_tmp]
# view settings
self.scope.write('VBS "app.Acquisition.%s.View = True' % (name_tmp))
self.scope.write('VBS "app.Acquisition.%s.InterpolateType = ""Linear"' % (name_tmp))
# bandwidth limitation
assert data_tmp["bandwidth"] in ["Full", "20MHz"]
self.scope.write('VBS "app.Acquisition.%s.BandwidthLimit = ""%s"' % (name_tmp, data_tmp["bandwidth"]))
# channel coupling type
assert data_tmp["coupling"] in ["AC1M", "DC1M", "DC50", "Gnd"]
self.scope.write('VBS "app.Acquisition.%s.Coupling = ""%s"' % (name_tmp, data_tmp["coupling"]))
# digital bit averaging
assert data_tmp["filter"] in ["0.5bits", "1.5bits", "1bits", "2.5bits", "2bits", "3bits", "None"]
self.scope.write('VBS "app.Acquisition.%s.EnhanceResType = ""%s"' % (name_tmp, data_tmp["filter"]))
# set scale, skew, offset, and invert
self.scope.write('VBS "app.Acquisition.%s.Invert = %s' % (name_tmp, str(data_tmp["invert"])))
self.scope.write('VBS "app.Acquisition.%s.Deskew = ""%e"' % (name_tmp, data_tmp["skew"]))
self.scope.write('VBS "app.Acquisition.%s.ProbeAttenuation = ""%e"' % (name_tmp, data_tmp["attenuation"]))
self.scope.write('VBS "app.Acquisition.%s.VerScale = ""%e"' % (name_tmp, data_tmp["div"]))
self.scope.write('VBS "app.Acquisition.%s.VerOffset = ""%e"' % (name_tmp, data_tmp["offset"]))
# check trigger channel name
name_tmp = self.trigger["channel"]
assert name_tmp in self.channel_name, "invalid channel"
# set trigger source
self.scope.write('VBS "app.Acquisition.Trigger.Source = ""%s"' % (name_tmp))
self.scope.write('VBS "app.Acquisition.Trigger.Type = ""edge"')
# set trigger slope
assert self.trigger["edge"] in ["Either", "Negative", "Positive", "Window"]
self.scope.write('VBS "app.Acquisition.Trigger.%s.Slope = ""%s"' % (name_tmp, self.trigger["edge"]))
# set trigger coupling
assert self.trigger["coupling"] in ["AC", "DC", "HFREJ", "LFREJ"]
self.scope.write('VBS "app.Acquisition.Trigger.%s.Coupling = ""%s"' % (name_tmp, self.trigger["coupling"]))
# set trigger level
self.scope.write('VBS "app.Acquisition.Trigger.%s.Level = ""%e"' % (name_tmp, self.trigger["level"]))
self.scope.write('VBS "app.Acquisition.Trigger.%s.WindowSize = ""%e"' % (name_tmp, self.trigger["window"]))
# force a first trigger event
self.force()
def reset_config(self):
"""
Reset the configuration of the scope, remove the traces.
"""
self.scope.write("*RST")
self.scope.write("*CLS")
self.scope.write("CLSW")
self.scope.write("COMB AUTO")
self.scope.write("CRMS OFF")
self.scope.write("CRS OFF")
self.scope.write("DISP ON")
self.scope.write("GRID SINGLE")
self.scope.write("OFCT VOLTS")
self.scope.write("PACL")
self.scope.write("ACAL OFF")
self.scope.write("CFMT DEF9, WORD, BIN")
self.scope.write("CHDR SHORT")
for name_tmp in self.channel_name:
self.scope.write("%s:TRA OFF" % name_tmp)
self.force()
def cal(self):
"""
Force the auto calibration of the scope.
"""
self.scope.write("*CAL?")
msg = self.scope.read()
if msg != '*CAL 0':
raise ValueError("invalid cal")
def buzz(self):
"""
Activate the buzzer.
"""
self.scope.write('BUZZ BEEP')
def single(self):
"""
Trigger single shot mode.
"""
self.scope.write("TRMD SINGLE")
def stop(self):
"""
Trigger stop.
"""
self.scope.write("TRMD STOP")
def normal(self):
"""
Trigger normal mode.
"""
self.scope.write("TRMD NORM")
def force(self):
"""
Force trigger event.
"""
self.scope.write("TRMD SINGLE")
self.scope.write("FRTR")
def auto(self):
"""
Automatic trigger mode.
"""
self.scope.write("TRMD AUTO")
def get_status(self):
"""
Get the trigger status.
"""
self.scope.write("TRMD?")
trig_status = {'TRMD STOP': "stop", 'TRMD SINGLE': 'single', 'TRMD NORM': 'normal', 'TRMD AUTO': 'auto'}
msg = self.scope.read()
if msg not in trig_status:
raise ValueError("invalid status")
return trig_status[msg]
def screenshot(self):
"""
Take a screenshot, return the PNG binary file content.
"""
self.scope.write("HCSU DEV, PNG, FORMAT,PORTRAIT, BCKG, WHITE, DEST, REMOTE, PORT, NET, AREA,GRIDAREAONLY")
self.scope.write("SCDP")
return self.scope.read_raw()
def waveform(self, skip):
"""
Download the waveform data, skip some points if required, return a dict.
"""
# get the trigger status
scope_status = self.get_status()
# init the cata
data_out = dict()
data_out["time"] = self.time
data_out["channel"] = self.channel
data_out["trigger"] = self.trigger
data_out["data"] = dict()
data_out["skip"] = skip
# only download the data if the scope trigger is not running
if (scope_status == "stop") and (self.time is not None) and (self.channel is not None):
# get the text template of the data
self.scope.write("TMPL?")
data_out["template"] = self.scope.read()
# get the data
data_out["data"] = self._get_waveform_sub(skip)
data_out["ok"] = True
else:
data_out["data"] = None
data_out["ok"] = False
# return the data
return data_out
def _get_waveform_sub(self, skip):
"""
Get the waveform, skip some points if specified, scale the data.
"""
# setup how many points to skip in the data
self.scope.write("WFSU SP, %i, NP, 0, FP, 0, SN, 0" % skip)
# for each channel get the data
data_out = dict()
for name_tmp in self.channel:
# init the dict
data_out_sub = dict()
# get the header the describe the waveform
self.scope.write("%s:INSP? 'WAVEDESC'" % name_tmp)
data_out_sub["header"] = self.scope.read()
# get the waw data
self.scope.write('%s:WF?' % name_tmp)
msg = self.scope.read_raw()
data_out_sub["msg"] = msg
# parse and scale the data
(nb, t, v) = self._extract_bin(msg)
data_out_sub["nb"] = nb
data_out_sub["v"] = v
data_out_sub["t"] = t
# assign the data
data_out[name_tmp] = data_out_sub
# return the data
return data_out
def _extract_bin(self, msg):
"""
Extract and scale the data from the binary format.
"""
# find the header
start = msg.find('WAVEDESC')
msg = msg[start:]
# extract the number of elements in the binary data
nb_byte_1 = numpy.fromstring(msg[60:64], dtype=numpy.uint32)
nb_byte_2 = numpy.fromstring(msg[64:68], dtype=numpy.uint32)
n_start = numpy.fromstring(msg[124:128], dtype=numpy.uint32)
n_first = numpy.fromstring(msg[132:136], dtype=numpy.uint32)
n_end = numpy.fromstring(msg[128:132], dtype=numpy.uint32)
n_sparse = numpy.fromstring(msg[136:140], dtype=numpy.uint32)
# check the number of elements
assert nb_byte_2 == 0, "invalid array"
assert n_start == 0, "invalid array"
assert n_first == 0, "invalid array"
assert (nb_byte_1 % 2) == 0, "invalid array"
assert (nb_byte_1 / 2) == numpy.floor(n_end / n_sparse) + 1, "invalid array"
# extract the scaling and offset information
nb = int(nb_byte_1 / 2)
v_gain = numpy.fromstring(msg[156:160], dtype=numpy.float32)
v_offset = numpy.fromstring(msg[160:164], dtype=numpy.float32)
t_gain = numpy.fromstring(msg[176:180], dtype=numpy.float32)
t_offset = numpy.fromstring(msg[180:188], dtype=numpy.float64)
# extract the waveform data, scale, and offset
v = numpy.fromstring(msg[346:], dtype=numpy.int16, count=nb).astype(numpy.float)
v *= v_gain
v -= v_offset
# extract the time data, scale, and offset
t = numpy.arange(nb, dtype=numpy.float)
t *= (t_gain * n_sparse)
t += t_offset
# return the data
return (nb, t, v)