epicsdev 1.0.0__tar.gz → 1.0.2__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: epicsdev
3
- Version: 1.0.0
3
+ Version: 1.0.2
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
@@ -16,18 +16,17 @@ def slider(minValue,maxValue):
16
16
 
17
17
  LargeFont = {'color':'light gray', **font(18), 'fgColor':'dark green'}
18
18
  ButtonFont = {'font':['Open Sans Extrabold',14]}# Comic Sans MS
19
- # Attributes for gray row, it should be in the first cell:
20
- #GrayRow = {'ATTRIBUTES':{'color':'light gray', **font(12)}}
21
19
  LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
22
20
  lColor = color('lightGreen')
23
21
 
24
22
  # definition for plotting cell
25
23
  PyPath = 'python -m'
26
- PaneP2P = ' '.join([f'ch{i+1:01d}Mean' for i in range(1)])
24
+ PaneP2P = ' '.join([f'ch{i+1:01d}Mean ch{i+1:01d}Peak2Peak' for i in range(1)])
27
25
  PaneWF = ' '.join([f'ch{i+1:01d}Waveform' for i in range(1)])
28
26
  #PaneT = 'timing[1] timing[3]'
29
- Plot = {'Plot':{'launch':f'{PyPath} pvplot -aV:epicsDev0: -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
30
- **lColor, **ButtonFont}}
27
+ Plot = {'Plot':{'launch':
28
+ f'{PyPath} pvplot Y-5:5 -aV:epicsDev0: -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
29
+ **lColor, **ButtonFont}}
31
30
  print(f'Plot command: {Plot}')
32
31
  #``````````````````PyPage Object``````````````````````````````````````````````
33
32
  class PyPage():
@@ -43,7 +42,7 @@ class PyPage():
43
42
  self.title = title
44
43
 
45
44
  #``````````Page attributes, optional`````````````````````````
46
- self.page = {**color(240,240,240)}# Does not work
45
+ self.page = {**color(240,240,240)}
47
46
  #self.page['editable'] = False
48
47
 
49
48
  #``````````Definition of columns`````````````````````````````
@@ -74,40 +73,14 @@ string or device:parameter and the value is dictionary of the features.
74
73
  #FOption = ' -file '+logreqMap.get(D,'')
75
74
  #``````````mandatory member```````````````````````````````````````````
76
75
  self.rows = [
77
- ['Device:', D, {D+'version':span(2,1)}],#_, 'scope time:', #{D+'dateTime':span(2,1)},_],
76
+ ['Device:', D, {D+'version':span(2,1)},_,_,_,_],
78
77
  ['State:', D+'server','cycle:',D+'cycle',_,_,Plot], # 'Recall:', D+'setup',],
79
78
  ['Status:', {D+'status': span(8,1)}],
80
- ['Polling Interval:', D+'polling',_,_,_,],
81
- #['Triggers recorded:', D+'acqCount', 'Lost:', D+'lostTrigs',
82
- # 'Acquisitions:',D+'scopeAcqCount'],
83
- # ['Horizontal scale:', D+'timePerDiv', ' samples:', D+'recLength',
84
- # 'SamplRate:', {D+'samplingRate':span(2,1)},_],
85
- # #['Trigger:', D+'trigSourceS', D+'trigCouplingS', D+'trigSlopeS', 'level:', D+'trigLevelS', 'delay:', {D+'trigDelay':span(2,1)},''],
86
- # ['Trigger state:',D+'trigState',' trigMode:',D+'trigMode',
87
- # 'TrigLevel','TrigDelay'],
88
- # [{D+'trigger':color('lightCyan')}, D+'trigSource', D+'trigCoupling',
89
- # D+'trigSlope', D+'trigLevel', D+'trigDelay'],
90
- [{'ATTRIBUTES':color('lightGreen')}, 'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
91
- # ['Gain:']+ChLine('VoltsPerDiv'),
92
- # ['Offset:']+ChLine('Position'),
93
- # ['Coupling:']+ChLine('Coupling'),
94
- # ['Termination:']+ChLine('Termination'),
95
- # ['On/Off:']+ChLine('OnOff'),
96
- #['Delay:']+ChLine('DelayFromTriggerM'),
97
- #['Waveform:']+ChLine('WaveforM'),
79
+ ['Polling Interval:', D+'polling','nPoints:',D+'recordLength',
80
+ 'Noise:',D+'noiseLevel',_],
81
+ [{'ATTRIBUTES':color('lightCyan')},
82
+ 'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
83
+ ['V/div:']+ChLine('VoltsPerDiv'),
98
84
  ['Mean:']+ChLine('Mean'),
99
85
  ['Peak2Peak:']+ChLine('Peak2Peak'),
100
- #[''],
101
- # ["Trigger",D+'trigSourceS',D+'trigLevelS',D+'trigSlopeS',D+'trigModeS'],
102
- # ['',"Setup"],
103
- # ["Repair:",D+'updateDataA',D+'deviceClearA',D+'resetScopeA',D+'forceTrigA'],
104
- # ["Session",D+'SaveSession',D+'RecallSession',"folder:",D+'folderS'],
105
- # [D+'currentSessionS',"<-current",D+'nextSessionS',"out off",D+'sessionsM'],
106
- #[{'ATTRIBUTES':{'color':'yellow'}},
107
- #['tAxis:',D+'tAxis'],
108
- # [LYRow,'',{'For Experts only!':{**span(6,1),**font(14)}}],
109
- # [LYRow,'Scope command:', {D+'instrCmdS':span(2,1)},_,{D+'instrCmdR':span(4,1)}],
110
- # [LYRow,'Special commands', {D+'instrCtrl':span(2,1)},_,_,_,_,_,],
111
- # [LYRow,'Timing:',{D+'timing':span(6,1)}],
112
- # [LYRow,'ActOnEvent',D+'actOnEvent','AOE_Limit',D+'aOE_Limit',_,_,_],
113
86
  ]
@@ -1,10 +1,15 @@
1
1
  """Skeleton and helper functions for creating EPICS PVAccess server"""
2
2
  # pylint: disable=invalid-name
3
- __version__= 'v1.0.0 26-01-16'# re-factored and simplified, comments added. Main() re-writtedn.
3
+ __version__= 'v1.0.2 26-01-18'# --list define directory to save list of PVs.
4
4
  #TODO: NTEnums do not have structure display
5
+ #TODO: Add performance counters to demo.
6
+ #Issue: There is no way in PVAccess to specify if string PV is writable.
7
+ # As a workaround we append description with suffix ' Features: W' to indicate that.
5
8
 
6
9
  import sys
7
10
  import time
11
+ from time import perf_counter as timer
12
+ import os
8
13
  from p4p.nt import NTScalar, NTEnum
9
14
  from p4p.nt.enum import ntenum
10
15
  from p4p.server import Server
@@ -104,7 +109,11 @@ def _create_PVs(pvDefs):
104
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"""
105
110
  ts = time.time()
106
111
  for defs in pvDefs:
107
- pname,desc,spv,extra = defs
112
+ try:
113
+ pname,desc,spv,extra = defs
114
+ except ValueError:
115
+ printe(f'Invalid PV definition of {defs[0]}')
116
+ sys.exit(1)
108
117
  ivalue = spv.current()
109
118
  printv(f'created pv {pname}, initial: {type(ivalue),ivalue}, extra: {extra}')
110
119
  C_.PVs[C_.prefix+pname] = spv
@@ -202,7 +211,7 @@ def create_PVs(pvDefs=None):
202
211
  U,LL,LH = 'units','limitLow','limitHigh'
203
212
  C_.PVDefs = [
204
213
  ['version', 'Program version', SPV(__version__), {}],
205
- ['status', 'Server status', SPV('?','W'), {}],
214
+ ['status', 'Server status. Features: RWE', SPV('?','W'), {}],
206
215
  ['server', 'Server control',
207
216
  SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WE'),
208
217
  {'setter':set_server}],
@@ -217,76 +226,96 @@ def create_PVs(pvDefs=None):
217
226
  _create_PVs(C_.PVDefs)
218
227
  return C_.PVs
219
228
 
220
- def get_externalPV(pvName, timeout=0.5):
221
- """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."""
229
+ def get_externalPV(pvName:str, timeout=0.5):
230
+ """Get value of PV from another server. That can be used to check if the
231
+ server is already running, or to get values from other servers."""
222
232
  ctxt = Context('pva')
223
233
  return ctxt.get(pvName, timeout=timeout)
224
234
 
225
- def init_epicsdev(prefix, pvDefs, verbose=0):
226
- """Check if no other server is running with the same prefix, create PVs and return them as a dictionary."""
235
+ def init_epicsdev(prefix:str, pvDefs:list, listDir:str, verbose:str=0):
236
+ """Check if no other server is running with the same prefix.
237
+ Create PVs and return them as a dictionary.
238
+ The listDir is a directory to save list of all generated PVs,
239
+ if no directory is given, then </tmp/pvlist/><prefix> is assumed.
240
+ """
227
241
  C_.prefix = prefix
228
242
  C_.verbose = verbose
229
243
  try:
230
244
  get_externalPV(prefix+'version')
231
- print(f'Server for {prefix} already running. Exiting.')
245
+ print(f'ERROR: Server for {prefix} already running. Exiting.')
232
246
  sys.exit(1)
233
247
  except TimeoutError:
234
248
  pass
235
249
  pvs = create_PVs(pvDefs)
250
+ # Save list of PVs to a file, if requested
251
+ if listDir != '':
252
+ listDir = '/tmp/pvlist/' if listDir is None else listDir
253
+ if not os.path.exists(listDir):
254
+ os.makedirs(listDir)
255
+ filepath = f'{listDir}{prefix[:-1]}.txt'
256
+ print(f'Writing list of PVs to {filepath}')
257
+ with open(filepath, 'w', encoding="utf-8") as f:
258
+ for _pvname in pvs:
259
+ f.write(_pvname + '\n')
236
260
  return pvs
237
261
 
238
- #``````````````````Testing stuff``````````````````````````````````````````````
262
+ #``````````````````Demo````````````````````````````````````````````````````````
239
263
  if __name__ == "__main__":
264
+ print(f'epicsdev multiadc demo server {__version__}')
240
265
  import numpy as np
241
266
  import argparse
242
267
 
243
268
  def myPVDefs():
244
269
  """Example of PV definitions"""
245
270
  SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
246
- alarm = {'valueAlarm':{'lowAlarmLimit':0, 'highAlarmLimit':100}}
271
+ alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
247
272
  return [ # device-specific PVs
248
- ['noiseLevel', 'Noise amplitude', SPV(1.E-6,'W'), {SET:set_noise}],
273
+ ['noiseLevel', 'Noise amplitude', SPV(1.E-6,'W'), {SET:set_noise, U:'V'}],
249
274
  ['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
250
275
  ['recordLength','Max number of points', SPV(100,'W','u32'),
251
276
  {LL:4,LH:1000000, SET:set_recordLength}],
252
277
  ['ch1Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
253
278
  ['ch1VoltsPerDiv', 'Vertical scale', SPV(1E-3,'W'), {U:'V/du'}],
254
- ['timePerDiv', 'Horizontal scale', SPV(1.E-6,'W'), {U:'S/du'}],
255
- ['ch1Waveform', 'Waveform array', SPV([0.]), {}],
256
- ['ch1Mean', 'Mean of the waveform', SPV(0.,'A'), {}],
257
- ['ch1Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {}],
258
- ['alarm', 'PV with alarm', SPV(0,'WA'), alarm],
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}],
282
+ ['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
259
283
  ]
260
- nPatterns = 100 # number of patterns in the waveform.
284
+ nPatterns = 100 # number of waveform patterns.
261
285
  pargs = None
262
- nDivs = 10 # number of divisions on the oscilloscope screen. That is needed to set tAxis when recordLength is changed.
286
+ rng = np.random.default_rng(nPatterns)
287
+ nPoints = 100
263
288
 
264
289
  def set_recordLength(value):
265
290
  """Record length have changed. The tAxis should be updated accordingly."""
266
291
  printi(f'Setting tAxis to {value}')
267
- publish('tAxis', np.arange(value)*pvv('timePerDiv')/nDivs)
292
+ publish('tAxis', np.arange(value)*1.E-6)
268
293
  publish('recordLength', value)
294
+ set_noise(pvv('noiseLevel')) # Re-initialize noise array, because its size depends on recordLength
269
295
 
270
296
  def set_noise(level):
271
297
  """Noise level have changed. Update noise array."""
272
- printi(f'Setting noise level to {level}')
273
- pargs.noise = np.random.normal(scale=0.5*level, size=pargs.npoints+nPatterns)
274
- #printv(f'Noise array updated with level {level}: {pargs.noise}')
298
+ v = float(level)
299
+ recordLength = pvv('recordLength')
300
+ ts = timer()
301
+ pargs.noise = np.random.normal(scale=0.5*level, size=recordLength+nPatterns)# 45ms/1e6 points
302
+ printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
275
303
  publish('noiseLevel', level)
276
-
304
+
277
305
  def init(recordLength):
278
306
  """Testing function. Do not use in production code."""
279
307
  set_recordLength(recordLength)
280
- set_noise(pvv('noiseLevel'))
308
+ #set_noise(pvv('noiseLevel')) # already called from set_recordLength
281
309
 
282
310
  def poll():
283
311
  """Example of polling function"""
284
- pattern = C_.cycle % nPatterns
285
- C_.cycle += 1
286
- printv(f'cycle {C_.cycle}')
287
- publish('cycle', C_.cycle)
312
+ #pattern = C_.cycle % nPatterns# produces sliding
313
+ pattern = rng.integers(0, nPatterns)
314
+ cycle = pvv('cycle')
315
+ printv(f'cycle {repr(cycle)}')
316
+ publish('cycle', cycle + 1)
288
317
  wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
289
- wf *= pvv('ch1VoltsPerDiv')*nDivs
318
+ wf /= pvv('ch1VoltsPerDiv')
290
319
  wf += pvv('ch1Offset')
291
320
  publish('ch1Waveform', wf)
292
321
  publish('ch1Peak2Peak', np.ptp(wf))
@@ -296,22 +325,20 @@ if __name__ == "__main__":
296
325
  parser = argparse.ArgumentParser(description = __doc__,
297
326
  formatter_class=argparse.ArgumentDefaultsHelpFormatter,
298
327
  epilog=f'{__version__}')
299
- parser.add_argument('-l', '--listPVs', action='store_true', help=
300
- 'List all generated PVs')
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.')
301
330
  parser.add_argument('-p', '--prefix', default='epicsDev0:', help=
302
331
  'Prefix to be prepended to all PVs')
303
- parser.add_argument('-n', '--npoints', type=int, default=100, help=
332
+ # The rest of options are not essential, they can be controlled at runtime using PVs.
333
+ parser.add_argument('-n', '--npoints', type=int, default=nPoints, help=
304
334
  'Number of points in the waveform')
305
335
  parser.add_argument('-v', '--verbose', action='count', default=0, help=
306
- 'Show more log messages (-vv: show even more)')
336
+ 'Show more log messages (-vv: show even more)')
307
337
  pargs = parser.parse_args()
338
+ print(pargs)
308
339
 
309
340
  # Initialize epicsdev and PVs
310
- PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose)
311
- if pargs.listPVs:
312
- print('List of PVs:')
313
- for _pvname in PVs:
314
- print(_pvname)
341
+ PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.list, pargs.verbose)
315
342
 
316
343
  # Initialize the device, using pargs if needed. That can be used to set the number of points in the waveform, for example.
317
344
  init(pargs.npoints)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "epicsdev"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  authors = [
9
9
  { name="Andrey Sukhanov", email="sukhanov@bnl.gov" },
10
10
  ]
File without changes
File without changes
File without changes