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