epicsdev 3.1.4__tar.gz → 3.1.5__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: 3.1.4
3
+ Version: 3.1.5
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
@@ -11,6 +11,7 @@ Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.7
13
13
  Requires-Dist: p4p>=4.2.2
14
+ Requires-Dist: psutil
14
15
  Description-Content-Type: text/markdown
15
16
 
16
17
  # epicsdev
@@ -102,7 +103,7 @@ python -m epicsdev.putlog /tmp/putlog.txt
102
103
  Default PV prefix is `putlog0:`, so write text to:
103
104
 
104
105
  ```bash
105
- caput -p pva putlog0:dump "hello from client"
106
+ pvput putlog0:dump "hello from client"
106
107
  ```
107
108
  ---
108
109
  ## AI-Assisted Device Support Development
@@ -87,7 +87,7 @@ python -m epicsdev.putlog /tmp/putlog.txt
87
87
  Default PV prefix is `putlog0:`, so write text to:
88
88
 
89
89
  ```bash
90
- caput -p pva putlog0:dump "hello from client"
90
+ pvput putlog0:dump "hello from client"
91
91
  ```
92
92
  ---
93
93
  ## AI-Assisted Device Support Development
@@ -1,6 +1,6 @@
1
1
  """Helper functions for creating EPICS PVAccess server"""
2
2
  # pylint: disable=invalid-name
3
- __version__= 'v3.1.3 26-03-04'# putlog functionality added, some refactoring, new features of epicsdev v3.1.0 used, some bugs fixed.
3
+ __version__= 'v3.1.5 26-03-16'# Setters for enums were not working, recovered.
4
4
  # SPV removed, PvDefs definitions simplified, new features added.
5
5
  #TODO: add support for autosave, (feature 'A'), caputLog (feature 'H') and access rights
6
6
 
@@ -251,8 +251,8 @@ def create_PVs(pvDefs, pvcache=None):
251
251
  if spv.setter:
252
252
  spv.setter(vr, spv)
253
253
  # value will be updated by the setter, so get it again
254
- #vr = pvv(spv.name)
255
- vr = spv._wrap(spv.current())['value']
254
+ vr = pvv(spv.name)
255
+ #vr = spv._wrap(spv.current())['value']
256
256
  printv(f'putting {spv.name} = {vr}')
257
257
  ct = time.time()
258
258
  C_.lastPutTime = ct
@@ -263,7 +263,8 @@ def create_PVs(pvDefs, pvcache=None):
263
263
  ip = op.peer().split(':')[3][:-1]# peer looks like: [::ffff:192.168.27.6]:46362
264
264
  jmsg = {"date":dt[0], "time":dt[1],
265
265
  "host":ip, "user":op.account(),
266
- "pv":op.name(), "new":vr, "old":oldvr}
266
+ "pv":op.name(), "new":str(vr), "old":str(oldvr)}
267
+ printv(f'Logging put operation: {jmsg}')
267
268
  s = json.dumps(jmsg)
268
269
  try:
269
270
  IFace.put(C_.putlogPV, "'"+s+"'", timeout=0.5)# quote the string to avoid interpreting it as JSON
@@ -423,7 +424,11 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0, serverStateChanged=None,
423
424
  pvs = create_pvDefs(pvDefs, pvcache)
424
425
  # Set up autosave if requested. That will save PV values to a file, and restore them on the next startup.
425
426
  if autosaveDir is not None:
426
- os.makedirs(autosaveDir, exist_ok=True)
427
+ try:
428
+ os.makedirs(autosaveDir, exist_ok=True)
429
+ except PermissionError:
430
+ printe(f'Permission denied to create {autosaveDir}. Use --autosave option.')
431
+ sys.exit(1)
427
432
  autosaveFile = f'{autosaveDir}{prefix[:-1]}.cache'
428
433
  C_.cachefd = open(autosaveFile, 'w')
429
434
  printi(f'Autosave enabled. Saving to {autosaveFile}')
@@ -445,6 +450,7 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0, serverStateChanged=None,
445
450
  if putlogPV is not None:
446
451
  _ = IFace.get(putlogPV, timeout=0.5)
447
452
  C_.putlogPV = putlogPV
453
+ printi(f'caPutLog feature enabled for PV {putlogPV}')
448
454
  except TimeoutError:
449
455
  printw(f'WARNING: caPutLog feature will not work: PV {putlogPV} not accessible.')
450
456
  C_.putlogPV = None
@@ -0,0 +1,133 @@
1
+ # epicsdev
2
+
3
+ Helper module for building **EPICS PVAccess servers** using [p4p](https://github.com/epics-base/p4p).
4
+
5
+ `epicsdev` is designed for:
6
+
7
+ * Rapid PVAccess server development
8
+ * High-rate data simulation and stress testing
9
+ * GUI-based monitoring and control
10
+ * Rapid instrument integration
11
+ * AI-assisted automatic device support generation
12
+
13
+ It integrates following EPICS IOC services:<br>
14
+ * **Autosave**: automatically saves the values of EPICS process variables (PVs) to files on a server host, and restores those values when the server restarts.
15
+ * **IocStats**: provides support for PVs that show the health and status of the server, plus a few control PVs.
16
+ * **caPutLog**: logging of PVAccess **`put`** operations.
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ python -m pip install epicsdev
23
+ ```
24
+ ## Quick Demo
25
+
26
+ Start the demo PVAccess server:
27
+
28
+ ```bash
29
+ python -m epicsdev.epicsdev
30
+ ```
31
+ ### Control & Visualization
32
+
33
+ Install optional GUI and plotting tools:
34
+
35
+ ```bash
36
+ python -m pip install pypeto pvplot
37
+ ```
38
+
39
+ Launch the control interface:
40
+
41
+ ```bash
42
+ python -m pypeto -c config -f epicsdev
43
+ ```
44
+
45
+ This provides:
46
+
47
+ * Device control panel
48
+ * Live waveform plots
49
+ * Real-time parameter monitoring
50
+
51
+ The screenshots can be seen here: [control page](docs/epicsdev_pypet.png), [plots](docs/epicsdev_pvplot.jpg).
52
+
53
+ ### Phoebus Display
54
+
55
+ An example Phoebus display is provided: `config/epicsdev.bob`. [Screenshot](docs/phoebus_epicsdev.jpg).
56
+
57
+ ## Multi-Channel Waveform Generator
58
+
59
+ `epicsdev.multiadc` generates high-throughput synthetic data for stress-testing EPICS systems.
60
+
61
+ For example, the following command :
62
+ ```bash
63
+ python -m epicsdev.multiadc -s 0.1 -c 10000 -n 100
64
+ ```
65
+ Will start a server, which generates:
66
+
67
+ * **10,000** noisy waveforms per second
68
+ * **100 points per waveform**
69
+ * **40,000 scalar parameters per second**
70
+
71
+
72
+ ### Monitoring GUI
73
+
74
+ ```bash
75
+ python -m pypeto -c config -f multiadc
76
+ ```
77
+ ## Text Put Logger
78
+
79
+ `epicsdev.putlog` hosts a writable PV named `dump` and appends any written text to a file.
80
+
81
+ Start the logger server (required argument: output file path):
82
+
83
+ ```bash
84
+ python -m epicsdev.putlog /tmp/putlog.txt
85
+ ```
86
+
87
+ Default PV prefix is `putlog0:`, so write text to:
88
+
89
+ ```bash
90
+ pvput putlog0:dump "hello from client"
91
+ ```
92
+ ---
93
+ ## AI-Assisted Device Support Development
94
+
95
+ `epicsdev` is structured to enable automated server generation using AI tools such as GitHub Copilot.
96
+
97
+ ### Workflow Example
98
+
99
+ 1. Create a new GitHub repository.
100
+
101
+ 2. Provide an AI prompt such as:
102
+
103
+ ```
104
+ Build device support for Tektronix MSO oscilloscopes
105
+ using epicsdev_rigol_scope as a template and the
106
+ programming manual available at <PDF link>.
107
+ ```
108
+
109
+ 3. Within ~20–40 minutes, the AI can generate a pull request.
110
+
111
+ 4. Review, test, make minor corrections if needed, then merge.
112
+
113
+ ### Real-World Example
114
+
115
+ Using this method, a server implementation for [Tektronix MSO oscilloscopes](https://github.com/ASukhanov/epicsdev_tektronix) was:
116
+
117
+ * ~99% correct on first generation
118
+ * Required only minor adjustments
119
+
120
+ ---
121
+
122
+ ## Requirements
123
+
124
+ * Python 3.8+
125
+ * p4p 4.2.2+
126
+
127
+ Optional:
128
+
129
+ * pypeto
130
+ * pvplot
131
+ * Phoebus (for .bob display files)
132
+
133
+ ---
@@ -0,0 +1,631 @@
1
+ """Helper functions for creating EPICS PVAccess server"""
2
+ # pylint: disable=invalid-name
3
+ __version__= 'v3.2.0 26-03-17'# NDArrays supported. Setters for enums were not working, recovered.
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
+ from datetime import datetime
11
+ import os
12
+ #import shelve
13
+ import json
14
+ import threading
15
+ from socket import gethostname
16
+ import psutil
17
+ import p4p.nt
18
+ from p4p.server import Server
19
+ from p4p.server.thread import SharedPV
20
+ from p4p.client.thread import Context
21
+
22
+ #``````````````````Constants
23
+ PeriodicUpdateInterval = 10. # seconds
24
+ AutosaveInterval = 10. #
25
+ AutosaveDefaultDirectory = '/operations/app_store/pvCache/' # Directory to save
26
+ # autosave files. The actual file name will be <directory><prefix>.cache
27
+ IFace = Context('pva')# client context for getting values from other servers
28
+
29
+ dtype2p4p = {# mapping from numpy dtype to p4p type code
30
+ 's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
31
+ 'u64':'L', 'f32':'f', 'f64':'d', str:'s',
32
+ }
33
+
34
+ #``````````````````Module Storage`````````````````````````````````````````````
35
+ def _serverStateChanged(newState:str):
36
+ """Dummy serverStateChanged function"""
37
+ return
38
+
39
+ class C_():
40
+ """Storage for module members"""
41
+ prefix = ''
42
+ verbose = 0
43
+ startTime = 0.
44
+ cycle = 0
45
+ serverState = ''
46
+ PVs = {}
47
+ PVDefs = []
48
+ serverStateChanged = _serverStateChanged
49
+ lastCycleTime = timer()
50
+ lastUpdateTime = 0.
51
+ cycleTimeSum = 0.
52
+ cyclesAfterUpdate = 0
53
+ cachefd = None
54
+ lastPutTime = time.time()# last time when a put operation was performed.
55
+ lastAutosaveTime = 0.# last time when the cache was saved to a file.
56
+ putlogPV = None # name of the PV where put operations are logged. If None, then put operations are not logged.
57
+
58
+ #```````````````````Helper methods````````````````````````````````````````````
59
+ def serverState():
60
+ """Return current server state. That is the value of the server PV, but
61
+ cached in C_ to avoid unnecessary get() calls."""
62
+ return C_.serverState
63
+ def _printTime():
64
+ return time.strftime("%m%d:%H%M%S")
65
+ def printi(msg):
66
+ """Print info message and publish it to status PV."""
67
+ print(f'inf_@{_printTime()}: {msg}')
68
+ def printw(msg):
69
+ """Print warning message and publish it to status PV."""
70
+ txt = f'WAR_@{_printTime()}: {msg}'
71
+ print(txt)
72
+ publish('status',txt)
73
+ def printe(msg):
74
+ """Print error message and publish it to status PV."""
75
+ txt = f'ERR_{_printTime()}: {msg}'
76
+ print(txt)
77
+ publish('status',txt)
78
+ def _printv(msg, level):
79
+ if C_.verbose >= level:
80
+ print(f'DBG{level}: {msg}')
81
+ def printv(msg):
82
+ """Print debug message if verbosity level >=1."""
83
+ _printv(msg, 1)
84
+ def printvv(msg):
85
+ """Print debug message if verbosity level >=2."""
86
+ _printv(msg, 2)
87
+ def printv3(msg):
88
+ """Print debug message if verbosity level >=3."""
89
+ _printv(msg, 3)
90
+
91
+ # def nt2py(nt):
92
+ # """Convert nt value to python value. That is to convert p4p scalar types
93
+ # to python scalars, and leave other types unchanged."""
94
+ # ntmap = {p4p.nt.scalar.ntint:int, p4p.nt.scalar.ntfloat:float,
95
+ # p4p.nt.scalar.ntstr:str, p4p.nt.enum.ntenum: int}
96
+ # return ntmap[type(nt)](nt)
97
+
98
+ def pvobj(pvName):
99
+ """Return PV with given name"""
100
+ return C_.PVs[C_.prefix+pvName]
101
+
102
+ def pvv(pvName:str):
103
+ """Return PV value"""
104
+ return pvobj(pvName).current()
105
+
106
+ def publish(pvName:str, value, ifChanged=False, t=None):
107
+ """Publish value to PV. If ifChanged is True, then publish only if the
108
+ value is different from the current value. If t is not None, then use
109
+ it as timestamp, otherwise use current time."""
110
+ #print(f'Publishing {pvName} = {value}')
111
+ try:
112
+ pv = pvobj(pvName)
113
+ except KeyError:
114
+ print(f'WARNING: PV {pvName} not found. Cannot publish value.')
115
+ return
116
+ if t is None:
117
+ t = time.time()
118
+ if not ifChanged or pv.current() != value:
119
+ pv.post(value, timestamp=t)
120
+
121
+ def write_cache():
122
+ """Write PV values to the cache file. That will be used for autosave."""
123
+ printv('Saving PV values to cache')
124
+ pvcacheMap = {}
125
+ for pvName, pv in C_.PVs.items():
126
+ if pv.writable:
127
+ value = pv._wrap(pv.current())['value']
128
+ if isinstance(value, str):
129
+ pyval = value
130
+ else:
131
+ # for discrete PVs, we need to save the index of the current choice, not the choice itself, because the choices can be changed in the next startup. That is a good example of using extra parameters in PV definitions.
132
+ try:
133
+ pyval = value.index
134
+ except Exception as e:
135
+ pyval = value
136
+ #print(f'Caching {pvName} = {value} of type {type(value)}, python value: {pyval} of type {type(pyval)}')
137
+ pvcacheMap[pvName[len(C_.prefix):]] = {'value': pyval, 'time': time.time()}
138
+ #print(f'pvCache: {pvcacheMap}')
139
+ C_.cachefd.seek(0)
140
+ json.dump(pvcacheMap, C_.cachefd)
141
+ C_.cachefd.truncate()
142
+ C_.cachefd.flush()
143
+
144
+ #``````````````````create_PVs()```````````````````````````````````````````````
145
+
146
+ def create_PVs(pvDefs, pvcache=None):
147
+ """Create PVs from the definitions in pvDefs."""
148
+ if pvcache is None:
149
+ pvcache = {}
150
+
151
+ ts = time.time()
152
+ for defs in pvDefs:
153
+ try:
154
+ pname,desc,initial,*extra = defs
155
+ except ValueError:
156
+ printe(f'Invalid PV definition of {defs[0]}')
157
+ sys.exit(1)
158
+ extra = extra[0] if extra else {}
159
+
160
+ #
161
+ iterable = type(initial) not in (int,float,str)
162
+ allowed_chars = 'WRAD'
163
+ meta = extra.get('features','')
164
+ writable = 'W' in meta
165
+ valueAlarm = extra.get('valueAlarm')
166
+ ntextra = [('features', p4p.nt.Type([('writable', '?')]))]
167
+ for ch in meta:
168
+ if ch not in allowed_chars:
169
+ printe(f'Unknown meta character {ch} in SPV definition')
170
+ sys.exit(1)
171
+
172
+ if 'D' in meta:# discrete PV, that is a PV with a list of choices. The value of the PV is one of the choices. The initial value should be one of the choices or an index of the choice in the list.
173
+ initial = {'choices': initial, 'index': 0}
174
+ nt = p4p.nt.NTEnum(display=True, extra=ntextra)
175
+ else:
176
+ if isinstance(initial, np.ndarray):
177
+ print(f'Initial value is numpy array of shape {initial.shape} and dtype {initial.dtype}')
178
+ initial = initial.tolist()# convert to list
179
+ iterable = True
180
+ else:
181
+ # NTScalar or NTScalarArray, depending on whether initial value is iterable or not. The type is determined from the initial value, but it can be overridden by extra['type']. For discrete PVs, the type is always NTEnum, and the choices are taken from the initial value.
182
+ vtype = extra.get('type')
183
+ if vtype is None:
184
+ firstItem = initial[0] if iterable else initial
185
+ itype = type(firstItem)
186
+ vtype = {int:'i32', float:'f32'}.get(itype,itype)
187
+ tcode = dtype2p4p[vtype]
188
+ prefix = 'a' if iterable else ''
189
+ nt = p4p.nt.NTScalar(prefix+tcode, display=True, control=writable,
190
+ valueAlarm = valueAlarm is not None, extra=ntextra)
191
+
192
+ # If the PV value is cached in pvcache, then use the cached value as initial value. That allows to restore PV values after server restart. For discrete PVs, we need to save the index of the current choice, not the choice itself, because the choices can be changed in the next startup. That is a good example of using extra parameters in PV definitions.
193
+ if pname in pvcache:
194
+ cached = pvcache[pname]['value']
195
+ if isinstance(initial, dict):
196
+ initial['index'] = cached
197
+ else:
198
+ initial = cached
199
+ #printi(f'Loaded initial value for {pname} from autosave: {initial}')
200
+
201
+ #print(f'Creating PV {pname}, initial: {initial}')
202
+ spv = SharedPV(nt=nt, initial=initial)
203
+ spv.lastTimeSaved = 0.
204
+ spv.writable = writable
205
+
206
+ # Set initial value and description and add to the map of PVs
207
+ ivalue = spv.current()
208
+ printv((f'created pv {pname}, initial: {type(ivalue),ivalue},'
209
+ f'extra: {extra}'))
210
+ key = C_.prefix + pname
211
+ if key in C_.PVs:
212
+ printe(f'Duplicate PV name: {pname}')
213
+ sys.exit(1)
214
+ C_.PVs[C_.prefix+pname] = spv
215
+ ntNamedTuples = spv._wrap(ivalue, timestamp=ts)
216
+ ntNamedTuples['features.writable'] = writable
217
+ ntNamedTuples['display.description'] = desc
218
+
219
+ # set extra parameters
220
+ for field in extra.keys():
221
+ try:
222
+ if field in ['limitLow','limitHigh','format','units']:
223
+ ntNamedTuples[f'display.{field}'] = extra[field]
224
+ if field.startswith('limit'):
225
+ ntNamedTuples[f'control.{field}'] = extra[field]
226
+ if field == 'valueAlarm':
227
+ for key,value in extra[field].items():
228
+ ntNamedTuples[f'valueAlarm.{key}'] = value
229
+ except KeyError as e:
230
+ print(f'ERROR. Cannot set {field} for {pname}: {e}')
231
+ sys.exit(1)
232
+ spv.post(ntNamedTuples)
233
+
234
+ if writable:
235
+ # add new attributes, that will be used in the put handler
236
+ spv.name = pname
237
+ spv.setter = extra.get('setter')
238
+
239
+ # add a put handler
240
+ @spv.put
241
+ def handle(spv, op):
242
+ vv = op.value()
243
+ vr = vv.raw.value
244
+ ntNamedTuples = spv._wrap(spv.current())
245
+ oldvr = ntNamedTuples['value']
246
+ #print(f'Put request for {spv.name} = {repr(vv)}, current value: {repr(ntNamedTuples)}')
247
+ # check limits, if they are defined. That will be a good
248
+ # example of using control structure and valueAlarm.
249
+ #print(f'Put request for {spv.name} = {repr(vr)}, value: {ntNamedTuples["value"]}, peer: {op.name()}, {op.peer()}, {op.account()}, {op.roles()}')
250
+ try:
251
+ limitLow = ntNamedTuples['control.limitLow']
252
+ limitHigh = ntNamedTuples['control.limitHigh']
253
+ if limitLow != limitHigh and not (limitLow <= vr <= limitHigh):
254
+ printw(f'Value {vr} is out of limits [{limitLow}, {limitHigh}]. Ignoring.')
255
+ op.done(error=f'Value out of limits [{limitLow}, {limitHigh}]')
256
+ return
257
+ except KeyError:
258
+ pass
259
+ if isinstance(vv, p4p.nt.enum.ntenum):
260
+ vr = str(vv)
261
+ if spv.setter:
262
+ spv.setter(vr, spv)
263
+ # value will be updated by the setter, so get it again
264
+ vr = pvv(spv.name)
265
+ #vr = spv._wrap(spv.current())['value']
266
+ printv(f'putting {spv.name} = {vr}')
267
+ ct = time.time()
268
+ C_.lastPutTime = ct
269
+ spv.post(vr, timestamp=ct) # update subscribers
270
+
271
+ if C_.putlogPV is not None:
272
+ dt = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3].split()
273
+ ip = op.peer().split(':')[3][:-1]# peer looks like: [::ffff:192.168.27.6]:46362
274
+ jmsg = {"date":dt[0], "time":dt[1],
275
+ "host":ip, "user":op.account(),
276
+ "pv":op.name(), "new":vr, "old":oldvr}
277
+ s = json.dumps(jmsg)
278
+ try:
279
+ IFace.put(C_.putlogPV, "'"+s+"'", timeout=0.5)# quote the string to avoid interpreting it as JSON
280
+ except TimeoutError:
281
+ printw(f'WARNING: caPutLog feature will be disabled: PV {C_.putlogPV} not accessible.')
282
+ C_.putlogPV = None
283
+ op.done()
284
+ #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
285
+ #``````````````````Setters
286
+ def set_verbose(level, *_):
287
+ """Set verbosity level for debugging"""
288
+ C_.verbose = level
289
+ printi(f'Setting verbose to {level}')
290
+ publish('verbose',level)
291
+
292
+ def set_server(servState, *_):
293
+ """Example of the setter for the server PV.
294
+ servState can be 'Start', 'Stop', 'Exit' or 'Clear'. If servState is None,
295
+ then get the desired state from the server PV."""
296
+ #printv(f'>set_server({servState}), {type(servState)}')
297
+ if servState is None:
298
+ servState = pvv('server')
299
+ printi(f'Setting server state to {servState}')
300
+ servState = str(servState)
301
+ C_.serverStateChanged(servState)
302
+ if servState == 'Start':
303
+ printi('Starting the server')
304
+ publish('server','Started')
305
+ publish('status','Started')
306
+ elif servState == 'Stop':
307
+ printi('server stopped')
308
+ publish('server','Stopped')
309
+ publish('status','Stopped')
310
+ elif servState == 'Exit':
311
+ printi('server is exiting')
312
+ publish('server','Exited')
313
+ publish('status','Exited')
314
+ elif servState == 'Clear':
315
+ publish('status','Cleared')
316
+ # set server to previous servState
317
+ set_server(C_.serverState)
318
+ return
319
+ C_.serverState = servState
320
+
321
+ def create_pvDefs(pvDefs=None, pvcache=None):
322
+ """Create PVs from the definitions in pvDefs and return them as a dictionary.
323
+ pvDefs is a list of PV definitions. Each definition is a list of 3 or 4 items:
324
+ [pvName, description, initialValue, extraParameters]
325
+ extraParameters is a dictionary with optional keys:
326
+ 'features': string with characters W (writable), D (discrete). For example. By default, PV is read-only scalar.
327
+ 'type': string with data type, for example 'f32', 'i32', 's8', etc. By default, the type is determined from the initial value (float -> 'f32', int -> 'i32').
328
+ 'units': string with physical units, for example 'V', 'S', 'Mpts/s', etc.
329
+ 'limitLow': number with low limit for the value. If defined, then the put handler will check that the value is not below the low limit.
330
+ 'limitHigh': number with high limit for the value. If defined, then the put handler will check that the value is not above the high limit.
331
+ 'setter': function to be called when the PV value is changed. The function should have the signature:
332
+ def setter(value, spv):
333
+ where value is the new value, and spv is the SharedPV object.
334
+ The PVs defined in C_.PVDefs are created first, then the PVs from pvDefs are
335
+ created and appended to the map of PVs. That allows to have some common PVs
336
+ defined in C_.PVDefs, and device-specific PVs defined in pvDefs.
337
+ pvcache is a dictionary with initial values for PVs. It is used for autosave.
338
+ The function returns a dictionary with PVs, where the keys are PV names and the values are SharedPV objects.
339
+ """
340
+ F,T,U,LL,LH = 'features','type','units','limitLow','limitHigh'
341
+ C_.PVDefs = [
342
+ # EPICS PVs for iocStats, see https://epics.anl.gov/base/R3-14/7-docs/iocstats.html
343
+ ['HOSTNAME', 'Server host name', gethostname()],
344
+ ['VERSION', 'Program version', 'epicsdev '+__version__],
345
+ ['HEARTBEAT', 'Server heartbeat, Increments once per second', 0., {U:'S'}],
346
+ ['UPTIME', 'Server uptime in seconds', '', {U:'S'}],
347
+ ['STARTTOD', 'Server start time', time.strftime("%m/%d/%Y %H:%M:%S")],
348
+ ['CPU_LOAD', 'CPU load in %', 0., {U:'%'}],
349
+ ['CA_CONN_COUNT', 'Number of TCP connections', 0],
350
+ # Other popular stats: CA_CLIENTS, CA_CONN_COUNT, CPU_LOAD, FD_USED, THREAD_COUNT
351
+
352
+ # Epicsdev-specific PVs
353
+ ['status', 'Server status. Features: RWE', '', {F:'W'}],
354
+ ['server', 'Server control. Features: RWE',
355
+ 'Start Stop Clear Exit Started Stopped Exited'.split(),
356
+ {F:'WD', 'setter':set_server}],
357
+ ['verbose', 'Debugging verbosity',
358
+ C_.verbose, {F:'W', T:'u8', 'setter':set_verbose, LL:0,LH:3}],
359
+ ['sleep', 'Pause in the main loop, it could be useful for throttling the data output',
360
+ 1.0, {F:'W', T:'f32', U:'S', LL:0.001, LH:10.1}],
361
+ ['cycle', 'Cycle number, published every {PeriodicUpdateInterval} S.',
362
+ 0, {T:'u32'}],
363
+ ['cycleTime','Average cycle time including sleep, published every {PeriodicUpdateInterval} S',
364
+ 0., {U:'S'}],
365
+ ]
366
+ # append application's PVs, defined in the pvDefs and create map of
367
+ # providers
368
+ if pvDefs is not None:
369
+ C_.PVDefs += pvDefs
370
+ create_PVs(C_.PVDefs, pvcache)
371
+ return C_.PVs
372
+
373
+ def init_epicsdev(prefix:str, pvDefs:list, verbose=0, serverStateChanged=None,
374
+ listDir=None, autosaveDir=None, recall = True, putlogPV=None):
375
+ """Initialize epicsdev with given prefix and PV definitions.
376
+ prefix is a string that will be prepended to all PV names. It should end with ':'.
377
+ pvDefs is a list of PV definitions, each definition is a list of 3 or 4 items:
378
+ [pvName, description, initialValue, extraParameters]
379
+ pvName is the name of the PV (without prefix)
380
+ description is a string with the description of the PV
381
+ initialValue is the initial value of the PV
382
+ extraParameters is a dictionary with optional keys:
383
+ 'features': string with characters W (writable), D (discrete). For example. By default, PV is read-only scalar.
384
+ 'type': string with data type, for example 'f32', 'i32', 's8', etc. By default, the type is determined from the initial value (float -> 'f32', int -> 'i32').
385
+ 'units': string with physical units, for example 'V', 'S', 'Mpts/s', etc.
386
+ 'limitLow': number with low limit for the value. If defined, then the put handler will check that the value is not below the low limit.
387
+ 'limitHigh': number with high limit for the value. If defined, then the put handler will check that the value is not above the high limit.
388
+ 'setter': function to be called when the PV value is changed. The function should have the signature:
389
+ def setter(value, spv):
390
+ where value is the new value, and spv is the SharedPV object.
391
+ verbose is an integer that controls the verbosity level for debugging.
392
+ serverStateChanged is a function that will be called when the server state changes. It should have the signature:
393
+ def serverStateChanged(newState:str):
394
+ where newState is the new state of the server ('Start', 'Stop', 'Exit', 'Clear').
395
+ listDir is a string that specifies the directory where the list of PVs will be saved. If None, then no list will be saved.
396
+ autosaveDir is a string that specifies the directory where the autosave file will be saved. If None, then no autosave will be performed.
397
+ recall is a boolean that specifies whether to load initial values from the autosave file. If False, then the initial values will be taken from the PV definitions.
398
+ """
399
+
400
+ if not isinstance(verbose, int) or verbose < 0:
401
+ printe('init_epicsdev arguments should be (prefix:str, pvDefs:list, verbose:int, listDir:str)')
402
+ sys.exit(1)
403
+ printi(f'Initializing epicsdev with prefix {prefix}')
404
+ C_.prefix = prefix
405
+ C_.verbose = verbose
406
+
407
+ if serverStateChanged is not None:# set custom serverStateChanged function
408
+ C_.serverStateChanged = serverStateChanged
409
+ try: # check if server is already running
410
+ host = repr(IFace.get(prefix+'HOSTNAME', timeout=0.5)).replace("'",'')
411
+ print(f'ERROR: Server for {prefix} already running at {host}. Exiting.')
412
+ sys.exit(1)
413
+ except TimeoutError:
414
+ pass
415
+
416
+ # No existing server found. Creating PVs.
417
+ pvcache = {}
418
+ if autosaveDir == '':# autosaveDir enabled with default file name
419
+ autosaveDir = AutosaveDefaultDirectory
420
+ if recall:
421
+ try:
422
+ autosaveFile = f'{autosaveDir}{prefix[:-1]}.cache'
423
+ with open(autosaveFile, "r") as json_file:
424
+ pvcache = json.load(json_file)
425
+ except Exception:
426
+ print(f'WARNING: pvCache file {autosaveFile} not found. Using default values')
427
+ printv(f'AutosaveDir: {autosaveDir}, recall: {recall}')
428
+ if len(pvcache) == 0:
429
+ printi(f'Loading default values')
430
+ else:
431
+ printi(f'Loading initial values from {autosaveFile}')
432
+ printv(f'pvCache: {pvcache}')
433
+ pvs = create_pvDefs(pvDefs, pvcache)
434
+ # Set up autosave if requested. That will save PV values to a file, and restore them on the next startup.
435
+ if autosaveDir is not None:
436
+ try:
437
+ os.makedirs(autosaveDir, exist_ok=True)
438
+ except PermissionError:
439
+ printe(f'Permission denied to create {autosaveDir}. Use --autosave option.')
440
+ sys.exit(1)
441
+ autosaveFile = f'{autosaveDir}{prefix[:-1]}.cache'
442
+ C_.cachefd = open(autosaveFile, 'w')
443
+ printi(f'Autosave enabled. Saving to {autosaveFile}')
444
+
445
+ # Save list of PVs to a file, if requested
446
+ if listDir != '':
447
+ listDir = '/tmp/pvlist/' if listDir is None else listDir
448
+ if not os.path.exists(listDir):
449
+ os.makedirs(listDir)
450
+ filepath = f'{listDir}{prefix[:-1]}.txt'
451
+ printi(f'Writing list of PVs to {filepath}')
452
+ with open(filepath, 'w', encoding="utf-8") as f:
453
+ for _pvname in pvs:
454
+ f.write(_pvname + '\n')
455
+ printi(f'Hosting {len(pvs)} PVs')
456
+ C_.startTime = time.time()
457
+
458
+ try:
459
+ if putlogPV is not None:
460
+ _ = IFace.get(putlogPV, timeout=0.5)
461
+ C_.putlogPV = putlogPV
462
+ except TimeoutError:
463
+ printw(f'WARNING: caPutLog feature will not work: PV {putlogPV} not accessible.')
464
+ C_.putlogPV = None
465
+
466
+ threading.Thread(target=_heartbeat_thread, daemon=True).start()
467
+ return pvs
468
+
469
+ def _heartbeat_thread():
470
+ """Thread to update heartbeat and uptime PVs."""
471
+ while True:
472
+ time.sleep(1)
473
+ publish('HEARTBEAT', pvv('HEARTBEAT')+1)
474
+ publish('UPTIME', round(time.time() - C_.startTime, 1))
475
+
476
+ def sleep():
477
+ """Sleep function to be called in the main loop. It updates cycleTime PV
478
+ and sleeps for the time specified in sleep PV.
479
+ Returns False if a periodic update occurred.
480
+ """
481
+ time.sleep(pvv('sleep'))
482
+ sleeping = True
483
+ if serverState().startswith('Stop'):
484
+ return sleeping
485
+ tnow = timer()
486
+ C_.cycleTimeSum += tnow - C_.lastCycleTime
487
+ C_.lastCycleTime = tnow
488
+ C_.cyclesAfterUpdate += 1
489
+ C_.cycle += 1
490
+ printv(f'cycle {C_.cycle}')
491
+ if tnow - C_.lastUpdateTime > PeriodicUpdateInterval:
492
+ avgCycleTime = C_.cycleTimeSum / C_.cyclesAfterUpdate
493
+ printv(f'Average cycle time: {avgCycleTime:.6f} S.')
494
+ publish('cycle', C_.cycle)
495
+ publish('cycleTime', avgCycleTime)
496
+ publish('CPU_LOAD', round(psutil.cpu_percent(),1))
497
+ publish('CA_CONN_COUNT', len(psutil.net_connections(kind='tcp')))
498
+ C_.lastUpdateTime = tnow
499
+ C_.cycleTimeSum = 0.
500
+ C_.cyclesAfterUpdate = 0
501
+ sleeping = False
502
+
503
+ if C_.cachefd is not None and tnow - C_.lastAutosaveTime > AutosaveInterval:
504
+ C_.lastAutosaveTime = tnow
505
+ if C_.lastPutTime != 0.:
506
+ C_.lastPutTime = 0.
507
+ write_cache()
508
+ else:
509
+ printv('No changes to save')
510
+ return sleeping
511
+
512
+ #``````````````````Demo````````````````````````````````````````````````````````
513
+ if __name__ == "__main__":
514
+ import numpy as np
515
+ import argparse
516
+
517
+ def myPVDefs():
518
+ """Example of PV definitions"""
519
+ F,T,U,LL,LH,SET = 'features','type','units','limitLow','limitHigh','setter'
520
+ alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
521
+ return [ # device-specific PVs
522
+ ['noiseLevel', 'Noise amplitude', 1., {F:'W', U:'V'}],
523
+ ['tAxis', 'Full scale of horizontal axis', [0.], {U:'S'}],
524
+ ['recordLength','Max number of points',
525
+ 100, {F:'W', T:'u32', LL:4,LH:1000000, SET:set_recordLength}],
526
+ ['throughput', 'Performance metrics, points per second', 0., {U:'Mpts/s'}],
527
+ ['c01Offset', 'Offset', 0., {F:'W', U:'du'}],
528
+ ['c01VoltsPerDiv', 'Vertical scale', 0.1, {F:'W', U:'V/du'}],
529
+ ['c01Waveform', 'Waveform array', [0.], {U:'du'}],
530
+ ['c01Mean', 'Mean of the waveform', 0., {U:'du'}],
531
+ ['c01Peak2Peak','Peak-to-peak amplitude', 0., {U:'du', **alarm}],
532
+ #['image', 'Image array', np.ndarray([0],'int16')],
533
+ ['alarm', 'PV with alarm', 0, {U:'du', **alarm}],
534
+ ]
535
+
536
+ pargs = None
537
+ rng = np.random.default_rng()
538
+ nPoints = 100
539
+ _sum = {'points': 0, 'time': 0.}
540
+
541
+ def set_recordLength(value, *_):
542
+ """Record length have changed. The tAxis should be updated
543
+ accordingly."""
544
+ printi(f'Setting tAxis to {value}')
545
+ publish('tAxis', np.arange(value)*1.E-6)
546
+ publish('recordLength', value)
547
+
548
+ def init(recordLength):
549
+ """Example of device initialization function"""
550
+ set_recordLength(recordLength)
551
+
552
+ def poll():
553
+ """Example of polling function. Called every cycle when server is running.
554
+ It returns time, spent in publishing data"""
555
+ wf = rng.random(pvv('recordLength'))*pvv('noiseLevel')# it takes 5ms for 1M points
556
+ wf /= pvv('c01VoltsPerDiv')
557
+ wf += pvv('c01Offset')
558
+ ts = timer()
559
+ publish('c01Waveform', wf)
560
+ _sum['time'] += timer() - ts
561
+ _sum['points'] += len(wf)
562
+ publish('c01Peak2Peak', np.ptp(wf))
563
+ publish('c01Mean', np.mean(wf))
564
+
565
+ def periodic_update():
566
+ """Perform periodic update"""
567
+ #printi(f'periodic update for {C_.cyclesSinceUpdate} cycles: {ElapsedTime}')
568
+ if state.startswith('Stop'):
569
+ publish('throughput', 0.)
570
+ else:
571
+ pointsPerSecond = _sum['points']/_sum['time']/1.E6
572
+ publish('throughput', round(pointsPerSecond,6))
573
+ printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
574
+ _sum['points'] = 0
575
+ _sum['time'] = 0.
576
+
577
+ # Parse command line arguments
578
+ parser = argparse.ArgumentParser(description = __doc__,
579
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
580
+ epilog=f'{__version__}')
581
+ parser.add_argument('-a', '--autosave', nargs='?', default='', help=
582
+ 'Autosave control. If not given, then autosave is enabled with default file '\
583
+ 'name /tmp/<device><index>.cache. ' \
584
+ 'If given without argument, then autosave is disabled' \
585
+ 'If a file name is given, then it is used for autosave.')
586
+ parser.add_argument('-c', '--recall', action='store_false', help=
587
+ 'If given: Do not load initial values from pvCache file. That is useful when you want to start with default values, but do not want to disable autosave. By default, the initial values are loaded from the cache file if it exists.')
588
+ parser.add_argument('-d', '--device', default='epicsDev', help=
589
+ 'Device name, the PV name will be <device><index>:')
590
+ parser.add_argument('-i', '--index', default='0', help=
591
+ 'Device index, the PV name will be <device><index>:')
592
+ parser.add_argument('-l', '--list', nargs='?', help=(
593
+ 'Directory to save list of all generated PVs, if no directory is given, '
594
+ 'then </tmp/pvlist/><prefix> is assumed.'))
595
+ # The rest of options are not essential, they can be controlled at runtime using PVs.
596
+ parser.add_argument('-n', '--npoints', type=int, default=nPoints, help=
597
+ 'Number of points in the waveform')
598
+ parser.add_argument('-p', '--putlogPV', default='putlog:dump', help=
599
+ 'Name of the PV where put operations are logged. If None, then put operations are not logged.')
600
+ parser.add_argument('-v', '--verbose', action='count', default=0, help=
601
+ 'Show more log messages (-vv: show even more)')
602
+ pargs = parser.parse_args()
603
+ print(pargs)
604
+
605
+ # Initialize epicsdev and PVs
606
+ pargs.prefix = f'{pargs.device}{pargs.index}:'
607
+ PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose, None,
608
+ pargs.list, pargs.autosave, pargs.recall, pargs.putlogPV)
609
+ # Initialize the device using pargs if needed.
610
+ init(pargs.npoints)
611
+
612
+ # Start the Server. Use your set_server, if needed.
613
+ set_server('Start')
614
+
615
+ # Main loop
616
+ # In this example, we just update the waveform and its stats in a loop,
617
+ # but in a real application, the loop can also read data from the device,
618
+ # and update PVs accordingly. The loop can be paused by setting server PV to 'Stop',
619
+ # and exited by setting server PV to 'Exit'.
620
+ # The performance metrics are updated every {PeriodicUpdateInterval} seconds.
621
+ server = Server(providers=[PVs])
622
+ printi(f'Server started. Sleeping per cycle: {repr(pvv("sleep"))} S.')
623
+ while True:
624
+ state = serverState()
625
+ if state.startswith('Exit'):
626
+ break
627
+ if not state.startswith('Stop'):
628
+ poll()
629
+ if not sleep():# Sleep and update performance metrics periodically
630
+ periodic_update()
631
+ printi('Server is exited')
@@ -0,0 +1,183 @@
1
+ """Simulated multi-channel ADC device server using epicsdev module."""
2
+ # pylint: disable=invalid-name
3
+ __version__= 'v3.1.1 26-03-03'# updated to use new features of epicsdev v3.1.0
4
+
5
+ import sys
6
+ from time import perf_counter as timer
7
+ import argparse
8
+ import numpy as np
9
+
10
+ from .epicsdev import Server, Context, init_epicsdev, serverState, publish
11
+ from .epicsdev import pvv, printi, printv, set_server, sleep
12
+
13
+
14
+ def myPVDefs():
15
+ """Define PVs for the multiadc device. The PVs are defined as a list of
16
+ lists, where each inner list contains the PV name, description, initial
17
+ value, and optional dictionary of additional attributes.
18
+ """
19
+ F,T,U,LL,LH,SET = 'features','type','units','limitLow','limitHigh','setter'
20
+ alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
21
+ pvDefs = [ # device-specific PVs
22
+ ['channels', 'Number of device channels', pargs.channels],
23
+ ['externalControl', 'Name of external PV, which controls the server',
24
+ 'Start Stop Clear Exit Started Stopped Exited'.split(), {F:'WD'}],
25
+ ['noiseLevel', 'Noise amplitude', 0.05, {F:'W', U:'V'}],
26
+ ['tAxis', 'Full scale of horizontal axis', [0.], {U:'S'}],
27
+ ['recordLength','Max number of points', pargs.npoints,
28
+ {F:'W', T:'u32', LL:4, LH:1000000, SET:set_recordLength}],
29
+ ['alarm', 'PV with alarm', 0, {F:'WA', U:'du', **alarm}],
30
+ #``````````````````Auxiliary PVs
31
+ ['timing', 'Elapsed time for waveform generation, publishing, total]', [0.], {U:'S'}],
32
+ ['throughput', 'Total number of points processed per second', 0., {U:'Mpts/s'}],
33
+ ]
34
+
35
+ # Templates for channel-related PVs.
36
+ ChannelTemplates = [
37
+ ['c0$VoltsPerDiv', 'Vertical scale', 0.1, {F:'W', U:'V/du'}],
38
+ ['c0$VoltOffset', 'Vertical offset', 0., {F:'W', U:'V'}],
39
+ ['c0$Waveform', 'Waveform array', [0.], {U:'du'}],
40
+ ['c0$Mean', 'Mean of the waveform', 0., {F:'A', U:'du'}],
41
+ ['c0$Peak2Peak','Peak-to-peak amplitude', 0., {F:'A', U:'du', **alarm}],
42
+ ]
43
+ # extend PvDefs with channel-related PVs
44
+ for ch in range(pargs.channels):
45
+ for pvdef in ChannelTemplates:
46
+ newpvdef = pvdef.copy()
47
+ newpvdef[0] = pvdef[0].replace('0$',f'{ch+1:02}')
48
+ if len(newpvdef) > 3:
49
+ newpvdef[3] = newpvdef[3].copy()
50
+ pvDefs.append(newpvdef)
51
+ return pvDefs
52
+
53
+ #``````````````````Module attributes
54
+ rng = np.random.default_rng()
55
+ ElapsedTime = {'waveform': 0., 'publish': 0., 'poll': 0.}
56
+ class C_():
57
+ cyclesSinceUpdate = 0
58
+
59
+ #``````````````````Setter functions for PVs```````````````````````````````````
60
+ def set_recordLength(value, *_):
61
+ """Record length have changed. The tAxis should be updated accordingly."""
62
+ printi(f'Setting tAxis to {repr(value)}')
63
+ publish('tAxis', np.arange(value)*1.E-6)
64
+ publish('recordLength', value)
65
+
66
+ def set_externalControl(value, *_):
67
+ """External control PV have changed. Control the server accordingly."""
68
+ pvname = str(value)
69
+ if pvname in (None,'0'):
70
+ print('External control is not activated.')
71
+ return
72
+ printi(f'External control PV: {pvname}')
73
+ ctxt = Context('pva')
74
+ try:
75
+ ctxt.get(pvname, timeout=0.5)
76
+ except TimeoutError:
77
+ printi(f'Cannot connect to external control PV {pvname}.')
78
+ sys.exit(1)
79
+ #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
80
+ def serverStateChanged(newState:str):
81
+ """Start device function called when server is started"""
82
+ if newState == 'Start':
83
+ printi('start_device called')
84
+ elif newState == 'Stop':
85
+ printi('stop_device called')
86
+ elif newState == 'Clear':
87
+ printi('clear_device called')
88
+ publish('cycle', 0)
89
+
90
+ def init():
91
+ """Device initialization function"""
92
+ set_recordLength(pvv('recordLength'))
93
+ # Set offset of each channel = channel index
94
+ for ch in range(pargs.channels):
95
+ publish(f'c{ch+1:02}VoltOffset', ch)
96
+ #set_externalControl(pargs.prefix + pargs.external)
97
+
98
+ def poll():
99
+ """Device polling function, called every cycle when server is running"""
100
+ C_.cyclesSinceUpdate += 1
101
+ ts0 = timer()
102
+ for ch in range(pargs.channels):
103
+ ts1 = timer()
104
+ chstr = f'c{ch+1:02}'
105
+ rwf = rng.random(pvv('recordLength'))*pvv('noiseLevel')
106
+ wf = rwf/pvv(f'{chstr}VoltsPerDiv') + pvv(f'{chstr}VoltOffset')# the time is comparable with rng.random
107
+ ts2 = timer()
108
+ ElapsedTime['waveform'] += ts2 - ts1
109
+ #print(f'ElapsedTime: {C_.cyclesSinceUpdate, ElapsedTime["waveform"]}')
110
+ publish(f'{chstr}Waveform', wf)
111
+ publish(f'{chstr}Peak2Peak', np.ptp(wf))
112
+ publish(f'{chstr}Mean', np.mean(wf))
113
+ ElapsedTime['publish'] += timer() - ts2
114
+ ElapsedTime['poll'] += timer() - ts0
115
+
116
+ def periodic_update():
117
+ """Perform periodic update"""
118
+ #printi(f'periodic update for {C_.cyclesSinceUpdate} cycles: {ElapsedTime}')
119
+ times = [(round(i/C_.cyclesSinceUpdate,6)) for i in ElapsedTime.values()]
120
+ publish('timing', times)
121
+ C_.cyclesSinceUpdate = 0
122
+ for key in ElapsedTime:
123
+ ElapsedTime[key] = 0.
124
+ pointsPerSecond = len(pvv('tAxis'))/(pvv('cycleTime')-pvv('sleep'))/1.E6
125
+ pointsPerSecond *= pvv('channels')
126
+ publish('throughput', round(pointsPerSecond,6))
127
+ printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
128
+
129
+
130
+ # Argument parsing
131
+ parser = argparse.ArgumentParser(description = __doc__,
132
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
133
+ epilog=f'{__version__}')
134
+ parser.add_argument('-a', '--autosave', nargs='?', default='', help=
135
+ 'Autosave control. If not given, then autosave is enabled with default file '\
136
+ 'name /tmp/<device><index>.cache. ' \
137
+ 'If given without argument, then autosave is disabled' \
138
+ 'If a file name is given, then it is used for autosave.')
139
+ parser.add_argument('-c', '--recall', action='store_false', help=
140
+ 'If given: Do not load initial values from pvCache file. That is useful when you want to start with default values, but do not want to disable autosave. By default, the initial values are loaded from the cache file if it exists.')
141
+ parser.add_argument('-C', '--channels', type=int, default=6, help=
142
+ 'Number of channels per device')
143
+ parser.add_argument('-e', '--external', help=
144
+ 'Name of external PV, which controls the server, if 0 then it will be <device>0:')
145
+ parser.add_argument('-l', '--list', default=None, nargs='?', help=
146
+ 'Directory to save list of all generated PVs, if None, then </tmp/pvlist/><prefix> is assumed.')
147
+ parser.add_argument('-d', '--device', default='multiadc', help=
148
+ 'Device name, the PV name will be <device><index>:')
149
+ parser.add_argument('-i', '--index', default='0', help=
150
+ 'Device index, the PV name will be <device><index>:')
151
+ # The rest of arguments are not essential, they can be changed at runtime using PVs.
152
+ parser.add_argument('-n', '--npoints', type=int, default=100, help=
153
+ 'Number of points in the waveform')
154
+ #parser.add_argument('-s', '--sleep', type=float, default=1.0, help=
155
+ #'Sleep time per cycle')
156
+ parser.add_argument('-v', '--verbose', action='count', default=0, help=
157
+ 'Show more log messages (-vv: show even more)')
158
+ pargs = parser.parse_args()
159
+ printv(f'pargs: {pargs}')
160
+
161
+ # Initialize epicsdev and PVs
162
+ pargs.prefix = f'{pargs.device}{pargs.index}:'
163
+ PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose,
164
+ serverStateChanged, pargs.list, pargs.autosave, pargs.recall)
165
+
166
+ # Initialize the device.
167
+ init()
168
+
169
+ # Start the Server.
170
+ set_server('Start')
171
+
172
+ #``````````````````Main loop``````````````````````````````````````````````````
173
+ server = Server(providers=[PVs])
174
+ printi(f'Server started. Sleeping per cycle: {float(pvv("sleep")):.3f} S.')
175
+ while True:
176
+ state = serverState()
177
+ if state.startswith('Exit'):
178
+ break
179
+ if not state.startswith('Stop'):
180
+ poll()
181
+ if not sleep():
182
+ periodic_update()
183
+ printi('Server has exited')
@@ -17,7 +17,8 @@ classifiers = [
17
17
  "Operating System :: OS Independent",
18
18
  ]
19
19
  dependencies = [
20
- "p4p>=4.2.2"
20
+ "p4p>=4.2.2",
21
+ "psutil",
21
22
  ]
22
23
  [project.urls]
23
24
  "Homepage" = "https://github.com/ASukhanov/epicsdev"
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "epicsdev"
7
+ version = "3.1.5"
8
+ authors = [
9
+ { name="Andrey Sukhanov", email="sukhanov@bnl.gov" },
10
+ ]
11
+ description = "Helper module for creating EPICS PVAccess servers using p4p"
12
+ readme = "README.md"
13
+ requires-python = ">=3.7"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "p4p>=4.2.2",
21
+ "psutil",
22
+ ]
23
+ [project.urls]
24
+ "Homepage" = "https://github.com/ASukhanov/epicsdev"
25
+ "Bug Tracker" = "https://github.com/ASukhanov/epicsdev"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes