epicsdev 1.0.2__py3-none-any.whl → 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.
- epicsdev/epicsdev.py +176 -82
- epicsdev/multiadc.py +161 -0
- {epicsdev-1.0.2.dist-info → epicsdev-2.1.0.dist-info}/METADATA +16 -2
- epicsdev-2.1.0.dist-info/RECORD +7 -0
- epicsdev-1.0.2.dist-info/RECORD +0 -6
- {epicsdev-1.0.2.dist-info → epicsdev-2.1.0.dist-info}/WHEEL +0 -0
- {epicsdev-1.0.2.dist-info → epicsdev-2.1.0.dist-info}/licenses/LICENSE +0 -0
epicsdev/epicsdev.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Skeleton and helper functions for creating EPICS PVAccess server"""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= '
|
|
4
|
-
#TODO:
|
|
5
|
-
#TODO: Add performance counters to demo.
|
|
3
|
+
__version__= 'v2.1.0 26-01-31'# polling renamed to sleep. Sleep function added.
|
|
4
|
+
#TODO add mandatory PV: host, to identify the server host.
|
|
6
5
|
#Issue: There is no way in PVAccess to specify if string PV is writable.
|
|
7
6
|
# As a workaround we append description with suffix ' Features: W' to indicate that.
|
|
8
7
|
|
|
@@ -10,13 +9,19 @@ import sys
|
|
|
10
9
|
import time
|
|
11
10
|
from time import perf_counter as timer
|
|
12
11
|
import os
|
|
12
|
+
from socket import gethostname
|
|
13
13
|
from p4p.nt import NTScalar, NTEnum
|
|
14
14
|
from p4p.nt.enum import ntenum
|
|
15
15
|
from p4p.server import Server
|
|
16
16
|
from p4p.server.thread import SharedPV
|
|
17
17
|
from p4p.client.thread import Context
|
|
18
18
|
|
|
19
|
+
PeriodicUpdateInterval = 10. # seconds
|
|
20
|
+
|
|
19
21
|
#``````````````````Module Storage`````````````````````````````````````````````
|
|
22
|
+
def _serverStateChanged(newState:str):
|
|
23
|
+
"""Dummy serverStateChanged function"""
|
|
24
|
+
return
|
|
20
25
|
class C_():
|
|
21
26
|
"""Storage for module members"""
|
|
22
27
|
prefix = ''
|
|
@@ -24,10 +29,17 @@ class C_():
|
|
|
24
29
|
cycle = 0
|
|
25
30
|
serverState = ''
|
|
26
31
|
PVs = {}
|
|
27
|
-
PVDefs = []
|
|
32
|
+
PVDefs = []
|
|
33
|
+
serverStateChanged = _serverStateChanged
|
|
34
|
+
lastCycleTime = time.time()
|
|
35
|
+
lastUpdateTime = 0.
|
|
36
|
+
cycleTimeSum = 0.
|
|
37
|
+
cyclesAfterUpdate = 0
|
|
38
|
+
|
|
28
39
|
#```````````````````Helper methods````````````````````````````````````````````
|
|
29
40
|
def serverState():
|
|
30
|
-
"""Return current server state. That is the value of the server PV, but
|
|
41
|
+
"""Return current server state. That is the value of the server PV, but
|
|
42
|
+
cached in C_ to avoid unnecessary get() calls."""
|
|
31
43
|
return C_.serverState
|
|
32
44
|
def _printTime():
|
|
33
45
|
return time.strftime("%m%d:%H%M%S")
|
|
@@ -66,11 +78,14 @@ def pvv(pvName:str):
|
|
|
66
78
|
return pvobj(pvName).current()
|
|
67
79
|
|
|
68
80
|
def publish(pvName:str, value, ifChanged=False, t=None):
|
|
69
|
-
"""Publish value to PV. If ifChanged is True, then publish only if the
|
|
81
|
+
"""Publish value to PV. If ifChanged is True, then publish only if the
|
|
82
|
+
value is different from the current value. If t is not None, then use
|
|
83
|
+
it as timestamp, otherwise use current time."""
|
|
84
|
+
#print(f'Publishing {pvName}')
|
|
70
85
|
try:
|
|
71
86
|
pv = pvobj(pvName)
|
|
72
87
|
except KeyError:
|
|
73
|
-
|
|
88
|
+
print(f'WARNING: PV {pvName} not found. Cannot publish value.')
|
|
74
89
|
return
|
|
75
90
|
if t is None:
|
|
76
91
|
t = time.time()
|
|
@@ -79,11 +94,15 @@ def publish(pvName:str, value, ifChanged=False, t=None):
|
|
|
79
94
|
|
|
80
95
|
def SPV(initial, meta='', vtype=None):
|
|
81
96
|
"""Construct SharedPV.
|
|
82
|
-
meta is a string with characters W,A,
|
|
83
|
-
|
|
97
|
+
meta is a string with characters W,R,A,D indicating if the PV is writable,
|
|
98
|
+
has alarm or it is discrete (ENUM).
|
|
99
|
+
vtype should be one of the p4p.nt type definitions
|
|
100
|
+
(see https://epics-base.github.io/p4p/values.html).
|
|
84
101
|
if vtype is None then the nominal type will be determined automatically.
|
|
102
|
+
initial is the initial value of the PV. It can be a single value or
|
|
103
|
+
a list/array of values (for array PVs).
|
|
85
104
|
"""
|
|
86
|
-
typeCode = {
|
|
105
|
+
typeCode = {# mapping from vtype to p4p type code
|
|
87
106
|
's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
|
|
88
107
|
'u64':'L', 'f32':'f', 'f64':'d', str:'s',
|
|
89
108
|
}
|
|
@@ -93,20 +112,28 @@ def SPV(initial, meta='', vtype=None):
|
|
|
93
112
|
itype = type(firstItem)
|
|
94
113
|
vtype = {int: 'i32', float: 'f32'}.get(itype,itype)
|
|
95
114
|
tcode = typeCode[vtype]
|
|
96
|
-
|
|
115
|
+
allowed_chars = 'WRAD'
|
|
116
|
+
discrete = False
|
|
117
|
+
for ch in meta:
|
|
118
|
+
if ch not in allowed_chars:
|
|
119
|
+
printe(f'Unknown meta character {ch} in SPV definition')
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
if 'D' in meta:
|
|
122
|
+
discrete = True
|
|
97
123
|
initial = {'choices': initial, 'index': 0}
|
|
98
124
|
nt = NTEnum(display=True, control='W' in meta)
|
|
99
125
|
else:
|
|
100
126
|
prefix = 'a' if iterable else ''
|
|
101
|
-
nt = NTScalar(prefix+tcode, display=True, control='W' in meta,
|
|
127
|
+
nt = NTScalar(prefix+tcode, display=True, control='W' in meta,
|
|
128
|
+
valueAlarm='A' in meta)
|
|
102
129
|
pv = SharedPV(nt=nt, initial=initial)
|
|
130
|
+
# add new attributes.
|
|
103
131
|
pv.writable = 'W' in meta
|
|
132
|
+
pv.discrete = discrete
|
|
104
133
|
return pv
|
|
105
134
|
|
|
106
135
|
#``````````````````create_PVs()```````````````````````````````````````````````
|
|
107
136
|
def _create_PVs(pvDefs):
|
|
108
|
-
"""Create PVs, using definitions from pvDEfs list. Each definition is a list of the form:
|
|
109
|
-
[pvname, description, SPV object, extra], where extra is a dictionary of extra parameters, like setter, units, limits etc. Setter is a function, that will be called when"""
|
|
110
137
|
ts = time.time()
|
|
111
138
|
for defs in pvDefs:
|
|
112
139
|
try:
|
|
@@ -115,15 +142,22 @@ def _create_PVs(pvDefs):
|
|
|
115
142
|
printe(f'Invalid PV definition of {defs[0]}')
|
|
116
143
|
sys.exit(1)
|
|
117
144
|
ivalue = spv.current()
|
|
118
|
-
printv(f'created pv {pname}, initial: {type(ivalue),ivalue},
|
|
145
|
+
printv((f'created pv {pname}, initial: {type(ivalue),ivalue},'
|
|
146
|
+
f'extra: {extra}'))
|
|
147
|
+
key = C_.prefix + pname
|
|
148
|
+
if key in C_.PVs:
|
|
149
|
+
printe(f'Duplicate PV name: {pname}')
|
|
150
|
+
sys.exit(1)
|
|
119
151
|
C_.PVs[C_.prefix+pname] = spv
|
|
120
152
|
v = spv._wrap(ivalue, timestamp=ts)
|
|
121
153
|
if spv.writable:
|
|
122
154
|
try:
|
|
123
|
-
# To indicate that the PV is writable, set control limits to
|
|
155
|
+
# To indicate that the PV is writable, set control limits to
|
|
156
|
+
# (0,0). Not very elegant, but it works for numerics and enums,
|
|
157
|
+
# not for strings.
|
|
124
158
|
v['control.limitLow'] = 0
|
|
125
159
|
v['control.limitHigh'] = 0
|
|
126
|
-
except KeyError
|
|
160
|
+
except KeyError:
|
|
127
161
|
#print(f'control not set for {pname}: {e}')
|
|
128
162
|
pass
|
|
129
163
|
if 'ntenum' in str(type(ivalue)):
|
|
@@ -140,7 +174,7 @@ def _create_PVs(pvDefs):
|
|
|
140
174
|
v[f'valueAlarm.{key}'] = value
|
|
141
175
|
spv.post(v)
|
|
142
176
|
|
|
143
|
-
# add new attributes.
|
|
177
|
+
# add new attributes.
|
|
144
178
|
spv.name = pname
|
|
145
179
|
spv.setter = extra.get('setter')
|
|
146
180
|
|
|
@@ -151,7 +185,8 @@ def _create_PVs(pvDefs):
|
|
|
151
185
|
vv = op.value()
|
|
152
186
|
vr = vv.raw.value
|
|
153
187
|
current = spv._wrap(spv.current())
|
|
154
|
-
# check limits, if they are defined. That will be a good
|
|
188
|
+
# check limits, if they are defined. That will be a good
|
|
189
|
+
# example of using control structure and valueAlarm.
|
|
155
190
|
try:
|
|
156
191
|
limitLow = current['control.limitLow']
|
|
157
192
|
limitHigh = current['control.limitHigh']
|
|
@@ -162,65 +197,84 @@ def _create_PVs(pvDefs):
|
|
|
162
197
|
except KeyError:
|
|
163
198
|
pass
|
|
164
199
|
if isinstance(vv, ntenum):
|
|
165
|
-
vr = vv
|
|
200
|
+
vr = str(vv)
|
|
166
201
|
if spv.setter:
|
|
167
|
-
spv.setter(vr)
|
|
202
|
+
spv.setter(vr, spv)
|
|
168
203
|
# value will be updated by the setter, so get it again
|
|
169
204
|
vr = pvv(spv.name)
|
|
170
205
|
printv(f'putting {spv.name} = {vr}')
|
|
171
206
|
spv.post(vr, timestamp=ct) # update subscribers
|
|
172
207
|
op.done()
|
|
173
|
-
#print(f'PV {pv.name} created: {spv}')
|
|
174
208
|
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
175
209
|
#``````````````````Setters
|
|
176
|
-
def
|
|
210
|
+
def set_verbose(level, *_):
|
|
177
211
|
"""Set verbosity level for debugging"""
|
|
178
212
|
C_.verbose = level
|
|
179
|
-
|
|
213
|
+
printi(f'Setting verbose to {level}')
|
|
214
|
+
publish('verbose',level)
|
|
180
215
|
|
|
181
|
-
def set_server(
|
|
182
|
-
"""Example of the setter for the server PV.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
216
|
+
def set_server(servState, *_):
|
|
217
|
+
"""Example of the setter for the server PV.
|
|
218
|
+
servState can be 'Start', 'Stop', 'Exit' or 'Clear'. If servState is None,
|
|
219
|
+
then get the desired state from the server PV."""
|
|
220
|
+
#printv(f'>set_server({servState}), {type(servState)}')
|
|
221
|
+
if servState is None:
|
|
222
|
+
servState = pvv('server')
|
|
223
|
+
printi(f'Setting server state to {servState}')
|
|
224
|
+
servState = str(servState)
|
|
225
|
+
C_.serverStateChanged(servState)
|
|
226
|
+
if servState == 'Start':
|
|
189
227
|
printi('Starting the server')
|
|
190
|
-
# configure_instrument()
|
|
191
|
-
# adopt_local_setting()
|
|
192
228
|
publish('server','Started')
|
|
193
229
|
publish('status','Started')
|
|
194
|
-
elif
|
|
230
|
+
elif servState == 'Stop':
|
|
195
231
|
printi('server stopped')
|
|
196
232
|
publish('server','Stopped')
|
|
197
233
|
publish('status','Stopped')
|
|
198
|
-
elif
|
|
234
|
+
elif servState == 'Exit':
|
|
199
235
|
printi('server is exiting')
|
|
200
236
|
publish('server','Exited')
|
|
201
237
|
publish('status','Exited')
|
|
202
|
-
elif
|
|
203
|
-
publish('acqCount', 0)
|
|
238
|
+
elif servState == 'Clear':
|
|
204
239
|
publish('status','Cleared')
|
|
205
|
-
# set server to previous
|
|
240
|
+
# set server to previous servState
|
|
206
241
|
set_server(C_.serverState)
|
|
207
|
-
|
|
242
|
+
return
|
|
243
|
+
C_.serverState = servState
|
|
208
244
|
|
|
209
245
|
def create_PVs(pvDefs=None):
|
|
210
|
-
"""Creates manadatory PVs and adds PVs specified in pvDefs list
|
|
246
|
+
"""Creates manadatory PVs and adds PVs specified in pvDefs list.
|
|
247
|
+
Returns dictionary of created PVs.
|
|
248
|
+
Each definition is a list of the form:
|
|
249
|
+
[pvname, description, SPV object, extra], where extra is a dictionary of
|
|
250
|
+
extra parameters.
|
|
251
|
+
Extra parameters can include:
|
|
252
|
+
'setter' : function to be called on put
|
|
253
|
+
'units' : string with units
|
|
254
|
+
'limitLow' : low control limit
|
|
255
|
+
'limitHigh' : high control limit
|
|
256
|
+
'format' : format string
|
|
257
|
+
'valueAlarm': dictionary with valueAlarm parameters, like
|
|
258
|
+
'lowAlarmLimit', 'highAlarmLimit', etc."""
|
|
211
259
|
U,LL,LH = 'units','limitLow','limitHigh'
|
|
212
260
|
C_.PVDefs = [
|
|
261
|
+
['host', 'Server host name', SPV(gethostname()), {}],
|
|
213
262
|
['version', 'Program version', SPV(__version__), {}],
|
|
214
|
-
['status', 'Server status. Features: RWE', SPV('
|
|
215
|
-
['server', 'Server control',
|
|
216
|
-
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), '
|
|
263
|
+
['status', 'Server status. Features: RWE', SPV('','W'), {}],
|
|
264
|
+
['server', 'Server control',
|
|
265
|
+
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WD'),
|
|
217
266
|
{'setter':set_server}],
|
|
218
|
-
['
|
|
219
|
-
{'setter':
|
|
220
|
-
['
|
|
221
|
-
|
|
267
|
+
['verbose', 'Debugging verbosity', SPV(C_.verbose,'W','u8'),
|
|
268
|
+
{'setter':set_verbose, LL:0,LH:3}],
|
|
269
|
+
['sleep', 'Pause in the main loop, it could be useful for throttling the data output',
|
|
270
|
+
SPV(1.0,'W'), {U:'S', LL:0.001, LH:10.1}],
|
|
271
|
+
['cycle', 'Cycle number, published every {PeriodicUpdateInterval} S.',
|
|
272
|
+
SPV(0,'','u32'), {}],
|
|
273
|
+
['cycleTime','Average cycle time including sleep, published every {PeriodicUpdateInterval} S',
|
|
274
|
+
SPV(0.), {U:'S'}],
|
|
222
275
|
]
|
|
223
|
-
# append application's PVs, defined in the pvDefs and create map of
|
|
276
|
+
# append application's PVs, defined in the pvDefs and create map of
|
|
277
|
+
# providers
|
|
224
278
|
if pvDefs is not None:
|
|
225
279
|
C_.PVDefs += pvDefs
|
|
226
280
|
_create_PVs(C_.PVDefs)
|
|
@@ -232,20 +286,36 @@ def get_externalPV(pvName:str, timeout=0.5):
|
|
|
232
286
|
ctxt = Context('pva')
|
|
233
287
|
return ctxt.get(pvName, timeout=timeout)
|
|
234
288
|
|
|
235
|
-
def init_epicsdev(prefix:str, pvDefs:list,
|
|
289
|
+
def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
|
|
290
|
+
serverStateChanged=None, listDir=None):
|
|
236
291
|
"""Check if no other server is running with the same prefix.
|
|
237
292
|
Create PVs and return them as a dictionary.
|
|
293
|
+
prefix is a string to be prepended to all PV names.
|
|
294
|
+
pvDefs is a list of PV definitions (see create_PVs()).
|
|
295
|
+
verbose is the verbosity level for debug messages.
|
|
296
|
+
serverStateChanged is a function to be called when the server PV changes.
|
|
297
|
+
The function should have the signature:
|
|
298
|
+
def serverStateChanged(newStatus:str):
|
|
299
|
+
If serverStateChanged is None, then a dummy function is used.
|
|
238
300
|
The listDir is a directory to save list of all generated PVs,
|
|
239
301
|
if no directory is given, then </tmp/pvlist/><prefix> is assumed.
|
|
240
302
|
"""
|
|
303
|
+
if not isinstance(verbose, int) or verbose < 0:
|
|
304
|
+
printe('init_epicsdev arguments should be (prefix:str, pvDefs:list, verbose:int, listDir:str)')
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
printi(f'Initializing epicsdev with prefix {prefix}')
|
|
241
307
|
C_.prefix = prefix
|
|
242
308
|
C_.verbose = verbose
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
309
|
+
if serverStateChanged is not None:# set custom serverStateChanged function
|
|
310
|
+
C_.serverStateChanged = serverStateChanged
|
|
311
|
+
try: # check if server is already running
|
|
312
|
+
host = repr(get_externalPV(prefix+'host')).replace("'",'')
|
|
313
|
+
print(f'ERROR: Server for {prefix} already running at {host}. Exiting.')
|
|
246
314
|
sys.exit(1)
|
|
247
315
|
except TimeoutError:
|
|
248
316
|
pass
|
|
317
|
+
|
|
318
|
+
# No existing server found. Creating PVs.
|
|
249
319
|
pvs = create_PVs(pvDefs)
|
|
250
320
|
# Save list of PVs to a file, if requested
|
|
251
321
|
if listDir != '':
|
|
@@ -257,11 +327,30 @@ def init_epicsdev(prefix:str, pvDefs:list, listDir:str, verbose:str=0):
|
|
|
257
327
|
with open(filepath, 'w', encoding="utf-8") as f:
|
|
258
328
|
for _pvname in pvs:
|
|
259
329
|
f.write(_pvname + '\n')
|
|
330
|
+
printi(f'Hosting {len(pvs)} PVs')
|
|
260
331
|
return pvs
|
|
261
332
|
|
|
333
|
+
def sleep():
|
|
334
|
+
"""Sleep function to be called in the main loop. It updates cycleTime PV
|
|
335
|
+
and sleeps for the time specified in sleep PV."""
|
|
336
|
+
tnow = time.time()
|
|
337
|
+
C_.cycleTimeSum += tnow - C_.lastCycleTime
|
|
338
|
+
C_.lastCycleTime = tnow
|
|
339
|
+
C_.cyclesAfterUpdate += 1
|
|
340
|
+
C_.cycle += 1
|
|
341
|
+
printv(f'cycle {C_.cycle}')
|
|
342
|
+
if tnow - C_.lastUpdateTime > PeriodicUpdateInterval:
|
|
343
|
+
avgCycleTime = C_.cycleTimeSum / C_.cyclesAfterUpdate
|
|
344
|
+
printv(f'Average cycle time: {avgCycleTime:.6f} S.')
|
|
345
|
+
publish('cycle', C_.cycle)
|
|
346
|
+
publish('cycleTime', avgCycleTime)
|
|
347
|
+
C_.lastUpdateTime = tnow
|
|
348
|
+
C_.cycleTimeSum = 0.
|
|
349
|
+
C_.cyclesAfterUpdate = 0
|
|
350
|
+
time.sleep(pvv('sleep'))
|
|
351
|
+
|
|
262
352
|
#``````````````````Demo````````````````````````````````````````````````````````
|
|
263
353
|
if __name__ == "__main__":
|
|
264
|
-
print(f'epicsdev multiadc demo server {__version__}')
|
|
265
354
|
import numpy as np
|
|
266
355
|
import argparse
|
|
267
356
|
|
|
@@ -274,11 +363,11 @@ if __name__ == "__main__":
|
|
|
274
363
|
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
275
364
|
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
276
365
|
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
277
|
-
['
|
|
278
|
-
['
|
|
279
|
-
['
|
|
280
|
-
['
|
|
281
|
-
['
|
|
366
|
+
['c01Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
|
|
367
|
+
['c01VoltsPerDiv', 'Vertical scale', SPV(1E-3,'W'), {U:'V/du'}],
|
|
368
|
+
['c01Waveform', 'Waveform array', SPV([0.]), {U:'du'}],
|
|
369
|
+
['c01Mean', 'Mean of the waveform', SPV(0.,'A'), {U:'du'}],
|
|
370
|
+
['c01Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {U:'du',**alarm}],
|
|
282
371
|
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
283
372
|
]
|
|
284
373
|
nPatterns = 100 # number of waveform patterns.
|
|
@@ -286,49 +375,52 @@ if __name__ == "__main__":
|
|
|
286
375
|
rng = np.random.default_rng(nPatterns)
|
|
287
376
|
nPoints = 100
|
|
288
377
|
|
|
289
|
-
def set_recordLength(value):
|
|
290
|
-
"""Record length have changed. The tAxis should be updated
|
|
378
|
+
def set_recordLength(value, *_):
|
|
379
|
+
"""Record length have changed. The tAxis should be updated
|
|
380
|
+
accordingly."""
|
|
291
381
|
printi(f'Setting tAxis to {value}')
|
|
292
382
|
publish('tAxis', np.arange(value)*1.E-6)
|
|
293
383
|
publish('recordLength', value)
|
|
294
|
-
|
|
384
|
+
# Re-initialize noise array, because its size depends on recordLength
|
|
385
|
+
set_noise(pvv('noiseLevel'))
|
|
295
386
|
|
|
296
|
-
def set_noise(level):
|
|
387
|
+
def set_noise(level, *_):
|
|
297
388
|
"""Noise level have changed. Update noise array."""
|
|
298
389
|
v = float(level)
|
|
299
390
|
recordLength = pvv('recordLength')
|
|
300
391
|
ts = timer()
|
|
301
|
-
pargs.noise = np.random.normal(scale=0.5*level,
|
|
392
|
+
pargs.noise = np.random.normal(scale=0.5*level,
|
|
393
|
+
size=recordLength+nPatterns)# 45ms/1e6 points
|
|
302
394
|
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
303
395
|
publish('noiseLevel', level)
|
|
304
396
|
|
|
305
397
|
def init(recordLength):
|
|
306
|
-
"""
|
|
398
|
+
"""Example of device initialization function"""
|
|
307
399
|
set_recordLength(recordLength)
|
|
308
400
|
#set_noise(pvv('noiseLevel')) # already called from set_recordLength
|
|
309
401
|
|
|
310
402
|
def poll():
|
|
311
|
-
"""Example of polling function"""
|
|
403
|
+
"""Example of polling function. Called every cycle when server is running."""
|
|
312
404
|
#pattern = C_.cycle % nPatterns# produces sliding
|
|
313
405
|
pattern = rng.integers(0, nPatterns)
|
|
314
|
-
cycle = pvv('cycle')
|
|
315
|
-
printv(f'cycle {repr(cycle)}')
|
|
316
|
-
publish('cycle', cycle + 1)
|
|
317
406
|
wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
|
|
318
|
-
wf /= pvv('
|
|
319
|
-
wf += pvv('
|
|
320
|
-
publish('
|
|
321
|
-
publish('
|
|
322
|
-
publish('
|
|
407
|
+
wf /= pvv('c01VoltsPerDiv')
|
|
408
|
+
wf += pvv('c01Offset')
|
|
409
|
+
publish('c01Waveform', wf)
|
|
410
|
+
publish('c01Peak2Peak', np.ptp(wf))
|
|
411
|
+
publish('c01Mean', np.mean(wf))
|
|
323
412
|
|
|
324
413
|
# Argument parsing
|
|
325
414
|
parser = argparse.ArgumentParser(description = __doc__,
|
|
326
415
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
327
416
|
epilog=f'{__version__}')
|
|
328
|
-
parser.add_argument('-
|
|
329
|
-
'
|
|
330
|
-
parser.add_argument('-
|
|
331
|
-
'
|
|
417
|
+
parser.add_argument('-d', '--device', default='epicsDev', help=
|
|
418
|
+
'Device name, the PV name will be <device><index>:')
|
|
419
|
+
parser.add_argument('-i', '--index', default='0', help=
|
|
420
|
+
'Device index, the PV name will be <device><index>:')
|
|
421
|
+
parser.add_argument('-l', '--list', nargs='?', help=(
|
|
422
|
+
'Directory to save list of all generated PVs, if no directory is given, '
|
|
423
|
+
'then </tmp/pvlist/><prefix> is assumed.'))
|
|
332
424
|
# The rest of options are not essential, they can be controlled at runtime using PVs.
|
|
333
425
|
parser.add_argument('-n', '--npoints', type=int, default=nPoints, help=
|
|
334
426
|
'Number of points in the waveform')
|
|
@@ -338,9 +430,11 @@ if __name__ == "__main__":
|
|
|
338
430
|
print(pargs)
|
|
339
431
|
|
|
340
432
|
# Initialize epicsdev and PVs
|
|
341
|
-
|
|
433
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
434
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose, None, pargs.list)
|
|
342
435
|
|
|
343
|
-
# Initialize the device
|
|
436
|
+
# Initialize the device using pargs if needed. That can be used to set
|
|
437
|
+
# the number of points in the waveform, for example.
|
|
344
438
|
init(pargs.npoints)
|
|
345
439
|
|
|
346
440
|
# Start the Server. Use your set_server, if needed.
|
|
@@ -348,12 +442,12 @@ if __name__ == "__main__":
|
|
|
348
442
|
|
|
349
443
|
# Main loop
|
|
350
444
|
server = Server(providers=[PVs])
|
|
351
|
-
printi(f'Server started
|
|
445
|
+
printi(f'Server started. Sleeping per cycle: {repr(pvv("sleep"))} S.')
|
|
352
446
|
while True:
|
|
353
447
|
state = serverState()
|
|
354
448
|
if state.startswith('Exit'):
|
|
355
449
|
break
|
|
356
450
|
if not state.startswith('Stop'):
|
|
357
451
|
poll()
|
|
358
|
-
|
|
452
|
+
sleep()
|
|
359
453
|
printi('Server is exited')
|
epicsdev/multiadc.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Simulated multi-channel ADC device server using epicsdev module."""
|
|
2
|
+
# pylint: disable=invalid-name
|
|
3
|
+
__version__= 'v2.1.0 26-01-31'# updated for epicsdev v2.1.0
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from time import perf_counter as timer
|
|
8
|
+
import numpy as np
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
from .epicsdev import Server, Context, init_epicsdev, serverState, publish
|
|
12
|
+
from .epicsdev import pvv, printi, printv, SPV, set_server, sleep
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def myPVDefs():
|
|
16
|
+
"""Example of PV definitions"""
|
|
17
|
+
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
18
|
+
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
19
|
+
pvDefs = [ # device-specific PVs
|
|
20
|
+
['channels', 'Number of device channels', SPV(pargs.channels), {}],
|
|
21
|
+
['externalControl', 'Name of external PV, which controls the server',
|
|
22
|
+
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WD'), {}],
|
|
23
|
+
['noiseLevel', 'Noise amplitude', SPV(1.E-4,'W'), {SET:set_noise, U:'V'}],
|
|
24
|
+
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
25
|
+
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
26
|
+
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
27
|
+
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Templates for channel-related PVs. Important: SPV cannot be used in this list!
|
|
31
|
+
ChannelTemplates = [
|
|
32
|
+
['c0$VoltsPerDiv', 'Vertical scale', (1E-3,'W'), {U:'V/du'}],
|
|
33
|
+
#['c0$VoltOffset', 'Vertical offset', (1E-3,), {U:'V/du'}],
|
|
34
|
+
['c0$Waveform', 'Waveform array', ([0.],), {U:'du'}],
|
|
35
|
+
['c0$Mean', 'Mean of the waveform', (0.,'A'), {U:'du'}],
|
|
36
|
+
['c0$Peak2Peak','Peak-to-peak amplitude', (0.,'A'), {U:'du',**alarm}],
|
|
37
|
+
]
|
|
38
|
+
# extend PvDefs with channel-related PVs
|
|
39
|
+
for ch in range(pargs.channels):
|
|
40
|
+
for pvdef in ChannelTemplates:
|
|
41
|
+
newpvdef = pvdef.copy()
|
|
42
|
+
newpvdef[0] = pvdef[0].replace('0$',f'{ch+1:02}')
|
|
43
|
+
newpvdef[2] = SPV(*pvdef[2])
|
|
44
|
+
pvDefs.append(newpvdef)
|
|
45
|
+
return pvDefs
|
|
46
|
+
|
|
47
|
+
#``````````````````Module constants
|
|
48
|
+
nPatterns = 100 # number of waveform patterns.
|
|
49
|
+
rng = np.random.default_rng(nPatterns)
|
|
50
|
+
|
|
51
|
+
#``````````````````Setter functions for PVs```````````````````````````````````
|
|
52
|
+
def set_recordLength(value, *_):
|
|
53
|
+
"""Record length have changed. The tAxis should be updated accordingly."""
|
|
54
|
+
printi(f'Setting tAxis to {value}')
|
|
55
|
+
publish('tAxis', np.arange(value)*1.E-6)
|
|
56
|
+
publish('recordLength', value)
|
|
57
|
+
# Re-initialize noise array, because its size depends on recordLength
|
|
58
|
+
set_noise(pvv('noiseLevel'))
|
|
59
|
+
|
|
60
|
+
def set_noise(level, *_):
|
|
61
|
+
"""Noise level have changed. Update noise array."""
|
|
62
|
+
v = float(level)
|
|
63
|
+
recordLength = pvv('recordLength')
|
|
64
|
+
ts = timer()
|
|
65
|
+
|
|
66
|
+
pargs.noise = np.random.normal(scale=0.5*level, size=recordLength+nPatterns)# 45ms/1e6 points
|
|
67
|
+
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
68
|
+
publish('noiseLevel', level)
|
|
69
|
+
|
|
70
|
+
def set_externalControl(value, *_):
|
|
71
|
+
"""External control PV have changed. Control the server accordingly."""
|
|
72
|
+
pvname = str(value)
|
|
73
|
+
if pvname in (None,'0'):
|
|
74
|
+
print('External control is not activated.')
|
|
75
|
+
return
|
|
76
|
+
printi(f'External control PV: {pvname}')
|
|
77
|
+
ctxt = Context('pva')
|
|
78
|
+
try:
|
|
79
|
+
r = ctxt.get(pvname, timeout=0.5)
|
|
80
|
+
except TimeoutError:
|
|
81
|
+
printi(f'Cannot connect to external control PV {pvname}.')
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
84
|
+
def serverStateChanged(newState:str):
|
|
85
|
+
"""Start device function called when server is started"""
|
|
86
|
+
if newState == 'Start':
|
|
87
|
+
printi('start_device called')
|
|
88
|
+
elif newState == 'Stop':
|
|
89
|
+
printi('stop_device called')
|
|
90
|
+
elif newState == 'Clear':
|
|
91
|
+
printi('clear_device called')
|
|
92
|
+
publish('cycle', 0)
|
|
93
|
+
|
|
94
|
+
def init(recordLength):
|
|
95
|
+
"""Device initialization function"""
|
|
96
|
+
set_recordLength(recordLength)
|
|
97
|
+
#set_externalControl(pargs.prefix + pargs.external)
|
|
98
|
+
|
|
99
|
+
def poll():
|
|
100
|
+
"""Device polling function, called every cycle when server is running"""
|
|
101
|
+
for ch in range(pargs.channels):
|
|
102
|
+
pattern = rng.integers(0, nPatterns)
|
|
103
|
+
chstr = f'c{ch+1:02}'
|
|
104
|
+
wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
|
|
105
|
+
#print(f'ch{ch}, {pattern}: {wf[0], wf.sum(), wf.mean(), np.mean(wf)}')
|
|
106
|
+
wf /= pvv(f'{chstr}VoltsPerDiv')
|
|
107
|
+
#wf += pvv(f'{chstr}Offset')
|
|
108
|
+
wf += ch
|
|
109
|
+
publish(f'{chstr}Waveform', list(wf))
|
|
110
|
+
publish(f'{chstr}Peak2Peak', np.ptp(wf))
|
|
111
|
+
publish(f'{chstr}Mean', np.mean(wf))
|
|
112
|
+
|
|
113
|
+
# Argument parsing
|
|
114
|
+
parser = argparse.ArgumentParser(description = __doc__,
|
|
115
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
116
|
+
epilog=f'{__version__}')
|
|
117
|
+
parser.add_argument('-c', '--channels', type=int, default=6, help=
|
|
118
|
+
'Number of channels per device')
|
|
119
|
+
parser.add_argument('-e', '--external', help=
|
|
120
|
+
'Name of external PV, which controls the server, if 0 then it will be <device>0:')
|
|
121
|
+
parser.add_argument('-l', '--list', default=None, nargs='?', help=
|
|
122
|
+
'Directory to save list of all generated PVs, if None, then </tmp/pvlist/><prefix> is assumed.')
|
|
123
|
+
parser.add_argument('-d', '--device', default='multiadc', help=
|
|
124
|
+
'Device name, the PV name will be <device><index>:')
|
|
125
|
+
parser.add_argument('-i', '--index', default='0', help=
|
|
126
|
+
'Device index, the PV name will be <device><index>:')
|
|
127
|
+
# The rest of arguments are not essential, they can be changed at runtime using PVs.
|
|
128
|
+
parser.add_argument('-n', '--npoints', type=int, default=100, help=
|
|
129
|
+
'Number of points in the waveform')
|
|
130
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
131
|
+
'Show more log messages (-vv: show even more)')
|
|
132
|
+
pargs = parser.parse_args()
|
|
133
|
+
print(f'pargs: {pargs}')
|
|
134
|
+
|
|
135
|
+
# Initialize epicsdev and PVs
|
|
136
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
137
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose,
|
|
138
|
+
serverStateChanged, pargs.list)
|
|
139
|
+
# if pargs.list != '':
|
|
140
|
+
# print('List of PVs:')
|
|
141
|
+
# for _pvname in PVs:
|
|
142
|
+
# print(_pvname)
|
|
143
|
+
|
|
144
|
+
# Initialize the device, using pargs if needed.
|
|
145
|
+
# That can be used to set the number of points in the waveform, for example.
|
|
146
|
+
init(pargs.npoints)
|
|
147
|
+
|
|
148
|
+
# Start the Server. Use your set_server, if needed.
|
|
149
|
+
set_server('Start')
|
|
150
|
+
|
|
151
|
+
#``````````````````Main loop``````````````````````````````````````````````````
|
|
152
|
+
server = Server(providers=[PVs])
|
|
153
|
+
printi(f'Server started. Sleeping per cycle: {repr(pvv("sleep"))} S.')
|
|
154
|
+
while True:
|
|
155
|
+
state = serverState()
|
|
156
|
+
if state.startswith('Exit'):
|
|
157
|
+
break
|
|
158
|
+
if not state.startswith('Stop'):
|
|
159
|
+
poll()
|
|
160
|
+
sleep()
|
|
161
|
+
printi('Server is exited')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epicsdev
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Helper module for creating EPICS PVAccess servers using p4p
|
|
5
5
|
Project-URL: Homepage, https://github.com/ASukhanov/epicsdev
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ASukhanov/epicsdev
|
|
@@ -19,7 +19,7 @@ Helper module for creating EPICS PVAccess servers.
|
|
|
19
19
|
Demo:
|
|
20
20
|
```
|
|
21
21
|
python pip install epicsdev
|
|
22
|
-
python -m epicsdev.epicsdev
|
|
22
|
+
python -m epicsdev.epicsdev
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
To control and plot:
|
|
@@ -28,4 +28,18 @@ python pip install pypeto,pvplot
|
|
|
28
28
|
python -m pypeto -c config -f epicsdev
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Multi-channel waveform generator
|
|
32
|
+
Module **epicdev.multiadc** can generate large amount of data for stress-testing
|
|
33
|
+
the EPICS environment. For example the following command will generate 100 of
|
|
34
|
+
1000-pont noisy waveforms and 300 of scalar parameters.
|
|
35
|
+
```
|
|
36
|
+
python -m epicsdev.multiadc -c100 -n1000
|
|
37
|
+
```
|
|
38
|
+
The GUI for monitoring:<br>
|
|
39
|
+
```python -m pypeto -c config -f multiadc```
|
|
40
|
+
|
|
41
|
+
The graphs should look like this:
|
|
42
|
+
[control page](docs/epicsdev_pypet.png),
|
|
43
|
+
[plots](docs/epicsdev_pvplot.jpg).
|
|
44
|
+
|
|
31
45
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
epicsdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
epicsdev/epicsdev.py,sha256=aOiVWY8sIrEGqxHJ9niyRtGl3z2Qg0ep5MFbnZskWxg,18017
|
|
3
|
+
epicsdev/multiadc.py,sha256=ZVsA1wzl4GfOepC_zQdjisNKQFcpJuEMV6vt1y3zBnw,6520
|
|
4
|
+
epicsdev-2.1.0.dist-info/METADATA,sha256=8bFFHdc7SnDooPlzHZsnzYAy9QnpqkYoZX7VGmVtfrE,1270
|
|
5
|
+
epicsdev-2.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
epicsdev-2.1.0.dist-info/licenses/LICENSE,sha256=qj3cUKUrX4oXTb0NwuJQ44ThYDEMUfOeIjw9kkT6Qck,1072
|
|
7
|
+
epicsdev-2.1.0.dist-info/RECORD,,
|
epicsdev-1.0.2.dist-info/RECORD
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
epicsdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
epicsdev/epicsdev.py,sha256=Cr4yUBCcCJVQiREgiultFJlUqWIHWVlewtzI45hHyNs,14598
|
|
3
|
-
epicsdev-1.0.2.dist-info/METADATA,sha256=LZvfLp-nbvHZso5JxY_w76cNcjh-1sZaHU8oGEoDcbE,786
|
|
4
|
-
epicsdev-1.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
-
epicsdev-1.0.2.dist-info/licenses/LICENSE,sha256=qj3cUKUrX4oXTb0NwuJQ44ThYDEMUfOeIjw9kkT6Qck,1072
|
|
6
|
-
epicsdev-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|