epicsdev 2.1.0__tar.gz → 2.1.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.1/.github/copilot-instructions.md +24 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/PKG-INFO +5 -5
- {epicsdev-2.1.0 → epicsdev-2.1.1}/README.md +4 -4
- epicsdev-2.1.1/config/epicsdev.bob +215 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/config/epicsdev_pp.py +2 -1
- {epicsdev-2.1.0 → epicsdev-2.1.1}/config/multiadc_pp.py +12 -7
- epicsdev-2.1.1/docs/phoebus_epicsdev.jpg +0 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/epicsdev/epicsdev.py +23 -26
- {epicsdev-2.1.0 → epicsdev-2.1.1}/epicsdev/multiadc.py +48 -35
- {epicsdev-2.1.0 → epicsdev-2.1.1}/pyproject.toml +1 -1
- epicsdev-2.1.0/.github/copilot-instructions.md +0 -28
- {epicsdev-2.1.0 → epicsdev-2.1.1}/LICENSE +0 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/config/epicsSimscope_pp.py +0 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/config/multiadc1_pp.py +0 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/docs/epicsdev_pvplot.jpg +0 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/docs/epicsdev_pypet.png +0 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/epicsdev/__init__.py +0 -0
- {epicsdev-2.1.0 → epicsdev-2.1.1}/fallback/multiadc.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copilot instructions for epicsdev
|
|
2
|
+
|
|
3
|
+
## Big picture
|
|
4
|
+
- Core EPICS PVAccess helpers live in [epicsdev/epicsdev.py](epicsdev/epicsdev.py). Module state is held in `C_` (prefix, PV map, verbosity, server state), and public helpers include `SPV()`, `publish()`, `pvv()`, `serverState()`, `sleep()`.
|
|
5
|
+
- Startup flow: `init_epicsdev(prefix, pvDefs, verbose=0, serverStateChanged=None, listDir=None)` checks for an existing server (via `host` PV), creates PVs, and optionally writes a PV list under `/tmp/pvlist/<prefix>.txt`. Then `Server(providers=[PVs])` runs a polling loop that checks `serverState()`; see [epicsdev/epicsdev.py](epicsdev/epicsdev.py) and the concrete device in [epicsdev/multiadc.py](epicsdev/multiadc.py).
|
|
6
|
+
- `create_PVs()` always prepends mandatory PVs (`host`, `version`, `status`, `server`, `verbose`, `sleep`, `cycle`, `cycleTime`) before app PVs; `sleep()` updates `cycle`/`cycleTime` every `PeriodicUpdateInterval` and uses the `sleep` PV as throttle.
|
|
7
|
+
- GUI pages for pypeto are defined in [config/epicsdev_pp.py](config/epicsdev_pp.py), [config/multiadc_pp.py](config/multiadc_pp.py), and [config/epicsSimscope_pp.py](config/epicsSimscope_pp.py); their PV names/prefixes must match the servers.
|
|
8
|
+
|
|
9
|
+
## Project-specific patterns & conventions
|
|
10
|
+
- PV definitions are `[name, description, SPV, extra]` and passed to `create_PVs()`; examples: `myPVDefs()` in [epicsdev/epicsdev.py](epicsdev/epicsdev.py) and [epicsdev/multiadc.py](epicsdev/multiadc.py).
|
|
11
|
+
- `SPV(initial, meta, vtype)` uses compact `meta`: `W` (writable), `R` (readable), `A` (alarm), `D` (discrete enum). `D` creates an `NTEnum` with `{choices,index}`.
|
|
12
|
+
- Writable PVs set `control.limitLow/High` to `0` as a PVAccess writability workaround (see `_create_PVs()` in [epicsdev/epicsdev.py](epicsdev/epicsdev.py)).
|
|
13
|
+
- `extra` dict keys commonly used: `setter`, `units`, `limitLow`, `limitHigh`, `format`, `valueAlarm`. `setter` receives `(value, spv)`.
|
|
14
|
+
- Prefer `publish()`/`pvv()` instead of direct `SharedPV` access; logging uses `printi/printw/printe`, which also updates the `status` PV.
|
|
15
|
+
- In multi-channel templates, don’t pre-create `SPV` objects; use tuples and convert per-channel (see `ChannelTemplates` in [epicsdev/multiadc.py](epicsdev/multiadc.py)).
|
|
16
|
+
|
|
17
|
+
## External deps & integration points
|
|
18
|
+
- Build system is hatchling; package requires Python >=3.7 and `p4p` (see [pyproject.toml](pyproject.toml)). Optional runtime tools: `pypeto`, `pvplot` for GUI/plotting (see [README.md](README.md)).
|
|
19
|
+
|
|
20
|
+
## Common workflows (from README)
|
|
21
|
+
- Run demo server: `python -m epicsdev.epicsdev`
|
|
22
|
+
- Control/plot demo (requires `pypeto`, `pvplot`): `python -m pypeto -c config -f epicsdev`
|
|
23
|
+
- Run multi-channel waveform generator: `python -m epicsdev.multiadc -c100 -n1000`
|
|
24
|
+
- Launch multiadc GUI: `python -m pypeto -c config -f multiadc`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epicsdev
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.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
|
|
@@ -30,10 +30,10 @@ python -m pypeto -c config -f epicsdev
|
|
|
30
30
|
|
|
31
31
|
## Multi-channel waveform generator
|
|
32
32
|
Module **epicdev.multiadc** can generate large amount of data for stress-testing
|
|
33
|
-
the EPICS environment. For example the following command will generate
|
|
34
|
-
|
|
33
|
+
the EPICS environment. For example the following command will generate 10000 of
|
|
34
|
+
100-pont noisy waveforms and 40000 of scalar parameters per second.
|
|
35
35
|
```
|
|
36
|
-
python -m epicsdev.multiadc -
|
|
36
|
+
python -m epicsdev.multiadc -s0.1 -c10000 -n100
|
|
37
37
|
```
|
|
38
38
|
The GUI for monitoring:<br>
|
|
39
39
|
```python -m pypeto -c config -f multiadc```
|
|
@@ -42,4 +42,4 @@ The graphs should look like this:
|
|
|
42
42
|
[control page](docs/epicsdev_pypet.png),
|
|
43
43
|
[plots](docs/epicsdev_pvplot.jpg).
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Example of [Phoebus display](docs/phoebus_epicsdev.jpg), as defined in config/epicsdev.bob.
|
|
@@ -15,10 +15,10 @@ python -m pypeto -c config -f epicsdev
|
|
|
15
15
|
|
|
16
16
|
## Multi-channel waveform generator
|
|
17
17
|
Module **epicdev.multiadc** can generate large amount of data for stress-testing
|
|
18
|
-
the EPICS environment. For example the following command will generate
|
|
19
|
-
|
|
18
|
+
the EPICS environment. For example the following command will generate 10000 of
|
|
19
|
+
100-pont noisy waveforms and 40000 of scalar parameters per second.
|
|
20
20
|
```
|
|
21
|
-
python -m epicsdev.multiadc -
|
|
21
|
+
python -m epicsdev.multiadc -s0.1 -c10000 -n100
|
|
22
22
|
```
|
|
23
23
|
The GUI for monitoring:<br>
|
|
24
24
|
```python -m pypeto -c config -f multiadc```
|
|
@@ -27,4 +27,4 @@ The graphs should look like this:
|
|
|
27
27
|
[control page](docs/epicsdev_pypet.png),
|
|
28
28
|
[plots](docs/epicsdev_pvplot.jpg).
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Example of [Phoebus display](docs/phoebus_epicsdev.jpg), as defined in config/epicsdev.bob.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<display version="2.0.0">
|
|
3
|
+
<name>epicsdev</name>
|
|
4
|
+
<widget type="xyplot" version="3.0.0">
|
|
5
|
+
<name>X/Y Plot</name>
|
|
6
|
+
<x>20</x>
|
|
7
|
+
<width>550</width>
|
|
8
|
+
<height>290</height>
|
|
9
|
+
<x_axis>
|
|
10
|
+
<title>ADC Sample</title>
|
|
11
|
+
<autoscale>true</autoscale>
|
|
12
|
+
<log_scale>false</log_scale>
|
|
13
|
+
<minimum>0.0</minimum>
|
|
14
|
+
<maximum>100.0</maximum>
|
|
15
|
+
<show_grid>false</show_grid>
|
|
16
|
+
<title_font>
|
|
17
|
+
<font name="Default Bold" family="Liberation Sans" style="BOLD" size="14.0">
|
|
18
|
+
</font>
|
|
19
|
+
</title_font>
|
|
20
|
+
<scale_font>
|
|
21
|
+
<font name="Default" family="Liberation Sans" style="REGULAR" size="14.0">
|
|
22
|
+
</font>
|
|
23
|
+
</scale_font>
|
|
24
|
+
<visible>true</visible>
|
|
25
|
+
</x_axis>
|
|
26
|
+
<y_axes>
|
|
27
|
+
<y_axis>
|
|
28
|
+
<title>Scope divisions</title>
|
|
29
|
+
<autoscale>true</autoscale>
|
|
30
|
+
<log_scale>false</log_scale>
|
|
31
|
+
<minimum>0.0</minimum>
|
|
32
|
+
<maximum>100.0</maximum>
|
|
33
|
+
<show_grid>false</show_grid>
|
|
34
|
+
<title_font>
|
|
35
|
+
<font name="Default Bold" family="Liberation Sans" style="BOLD" size="14.0">
|
|
36
|
+
</font>
|
|
37
|
+
</title_font>
|
|
38
|
+
<scale_font>
|
|
39
|
+
<font name="Default" family="Liberation Sans" style="REGULAR" size="14.0">
|
|
40
|
+
</font>
|
|
41
|
+
</scale_font>
|
|
42
|
+
<on_right>false</on_right>
|
|
43
|
+
<visible>true</visible>
|
|
44
|
+
<color>
|
|
45
|
+
<color name="Text" red="0" green="0" blue="0">
|
|
46
|
+
</color>
|
|
47
|
+
</color>
|
|
48
|
+
</y_axis>
|
|
49
|
+
</y_axes>
|
|
50
|
+
<traces>
|
|
51
|
+
<trace>
|
|
52
|
+
<name>$(traces[0].y_pv)</name>
|
|
53
|
+
<x_pv></x_pv>
|
|
54
|
+
<y_pv>pva://epicsDev0:c01Waveform</y_pv>
|
|
55
|
+
<err_pv></err_pv>
|
|
56
|
+
<axis>0</axis>
|
|
57
|
+
<trace_type>1</trace_type>
|
|
58
|
+
<color>
|
|
59
|
+
<color red="0" green="0" blue="255">
|
|
60
|
+
</color>
|
|
61
|
+
</color>
|
|
62
|
+
<line_width>1</line_width>
|
|
63
|
+
<line_style>0</line_style>
|
|
64
|
+
<point_type>0</point_type>
|
|
65
|
+
<point_size>10</point_size>
|
|
66
|
+
<visible>true</visible>
|
|
67
|
+
</trace>
|
|
68
|
+
</traces>
|
|
69
|
+
</widget>
|
|
70
|
+
<widget type="label" version="2.0.0">
|
|
71
|
+
<name>Label</name>
|
|
72
|
+
<class>TITLE</class>
|
|
73
|
+
<text> epicsDev main demo</text>
|
|
74
|
+
<x use_class="true">0</x>
|
|
75
|
+
<y use_class="true">0</y>
|
|
76
|
+
<width>440</width>
|
|
77
|
+
<height>31</height>
|
|
78
|
+
<font use_class="true">
|
|
79
|
+
<font name="Header 1" family="Liberation Sans" style="BOLD" size="22.0">
|
|
80
|
+
</font>
|
|
81
|
+
</font>
|
|
82
|
+
<foreground_color use_class="true">
|
|
83
|
+
<color name="Text" red="0" green="0" blue="0">
|
|
84
|
+
</color>
|
|
85
|
+
</foreground_color>
|
|
86
|
+
<transparent use_class="true">true</transparent>
|
|
87
|
+
</widget>
|
|
88
|
+
<widget type="stripchart" version="2.1.0">
|
|
89
|
+
<name>Strip Chart</name>
|
|
90
|
+
<x>30</x>
|
|
91
|
+
<y>290</y>
|
|
92
|
+
<width>550</width>
|
|
93
|
+
<y_axes>
|
|
94
|
+
<y_axis>
|
|
95
|
+
<title>Volts</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>
|
|
107
|
+
</y_axes>
|
|
108
|
+
<traces>
|
|
109
|
+
<trace>
|
|
110
|
+
<name>$(traces[0].y_pv)</name>
|
|
111
|
+
<y_pv>pva://epicsDev0:c01Peak2Peak</y_pv>
|
|
112
|
+
<axis>0</axis>
|
|
113
|
+
<trace_type>2</trace_type>
|
|
114
|
+
<color>
|
|
115
|
+
<color red="0" green="0" blue="255">
|
|
116
|
+
</color>
|
|
117
|
+
</color>
|
|
118
|
+
<line_width>1</line_width>
|
|
119
|
+
<point_type>0</point_type>
|
|
120
|
+
<point_size>10</point_size>
|
|
121
|
+
<visible>true</visible>
|
|
122
|
+
</trace>
|
|
123
|
+
<trace>
|
|
124
|
+
<name>$(traces[1].y_pv)</name>
|
|
125
|
+
<y_pv>pva://epicsDev0:c01Mean</y_pv>
|
|
126
|
+
<axis>0</axis>
|
|
127
|
+
<trace_type>2</trace_type>
|
|
128
|
+
<color>
|
|
129
|
+
<color red="255" green="0" blue="0">
|
|
130
|
+
</color>
|
|
131
|
+
</color>
|
|
132
|
+
<line_width>1</line_width>
|
|
133
|
+
<point_type>0</point_type>
|
|
134
|
+
<point_size>10</point_size>
|
|
135
|
+
<visible>true</visible>
|
|
136
|
+
</trace>
|
|
137
|
+
</traces>
|
|
138
|
+
</widget>
|
|
139
|
+
<widget type="textentry" version="3.0.0">
|
|
140
|
+
<name>Record_length</name>
|
|
141
|
+
<pv_name>pva://epicsDev0:recordLength</pv_name>
|
|
142
|
+
<x>580</x>
|
|
143
|
+
<y>71</y>
|
|
144
|
+
<precision>0</precision>
|
|
145
|
+
</widget>
|
|
146
|
+
<widget type="label" version="2.0.0">
|
|
147
|
+
<name>Label_1</name>
|
|
148
|
+
<text>Record length</text>
|
|
149
|
+
<x>580</x>
|
|
150
|
+
<y>50</y>
|
|
151
|
+
<horizontal_alignment>1</horizontal_alignment>
|
|
152
|
+
</widget>
|
|
153
|
+
<widget type="textentry" version="3.0.0">
|
|
154
|
+
<name>Noise</name>
|
|
155
|
+
<pv_name>pva://epicsDev0:noiseLevel</pv_name>
|
|
156
|
+
<x>580</x>
|
|
157
|
+
<y>120</y>
|
|
158
|
+
<precision>6</precision>
|
|
159
|
+
</widget>
|
|
160
|
+
<widget type="label" version="2.0.0">
|
|
161
|
+
<name>Label_2</name>
|
|
162
|
+
<text>Noise level</text>
|
|
163
|
+
<x>580</x>
|
|
164
|
+
<y>100</y>
|
|
165
|
+
<horizontal_alignment>1</horizontal_alignment>
|
|
166
|
+
</widget>
|
|
167
|
+
<widget type="label" version="2.0.0">
|
|
168
|
+
<name>Label_3</name>
|
|
169
|
+
<text>Volts/div</text>
|
|
170
|
+
<x>580</x>
|
|
171
|
+
<y>150</y>
|
|
172
|
+
<horizontal_alignment>1</horizontal_alignment>
|
|
173
|
+
</widget>
|
|
174
|
+
<widget type="textentry" version="3.0.0">
|
|
175
|
+
<name>Volts/div</name>
|
|
176
|
+
<pv_name>pva://epicsDev0:c01VoltsPerDiv</pv_name>
|
|
177
|
+
<x>580</x>
|
|
178
|
+
<y>170</y>
|
|
179
|
+
</widget>
|
|
180
|
+
<widget type="textentry" version="3.0.0">
|
|
181
|
+
<name>Sleep</name>
|
|
182
|
+
<pv_name>pva://epicsDev0:sleep</pv_name>
|
|
183
|
+
<x>580</x>
|
|
184
|
+
<y>220</y>
|
|
185
|
+
<precision>1</precision>
|
|
186
|
+
</widget>
|
|
187
|
+
<widget type="label" version="2.0.0">
|
|
188
|
+
<name>Label_4</name>
|
|
189
|
+
<text>Sleep</text>
|
|
190
|
+
<x>580</x>
|
|
191
|
+
<y>200</y>
|
|
192
|
+
<horizontal_alignment>1</horizontal_alignment>
|
|
193
|
+
</widget>
|
|
194
|
+
<widget type="textentry" version="3.0.0">
|
|
195
|
+
<name>Cycle time</name>
|
|
196
|
+
<pv_name>pva://epicsDev0:cycleTime</pv_name>
|
|
197
|
+
<x>580</x>
|
|
198
|
+
<y>240</y>
|
|
199
|
+
</widget>
|
|
200
|
+
<widget type="textentry" version="3.0.0">
|
|
201
|
+
<name>Cycle</name>
|
|
202
|
+
<pv_name>pva://epicsDev0:cycle</pv_name>
|
|
203
|
+
<x>620</x>
|
|
204
|
+
<y>10</y>
|
|
205
|
+
<width>60</width>
|
|
206
|
+
<precision>0</precision>
|
|
207
|
+
</widget>
|
|
208
|
+
<widget type="label" version="2.0.0">
|
|
209
|
+
<name>Label_6</name>
|
|
210
|
+
<text>Cycle</text>
|
|
211
|
+
<x>579</x>
|
|
212
|
+
<y>10</y>
|
|
213
|
+
<width>40</width>
|
|
214
|
+
</widget>
|
|
215
|
+
</display>
|
|
@@ -84,7 +84,8 @@ string or device:parameter and the value is dictionary of the features.
|
|
|
84
84
|
['Device:',D, D+'server', D+'version', 'host:',D+'host',_],
|
|
85
85
|
['Status:', {D+'status': span(8,1)}],
|
|
86
86
|
['Cycle time:',D+'cycleTime', 'Sleep:',D+'sleep', 'Cycle:',D+'cycle', Plot],
|
|
87
|
-
['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
|
|
87
|
+
['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
|
|
88
|
+
'Throughput:',{D+'throughput':span(2,1)},_],
|
|
88
89
|
[{'ATTRIBUTES':{**color('lightCyan'),**just(1)}},
|
|
89
90
|
'Channels:', 'CH1', 'CH2', 'CH3', 'CH4', 'CH5', 'CH6'],
|
|
90
91
|
['V/div:']+ChLine('VoltsPerDiv'),
|
|
@@ -18,7 +18,7 @@ def slider(minValue,maxValue):
|
|
|
18
18
|
return {'widget':'hslider','opLimits':[minValue,maxValue],'span':[2,1]}
|
|
19
19
|
|
|
20
20
|
LargeFont = {'color':'light gray', **font(18), 'fgColor':'dark green'}
|
|
21
|
-
ButtonFont = {'font':['Open Sans
|
|
21
|
+
ButtonFont = {'font':['Open Sans Bold,14']}# Comic Sans MS
|
|
22
22
|
LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
|
|
23
23
|
lColor = color('lightGreen')
|
|
24
24
|
PyPath = 'python -m'
|
|
@@ -75,20 +75,25 @@ string or device:parameter and the value is dictionary of the features.
|
|
|
75
75
|
PaneP2P = ' '.join([f'c{i+1:02d}Mean c{i+1:02d}Peak2Peak' for i in range(channels)])
|
|
76
76
|
PaneWF = ' '.join([f'c{i+1:02d}Waveform' for i in range(channels)])
|
|
77
77
|
#PaneT = 'timing[1] timing[3]'
|
|
78
|
-
Plot = {'Plot':{'launch':
|
|
78
|
+
Plot = {'Plot Channels':{'launch':
|
|
79
79
|
f'{PyPath} pvplot Y-5:5 -aV:{instance} -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
|
|
80
80
|
**lColor, **ButtonFont}}
|
|
81
|
-
print(f'Plot
|
|
81
|
+
print(f'Plot button: {Plot}')
|
|
82
|
+
Timing = {'Plot':{'launch':f'{PyPath} pvplot -aV:{instance}timing "[0] [1] [2]"', **lColor}}
|
|
83
|
+
print(f'Timing button: {Timing}')
|
|
82
84
|
#``````````mandatory member```````````````````````````````````````````
|
|
83
85
|
self.rows = [
|
|
84
|
-
['Device:',D, D+'server', D+'
|
|
86
|
+
['Device:',D, D+'server', {D+'channels':just(2)},'chnls, host:',D+'host',D+'version'],
|
|
85
87
|
['Status:', {D+'status': span(8,1)}],
|
|
86
|
-
['Cycle time:',D+'cycleTime', 'Sleep:',D+'sleep', 'Cycle:',D+'cycle'
|
|
87
|
-
['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
|
|
88
|
+
['Cycle time:',D+'cycleTime', 'Sleep:',D+'sleep', 'Cycle:',D+'cycle'],
|
|
89
|
+
['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
|
|
90
|
+
'Throughput:',D+'throughput',Timing],
|
|
88
91
|
[{'ATTRIBUTES':{**color('lightCyan'),**just(1)}},
|
|
89
|
-
|
|
92
|
+
Plot,'CH1','CH2','CH3','CH4','CH5','CH6'],
|
|
90
93
|
['V/div:']+ChLine('VoltsPerDiv'),
|
|
94
|
+
['VoltOffset:']+ChLine('VoltOffset'),
|
|
91
95
|
['Mean:']+ChLine('Mean'),
|
|
92
96
|
['Peak2Peak:']+ChLine('Peak2Peak'),
|
|
93
97
|
#['Waveform:']+ChLine('Waveform'),
|
|
98
|
+
['Timing:',{D+'timing':span(3,1)}],
|
|
94
99
|
]
|
|
Binary file
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Skeleton and helper functions for creating EPICS PVAccess server"""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= 'v2.1.
|
|
4
|
-
#TODO add mandatory PV: host, to identify the server host.
|
|
3
|
+
__version__= 'v2.1.1 26-02-05'# sleep() returns False if a periodic update occurred. Simplified waveform randomization.
|
|
5
4
|
#Issue: There is no way in PVAccess to specify if string PV is writable.
|
|
6
5
|
# As a workaround we append description with suffix ' Features: W' to indicate that.
|
|
7
6
|
|
|
@@ -31,7 +30,7 @@ class C_():
|
|
|
31
30
|
PVs = {}
|
|
32
31
|
PVDefs = []
|
|
33
32
|
serverStateChanged = _serverStateChanged
|
|
34
|
-
lastCycleTime =
|
|
33
|
+
lastCycleTime = timer()
|
|
35
34
|
lastUpdateTime = 0.
|
|
36
35
|
cycleTimeSum = 0.
|
|
37
36
|
cyclesAfterUpdate = 0
|
|
@@ -332,13 +331,17 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
|
|
|
332
331
|
|
|
333
332
|
def sleep():
|
|
334
333
|
"""Sleep function to be called in the main loop. It updates cycleTime PV
|
|
335
|
-
and sleeps for the time specified in sleep PV.
|
|
336
|
-
|
|
334
|
+
and sleeps for the time specified in sleep PV.
|
|
335
|
+
Returns False if a periodic update occurred.
|
|
336
|
+
"""
|
|
337
|
+
time.sleep(pvv('sleep'))
|
|
338
|
+
tnow = timer()
|
|
337
339
|
C_.cycleTimeSum += tnow - C_.lastCycleTime
|
|
338
340
|
C_.lastCycleTime = tnow
|
|
339
341
|
C_.cyclesAfterUpdate += 1
|
|
340
342
|
C_.cycle += 1
|
|
341
343
|
printv(f'cycle {C_.cycle}')
|
|
344
|
+
sleeping = True
|
|
342
345
|
if tnow - C_.lastUpdateTime > PeriodicUpdateInterval:
|
|
343
346
|
avgCycleTime = C_.cycleTimeSum / C_.cyclesAfterUpdate
|
|
344
347
|
printv(f'Average cycle time: {avgCycleTime:.6f} S.')
|
|
@@ -347,7 +350,8 @@ def sleep():
|
|
|
347
350
|
C_.lastUpdateTime = tnow
|
|
348
351
|
C_.cycleTimeSum = 0.
|
|
349
352
|
C_.cyclesAfterUpdate = 0
|
|
350
|
-
|
|
353
|
+
sleeping = False
|
|
354
|
+
return sleeping
|
|
351
355
|
|
|
352
356
|
#``````````````````Demo````````````````````````````````````````````````````````
|
|
353
357
|
if __name__ == "__main__":
|
|
@@ -359,20 +363,20 @@ if __name__ == "__main__":
|
|
|
359
363
|
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
360
364
|
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
361
365
|
return [ # device-specific PVs
|
|
362
|
-
['noiseLevel', 'Noise amplitude', SPV(1
|
|
366
|
+
['noiseLevel', 'Noise amplitude', SPV(1.,'W'), {U:'V'}],
|
|
363
367
|
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
364
368
|
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
365
369
|
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
370
|
+
['throughput', 'Performance metrics, points per second', SPV(0.), {U:'Mpts/s'}],
|
|
366
371
|
['c01Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
|
|
367
|
-
['c01VoltsPerDiv', 'Vertical scale', SPV(
|
|
372
|
+
['c01VoltsPerDiv', 'Vertical scale', SPV(0.1,'W'), {U:'V/du'}],
|
|
368
373
|
['c01Waveform', 'Waveform array', SPV([0.]), {U:'du'}],
|
|
369
374
|
['c01Mean', 'Mean of the waveform', SPV(0.,'A'), {U:'du'}],
|
|
370
375
|
['c01Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {U:'du',**alarm}],
|
|
371
376
|
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
372
377
|
]
|
|
373
|
-
nPatterns = 100 # number of waveform patterns.
|
|
374
378
|
pargs = None
|
|
375
|
-
rng = np.random.default_rng(
|
|
379
|
+
rng = np.random.default_rng()
|
|
376
380
|
nPoints = 100
|
|
377
381
|
|
|
378
382
|
def set_recordLength(value, *_):
|
|
@@ -381,18 +385,6 @@ if __name__ == "__main__":
|
|
|
381
385
|
printi(f'Setting tAxis to {value}')
|
|
382
386
|
publish('tAxis', np.arange(value)*1.E-6)
|
|
383
387
|
publish('recordLength', value)
|
|
384
|
-
# Re-initialize noise array, because its size depends on recordLength
|
|
385
|
-
set_noise(pvv('noiseLevel'))
|
|
386
|
-
|
|
387
|
-
def set_noise(level, *_):
|
|
388
|
-
"""Noise level have changed. Update noise array."""
|
|
389
|
-
v = float(level)
|
|
390
|
-
recordLength = pvv('recordLength')
|
|
391
|
-
ts = timer()
|
|
392
|
-
pargs.noise = np.random.normal(scale=0.5*level,
|
|
393
|
-
size=recordLength+nPatterns)# 45ms/1e6 points
|
|
394
|
-
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
395
|
-
publish('noiseLevel', level)
|
|
396
388
|
|
|
397
389
|
def init(recordLength):
|
|
398
390
|
"""Example of device initialization function"""
|
|
@@ -401,14 +393,15 @@ if __name__ == "__main__":
|
|
|
401
393
|
|
|
402
394
|
def poll():
|
|
403
395
|
"""Example of polling function. Called every cycle when server is running."""
|
|
404
|
-
#
|
|
405
|
-
|
|
406
|
-
wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
|
|
396
|
+
#ts = timer()
|
|
397
|
+
wf = rng.random(pvv('recordLength'))*pvv('noiseLevel')# it takes 5ms for 1M points
|
|
407
398
|
wf /= pvv('c01VoltsPerDiv')
|
|
408
399
|
wf += pvv('c01Offset')
|
|
400
|
+
#print(f'Waveform updated in {timer()-ts:.6g} S.')
|
|
409
401
|
publish('c01Waveform', wf)
|
|
410
402
|
publish('c01Peak2Peak', np.ptp(wf))
|
|
411
403
|
publish('c01Mean', np.mean(wf))
|
|
404
|
+
#print(f'Polling completed in {timer()-ts:.6g} S.')
|
|
412
405
|
|
|
413
406
|
# Argument parsing
|
|
414
407
|
parser = argparse.ArgumentParser(description = __doc__,
|
|
@@ -449,5 +442,9 @@ if __name__ == "__main__":
|
|
|
449
442
|
break
|
|
450
443
|
if not state.startswith('Stop'):
|
|
451
444
|
poll()
|
|
452
|
-
sleep()
|
|
445
|
+
if not sleep():# Sleep and update performance metrics periodically
|
|
446
|
+
if not state.startswith('Stop'):
|
|
447
|
+
pointsPerSecond = len(pvv('c01Waveform'))/(pvv('cycleTime')-pvv('sleep'))/1.E6
|
|
448
|
+
publish('throughput', round(pointsPerSecond,6))
|
|
449
|
+
printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
|
|
453
450
|
printi('Server is exited')
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
"""Simulated multi-channel ADC device server using epicsdev module."""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= 'v2.1.
|
|
3
|
+
__version__= 'v2.1.1 26-02-04'# added timing, throughput and c0$VoltOffset PVs
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
|
-
import time
|
|
7
6
|
from time import perf_counter as timer
|
|
8
|
-
import numpy as np
|
|
9
7
|
import argparse
|
|
8
|
+
import numpy as np
|
|
10
9
|
|
|
11
10
|
from .epicsdev import Server, Context, init_epicsdev, serverState, publish
|
|
12
11
|
from .epicsdev import pvv, printi, printv, SPV, set_server, sleep
|
|
@@ -20,17 +19,20 @@ def myPVDefs():
|
|
|
20
19
|
['channels', 'Number of device channels', SPV(pargs.channels), {}],
|
|
21
20
|
['externalControl', 'Name of external PV, which controls the server',
|
|
22
21
|
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WD'), {}],
|
|
23
|
-
['noiseLevel', 'Noise amplitude', SPV(
|
|
22
|
+
['noiseLevel', 'Noise amplitude', SPV(0.05,'W'), {U:'V'}],
|
|
24
23
|
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
25
24
|
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
26
25
|
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
27
26
|
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
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
30
|
]
|
|
29
31
|
|
|
30
32
|
# Templates for channel-related PVs. Important: SPV cannot be used in this list!
|
|
31
33
|
ChannelTemplates = [
|
|
32
|
-
['c0$VoltsPerDiv', 'Vertical scale', (
|
|
33
|
-
|
|
34
|
+
['c0$VoltsPerDiv', 'Vertical scale', (0.1,'W'), {U:'V/du'}],
|
|
35
|
+
['c0$VoltOffset', 'Vertical offset', (0.,'W'), {U:'V'}],
|
|
34
36
|
['c0$Waveform', 'Waveform array', ([0.],), {U:'du'}],
|
|
35
37
|
['c0$Mean', 'Mean of the waveform', (0.,'A'), {U:'du'}],
|
|
36
38
|
['c0$Peak2Peak','Peak-to-peak amplitude', (0.,'A'), {U:'du',**alarm}],
|
|
@@ -44,9 +46,11 @@ def myPVDefs():
|
|
|
44
46
|
pvDefs.append(newpvdef)
|
|
45
47
|
return pvDefs
|
|
46
48
|
|
|
47
|
-
#``````````````````Module
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
#``````````````````Module attributes
|
|
50
|
+
rng = np.random.default_rng()
|
|
51
|
+
ElapsedTime = {'waveform': 0., 'publish': 0., 'poll': 0.}
|
|
52
|
+
class C_():
|
|
53
|
+
cyclesSinceUpdate = 0
|
|
50
54
|
|
|
51
55
|
#``````````````````Setter functions for PVs```````````````````````````````````
|
|
52
56
|
def set_recordLength(value, *_):
|
|
@@ -54,18 +58,6 @@ def set_recordLength(value, *_):
|
|
|
54
58
|
printi(f'Setting tAxis to {value}')
|
|
55
59
|
publish('tAxis', np.arange(value)*1.E-6)
|
|
56
60
|
publish('recordLength', value)
|
|
57
|
-
# Re-initialize noise array, because its size depends on recordLength
|
|
58
|
-
set_noise(pvv('noiseLevel'))
|
|
59
|
-
|
|
60
|
-
def set_noise(level, *_):
|
|
61
|
-
"""Noise level have changed. Update noise array."""
|
|
62
|
-
v = float(level)
|
|
63
|
-
recordLength = pvv('recordLength')
|
|
64
|
-
ts = timer()
|
|
65
|
-
|
|
66
|
-
pargs.noise = np.random.normal(scale=0.5*level, size=recordLength+nPatterns)# 45ms/1e6 points
|
|
67
|
-
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
68
|
-
publish('noiseLevel', level)
|
|
69
61
|
|
|
70
62
|
def set_externalControl(value, *_):
|
|
71
63
|
"""External control PV have changed. Control the server accordingly."""
|
|
@@ -94,21 +86,43 @@ def serverStateChanged(newState:str):
|
|
|
94
86
|
def init(recordLength):
|
|
95
87
|
"""Device initialization function"""
|
|
96
88
|
set_recordLength(recordLength)
|
|
89
|
+
# Set offset of each channel = channel index
|
|
90
|
+
for ch in range(pargs.channels):
|
|
91
|
+
publish(f'c{ch+1:02}VoltOffset', ch)
|
|
97
92
|
#set_externalControl(pargs.prefix + pargs.external)
|
|
93
|
+
publish('sleep', pargs.sleep)
|
|
98
94
|
|
|
99
95
|
def poll():
|
|
100
96
|
"""Device polling function, called every cycle when server is running"""
|
|
97
|
+
C_.cyclesSinceUpdate += 1
|
|
98
|
+
ts0 = timer()
|
|
101
99
|
for ch in range(pargs.channels):
|
|
102
|
-
|
|
100
|
+
ts1 = timer()
|
|
103
101
|
chstr = f'c{ch+1:02}'
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
publish(f'{chstr}Waveform',
|
|
102
|
+
rwf = rng.random(pvv('recordLength'))*pvv('noiseLevel')
|
|
103
|
+
wf = rwf/pvv(f'{chstr}VoltsPerDiv') + pvv(f'{chstr}VoltOffset')# the time is comparable with rng.random
|
|
104
|
+
ts2 = timer()
|
|
105
|
+
ElapsedTime['waveform'] += ts2 - ts1
|
|
106
|
+
#print(f'ElapsedTime: {C_.cyclesSinceUpdate, ElapsedTime["waveform"]}')
|
|
107
|
+
publish(f'{chstr}Waveform', wf)
|
|
110
108
|
publish(f'{chstr}Peak2Peak', np.ptp(wf))
|
|
111
109
|
publish(f'{chstr}Mean', np.mean(wf))
|
|
110
|
+
ElapsedTime['publish'] += timer() - ts2
|
|
111
|
+
ElapsedTime['poll'] += timer() - ts0
|
|
112
|
+
|
|
113
|
+
def periodic_update():
|
|
114
|
+
"""Perform periodic update"""
|
|
115
|
+
#printi(f'periodic update for {C_.cyclesSinceUpdate} cycles: {ElapsedTime}')
|
|
116
|
+
times = [(round(i/C_.cyclesSinceUpdate,6)) for i in ElapsedTime.values()]
|
|
117
|
+
publish('timing', times)
|
|
118
|
+
C_.cyclesSinceUpdate = 0
|
|
119
|
+
for key in ElapsedTime:
|
|
120
|
+
ElapsedTime[key] = 0.
|
|
121
|
+
pointsPerSecond = len(pvv('tAxis'))/(pvv('cycleTime')-pvv('sleep'))/1.E6
|
|
122
|
+
pointsPerSecond *= pvv('channels')
|
|
123
|
+
publish('throughput', round(pointsPerSecond,6))
|
|
124
|
+
printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
|
|
125
|
+
|
|
112
126
|
|
|
113
127
|
# Argument parsing
|
|
114
128
|
parser = argparse.ArgumentParser(description = __doc__,
|
|
@@ -127,6 +141,8 @@ parser.add_argument('-i', '--index', default='0', help=
|
|
|
127
141
|
# The rest of arguments are not essential, they can be changed at runtime using PVs.
|
|
128
142
|
parser.add_argument('-n', '--npoints', type=int, default=100, help=
|
|
129
143
|
'Number of points in the waveform')
|
|
144
|
+
parser.add_argument('-s', '--sleep', type=float, default=1.0, help=
|
|
145
|
+
'Sleep time per cycle')
|
|
130
146
|
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
131
147
|
'Show more log messages (-vv: show even more)')
|
|
132
148
|
pargs = parser.parse_args()
|
|
@@ -136,10 +152,6 @@ print(f'pargs: {pargs}')
|
|
|
136
152
|
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
137
153
|
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose,
|
|
138
154
|
serverStateChanged, pargs.list)
|
|
139
|
-
# if pargs.list != '':
|
|
140
|
-
# print('List of PVs:')
|
|
141
|
-
# for _pvname in PVs:
|
|
142
|
-
# print(_pvname)
|
|
143
155
|
|
|
144
156
|
# Initialize the device, using pargs if needed.
|
|
145
157
|
# That can be used to set the number of points in the waveform, for example.
|
|
@@ -150,12 +162,13 @@ set_server('Start')
|
|
|
150
162
|
|
|
151
163
|
#``````````````````Main loop``````````````````````````````````````````````````
|
|
152
164
|
server = Server(providers=[PVs])
|
|
153
|
-
printi(f'Server started. Sleeping per cycle: {
|
|
165
|
+
printi(f'Server started. Sleeping per cycle: {float(pvv("sleep")):.3f} S.')
|
|
154
166
|
while True:
|
|
155
167
|
state = serverState()
|
|
156
168
|
if state.startswith('Exit'):
|
|
157
169
|
break
|
|
158
170
|
if not state.startswith('Stop'):
|
|
159
171
|
poll()
|
|
160
|
-
sleep()
|
|
161
|
-
|
|
172
|
+
if not sleep():
|
|
173
|
+
periodic_update()
|
|
174
|
+
printi('Server has exited')
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Copilot instructions for epicsdev
|
|
2
|
-
|
|
3
|
-
## Big picture
|
|
4
|
-
- Core EPICS PVAccess helpers are in [epicsdev/epicsdev.py](epicsdev/epicsdev.py). It keeps global server state in `C_` (prefix, PV map, verbosity, server state) and exposes `SPV()`, `publish()`, `pvv()`, `serverState()`.
|
|
5
|
-
- Server startup flow: `init_epicsdev(prefix, pvDefs, verbose=0, serverStateChanged=None, listDir=None)` builds PVs, then `Server(providers=[PVs])` runs a polling loop that checks `serverState()`; see [epicsdev/epicsdev.py](epicsdev/epicsdev.py) and [epicsdev/multiadc.py](epicsdev/multiadc.py).
|
|
6
|
-
- `create_PVs()` adds mandatory PVs (`host`, `version`, `status`, `server`, `verbose`, `polling`, `cycle`) before app-specific PVs; see [epicsdev/epicsdev.py](epicsdev/epicsdev.py).
|
|
7
|
-
- GUI pages for pypeto live in [config/epicsdev_pp.py](config/epicsdev_pp.py), [config/multiadc_pp.py](config/multiadc_pp.py), and [config/epicsSimscope_pp.py](config/epicsSimscope_pp.py); they assume the PV names/prefixes defined by the servers.
|
|
8
|
-
|
|
9
|
-
## Project-specific patterns & conventions
|
|
10
|
-
- PV definitions are `[name, description, SPV, extra]` and passed to `create_PVs()`; examples: `myPVDefs()` in [epicsdev/epicsdev.py](epicsdev/epicsdev.py) and [epicsdev/multiadc.py](epicsdev/multiadc.py).
|
|
11
|
-
- `SPV(initial, meta, vtype)` uses compact `meta`: `W` (writable), `R` (readable), `A` (alarm), `D` (discrete enum). `D` creates an `NTEnum` with `{choices,index}`.
|
|
12
|
-
- Writable PVs set `control.limitLow/High` to `0` as a PVA writability workaround (see `_create_PVs()` in [epicsdev/epicsdev.py](epicsdev/epicsdev.py)).
|
|
13
|
-
- `extra` dict keys commonly used: `setter`, `units`, `limitLow`, `limitHigh`, `format`, `valueAlarm`. `setter` receives `(value, spv)`.
|
|
14
|
-
- Use `publish()`/`pvv()` instead of direct `SharedPV` access; logging goes through `printi/printw/printe`, which also posts to `status`.
|
|
15
|
-
- In multi-channel templates, don’t pre-create `SPV` objects; use tuples and convert per-channel (see `ChannelTemplates` in [epicsdev/multiadc.py](epicsdev/multiadc.py)).
|
|
16
|
-
|
|
17
|
-
## External deps & integration points
|
|
18
|
-
- Requires `p4p` (see [pyproject.toml](pyproject.toml)). Optional runtime tools: `pypeto`, `pvplot` for GUI/plotting (see [README.md](README.md)).
|
|
19
|
-
|
|
20
|
-
## Common workflows (from README)
|
|
21
|
-
- Install and run demo server:
|
|
22
|
-
- `python -m epicsdev.epicsdev`
|
|
23
|
-
- Control/plot demo (requires `pypeto`, `pvplot`):
|
|
24
|
-
- `python -m pypeto -c config -f epicsdev`
|
|
25
|
-
- Run multi-channel waveform generator:
|
|
26
|
-
- `python -m epicsdev.multiadc -c100 -n1000`
|
|
27
|
-
- Launch multiadc GUI:
|
|
28
|
-
- `python -m pypeto -c config -f multiadc`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|