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.
- {epicsdev-2.1.2 → epicsdev-3.0.1}/PKG-INFO +2 -2
- {epicsdev-2.1.2 → epicsdev-3.0.1}/README.md +1 -1
- {epicsdev-2.1.2 → epicsdev-3.0.1}/config/epicsdev.bob +72 -20
- {epicsdev-2.1.2 → epicsdev-3.0.1}/config/epicsdev_pp.py +1 -1
- {epicsdev-2.1.2 → epicsdev-3.0.1}/config/multiadc_pp.py +1 -1
- epicsdev-3.0.1/epicsdev/epicsdev.py +493 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/epicsdev/multiadc.py +21 -20
- epicsdev-2.1.2/epicsdev/epicsdev.py → epicsdev-3.0.1/fallback/epicsdev-300.py +48 -6
- {epicsdev-2.1.2 → epicsdev-3.0.1}/pyproject.toml +1 -1
- {epicsdev-2.1.2 → epicsdev-3.0.1}/.github/copilot-instructions.md +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/LICENSE +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/config/epicsSimscope_pp.py +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/config/multiadc1_pp.py +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/docs/epicsdev_pvplot.jpg +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/docs/epicsdev_pypet.png +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/docs/phoebus_epicsdev.jpg +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/epicsdev/__init__.py +0 -0
- {epicsdev-2.1.2 → epicsdev-3.0.1}/fallback/multiadc.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epicsdev
|
|
3
|
-
Version:
|
|
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
|
```
|
|
@@ -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>
|
|
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>
|
|
65
|
-
<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
|
|
123
|
+
<name>Peak2Peak</name>
|
|
111
124
|
<y_pv>pva://epicsDev0:c01Peak2Peak</y_pv>
|
|
112
|
-
<axis>
|
|
113
|
-
<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
|
|
137
|
+
<name>Mean</name>
|
|
125
138
|
<y_pv>pva://epicsDev0:c01Mean</y_pv>
|
|
126
|
-
<axis>
|
|
127
|
-
<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="
|
|
195
|
-
<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="
|
|
201
|
-
<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>
|
|
204
|
-
<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="
|
|
209
|
-
<name>
|
|
210
|
-
<
|
|
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
|
-
<
|
|
213
|
-
|
|
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+'
|
|
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+'
|
|
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__= '
|
|
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,
|
|
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
|
-
|
|
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',
|
|
19
|
+
['channels', 'Number of device channels', pargs.channels],
|
|
20
20
|
['externalControl', 'Name of external PV, which controls the server',
|
|
21
|
-
|
|
22
|
-
['noiseLevel', 'Noise amplitude',
|
|
23
|
-
['tAxis', 'Full scale of horizontal axis',
|
|
24
|
-
['recordLength','Max number of points',
|
|
25
|
-
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
26
|
-
['alarm', 'PV with 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]',
|
|
29
|
-
['throughput', 'Total number of points processed per second',
|
|
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.
|
|
32
|
+
# Templates for channel-related PVs.
|
|
33
33
|
ChannelTemplates = [
|
|
34
|
-
['c0$VoltsPerDiv', 'Vertical scale',
|
|
35
|
-
['c0$VoltOffset', 'Vertical offset',
|
|
36
|
-
['c0$Waveform', 'Waveform array',
|
|
37
|
-
['c0$Mean', 'Mean of the waveform',
|
|
38
|
-
['c0$Peak2Peak','Peak-to-peak amplitude',
|
|
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
|
|
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
|
-
|
|
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__= '
|
|
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
|
-
|
|
265
|
-
['
|
|
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+'
|
|
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
|
|
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
|