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 CHANGED
@@ -1,8 +1,7 @@
1
1
  """Skeleton and helper functions for creating EPICS PVAccess server"""
2
2
  # pylint: disable=invalid-name
3
- __version__= 'v1.0.2 26-01-18'# --list define directory to save list of PVs.
4
- #TODO: NTEnums do not have structure display
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 cached in C_ to avoid unnecessary get() calls."""
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 value is different from the current value. If t is not None, then use it as timestamp, otherwise use current time."""
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
- printw(f'PV {pvName} not found. Cannot publish value.')
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,E indicating if the PV is writable, has alarm or it is NTEnum.
83
- vtype should be one of the p4p.nt type definitions (see https://epics-base.github.io/p4p/values.html).
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
- if 'E' in meta:
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, valueAlarm='A' 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}, extra: {extra}')
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 (0,0). Not very elegant, but it works for numerics and enums, not for strings.
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 as e:
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. To my surprise that works!
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 example of using control structure and valueAlarm.
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 set_verbosity(level):
210
+ def set_verbose(level, *_):
177
211
  """Set verbosity level for debugging"""
178
212
  C_.verbose = level
179
- publish('verbosity',level)
213
+ printi(f'Setting verbose to {level}')
214
+ publish('verbose',level)
180
215
 
181
- def set_server(state=None):
182
- """Example of the setter for the server PV."""
183
- #printv(f'>set_server({state}), {type(state)}')
184
- if state is None:
185
- state = pvv('server')
186
- printi(f'Setting server state to {state}')
187
- state = str(state)
188
- if state == 'Start':
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 state == 'Stop':
230
+ elif servState == 'Stop':
195
231
  printi('server stopped')
196
232
  publish('server','Stopped')
197
233
  publish('status','Stopped')
198
- elif state == 'Exit':
234
+ elif servState == 'Exit':
199
235
  printi('server is exiting')
200
236
  publish('server','Exited')
201
237
  publish('status','Exited')
202
- elif state == 'Clear':
203
- publish('acqCount', 0)
238
+ elif servState == 'Clear':
204
239
  publish('status','Cleared')
205
- # set server to previous state
240
+ # set server to previous servState
206
241
  set_server(C_.serverState)
207
- C_.serverState = state
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('?','W'), {}],
215
- ['server', 'Server control',
216
- SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WE'),
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
- ['verbosity', 'Debugging verbosity', SPV(0,'W','u8'),
219
- {'setter':set_verbosity}],
220
- ['polling', 'Polling interval', SPV(1.0,'W'), {U:'S', LL:0.001, LH:10.1}],
221
- ['cycle', 'Cycle number', SPV(0,'','u32'), {}],
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 providers
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, listDir:str, verbose:str=0):
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
- try:
244
- get_externalPV(prefix+'version')
245
- print(f'ERROR: Server for {prefix} already running. Exiting.')
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
- ['ch1Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
278
- ['ch1VoltsPerDiv', 'Vertical scale', SPV(1E-3,'W'), {U:'V/du'}],
279
- ['ch1Waveform', 'Waveform array', SPV([0.]), {U:'du'}],
280
- ['ch1Mean', 'Mean of the waveform', SPV(0.,'A'), {U:'du'}],
281
- ['ch1Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {U:'du',**alarm}],
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 accordingly."""
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
- set_noise(pvv('noiseLevel')) # Re-initialize noise array, because its size depends on recordLength
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, size=recordLength+nPatterns)# 45ms/1e6 points
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
- """Testing function. Do not use in production code."""
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('ch1VoltsPerDiv')
319
- wf += pvv('ch1Offset')
320
- publish('ch1Waveform', wf)
321
- publish('ch1Peak2Peak', np.ptp(wf))
322
- publish('ch1Mean', np.mean(wf))
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('-l', '--list', default='', nargs='?', help=
329
- 'Directory to save list of all generated PVs, if no directory is given, then </tmp/pvlist/><prefix> is assumed.')
330
- parser.add_argument('-p', '--prefix', default='epicsDev0:', help=
331
- 'Prefix to be prepended to all PVs')
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
- PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.list, pargs.verbose)
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, using pargs if needed. That can be used to set the number of points in the waveform, for example.
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 with polling interval {repr(pvv("polling"))} S.')
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
- time.sleep(pvv("polling"))
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.2
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 -l
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,,
@@ -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,,