epicsdev-rigol-scope 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,575 @@
1
+ """Simulated multi-channel ADC device server using epicsdev module."""
2
+ # pylint: disable=invalid-name
3
+ __version__= 'v2.1.0 26-02-01'# updated for epicsdev v2.1.0
4
+ #TODO: Stop acquisition once for all channels, not per channel.
5
+
6
+ import sys
7
+ import time
8
+ from time import perf_counter as timer
9
+ import argparse
10
+ import threading
11
+ import numpy as np
12
+
13
+ import pyvisa as visa
14
+ from pyvisa.errors import VisaIOError
15
+
16
+ from epicsdev.epicsdev import Server, SPV, init_epicsdev, sleep,\
17
+ serverState, set_server, publish, pvobj, pvv,\
18
+ printi, printe, printw, printv, printvv
19
+
20
+ #``````````````````PVs defined here```````````````````````````````````````````
21
+ def myPVDefs():
22
+ """PV definitions"""
23
+ SET,U,LL,LH,SCPI = 'setter','units','limitLow','limitHigh','scpi'
24
+ alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
25
+ pvDefs = [
26
+ # instruments's PVs
27
+ ['setup', 'Save/recall instrument state to/from latest or operational setup',
28
+ SPV(['Setup','Save latest','Save oper','Recall latest','Recall oper'],'WD'),
29
+ {SET:set_setup}],
30
+ ['visaResource', 'VISA resource to access the device', SPV(pargs.resource,'R'), {}],
31
+ ['dateTime', 'Scope`s date & time', SPV('N/A'), {}],
32
+ ['acqCount', 'Number of acquisition recorded', SPV(0), {}],
33
+ ['scopeAcqCount', 'Acquisition count of the scope', SPV(0), {}],# N/A for RIGOL
34
+ ['lostTrigs', 'Number of triggers lost', SPV(0), {}],
35
+ ['instrCtrl', 'Scope control commands',
36
+ SPV('*IDN?,*RST,*CLS,*ESR?,*OPC?,*STB?'.split(','),'WD'), {}],
37
+ ['instrCmdS', 'Execute a scope command. Features: RWE', SPV('*IDN?','W'), {
38
+ SET:set_instrCmdS}],
39
+ ['instrCmdR', 'Response of the instrCmdS', SPV(''), {}],
40
+ #``````````````````Horizontal PVs
41
+ ['recLengthS', 'Number of points per waveform',
42
+ SPV(['AUTO','1k','10k','100k','1M','5M','10M','25M','50M'],'WD'), {
43
+ SET:set_recLengthS}],
44
+ ['recLengthR', 'Number of points per waveform read', SPV(0.), {
45
+ SCPI:'ACQuire:MDEPth'}],
46
+ ['samplingRate', 'Sampling Rate', SPV(0.), {U:'Hz',
47
+ SCPI:'ACQuire:SRATe'}],
48
+ ['timePerDiv', f'Horizontal scale (1/{NDIVSX} of full scale)', SPV(2.e-6,'W'), {U:'S/du',
49
+ SCPI: 'TIMebase:SCALe', SET:set_scpi}],
50
+ ['tAxis', 'Horizontal axis array', SPV([0.]), {U:'S'}],
51
+
52
+ #``````````````````Trigger PVs
53
+ ['trigger', 'Click to force trigger event to occur',
54
+ SPV(['Trigger','Force!'],'WD'), {SET:set_trigger}],
55
+ ['trigType', 'Trigger ', SPV(['EDGE','PULS','SLOP','VID'],'WD'),
56
+ #PATTern|DURation|TIMeout|RUNT|WINDow|DELay|SETup|NEDGe|RS232|IIC|SPI|CAN|LIN
57
+ {SCPI:'TRIGger:MODE',SET:set_scpi}],
58
+ ['trigCoupling', 'Trigger coupling', SPV(['DC','AC','LFR','HFR'],'D'),
59
+ {SCPI:'TRIGger:COUPling'}],
60
+ ['trigState', 'Current trigger status: TD,WAIT,RUN,AUTO and STOP', SPV('?'),
61
+ {SCPI:'TRIGger:STATus'}],
62
+ ['trigMode', 'Trigger mode', SPV(['NORM','AUTO','SING'],'WD'),
63
+ {SCPI:'TRIGger:SWEep',SET:set_scpi}],
64
+ ['trigDelay', 'Trigger position', SPV(0.), {U:'S',
65
+ SCPI:'TRIGger:POSition'}],
66
+ ['trigSource', 'Trigger source',
67
+ SPV('CHAN1,CHAN2,CHAN3,CHAN4,EXT,D0,D1,D2,D3,D4,D5,D6,D7'.split(','),'WD'),
68
+ {SCPI:'TRIGger:EDGE:SOURce',SET:set_scpi}],
69
+ ['trigSlope', 'Trigger slope', SPV(['POS','NEG','RFALI'],'WD'),
70
+ {SCPI:'TRIGger:EDGE:SLOPe',SET:set_scpi}],
71
+ ['trigLevel', 'Trigger level', SPV(0.,'W'), {U:'V',
72
+ SCPI:'TRIGger:EDGE:LEVel',SET:set_scpi}],
73
+ #``````````````````Auxiliary PVs
74
+ ['timing', 'Performance timing', SPV([0.]), {U:'S'}],
75
+ ]
76
+
77
+ #``````````````Templates for channel-related PVs.
78
+ # The <n> in the name will be replaced with channel number.
79
+ # Important: SPV cannot be used in this list!
80
+ ChannelTemplates = [
81
+ ['c<n>OnOff', 'Enable/disable channel', (['1','0'],'WD'),
82
+ {SET:set_scpi, SCPI:'CHANnel<n>:DISPlay'}],
83
+ ['c<n>Coupling', 'Channel coupling', (['DC','AC','GND'],'WD'),
84
+ {SCPI:'CHANnel<n>:COUPling'}],
85
+ ['c<n>VoltsPerDiv', 'Vertical scale', (1E-3,'W'), {U:'V/du',
86
+ SCPI:'CHANnel<n>:SCALe', LL:500E-6, LH:10.}],
87
+ ['c<n>VoltOffset', 'Vertical offset', (0.,), {U:'V',
88
+ SCPI:'CHANnel<n>:OFFSet'}],
89
+ ['c<n>Termination', 'Input termination', ('1M','R'), {U:'Ohm'}],# fixed in RIGOL
90
+ ['c<n>Waveform', 'Waveform array', ([0.],), {U:'du'}],
91
+ ['c<n>Mean', 'Mean of the waveform', (0.,'A'), {U:'du'}],
92
+ ['c<n>Peak2Peak','Peak-to-peak amplitude', (0.,'A'), {U:'du',**alarm}],
93
+ ]
94
+ # extend PvDefs with channel-related PVs
95
+ for ch in range(pargs.channels):
96
+ for pvdef in ChannelTemplates:
97
+ newpvdef = pvdef.copy()
98
+ newpvdef[0] = pvdef[0].replace('<n>',f'{ch+1:02}')
99
+ newpvdef[2] = SPV(*pvdef[2])
100
+ pvDefs.append(newpvdef)
101
+ return pvDefs
102
+ #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
103
+ #``````````````````Constants
104
+ Threadlock = threading.Lock()
105
+ OK = 0
106
+ NotOK = -1
107
+ IF_CHANGED =True
108
+ ElapsedTime = {}
109
+ NDIVSX = 10# number of vertical divisions of the scope display
110
+ NDIVSY = 10#
111
+ #,,,,,,,,,,,,,,,,,,
112
+ class C_():
113
+ """Namespace for module properties"""
114
+ scope = None
115
+ scpi = {}# {pvName:SCPI} map
116
+ setterMap = {}
117
+ PvDefs = []
118
+ readSettingQuery = None
119
+ lastRareUpdate = 0
120
+ exceptionCount = {}
121
+ numacq = 0
122
+ triggersLost = 0
123
+ trigTime = 0
124
+ previousScopeParametersQuery = ''
125
+ channelsTriggered = []
126
+ prevTscale = 0.
127
+ xorigin = 0.
128
+ xincrement = 0.
129
+ npoints = 0
130
+ ypars = None
131
+ #``````````````````Setters````````````````````````````````````````````````````
132
+ def scopeCmd(cmd):
133
+ """Send command to scope, return reply if any."""
134
+ printv(f'>scopeCmd: {cmd}')
135
+ reply = None
136
+ try:
137
+ if cmd[-1] == '?':
138
+ with Threadlock:
139
+ reply = C_.scope.query(cmd)
140
+ else:
141
+ with Threadlock:
142
+ C_.scope.write(cmd)
143
+ except:
144
+ handle_exception(f'in scopeCmd{cmd}')
145
+ return reply
146
+
147
+ def set_instrCmdS(cmd, *_):
148
+ """Setter for the instrCmdS PV"""
149
+ publish('instrCmdR','')
150
+ reply = scopeCmd(cmd)
151
+ if reply is not None:
152
+ publish('instrCmdR',reply)
153
+ publish('instrCmdS',cmd)
154
+
155
+ def serverStateChanged(newState:str):
156
+ """Start device function called when server is started"""
157
+ if newState == 'Start':
158
+ printi('start_device called')
159
+ configure_scope()
160
+ adopt_local_setting()
161
+ elif newState == 'Stop':
162
+ printi('stop_device called')
163
+ elif newState == 'Clear':
164
+ printi('clear_device called')
165
+
166
+ def set_setup(action_slot, *_):
167
+ """setter for the setup PV"""
168
+ if action_slot == 'Setup':
169
+ return
170
+ action,slot = str(action_slot).split()
171
+ fileName = {'latest':'C:/latest.stp','oper':'C:/operational.stp'}[slot]
172
+ #print(f'set_setup: {action} {fileName}')
173
+ if action == 'Save':
174
+ status = f'Setup was saved to {fileName}'
175
+ with Threadlock:
176
+ C_.scope.write(f'SAVE:SETup {fileName}')
177
+ elif action == 'Recall':
178
+ status = f'Setup was recalled from {fileName}'
179
+ if str(pvv('server')).startswith('Start'):
180
+ printw('Please set server to Stop before Recalling')
181
+ publish('setup','Setup')
182
+ return NotOK
183
+ with Threadlock:
184
+ C_.scope.write(f"LOAD:SETUp {fileName}")
185
+ publish('setup','Setup')
186
+ publish('status', status)
187
+ if action == 'Recall':
188
+ adopt_local_setting()
189
+
190
+ def set_trigger(value, *_):
191
+ """setter for the trigger PV"""
192
+ printv(f'set_trigger: {value}')
193
+ if str(value) == 'Force!':
194
+ with Threadlock:
195
+ C_.scope.write('TFORce')
196
+ publish('trigger','Trigger')
197
+
198
+ def set_recLengthS(value, *_):
199
+ """setter for the recLengthS PV"""
200
+ printv(f'set_recLengthS: {value}')
201
+ with Threadlock:
202
+ C_.scope.write(f'ACQuire:MDEPth {value}')
203
+ publish('recLengthS', value)
204
+
205
+ def set_scpi(value, pv, *_):
206
+ """setter for SCPI-associated PVs"""
207
+ print(f'set_scpi({value},{pv.name})')
208
+ scpi = C_.scpi.get(pv.name,None)
209
+ if scpi is None:
210
+ printe(f'No SCPI defined for PV {pv.name}')
211
+ return
212
+ scpi = scpi.replace('<n>',pv.name[2])# replace <n> with channel number
213
+ print(f'set_scpi: {scpi} {value}')
214
+ scpi += f' {value}' if pv.writable else '?'
215
+ printv(f'set_scpi command: {scpi}')
216
+ reply = scopeCmd(scpi)
217
+ if reply is not None:
218
+ publish(pv.name, reply)
219
+ publish(pv.name, value)
220
+
221
+ #``````````````````Instrument communication functions`````````````````````````
222
+ def query(pvnames, explicitSCPIs=None):
223
+ """Execute query request of the instrument for multiple PVs"""
224
+ scpis = [C_.scpi[pvname] for pvname in pvnames]
225
+ if explicitSCPIs:
226
+ scpis += explicitSCPIs
227
+ combinedScpi = '?;:'.join(scpis) + '?'
228
+ print(f'combinedScpi: {combinedScpi}')
229
+ with Threadlock:
230
+ r = C_.scope.query(combinedScpi)
231
+ return r.split(';')
232
+
233
+ def configure_scope():
234
+ """Send commands to configure data transfer"""
235
+ printi('configure_scope')
236
+ with Threadlock:
237
+ C_.scope.write(":WAV:FORM WORD;:MODE RAW;:SAVE:OVERlap ON")
238
+
239
+ def update_scopeParameters():
240
+ """Update scope timing PVs"""
241
+ with Threadlock:
242
+ xscpi = (":WAV:XORigin?;:XINC?;POINts?;:CHAN1:DISP?;"
243
+ ":CHAN2:DISP?;:CHAN3:DISP?;:CHAN4:DISP?;:TRIG:EDGE:LEV?")
244
+ r = C_.scope.query(xscpi)
245
+ if r != (C_.previousScopeParametersQuery):
246
+ printi(f'Scope parameters changed: {r}')
247
+ l = r.split(';')
248
+ C_.xorigin,C_.xincrement = float(l[0]), float(l[1])
249
+ C_.npoints = int(l[2])
250
+ taxis = np.arange(0, C_.npoints) * C_.xincrement + C_.xorigin
251
+ publish('tAxis', taxis)
252
+ publish('recLengthR', C_.npoints, IF_CHANGED)
253
+ publish('timePerDiv', C_.npoints*C_.xincrement/NDIVSX, IF_CHANGED)
254
+ publish('samplingRate', 1./C_.xincrement, IF_CHANGED)
255
+ C_.channelsTriggered = []
256
+ for ch in range(pargs.channels):
257
+ letter = l[ch+3]
258
+ publish(f'c{ch+1:02}OnOff', letter, IF_CHANGED)
259
+ if letter == '1':
260
+ C_.channelsTriggered.append(ch+1)
261
+ publish('trigLevel', float(l[7]), IF_CHANGED)
262
+ C_.previousScopeParametersQuery = r
263
+
264
+ def init_visa():
265
+ '''Init VISA interface to device'''
266
+ try:
267
+ rm = visa.ResourceManager('@py')
268
+ except ModuleNotFoundError as e:
269
+ printe(f'in visa.ResourceManager: {e}')
270
+ sys.exit(1)
271
+ # # Check if instrument is on netwok
272
+ #ipAddress = repr(pvv('address')).replace('\'','')
273
+ # if os.system(f'ping -c 1 -W 1 {ipAddress}') != 0:
274
+ # printe(f'IP address {ipAddress} is not pingable')
275
+ # sys.exit(1)
276
+
277
+ resourceName = pargs.resource
278
+ printv(f'Opening resource {resourceName}')
279
+ try:
280
+ C_.scope = rm.open_resource(resourceName)
281
+ except visa.errors.VisaIOError as e:
282
+ printe(f'Could not open resource {resourceName}: {e}')
283
+ sys.exit(1)
284
+ C_.scope.set_visa_attribute( visa.constants.VI_ATTR_TERMCHAR_EN, True)
285
+ C_.scope.timeout = 2000 # ms
286
+ try:
287
+ C_.scope.write('*CLS') # clear ESR, previous error messages will be cleared
288
+ except Exception as e:
289
+ printe(f'Resource {resourceName} not responding: {e}')
290
+ sys.exit()
291
+ C_.scope.write('*OPC')# that does not work!
292
+ resetNeeded = False
293
+ try: printi('*OPC?'+C_.scope.query('*OPC?'))
294
+ except:
295
+ printw('*OPC? failed'); resetNeeded = True
296
+ try: printi('*ESR?'+C_.scope.query('*ESR?'))
297
+ except:
298
+ printw('*ESR? failed'); resetNeeded = True
299
+
300
+ if resetNeeded:
301
+ printi('Resetting instrument to factory defaults')
302
+ C_.scope.write('*RST')
303
+ sys.exit(1)
304
+
305
+ idn = C_.scope.query('*IDN?')
306
+ print(f"IDN: {idn}")
307
+ if not idn.startswith('RIGOL'):
308
+ print('ERROR: instrument is not RIGOL')
309
+ sys.exit(1)
310
+
311
+ C_.scope.encoding = 'latin_1'
312
+ C_.scope.read_termination = '\n'#Important.
313
+
314
+ # def close_visa(C_):
315
+ # rm.close()
316
+ # C_.scope = None
317
+ #``````````````````````````````````````````````````````````````````````````````
318
+ def handle_exception(where):
319
+ """Handle exception"""
320
+ #print('handle_exception',sys.exc_info())
321
+ exceptionText = str(sys.exc_info()[1])
322
+ tokens = exceptionText.split()
323
+ msg = 'ERR:'+tokens[0] if tokens[0] == 'VI_ERROR_TMO' else exceptionText
324
+ msg = msg+': '+where
325
+ printe(msg)
326
+ with Threadlock:
327
+ C_.scope.write('*CLS')
328
+ return -1
329
+
330
+ def adopt_local_setting():
331
+ """Read scope setting and update PVs"""
332
+ printi('adopt_local_setting')
333
+ ct = time.time()
334
+ nothingChanged = True
335
+ try:
336
+ printvv(f'readSettingQuery: {C_.readSettingQuery}')
337
+ with Threadlock:
338
+ values = C_.scope.query(C_.readSettingQuery).split(';')
339
+ printvv(f'parnames: {C_.scpi.keys()}')
340
+ printvv(f'C_.readSettingQuery: {C_.readSettingQuery}')
341
+ printvv(f'values: {values}')
342
+ if len(C_.scpi) != len(values):
343
+ l = min(len(C_.scpi),len(values))
344
+ printe(f'ReadSetting failed for {list(C_.scpi.keys())[l]}')
345
+ sys.exit(1)
346
+ for parname,v in zip(C_.scpi, values):
347
+ pv = pvobj(parname)
348
+ pvValue = pv.current()
349
+ if pv.discrete:
350
+ pvValue = str(pvValue)
351
+ else:
352
+ try:
353
+ v = type(pvValue.raw.value)(v)
354
+ except ValueError:
355
+ printe(f'ValueError converting {v} to {type(pvValue.raw.value)} for PV {parname}')
356
+ sys.exit(1)
357
+ #printv(f'parname,v: {parname, type(v), v, type(pvValue), pvValue}')
358
+ valueChanged = pvValue != v
359
+ if valueChanged:
360
+ printv(f'posting {pv.name}={v}')
361
+ pv.post(v, timestamp=ct)
362
+ nothingChanged = False
363
+
364
+ except visa.errors.VisaIOError as e:
365
+ printe('VisaIOError in adopt_local_setting:'+str(e))
366
+ if nothingChanged:
367
+ printi('Local setting did not change.')
368
+
369
+ def trigLevelCmd():
370
+ """Generate SCPI command for trigger level control"""
371
+ ch = str(pvv('trigSource'))
372
+ if ch[:2] != 'CH':
373
+ return ''
374
+ r = 'TRIGger:A:LEVel:'+ch
375
+ printv(f'tlcmd: {r}')
376
+ return r
377
+ #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
378
+ #``````````````````Acquisition-related functions``````````````````````````````
379
+ def trigger_is_detected():
380
+ """check if scope was triggered"""
381
+ ts = timer()
382
+ try:
383
+ with Threadlock:
384
+ r = C_.scope.query(':TRIGger:STATus?')
385
+ except visa.errors.VisaIOError as e:
386
+ printe(f'Exception in query for trigger: {e}')
387
+ for exc in C_.exceptionCount:
388
+ if exc in str(e):
389
+ C_.exceptionCount[exc] += 1
390
+ errCountLimit = 2
391
+ if C_.exceptionCount[exc] >= errCountLimit:
392
+ printe(f'Processing stopped due to {exc} happened {errCountLimit} times')
393
+ set_server('Exit')
394
+ else:
395
+ printw(f'Exception #{C_.exceptionCount[exc]} during processing: {exc}')
396
+ return False
397
+
398
+ # last query was successfull, clear error counts
399
+ for i in C_.exceptionCount:
400
+ C_.exceptionCount[i] = 0
401
+ publish('trigState', r, IF_CHANGED)
402
+
403
+ if not r.startswith('TD'):
404
+ return False
405
+
406
+ # trigger detected
407
+ C_.numacq += 1
408
+ C_.trigtime = time.time()
409
+ ElapsedTime['trigger_detection'] = round(ts - timer(),6)
410
+ printv(f'Trigger detected {C_.numacq}')
411
+ return True
412
+
413
+ #``````````````````Acquisition-related functions``````````````````````````````
414
+ def acquire_waveforms():
415
+ """Acquire waveforms from the device and publish them."""
416
+ printv(f'>acquire_waveform for channels {C_.channelsTriggered}')
417
+ publish('acqCount', pvv('acqCount') + 1, t=C_.trigTime)
418
+ ElapsedTime['acquire_wf'] = timer()
419
+ ElapsedTime['preamble'] = 0.
420
+ ElapsedTime['query_wf'] = 0.
421
+ ElapsedTime['publish_wf'] = 0.
422
+ for ch in C_.channelsTriggered:
423
+ # refresh scalings
424
+ ts = timer()
425
+ operation = 'getting preamble'
426
+ try:
427
+ # most of the time is spent here, 4 times longer than the reading of waveform:
428
+ with Threadlock:
429
+ # stop acquisition to read preamble and waveform,
430
+ # because they may change during acquisition
431
+ C_.scope.write(f':STOP;:WAV:SOURce CHANnel{ch}')
432
+ #r = C_.scope.query(':WAV:YINC?;:WAV:YREFerence?;WAV:YORigin?')
433
+ preamble = C_.scope.query(':WAV:PRE?')
434
+ dt = timer() - ts
435
+ printvv(f'aw preamble{ch}: {preamble}, dt: {ch}: {dt}')
436
+ ElapsedTime['preamble'] -= dt
437
+ # if preamble did not change, then we can skip its decoding, we can save ~65us
438
+ preamble = preamble.split(',')
439
+ ypars = tuple([float(i) for i in preamble[7:]])
440
+ yincr, yorig, yref = ypars
441
+ # if ypars != C_.ypars:
442
+ # msg = f'vertical scaling changed: {ypars}'
443
+ # C_.ypars = ypars
444
+ # printi(msg)
445
+ # publish('status',msg)
446
+ #publish(f'c{ch:02}VoltsPerDiv', yincr, IF_CHANGED,
447
+ # t=C_.trigtime)
448
+ #publish(f'c{ch:02}VoltOffset', yorig, IF_CHANGED,
449
+ # t=C_.trigtime)
450
+
451
+ # acquire the waveform
452
+ ts = timer()
453
+ operation = 'getting waveform'
454
+ with Threadlock:
455
+ waveform = C_.scope.query_binary_values(":WAV:DATA?",
456
+ datatype='H', container=np.array)
457
+ C_.scope.write(':RUN')
458
+ ElapsedTime['query_wf'] -= timer() - ts
459
+ v = (waveform - yorig - yref) * yincr
460
+
461
+ # publish
462
+ ts = timer()
463
+ operation = 'publishing'
464
+ publish(f'c{ch:02}Waveform', v, t=C_.trigTime)
465
+ publish(f'c{ch:02}Peak2Peak',
466
+ (v.max() - v.min()),
467
+ t = C_.trigtime)
468
+ except visa.errors.VisaIOError as e:
469
+ printe(f'Visa exception in {operation} for {ch}:{e}')
470
+ break
471
+ except Exception as e:
472
+ printe(f'Exception in processing channel {ch}: {e}')
473
+
474
+ ElapsedTime['publish_wf'] -= timer() - ts
475
+ ElapsedTime['acquire_wf'] -= timer()
476
+ printvv(f'elapsedTime: {ElapsedTime}')
477
+
478
+ def make_readSettingQuery():
479
+ """Create combined SCPI query to read all settings at once"""
480
+ for pvdef in C_.PvDefs:
481
+ pvname = pvdef[0]
482
+ # if setter is defined, add it to the setterMap
483
+ setter = pvdef[3].get('setter',None)
484
+ if setter is not None:
485
+ C_.setterMap[pvname] = setter
486
+ # if SCPI is defined, add it to the readSettingQuery
487
+ scpi = pvdef[3].get('scpi',None)
488
+ if scpi is None:
489
+ continue
490
+ scpi = scpi.replace('<n>',pvname[2])#
491
+ scpi = ''.join([char for char in scpi if not char.islower()])# remove lowercase letters
492
+ # check if scpi is correct:
493
+ s = scpi+'?'
494
+ try:
495
+ with Threadlock:
496
+ r = C_.scope.query(s)
497
+ except VisaIOError as e:
498
+ printe(f'Invalid SCPI in PV {pvname}: {scpi}? : {e}')
499
+ sys.exit(1)
500
+ printvv(f'SCPI for PV {pvname}: {scpi}, reply: {r}')
501
+ if not scpi[0] in '!*':# only SCPI starting with !,* are not added
502
+ C_.scpi[pvname] = scpi
503
+
504
+ C_.readSettingQuery = '?;'.join(C_.scpi.values()) + '?'
505
+ printv(f'readSettingQuery: {C_.readSettingQuery}')
506
+ printv(f'setterMap: {C_.setterMap}')
507
+
508
+ def init():
509
+ """Module initialization"""
510
+ init_visa()
511
+ make_readSettingQuery()
512
+ adopt_local_setting()
513
+
514
+ def rareUpdate():
515
+ """Called for infrequent updates"""
516
+ update_scopeParameters()
517
+ #publish('scopeAcqCount', C_.numacq, IF_CHANGED)
518
+ publish('lostTrigs', C_.triggersLost, IF_CHANGED)
519
+ ##print(f'ElapsedTime: {ElapsedTime}')
520
+ if str(pvv('trigState')).startswith('STOP'):
521
+ printe('Acquisition is stopped')
522
+ publish('timing', [(round(-i,6)) for i in ElapsedTime.values()])
523
+
524
+ def poll():
525
+ """Example of polling function"""
526
+ tnow = time.time()
527
+ if tnow - C_.lastRareUpdate > 1.:
528
+ C_.lastRareUpdate = tnow
529
+ rareUpdate()
530
+
531
+ if trigger_is_detected():
532
+ acquire_waveforms()
533
+
534
+ #``````````````````Main```````````````````````````````````````````````````````
535
+ if __name__ == "__main__":
536
+ # Argument parsing
537
+ parser = argparse.ArgumentParser(description = __doc__,
538
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
539
+ epilog=f'{__version__}')
540
+ parser.add_argument('-c', '--channels', type=int, default=4, help=
541
+ 'Number of channels per device')
542
+ parser.add_argument('-d', '--device', default='rigol', help=
543
+ 'Device name, the PV name will be <device><index>:')
544
+ parser.add_argument('-i', '--index', default='0', help=
545
+ 'Device index, the PV name will be <device><index>:')
546
+ parser.add_argument('-r', '--resource', default='TCPIP::192.168.27.31::INSTR', help=
547
+ 'Resource string to access the device')
548
+ parser.add_argument('-v', '--verbose', action='count', default=0, help=
549
+ 'Show more log messages (-vv: show even more)')
550
+ pargs = parser.parse_args()
551
+ print(f'pargs: {pargs}')
552
+
553
+ # Initialize epicsdev and PVs
554
+ pargs.prefix = f'{pargs.device}{pargs.index}:'
555
+ C_.PvDefs = myPVDefs()
556
+ PVs = init_epicsdev(pargs.prefix, C_.PvDefs, pargs.verbose, serverStateChanged)
557
+
558
+ # Initialize the device, using pargs if needed.
559
+ # That can be used to set the number of points in the waveform, for example.
560
+ init()
561
+
562
+ # Start the Server. Use your set_server, if needed.
563
+ set_server('Start')
564
+
565
+ # Main loop
566
+ server = Server(providers=[PVs])
567
+ printi(f'Server for {pargs.prefix} started. Sleeping per cycle: {repr(pvv("sleep"))} S.')
568
+ while True:
569
+ state = serverState()
570
+ if state.startswith('Exit'):
571
+ break
572
+ if not state.startswith('Stop'):
573
+ poll()
574
+ sleep()
575
+ printi('Server is exited')
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: epicsdev_rigol_scope
3
+ Version: 2.1.0
4
+ Summary: Helper module for creating EPICS PVAccess servers using p4p
5
+ Project-URL: Homepage, https://github.com/ASukhanov/epicsdev_rigol_scope
6
+ Project-URL: Bug Tracker, https://github.com/ASukhanov/epicsdev_rigol_scope
7
+ Author-email: Andrey Sukhanov <sukhanov@bnl.gov>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.7
13
+ Requires-Dist: epicsdev
14
+ Requires-Dist: p4p
15
+ Description-Content-Type: text/markdown
16
+
17
+ # epicsdev_rigol_scope.
18
+ Python-based EPICS PVAccess server for RIGOL oscilloscopes.
19
+ It is based on [p4p](https://epics-base.github.io/p4p/) and [epicsdev](https://github.com/ASukhanov/epicsdev) packages
20
+ and it can run standalone on Linux, OSX, and Windows platforms.<br>
21
+ It was tested with RIGOL DHO924 on linux.
22
+
23
+ ## Installation
24
+ ```pip install epicsdev_rigol_scope```<br>
25
+ For control GUI and plotting:
26
+ ```pip install pypeto,pvplot```
27
+
28
+ ## Run
29
+ To start: ```python -m epicsdev_rigol_scope -r'TCPIP::192.168.27.31::INSTR'```<br>
30
+ Control GUI:<br>
31
+ ```python -m pypeto -c path_to_repository/config -f epicsdev_rigol_scope```<br>
32
+
33
+ ![Control page](docs/pypet.jpg), ![Plots](docs/pvplot.jpg)
@@ -0,0 +1,6 @@
1
+ epicsdev_rigol_scope/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ epicsdev_rigol_scope/__main__.py,sha256=6vMe6_DlhQDN7M8BSLjwqV7cOL8BFxkBiPhjHySqmh0,21784
3
+ epicsdev_rigol_scope-2.1.0.dist-info/METADATA,sha256=1TYfPvLWeA3FBP0lzPgUeL10gg3pD53sC3sxRhjKhEg,1290
4
+ epicsdev_rigol_scope-2.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ epicsdev_rigol_scope-2.1.0.dist-info/licenses/LICENSE,sha256=qj3cUKUrX4oXTb0NwuJQ44ThYDEMUfOeIjw9kkT6Qck,1072
6
+ epicsdev_rigol_scope-2.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrey Sukhanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.