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
|
+
, 
|
|
@@ -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,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.
|