epicsdev 2.1.2__tar.gz → 3.0.1__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: 2.1.2
3
+ Version: 3.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
@@ -14,7 +14,7 @@ Requires-Dist: p4p
14
14
  Description-Content-Type: text/markdown
15
15
 
16
16
  # epicsdev
17
- Helper module for creating EPICS PVAccess servers.
17
+ Helper module for creating EPICS PVAccess servers using [p4p](https://epics-base.github.io/p4p/).
18
18
 
19
19
  Demo:
20
20
  ```
@@ -1,5 +1,5 @@
1
1
  # epicsdev
2
- Helper module for creating EPICS PVAccess servers.
2
+ Helper module for creating EPICS PVAccess servers using [p4p](https://epics-base.github.io/p4p/).
3
3
 
4
4
  Demo:
5
5
  ```
@@ -54,15 +54,15 @@
54
54
  <y_pv>pva://epicsDev0:c01Waveform</y_pv>
55
55
  <err_pv></err_pv>
56
56
  <axis>0</axis>
57
- <trace_type>1</trace_type>
57
+ <trace_type>0</trace_type>
58
58
  <color>
59
59
  <color red="0" green="0" blue="255">
60
60
  </color>
61
61
  </color>
62
62
  <line_width>1</line_width>
63
63
  <line_style>0</line_style>
64
- <point_type>0</point_type>
65
- <point_size>10</point_size>
64
+ <point_type>1</point_type>
65
+ <point_size>2</point_size>
66
66
  <visible>true</visible>
67
67
  </trace>
68
68
  </traces>
@@ -91,6 +91,19 @@
91
91
  <y>290</y>
92
92
  <width>550</width>
93
93
  <y_axes>
94
+ <y_axis>
95
+ <title>Mpts/s</title>
96
+ <autoscale>true</autoscale>
97
+ <log_scale>false</log_scale>
98
+ <minimum>0.0</minimum>
99
+ <maximum>100.0</maximum>
100
+ <show_grid>false</show_grid>
101
+ <visible>true</visible>
102
+ <color>
103
+ <color name="Text" red="0" green="0" blue="0">
104
+ </color>
105
+ </color>
106
+ </y_axis>
94
107
  <y_axis>
95
108
  <title>Volts</title>
96
109
  <autoscale>true</autoscale>
@@ -107,10 +120,10 @@
107
120
  </y_axes>
108
121
  <traces>
109
122
  <trace>
110
- <name>$(traces[0].y_pv)</name>
123
+ <name>Peak2Peak</name>
111
124
  <y_pv>pva://epicsDev0:c01Peak2Peak</y_pv>
112
- <axis>0</axis>
113
- <trace_type>2</trace_type>
125
+ <axis>1</axis>
126
+ <trace_type>1</trace_type>
114
127
  <color>
115
128
  <color red="0" green="0" blue="255">
116
129
  </color>
@@ -121,10 +134,10 @@
121
134
  <visible>true</visible>
122
135
  </trace>
123
136
  <trace>
124
- <name>$(traces[1].y_pv)</name>
137
+ <name>Mean</name>
125
138
  <y_pv>pva://epicsDev0:c01Mean</y_pv>
126
- <axis>0</axis>
127
- <trace_type>2</trace_type>
139
+ <axis>1</axis>
140
+ <trace_type>1</trace_type>
128
141
  <color>
129
142
  <color red="255" green="0" blue="0">
130
143
  </color>
@@ -134,6 +147,20 @@
134
147
  <point_size>10</point_size>
135
148
  <visible>true</visible>
136
149
  </trace>
150
+ <trace>
151
+ <name>Throughput</name>
152
+ <y_pv>pva://epicsDev0:throughput</y_pv>
153
+ <axis>2</axis>
154
+ <trace_type>1</trace_type>
155
+ <color>
156
+ <color red="0" green="255" blue="0">
157
+ </color>
158
+ </color>
159
+ <line_width>2</line_width>
160
+ <point_type>0</point_type>
161
+ <point_size>10</point_size>
162
+ <visible>true</visible>
163
+ </trace>
137
164
  </traces>
138
165
  </widget>
139
166
  <widget type="textentry" version="3.0.0">
@@ -191,25 +218,50 @@
191
218
  <y>200</y>
192
219
  <horizontal_alignment>1</horizontal_alignment>
193
220
  </widget>
194
- <widget type="textentry" version="3.0.0">
195
- <name>Cycle time</name>
221
+ <widget type="label" version="2.0.0">
222
+ <name>Label_6</name>
223
+ <text>Cycle:</text>
224
+ <x>579</x>
225
+ <y>29</y>
226
+ <width>40</width>
227
+ </widget>
228
+ <widget type="textupdate" version="2.0.0">
229
+ <name>throughput</name>
230
+ <pv_name>pva://epicsDev0:throughput</pv_name>
231
+ <x>580</x>
232
+ <y>260</y>
233
+ </widget>
234
+ <widget type="label" version="2.0.0">
235
+ <name>Label_5</name>
236
+ <text>Throughput:: text</text>
237
+ <x>490</x>
238
+ <y>261</y>
239
+ <width>90</width>
240
+ </widget>
241
+ <widget type="textupdate" version="2.0.0">
242
+ <name>cycleTime</name>
196
243
  <pv_name>pva://epicsDev0:cycleTime</pv_name>
197
244
  <x>580</x>
198
245
  <y>240</y>
199
246
  </widget>
200
- <widget type="textentry" version="3.0.0">
201
- <name>Cycle</name>
247
+ <widget type="textupdate" version="2.0.0">
248
+ <name>Text Update_3</name>
202
249
  <pv_name>pva://epicsDev0:cycle</pv_name>
203
- <x>620</x>
204
- <y>10</y>
250
+ <x>629</x>
251
+ <y>29</y>
205
252
  <width>60</width>
206
253
  <precision>0</precision>
207
254
  </widget>
208
- <widget type="label" version="2.0.0">
209
- <name>Label_6</name>
210
- <text>Cycle</text>
255
+ <widget type="combo" version="2.0.0">
256
+ <name>Server</name>
257
+ <pv_name>pva://epicsDev0:server</pv_name>
211
258
  <x>579</x>
212
- <y>10</y>
213
- <width>40</width>
259
+ <height>20</height>
260
+ </widget>
261
+ <widget type="label" version="2.0.0">
262
+ <name>Label_7</name>
263
+ <text>Server:</text>
264
+ <x>520</x>
265
+ <width>49</width>
214
266
  </widget>
215
267
  </display>
@@ -81,7 +81,7 @@ string or device:parameter and the value is dictionary of the features.
81
81
  print(f'Plot command: {Plot}')
82
82
  #``````````mandatory member```````````````````````````````````````````
83
83
  self.rows = [
84
- ['Device:',D, D+'server', D+'version', 'host:',D+'host',_],
84
+ ['Device:',D, D+'server', D+'VERSION', 'host:',D+'HOSTNAME',D+'CPU_LOAD'],
85
85
  ['Status:', {D+'status': span(8,1)}],
86
86
  ['Cycle time:',D+'cycleTime', 'Sleep:',D+'sleep', 'Cycle:',D+'cycle', Plot],
87
87
  ['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
@@ -83,7 +83,7 @@ string or device:parameter and the value is dictionary of the features.
83
83
  print(f'Timing button: {Timing}')
84
84
  #``````````mandatory member```````````````````````````````````````````
85
85
  self.rows = [
86
- ['Device:',D, D+'server', {D+'channels':just(2)},'chnls, host:',D+'host',D+'version'],
86
+ ['Device:',D, D+'server', {D+'channels':just(2)},'chnls, host:',D+'HOSTNAME',D+'VERSION'],
87
87
  ['Status:', {D+'status': span(8,1)}],
88
88
  ['Cycle time:',D+'cycleTime', 'Sleep:',D+'sleep', 'Cycle:',D+'cycle'],
89
89
  ['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
@@ -0,0 +1,493 @@
1
+ """Skeleton and helper functions for creating EPICS PVAccess server"""
2
+ # pylint: disable=invalid-name
3
+ __version__= 'v3.0.1 26-02-24'# Major upgrade. Added standard EPICS iocStats PV,
4
+ # SPV removed, PvDefs definitions simplified, new features added.
5
+ #TODO: add support for autoSave, (feature 'A'), caputLog (feature 'H') and access rights
6
+
7
+ import sys
8
+ import time
9
+ from time import perf_counter as timer
10
+ import os
11
+ import threading
12
+ from socket import gethostname
13
+ import psutil
14
+ from p4p.nt import NTScalar, NTEnum
15
+ from p4p.nt.enum import ntenum
16
+ from p4p.server import Server
17
+ from p4p.server.thread import SharedPV
18
+ from p4p.client.thread import Context
19
+ from p4p.nt import Type
20
+
21
+ PeriodicUpdateInterval = 10. # seconds
22
+
23
+ #``````````````````Module Storage`````````````````````````````````````````````
24
+ def _serverStateChanged(newState:str):
25
+ """Dummy serverStateChanged function"""
26
+ return
27
+
28
+ class C_():
29
+ """Storage for module members"""
30
+ prefix = ''
31
+ verbose = 0
32
+ startTime = 0.
33
+ cycle = 0
34
+ serverState = ''
35
+ PVs = {}
36
+ PVDefs = []
37
+ serverStateChanged = _serverStateChanged
38
+ lastCycleTime = timer()
39
+ lastUpdateTime = 0.
40
+ cycleTimeSum = 0.
41
+ cyclesAfterUpdate = 0
42
+ #``````````````````Constants
43
+ dtype2p4p = {# mapping from numpy dtype to p4p type code
44
+ 's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
45
+ 'u64':'L', 'f32':'f', 'f64':'d', str:'s',
46
+ }
47
+
48
+ #```````````````````Helper methods````````````````````````````````````````````
49
+ def serverState():
50
+ """Return current server state. That is the value of the server PV, but
51
+ cached in C_ to avoid unnecessary get() calls."""
52
+ return C_.serverState
53
+ def _printTime():
54
+ return time.strftime("%m%d:%H%M%S")
55
+ def printi(msg):
56
+ """Print info message and publish it to status PV."""
57
+ print(f'inf_@{_printTime()}: {msg}')
58
+ def printw(msg):
59
+ """Print warning message and publish it to status PV."""
60
+ txt = f'WAR_@{_printTime()}: {msg}'
61
+ print(txt)
62
+ publish('status',txt)
63
+ def printe(msg):
64
+ """Print error message and publish it to status PV."""
65
+ txt = f'ERR_{_printTime()}: {msg}'
66
+ print(txt)
67
+ publish('status',txt)
68
+ def _printv(msg, level):
69
+ if C_.verbose >= level:
70
+ print(f'DBG{level}: {msg}')
71
+ def printv(msg):
72
+ """Print debug message if verbosity level >=1."""
73
+ _printv(msg, 1)
74
+ def printvv(msg):
75
+ """Print debug message if verbosity level >=2."""
76
+ _printv(msg, 2)
77
+ def printv3(msg):
78
+ """Print debug message if verbosity level >=3."""
79
+ _printv(msg, 3)
80
+
81
+ def pvobj(pvName):
82
+ """Return PV with given name"""
83
+ return C_.PVs[C_.prefix+pvName]
84
+
85
+ def pvv(pvName:str):
86
+ """Return PV value"""
87
+ return pvobj(pvName).current()
88
+
89
+ def publish(pvName:str, value, ifChanged=False, t=None):
90
+ """Publish value to PV. If ifChanged is True, then publish only if the
91
+ value is different from the current value. If t is not None, then use
92
+ it as timestamp, otherwise use current time."""
93
+ #print(f'Publishing {pvName}')
94
+ try:
95
+ pv = pvobj(pvName)
96
+ except KeyError:
97
+ print(f'WARNING: PV {pvName} not found. Cannot publish value.')
98
+ return
99
+ if t is None:
100
+ t = time.time()
101
+ if not ifChanged or pv.current() != value:
102
+ pv.post(value, timestamp=t)
103
+
104
+ #``````````````````create_PVs()```````````````````````````````````````````````
105
+ def _create_PVs(pvDefs):
106
+ """Create PVs from the definitions in pvDefs and add them to the map of PVs."""
107
+
108
+ ts = time.time()
109
+ for defs in pvDefs:
110
+ try:
111
+ pname,desc,initial,*extra = defs
112
+ except ValueError:
113
+ printe(f'Invalid PV definition of {defs[0]}')
114
+ sys.exit(1)
115
+ extra = extra[0] if extra else {}
116
+
117
+ # Determine PV type and create SharedPV
118
+ iterable = type(initial) not in (int,float,str)
119
+ vtype = extra.get('type')
120
+ if vtype is None:
121
+ firstItem = initial[0] if iterable else initial
122
+ itype = type(firstItem)
123
+ vtype = {int: 'i32', float: 'f32'}.get(itype,itype)
124
+ tcode = dtype2p4p[vtype]
125
+ allowed_chars = 'WRAD'
126
+ meta = extra.get('features','')
127
+ writable = 'W' in meta
128
+ valueAlarm = extra.get('valueAlarm')
129
+ ntextra = [('features', Type([('writable', '?')]))]
130
+ for ch in meta:
131
+ if ch not in allowed_chars:
132
+ printe(f'Unknown meta character {ch} in SPV definition')
133
+ sys.exit(1)
134
+ if 'D' in meta:
135
+ initial = {'choices': initial, 'index': 0}
136
+ nt = NTEnum(display=True, extra=ntextra)
137
+ else:
138
+ prefix = 'a' if iterable else ''
139
+ nt = NTScalar(prefix+tcode, display=True, control=writable,
140
+ valueAlarm = valueAlarm is not None, extra=ntextra)
141
+ spv = SharedPV(nt=nt, initial=initial)
142
+
143
+ # Set initial value and description and add to the map of PVs
144
+ ivalue = spv.current()
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)
151
+ C_.PVs[C_.prefix+pname] = spv
152
+ v = spv._wrap(ivalue, timestamp=ts)
153
+ v['features.writable'] = writable
154
+ v['display.description'] = desc
155
+
156
+ # set extra parameters
157
+ for field in extra.keys():
158
+ try:
159
+ if field in ['limitLow','limitHigh','format','units']:
160
+ v[f'display.{field}'] = extra[field]
161
+ if field.startswith('limit'):
162
+ v[f'control.{field}'] = extra[field]
163
+ if field == 'valueAlarm':
164
+ for key,value in extra[field].items():
165
+ v[f'valueAlarm.{key}'] = value
166
+ except KeyError as e:
167
+ print(f'Cannot set {field} for {pname}: {e}')
168
+ sys.exit(1)
169
+ spv.post(v)
170
+
171
+ if writable:
172
+ # add new attributes, that will be used in the put handler
173
+ spv.name = pname
174
+ spv.setter = extra.get('setter')
175
+
176
+ # add put handler
177
+ @spv.put
178
+ def handle(spv, op):
179
+ ct = time.time()
180
+ vv = op.value()
181
+ vr = vv.raw.value
182
+ current = spv._wrap(spv.current())
183
+ # check limits, if they are defined. That will be a good
184
+ # example of using control structure and valueAlarm.
185
+ try:
186
+ limitLow = current['control.limitLow']
187
+ limitHigh = current['control.limitHigh']
188
+ if limitLow != limitHigh and not (limitLow <= vr <= limitHigh):
189
+ printw(f'Value {vr} is out of limits [{limitLow}, {limitHigh}]. Ignoring.')
190
+ op.done(error=f'Value out of limits [{limitLow}, {limitHigh}]')
191
+ return
192
+ except KeyError:
193
+ pass
194
+ if isinstance(vv, ntenum):
195
+ vr = str(vv)
196
+ if spv.setter:
197
+ spv.setter(vr, spv)
198
+ # value will be updated by the setter, so get it again
199
+ vr = pvv(spv.name)
200
+ printv(f'putting {spv.name} = {vr}')
201
+ spv.post(vr, timestamp=ct) # update subscribers
202
+ op.done()
203
+ #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
204
+ #``````````````````Setters
205
+ def set_verbose(level, *_):
206
+ """Set verbosity level for debugging"""
207
+ C_.verbose = level
208
+ printi(f'Setting verbose to {level}')
209
+ publish('verbose',level)
210
+
211
+ def set_server(servState, *_):
212
+ """Example of the setter for the server PV.
213
+ servState can be 'Start', 'Stop', 'Exit' or 'Clear'. If servState is None,
214
+ then get the desired state from the server PV."""
215
+ #printv(f'>set_server({servState}), {type(servState)}')
216
+ if servState is None:
217
+ servState = pvv('server')
218
+ printi(f'Setting server state to {servState}')
219
+ servState = str(servState)
220
+ C_.serverStateChanged(servState)
221
+ if servState == 'Start':
222
+ printi('Starting the server')
223
+ publish('server','Started')
224
+ publish('status','Started')
225
+ elif servState == 'Stop':
226
+ printi('server stopped')
227
+ publish('server','Stopped')
228
+ publish('status','Stopped')
229
+ elif servState == 'Exit':
230
+ printi('server is exiting')
231
+ publish('server','Exited')
232
+ publish('status','Exited')
233
+ elif servState == 'Clear':
234
+ publish('status','Cleared')
235
+ # set server to previous servState
236
+ set_server(C_.serverState)
237
+ return
238
+ C_.serverState = servState
239
+
240
+ def create_PVs(pvDefs=None):
241
+ """Create PVs from the definitions in C_.PVDefs and return the map of PVs.
242
+ pvDefs is a list of PV definitions, that will be appended to C_.PVDefs.
243
+ Each PV definition is a list of 3 or 4 items:
244
+ [pvName:str, description:str, initialValue, extra:dict]
245
+ The extra dict is optional and can have the following keys:
246
+ features: string with characters W (writable),
247
+ D (discrete). For example. By default, PV is read-only scalar.
248
+ type: string with data type, for example 'f32', 'i32', 's8', etc. By default,
249
+ the type is determined from the initial value (float -> 'f32', int -> 'i32').
250
+ units: string with physical units, for example 'V', 'S', 'Mpts/s', etc.
251
+ limitLow: number with low limit for the value. If defined, then the put
252
+ handler will check that the value is not below the low limit.
253
+ limitHigh: number with high limit for the value. If defined, then the put
254
+ handler will check that the value is not above the high limit.
255
+ setter: function to be called when the PV value is changed. The function
256
+ should have the signature:
257
+ def setter(value, spv):
258
+ where value is the new value, and spv is the SharedPV object.
259
+ The PVs defined in C_.PVDefs are created first, then the PVs from pvDefs are
260
+ created and appended to the map of PVs. That allows to have some common PVs
261
+ defined in C_.PVDefs, and device-specific PVs defined in pvDefs.
262
+ The function returns the map of PVs, where the keys are PV names with prefix,
263
+ and the values are SharedPV objects."""
264
+
265
+ F,T,U,LL,LH = 'features','type','units','limitLow','limitHigh'
266
+ C_.PVDefs = [
267
+ # iocStats-related PVs
268
+ ['HOSTNAME', 'Server host name', gethostname()],
269
+ ['VERSION', 'Program version', 'epicsdev '+__version__],
270
+ ['HEARTBEAT', 'Server heartbeat, Increments once per second', 0., {U:'S'}],
271
+ ['UPTIME', 'Server uptime in seconds', '', {U:'S'}],
272
+ ['STARTTOD', 'Server start time', time.strftime("%m/%d/%Y %H:%M:%S")],
273
+ ['CPU_LOAD', 'CPU load in %', 0., {U:'%'}],
274
+ ['CA_CONN_COUNT', 'Number of TCP connections', 0],
275
+ # Other popular stats: CA_CLIENTS, CA_CONN_COUNT, CPU_LOAD, FD_USED, THREAD_COUNT
276
+
277
+ ['status', 'Server status. Features: RWE', '', {F:'W'}],
278
+ ['server', 'Server control. Features: RWE',
279
+ 'Start Stop Clear Exit Started Stopped Exited'.split(),
280
+ {F:'WD', 'setter':set_server}],
281
+ ['verbose', 'Debugging verbosity',
282
+ C_.verbose, {F:'W', T:'u8', 'setter':set_verbose, LL:0,LH:3}],
283
+ ['sleep', 'Pause in the main loop, it could be useful for throttling the data output',
284
+ 1.0, {F:'W', T:'f32', U:'S', LL:0.001, LH:10.1}],
285
+ ['cycle', 'Cycle number, published every {PeriodicUpdateInterval} S.',
286
+ 0, {T:'u32'}],
287
+ ['cycleTime','Average cycle time including sleep, published every {PeriodicUpdateInterval} S',
288
+ 0., {U:'S'}],
289
+ ]
290
+ # append application's PVs, defined in the pvDefs and create map of
291
+ # providers
292
+ if pvDefs is not None:
293
+ C_.PVDefs += pvDefs
294
+ _create_PVs(C_.PVDefs)
295
+ return C_.PVs
296
+
297
+ def get_externalPV(pvName:str, timeout=0.5):
298
+ """Get value of PV from another server. That can be used to check if the
299
+ server is already running, or to get values from other servers."""
300
+ ctxt = Context('pva')
301
+ return ctxt.get(pvName, timeout=timeout)
302
+
303
+ def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
304
+ serverStateChanged=None, listDir=None):
305
+ """Check if no other server is running with the same prefix.
306
+ Create PVs and return them as a dictionary.
307
+ prefix is a string to be prepended to all PV names.
308
+ pvDefs is a list of PV definitions (see create_PVs()).
309
+ verbose is the verbosity level for debug messages.
310
+ serverStateChanged is a function to be called when the server PV changes.
311
+ The function should have the signature:
312
+ def serverStateChanged(newStatus:str):
313
+ If serverStateChanged is None, then a dummy function is used.
314
+ The listDir is a directory to save list of all generated PVs,
315
+ if no directory is given, then </tmp/pvlist/><prefix> is assumed.
316
+ """
317
+ if not isinstance(verbose, int) or verbose < 0:
318
+ printe('init_epicsdev arguments should be (prefix:str, pvDefs:list, verbose:int, listDir:str)')
319
+ sys.exit(1)
320
+ printi(f'Initializing epicsdev with prefix {prefix}')
321
+ C_.prefix = prefix
322
+ C_.verbose = verbose
323
+ if serverStateChanged is not None:# set custom serverStateChanged function
324
+ C_.serverStateChanged = serverStateChanged
325
+ try: # check if server is already running
326
+ host = repr(get_externalPV(prefix+'HOSTNAME')).replace("'",'')
327
+ print(f'ERROR: Server for {prefix} already running at {host}. Exiting.')
328
+ sys.exit(1)
329
+ except TimeoutError:
330
+ pass
331
+
332
+ # No existing server found. Creating PVs.
333
+ pvs = create_PVs(pvDefs)
334
+ # Save list of PVs to a file, if requested
335
+ if listDir != '':
336
+ listDir = '/tmp/pvlist/' if listDir is None else listDir
337
+ if not os.path.exists(listDir):
338
+ os.makedirs(listDir)
339
+ filepath = f'{listDir}{prefix[:-1]}.txt'
340
+ print(f'Writing list of PVs to {filepath}')
341
+ with open(filepath, 'w', encoding="utf-8") as f:
342
+ for _pvname in pvs:
343
+ f.write(_pvname + '\n')
344
+ printi(f'Hosting {len(pvs)} PVs')
345
+ C_.startTime = time.time()
346
+ threading.Thread(target=_heartbeat_thread, daemon=True).start()
347
+ return pvs
348
+
349
+ def _heartbeat_thread():
350
+ """Thread to update heartbeat and uptime PVs."""
351
+ while True:
352
+ time.sleep(1)
353
+ publish('HEARTBEAT', pvv('HEARTBEAT')+1)
354
+ publish('UPTIME', round(time.time() - C_.startTime, 1))
355
+
356
+ def sleep():
357
+ """Sleep function to be called in the main loop. It updates cycleTime PV
358
+ and sleeps for the time specified in sleep PV.
359
+ Returns False if a periodic update occurred.
360
+ """
361
+ time.sleep(pvv('sleep'))
362
+ sleeping = True
363
+ if serverState().startswith('Stop'):
364
+ return sleeping
365
+ tnow = timer()
366
+ C_.cycleTimeSum += tnow - C_.lastCycleTime
367
+ C_.lastCycleTime = tnow
368
+ C_.cyclesAfterUpdate += 1
369
+ C_.cycle += 1
370
+ printv(f'cycle {C_.cycle}')
371
+ if tnow - C_.lastUpdateTime > PeriodicUpdateInterval:
372
+ avgCycleTime = C_.cycleTimeSum / C_.cyclesAfterUpdate
373
+ printv(f'Average cycle time: {avgCycleTime:.6f} S.')
374
+ publish('cycle', C_.cycle)
375
+ publish('cycleTime', avgCycleTime)
376
+ publish('CPU_LOAD', round(psutil.cpu_percent(),1))
377
+ publish('CA_CONN_COUNT', len(psutil.net_connections(kind='tcp')))
378
+ C_.lastUpdateTime = tnow
379
+ C_.cycleTimeSum = 0.
380
+ C_.cyclesAfterUpdate = 0
381
+ sleeping = False
382
+ return sleeping
383
+
384
+ #``````````````````Demo````````````````````````````````````````````````````````
385
+ if __name__ == "__main__":
386
+ import numpy as np
387
+ import argparse
388
+
389
+ def myPVDefs():
390
+ """Example of PV definitions"""
391
+ F,T,U,LL,LH,SET = 'features','type','units','limitLow','limitHigh','setter'
392
+ alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
393
+ return [ # device-specific PVs
394
+ ['noiseLevel', 'Noise amplitude', 1., {F:'W', U:'V'}],
395
+ ['tAxis', 'Full scale of horizontal axis', [0.], {U:'S'}],
396
+ ['recordLength','Max number of points',
397
+ 100, {F:'W', T:'u32', LL:4,LH:1000000, SET:set_recordLength}],
398
+ ['throughput', 'Performance metrics, points per second', 0., {U:'Mpts/s'}],
399
+ ['c01Offset', 'Offset', 0., {F:'W', U:'du'}],
400
+ ['c01VoltsPerDiv', 'Vertical scale', 0.1, {F:'W', U:'V/du'}],
401
+ ['c01Waveform', 'Waveform array', [0.], {U:'du'}],
402
+ ['c01Mean', 'Mean of the waveform', 0., {U:'du'}],
403
+ ['c01Peak2Peak','Peak-to-peak amplitude', 0., {U:'du', **alarm}],
404
+ ['alarm', 'PV with alarm', 0, {U:'du', **alarm}],
405
+ ]
406
+ pargs = None
407
+ rng = np.random.default_rng()
408
+ nPoints = 100
409
+ _sum = {'points': 0, 'time': 0.}
410
+
411
+ def set_recordLength(value, *_):
412
+ """Record length have changed. The tAxis should be updated
413
+ accordingly."""
414
+ printi(f'Setting tAxis to {value}')
415
+ publish('tAxis', np.arange(value)*1.E-6)
416
+ publish('recordLength', value)
417
+
418
+ def init(recordLength):
419
+ """Example of device initialization function"""
420
+ set_recordLength(recordLength)
421
+ #set_noise(pvv('noiseLevel')) # already called from set_recordLength
422
+
423
+ def poll():
424
+ """Example of polling function. Called every cycle when server is running.
425
+ It returns time, spent in publishing data"""
426
+ wf = rng.random(pvv('recordLength'))*pvv('noiseLevel')# it takes 5ms for 1M points
427
+ wf /= pvv('c01VoltsPerDiv')
428
+ wf += pvv('c01Offset')
429
+ ts = timer()
430
+ publish('c01Waveform', wf)
431
+ _sum['time'] += timer() - ts
432
+ _sum['points'] += len(wf)
433
+ publish('c01Peak2Peak', np.ptp(wf))
434
+ publish('c01Mean', np.mean(wf))
435
+
436
+ def periodic_update():
437
+ """Perform periodic update"""
438
+ #printi(f'periodic update for {C_.cyclesSinceUpdate} cycles: {ElapsedTime}')
439
+ if state.startswith('Stop'):
440
+ publish('throughput', 0.)
441
+ else:
442
+ pointsPerSecond = _sum['points']/_sum['time']/1.E6
443
+ publish('throughput', round(pointsPerSecond,6))
444
+ printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
445
+ _sum['points'] = 0
446
+ _sum['time'] = 0.
447
+
448
+ # Argument parsing
449
+ parser = argparse.ArgumentParser(description = __doc__,
450
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
451
+ epilog=f'{__version__}')
452
+ parser.add_argument('-d', '--device', default='epicsDev', help=
453
+ 'Device name, the PV name will be <device><index>:')
454
+ parser.add_argument('-i', '--index', default='0', help=
455
+ 'Device index, the PV name will be <device><index>:')
456
+ parser.add_argument('-l', '--list', nargs='?', help=(
457
+ 'Directory to save list of all generated PVs, if no directory is given, '
458
+ 'then </tmp/pvlist/><prefix> is assumed.'))
459
+ # The rest of options are not essential, they can be controlled at runtime using PVs.
460
+ parser.add_argument('-n', '--npoints', type=int, default=nPoints, help=
461
+ 'Number of points in the waveform')
462
+ parser.add_argument('-v', '--verbose', action='count', default=0, help=
463
+ 'Show more log messages (-vv: show even more)')
464
+ pargs = parser.parse_args()
465
+ print(pargs)
466
+
467
+ # Initialize epicsdev and PVs
468
+ pargs.prefix = f'{pargs.device}{pargs.index}:'
469
+ PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose, None, pargs.list)
470
+
471
+ # Initialize the device using pargs if needed.
472
+ init(pargs.npoints)
473
+
474
+ # Start the Server. Use your set_server, if needed.
475
+ set_server('Start')
476
+
477
+ # Main loop
478
+ # In this example, we just update the waveform and its stats in a loop,
479
+ # but in a real application, the loop can also read data from the device,
480
+ # and update PVs accordingly. The loop can be paused by setting server PV to 'Stop',
481
+ # and exited by setting server PV to 'Exit'.
482
+ # The performance metrics are updated every {PeriodicUpdateInterval} seconds.
483
+ server = Server(providers=[PVs])
484
+ printi(f'Server started. Sleeping per cycle: {repr(pvv("sleep"))} S.')
485
+ while True:
486
+ state = serverState()
487
+ if state.startswith('Exit'):
488
+ break
489
+ if not state.startswith('Stop'):
490
+ poll()
491
+ if not sleep():# Sleep and update performance metrics periodically
492
+ periodic_update()
493
+ printi('Server is exited')
@@ -1,6 +1,6 @@
1
1
  """Simulated multi-channel ADC device server using epicsdev module."""
2
2
  # pylint: disable=invalid-name
3
- __version__= 'v2.1.1 26-02-04'# added timing, throughput and c0$VoltOffset PVs
3
+ __version__= 'v3.0.1 26-02-23'# updated to use new features of epicsdev v3.0.1, see epicsdev.py for details.
4
4
 
5
5
  import sys
6
6
  from time import perf_counter as timer
@@ -8,41 +8,42 @@ import argparse
8
8
  import numpy as np
9
9
 
10
10
  from .epicsdev import Server, Context, init_epicsdev, serverState, publish
11
- from .epicsdev import pvv, printi, printv, SPV, set_server, sleep
11
+ from .epicsdev import pvv, printi, printv, set_server, sleep
12
12
 
13
13
 
14
14
  def myPVDefs():
15
15
  """Example of PV definitions"""
16
- SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
16
+ F,T,U,LL,LH,SET = 'features','type','units','limitLow','limitHigh','setter'
17
17
  alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
18
18
  pvDefs = [ # device-specific PVs
19
- ['channels', 'Number of device channels', SPV(pargs.channels), {}],
19
+ ['channels', 'Number of device channels', pargs.channels],
20
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(0.05,'W'), {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}],
21
+ 'Start Stop Clear Exit Started Stopped Exited'.split(), {F:'WD'}],
22
+ ['noiseLevel', 'Noise amplitude', 0.05, {F:'W', U:'V'}],
23
+ ['tAxis', 'Full scale of horizontal axis', [0.], {U:'S'}],
24
+ ['recordLength','Max number of points', 100,
25
+ {F:'W', T:'u32', LL:4, LH:1000000, SET:set_recordLength}],
26
+ ['alarm', 'PV with alarm', 0, {F:'WA', U:'du', **alarm}],
27
27
  #``````````````````Auxiliary PVs
28
- ['timing', 'Elapsed time for waveform generation, publishing, total]', SPV([0.]), {U:'S'}],
29
- ['throughput', 'Total number of points processed per second', SPV(0.), {U:'Mpts/s'}],
28
+ ['timing', 'Elapsed time for waveform generation, publishing, total]', [0.], {U:'S'}],
29
+ ['throughput', 'Total number of points processed per second', 0., {U:'Mpts/s'}],
30
30
  ]
31
31
 
32
- # Templates for channel-related PVs. Important: SPV cannot be used in this list!
32
+ # Templates for channel-related PVs.
33
33
  ChannelTemplates = [
34
- ['c0$VoltsPerDiv', 'Vertical scale', (0.1,'W'), {U:'V/du'}],
35
- ['c0$VoltOffset', 'Vertical offset', (0.,'W'), {U:'V'}],
36
- ['c0$Waveform', 'Waveform array', ([0.],), {U:'du'}],
37
- ['c0$Mean', 'Mean of the waveform', (0.,'A'), {U:'du'}],
38
- ['c0$Peak2Peak','Peak-to-peak amplitude', (0.,'A'), {U:'du',**alarm}],
34
+ ['c0$VoltsPerDiv', 'Vertical scale', 0.1, {F:'W', U:'V/du'}],
35
+ ['c0$VoltOffset', 'Vertical offset', 0., {F:'W', U:'V'}],
36
+ ['c0$Waveform', 'Waveform array', [0.], {U:'du'}],
37
+ ['c0$Mean', 'Mean of the waveform', 0., {F:'A', U:'du'}],
38
+ ['c0$Peak2Peak','Peak-to-peak amplitude', 0., {F:'A', U:'du', **alarm}],
39
39
  ]
40
40
  # extend PvDefs with channel-related PVs
41
41
  for ch in range(pargs.channels):
42
42
  for pvdef in ChannelTemplates:
43
43
  newpvdef = pvdef.copy()
44
44
  newpvdef[0] = pvdef[0].replace('0$',f'{ch+1:02}')
45
- newpvdef[2] = SPV(*pvdef[2])
45
+ if len(newpvdef) > 3:
46
+ newpvdef[3] = newpvdef[3].copy()
46
47
  pvDefs.append(newpvdef)
47
48
  return pvDefs
48
49
 
@@ -68,7 +69,7 @@ def set_externalControl(value, *_):
68
69
  printi(f'External control PV: {pvname}')
69
70
  ctxt = Context('pva')
70
71
  try:
71
- r = ctxt.get(pvname, timeout=0.5)
72
+ ctxt.get(pvname, timeout=0.5)
72
73
  except TimeoutError:
73
74
  printi(f'Cannot connect to external control PV {pvname}.')
74
75
  sys.exit(1)
@@ -1,6 +1,6 @@
1
1
  """Skeleton and helper functions for creating EPICS PVAccess server"""
2
2
  # pylint: disable=invalid-name
3
- __version__= 'v2.1.2 26-02-07'# do nothing in sleep() if stopped.
3
+ __version__= 'v3.0.0 26-02-22'#
4
4
  #Issue: There is no way in PVAccess to specify if string PV is writable.
5
5
  # As a workaround we append description with suffix ' Features: W' to indicate that.
6
6
 
@@ -8,12 +8,15 @@ import sys
8
8
  import time
9
9
  from time import perf_counter as timer
10
10
  import os
11
+ import threading
11
12
  from socket import gethostname
13
+ import psutil
12
14
  from p4p.nt import NTScalar, NTEnum
13
15
  from p4p.nt.enum import ntenum
14
16
  from p4p.server import Server
15
17
  from p4p.server.thread import SharedPV
16
18
  from p4p.client.thread import Context
19
+ from p4p import Type
17
20
 
18
21
  PeriodicUpdateInterval = 10. # seconds
19
22
 
@@ -25,6 +28,7 @@ class C_():
25
28
  """Storage for module members"""
26
29
  prefix = ''
27
30
  verbose = 0
31
+ startTime = 0.
28
32
  cycle = 0
29
33
  serverState = ''
30
34
  PVs = {}
@@ -93,8 +97,8 @@ def publish(pvName:str, value, ifChanged=False, t=None):
93
97
 
94
98
  def SPV(initial, meta='', vtype=None):
95
99
  """Construct SharedPV.
96
- meta is a string with characters W,R,A,D indicating if the PV is writable,
97
- has alarm or it is discrete (ENUM).
100
+ meta is a string with characters W,R,A,D indicating if the PV is W) writable,
101
+ R) readable, A) has alarm or it is discrete (ENUM), D) discrete (ENUM).
98
102
  vtype should be one of the p4p.nt type definitions
99
103
  (see https://epics-base.github.io/p4p/values.html).
100
104
  if vtype is None then the nominal type will be determined automatically.
@@ -125,7 +129,26 @@ def SPV(initial, meta='', vtype=None):
125
129
  prefix = 'a' if iterable else ''
126
130
  nt = NTScalar(prefix+tcode, display=True, control='W' in meta,
127
131
  valueAlarm='A' in meta)
132
+
128
133
  pv = SharedPV(nt=nt, initial=initial)
134
+ # pv_type = Type([
135
+ # # Main data value
136
+ # ('value', nt),
137
+ # # # Optional fields for numeric types
138
+ # # ('display.description', 's'),
139
+ # # ('display.units', 's'),
140
+ # # ('display.limitLow', 'f'),
141
+ # # ('display.limitHigh', 'f'),
142
+ # # # Optional fields for enums ('choices', 'as'),
143
+ # # ('index', 'i'),
144
+ # ('properties', Type([
145
+ # ('writeable', '?'),
146
+ # #('archivable', '?'),
147
+ # ])),
148
+ # ])
149
+ # V0 = pv_type()
150
+ # pv = SharedPV(nt=pv_type, initial={'value': initial, 'writable': 'W' in meta})
151
+
129
152
  # add new attributes.
130
153
  pv.writable = 'W' in meta
131
154
  pv.discrete = discrete
@@ -261,8 +284,16 @@ def create_PVs(pvDefs=None):
261
284
  'lowAlarmLimit', 'highAlarmLimit', etc."""
262
285
  U,LL,LH = 'units','limitLow','limitHigh'
263
286
  C_.PVDefs = [
264
- ['host', 'Server host name', SPV(gethostname()), {}],
265
- ['version', 'Program version', SPV(__version__), {}],
287
+ # iocStats-related PVs
288
+ ['HOSTNAME', 'Server host name', SPV(gethostname()), {}],
289
+ ['VERSION', 'Program version', SPV('epicsdev '+__version__), {}],
290
+ ['HEARTBEAT', 'Server heartbeat, Increments once per second', SPV(0.), {U:'S'}],
291
+ ['UPTIME', 'Server uptime in seconds', SPV(''), {U:'S'}],
292
+ ['STARTTOD', 'Server start time', SPV(time.strftime("%m/%d/%Y %H:%M:%S")), {}],
293
+ ['CPU_LOAD', 'CPU load in %', SPV(0.), {U:'%'}],
294
+ ['CA_CONN_COUNT', 'Number of TCP connections', SPV(0), {}],
295
+ # Other popular stats: CA_CLIENTS, CA_CONN_COUNT, CPU_LOAD, FD_USED, THREAD_COUNT
296
+
266
297
  ['status', 'Server status. Features: RWE', SPV('','W'), {}],
267
298
  ['server', 'Server control',
268
299
  SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WD'),
@@ -312,7 +343,7 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
312
343
  if serverStateChanged is not None:# set custom serverStateChanged function
313
344
  C_.serverStateChanged = serverStateChanged
314
345
  try: # check if server is already running
315
- host = repr(get_externalPV(prefix+'host')).replace("'",'')
346
+ host = repr(get_externalPV(prefix+'HOSTNAME')).replace("'",'')
316
347
  print(f'ERROR: Server for {prefix} already running at {host}. Exiting.')
317
348
  sys.exit(1)
318
349
  except TimeoutError:
@@ -331,8 +362,17 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
331
362
  for _pvname in pvs:
332
363
  f.write(_pvname + '\n')
333
364
  printi(f'Hosting {len(pvs)} PVs')
365
+ C_.startTime = time.time()
366
+ threading.Thread(target=_heartbeat_thread, daemon=True).start()
334
367
  return pvs
335
368
 
369
+ def _heartbeat_thread():
370
+ """Thread to update heartbeat and uptime PVs."""
371
+ while True:
372
+ time.sleep(1)
373
+ publish('HEARTBEAT', pvv('HEARTBEAT')+1)
374
+ publish('UPTIME', round(time.time() - C_.startTime, 1))
375
+
336
376
  def sleep():
337
377
  """Sleep function to be called in the main loop. It updates cycleTime PV
338
378
  and sleeps for the time specified in sleep PV.
@@ -353,6 +393,8 @@ def sleep():
353
393
  printv(f'Average cycle time: {avgCycleTime:.6f} S.')
354
394
  publish('cycle', C_.cycle)
355
395
  publish('cycleTime', avgCycleTime)
396
+ publish('CPU_LOAD', round(psutil.cpu_percent(),1))
397
+ publish('CA_CONN_COUNT', len(psutil.net_connections(kind='tcp')))
356
398
  C_.lastUpdateTime = tnow
357
399
  C_.cycleTimeSum = 0.
358
400
  C_.cyclesAfterUpdate = 0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "epicsdev"
7
- version = "2.1.2"
7
+ version = "3.0.1"
8
8
  authors = [
9
9
  { name="Andrey Sukhanov", email="sukhanov@bnl.gov" },
10
10
  ]
File without changes
File without changes
File without changes