epicsdev 1.0.1__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.1
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,42 +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','RecLength:',D+'recordLength',
81
- 'V/Div:',D+'ch1VoltsPerDiv'],
82
- ['Noise level:',D+'noiseLevel'],
83
- #['Triggers recorded:', D+'acqCount', 'Lost:', D+'lostTrigs',
84
- # 'Acquisitions:',D+'scopeAcqCount'],
85
- # ['Horizontal scale:', D+'timePerDiv', ' samples:', D+'recLength',
86
- # 'SamplRate:', {D+'samplingRate':span(2,1)},_],
87
- # #['Trigger:', D+'trigSourceS', D+'trigCouplingS', D+'trigSlopeS', 'level:', D+'trigLevelS', 'delay:', {D+'trigDelay':span(2,1)},''],
88
- # ['Trigger state:',D+'trigState',' trigMode:',D+'trigMode',
89
- # 'TrigLevel','TrigDelay'],
90
- # [{D+'trigger':color('lightCyan')}, D+'trigSource', D+'trigCoupling',
91
- # D+'trigSlope', D+'trigLevel', D+'trigDelay'],
92
- [{'ATTRIBUTES':color('lightGreen')}, 'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
93
- # ['Gain:']+ChLine('VoltsPerDiv'),
94
- # ['Offset:']+ChLine('Position'),
95
- # ['Coupling:']+ChLine('Coupling'),
96
- # ['Termination:']+ChLine('Termination'),
97
- # ['On/Off:']+ChLine('OnOff'),
98
- #['Delay:']+ChLine('DelayFromTriggerM'),
99
- #['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'),
100
84
  ['Mean:']+ChLine('Mean'),
101
85
  ['Peak2Peak:']+ChLine('Peak2Peak'),
102
- #[''],
103
- # ["Trigger",D+'trigSourceS',D+'trigLevelS',D+'trigSlopeS',D+'trigModeS'],
104
- # ['',"Setup"],
105
- # ["Repair:",D+'updateDataA',D+'deviceClearA',D+'resetScopeA',D+'forceTrigA'],
106
- # ["Session",D+'SaveSession',D+'RecallSession',"folder:",D+'folderS'],
107
- # [D+'currentSessionS',"<-current",D+'nextSessionS',"out off",D+'sessionsM'],
108
- #[{'ATTRIBUTES':{'color':'yellow'}},
109
- #['tAxis:',D+'tAxis'],
110
- # [LYRow,'',{'For Experts only!':{**span(6,1),**font(14)}}],
111
- # [LYRow,'Scope command:', {D+'instrCmdS':span(2,1)},_,{D+'instrCmdR':span(4,1)}],
112
- # [LYRow,'Special commands', {D+'instrCtrl':span(2,1)},_,_,_,_,_,],
113
- # [LYRow,'Timing:',{D+'timing':span(6,1)}],
114
- # [LYRow,'ActOnEvent',D+'actOnEvent','AOE_Limit',D+'aOE_Limit',_,_,_],
115
86
  ]
@@ -1,11 +1,15 @@
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
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
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.
6
8
 
7
9
  import sys
8
10
  import time
11
+ from time import perf_counter as timer
12
+ import os
9
13
  from p4p.nt import NTScalar, NTEnum
10
14
  from p4p.nt.enum import ntenum
11
15
  from p4p.server import Server
@@ -105,7 +109,11 @@ def _create_PVs(pvDefs):
105
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"""
106
110
  ts = time.time()
107
111
  for defs in pvDefs:
108
- 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)
109
117
  ivalue = spv.current()
110
118
  printv(f'created pv {pname}, initial: {type(ivalue),ivalue}, extra: {extra}')
111
119
  C_.PVs[C_.prefix+pname] = spv
@@ -203,7 +211,7 @@ def create_PVs(pvDefs=None):
203
211
  U,LL,LH = 'units','limitLow','limitHigh'
204
212
  C_.PVDefs = [
205
213
  ['version', 'Program version', SPV(__version__), {}],
206
- ['status', 'Server status', SPV('?','W'), {}],
214
+ ['status', 'Server status. Features: RWE', SPV('?','W'), {}],
207
215
  ['server', 'Server control',
208
216
  SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WE'),
209
217
  {'setter':set_server}],
@@ -218,80 +226,96 @@ def create_PVs(pvDefs=None):
218
226
  _create_PVs(C_.PVDefs)
219
227
  return C_.PVs
220
228
 
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."""
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."""
223
232
  ctxt = Context('pva')
224
233
  return ctxt.get(pvName, timeout=timeout)
225
234
 
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."""
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
+ """
228
241
  C_.prefix = prefix
229
242
  C_.verbose = verbose
230
243
  try:
231
244
  get_externalPV(prefix+'version')
232
- print(f'Server for {prefix} already running. Exiting.')
245
+ print(f'ERROR: Server for {prefix} already running. Exiting.')
233
246
  sys.exit(1)
234
247
  except TimeoutError:
235
248
  pass
236
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')
237
260
  return pvs
238
261
 
239
262
  #``````````````````Demo````````````````````````````````````````````````````````
240
263
  if __name__ == "__main__":
264
+ print(f'epicsdev multiadc demo server {__version__}')
241
265
  import numpy as np
242
266
  import argparse
243
267
 
244
268
  def myPVDefs():
245
269
  """Example of PV definitions"""
246
270
  SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
247
- alarm = {'valueAlarm':{'lowAlarmLimit':0, 'highAlarmLimit':100}}
271
+ alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
248
272
  return [ # device-specific PVs
249
- ['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'}],
250
274
  ['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
251
275
  ['recordLength','Max number of points', SPV(100,'W','u32'),
252
276
  {LL:4,LH:1000000, SET:set_recordLength}],
253
277
  ['ch1Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
254
278
  ['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],
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}],
260
283
  ]
261
284
  nPatterns = 100 # number of waveform patterns.
262
285
  pargs = None
263
- nDivs = 10 # number of divisions on the oscilloscope screen. That is needed to set tAxis when recordLength is changed.
264
286
  rng = np.random.default_rng(nPatterns)
287
+ nPoints = 100
265
288
 
266
289
  def set_recordLength(value):
267
290
  """Record length have changed. The tAxis should be updated accordingly."""
268
291
  printi(f'Setting tAxis to {value}')
269
- publish('tAxis', np.arange(value)*pvv('timePerDiv')/nDivs)
292
+ publish('tAxis', np.arange(value)*1.E-6)
270
293
  publish('recordLength', value)
271
294
  set_noise(pvv('noiseLevel')) # Re-initialize noise array, because its size depends on recordLength
272
295
 
273
296
  def set_noise(level):
274
297
  """Noise level have changed. Update noise array."""
275
- printi(f'Setting noise level to {repr(level)}')
298
+ v = float(level)
276
299
  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)}')
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.')
279
303
  publish('noiseLevel', level)
280
304
 
281
305
  def init(recordLength):
282
306
  """Testing function. Do not use in production code."""
283
307
  set_recordLength(recordLength)
284
- set_noise(pvv('noiseLevel'))
308
+ #set_noise(pvv('noiseLevel')) # already called from set_recordLength
285
309
 
286
310
  def poll():
287
311
  """Example of polling function"""
288
312
  #pattern = C_.cycle % nPatterns# produces sliding
289
313
  pattern = rng.integers(0, nPatterns)
290
- C_.cycle += 1
291
- printv(f'cycle {C_.cycle}')
292
- publish('cycle', C_.cycle)
314
+ cycle = pvv('cycle')
315
+ printv(f'cycle {repr(cycle)}')
316
+ publish('cycle', cycle + 1)
293
317
  wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
294
- wf *= pvv('ch1VoltsPerDiv')*nDivs
318
+ wf /= pvv('ch1VoltsPerDiv')
295
319
  wf += pvv('ch1Offset')
296
320
  publish('ch1Waveform', wf)
297
321
  publish('ch1Peak2Peak', np.ptp(wf))
@@ -301,22 +325,20 @@ if __name__ == "__main__":
301
325
  parser = argparse.ArgumentParser(description = __doc__,
302
326
  formatter_class=argparse.ArgumentDefaultsHelpFormatter,
303
327
  epilog=f'{__version__}')
304
- parser.add_argument('-l', '--listPVs', action='store_true', help=
305
- '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.')
306
330
  parser.add_argument('-p', '--prefix', default='epicsDev0:', help=
307
331
  'Prefix to be prepended to all PVs')
308
- 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=
309
334
  'Number of points in the waveform')
310
335
  parser.add_argument('-v', '--verbose', action='count', default=0, help=
311
- 'Show more log messages (-vv: show even more)')
336
+ 'Show more log messages (-vv: show even more)')
312
337
  pargs = parser.parse_args()
338
+ print(pargs)
313
339
 
314
340
  # 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)
341
+ PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.list, pargs.verbose)
320
342
 
321
343
  # Initialize the device, using pargs if needed. That can be used to set the number of points in the waveform, for example.
322
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.1"
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