epicsdev 2.0.1__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.0.1 → epicsdev-2.1.1}/PKG-INFO +5 -5
- {epicsdev-2.0.1 → epicsdev-2.1.1}/README.md +4 -4
- epicsdev-2.1.1/config/epicsdev.bob +215 -0
- epicsdev-2.0.1/config/multiadc_pp.py → epicsdev-2.1.1/config/epicsdev_pp.py +30 -23
- epicsdev-2.1.1/config/multiadc1_pp.py +6 -0
- epicsdev-2.0.1/config/epicsdev_pp.py → epicsdev-2.1.1/config/multiadc_pp.py +38 -25
- epicsdev-2.1.1/docs/phoebus_epicsdev.jpg +0 -0
- {epicsdev-2.0.1 → epicsdev-2.1.1}/epicsdev/epicsdev.py +72 -46
- {epicsdev-2.0.1 → epicsdev-2.1.1}/epicsdev/multiadc.py +54 -45
- {epicsdev-2.0.1 → epicsdev-2.1.1}/pyproject.toml +1 -1
- epicsdev-2.0.1/.github/copilot-instructions.md +0 -28
- {epicsdev-2.0.1 → epicsdev-2.1.1}/LICENSE +0 -0
- {epicsdev-2.0.1 → epicsdev-2.1.1}/config/epicsSimscope_pp.py +0 -0
- {epicsdev-2.0.1 → epicsdev-2.1.1}/docs/epicsdev_pvplot.jpg +0 -0
- {epicsdev-2.0.1 → epicsdev-2.1.1}/docs/epicsdev_pypet.png +0 -0
- {epicsdev-2.0.1 → epicsdev-2.1.1}/epicsdev/__init__.py +0 -0
- {epicsdev-2.0.1 → 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.
|
|
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>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
"""Pypet page for epicdev.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Pypet page for epicdev.epicsdev.
|
|
2
|
+
Device instance is configured via environment variable EPICSDEV,
|
|
3
|
+
default instance is epicsdev0:."""
|
|
4
|
+
# pylint: disable=invalid-name
|
|
5
|
+
__version__ = 'v0.1.0 2026-01-31'#
|
|
6
|
+
|
|
7
|
+
import os
|
|
5
8
|
|
|
6
9
|
#``````````````````Definitions````````````````````````````````````````````````
|
|
7
10
|
# python expressions and functions, used in the spreadsheet
|
|
@@ -18,28 +21,27 @@ LargeFont = {'color':'light gray', **font(18), 'fgColor':'dark green'}
|
|
|
18
21
|
ButtonFont = {'font':['Open Sans Extrabold',14]}# Comic Sans MS
|
|
19
22
|
LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
|
|
20
23
|
lColor = color('lightGreen')
|
|
21
|
-
|
|
22
|
-
# definition for plotting cell
|
|
23
24
|
PyPath = 'python -m'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
print(f'Plot command: {Plot}')
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
Instance = f"epicsDev{os.environ['EPICSDEV']}:"
|
|
28
|
+
except KeyError:
|
|
29
|
+
Instance = 'epicsDev0:'
|
|
30
|
+
|
|
31
31
|
#``````````````````PyPage Object``````````````````````````````````````````````
|
|
32
32
|
class PyPage():
|
|
33
|
-
def __init__(self, instance=
|
|
34
|
-
title="
|
|
33
|
+
def __init__(self, instance=None,
|
|
34
|
+
title="epicsdev", channels=1):
|
|
35
35
|
"""instance: unique name of the page.
|
|
36
36
|
For EPICS it is usually device prefix
|
|
37
37
|
"""
|
|
38
|
+
if instance is None:
|
|
39
|
+
instance = Instance
|
|
38
40
|
print(f'Instantiating Page {instance,title} with {channels} channels')
|
|
39
41
|
|
|
40
42
|
#``````````Mandatory class members starts here````````````````````````
|
|
41
43
|
self.namespace = 'PVA'
|
|
42
|
-
self.title =
|
|
44
|
+
self.title = instance[:-1]
|
|
43
45
|
|
|
44
46
|
#``````````Page attributes, optional`````````````````````````
|
|
45
47
|
self.page = {**color(240,240,240)}
|
|
@@ -70,18 +72,23 @@ string or device:parameter and the value is dictionary of the features.
|
|
|
70
72
|
#``````````Abbreviations, used in cell definitions
|
|
71
73
|
def ChLine(suffix):
|
|
72
74
|
return [f'{D}c{ch+1:02d}{suffix}' for ch in range(channels)]
|
|
73
|
-
|
|
75
|
+
PaneP2P = ' '.join([f'c{i+1:02d}Mean c{i+1:02d}Peak2Peak' for i in range(channels)])
|
|
76
|
+
PaneWF = ' '.join([f'c{i+1:02d}Waveform' for i in range(channels)])
|
|
77
|
+
#PaneT = 'timing[1] timing[3]'
|
|
78
|
+
Plot = {'Plot':{'launch':
|
|
79
|
+
f'{PyPath} pvplot Y-5:5 -aV:{instance} -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
|
|
80
|
+
**lColor, **ButtonFont}}
|
|
81
|
+
print(f'Plot command: {Plot}')
|
|
74
82
|
#``````````mandatory member```````````````````````````````````````````
|
|
75
83
|
self.rows = [
|
|
76
|
-
['Device:', D,
|
|
77
|
-
['State:', D+'server','cycle:', D+'cycle',_,_,Plot], # 'Recall:', D+'setup',],
|
|
84
|
+
['Device:',D, D+'server', D+'version', 'host:',D+'host',_],
|
|
78
85
|
['Status:', {D+'status': span(8,1)}],
|
|
79
|
-
['
|
|
80
|
-
|
|
86
|
+
['Cycle time:',D+'cycleTime', 'Sleep:',D+'sleep', 'Cycle:',D+'cycle', Plot],
|
|
87
|
+
['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
|
|
88
|
+
'Throughput:',{D+'throughput':span(2,1)},_],
|
|
81
89
|
[{'ATTRIBUTES':{**color('lightCyan'),**just(1)}},
|
|
82
|
-
'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
|
|
90
|
+
'Channels:', 'CH1', 'CH2', 'CH3', 'CH4', 'CH5', 'CH6'],
|
|
83
91
|
['V/div:']+ChLine('VoltsPerDiv'),
|
|
84
92
|
['Mean:']+ChLine('Mean'),
|
|
85
93
|
['Peak2Peak:']+ChLine('Peak2Peak'),
|
|
86
|
-
#['Waveform:']+ChLine('Waveform'),
|
|
87
94
|
]
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
"""Pypet page for epicdev.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Pypet page for epicdev.multiadc, simulated multi-channel ADC device server.
|
|
2
|
+
Device instance is configured via environment variable MULTIADC,
|
|
3
|
+
default instance is multiadc0:."""
|
|
4
|
+
# pylint: disable=invalid-name
|
|
5
|
+
__version__ = 'v0.1.0 2026-01-31'#
|
|
6
|
+
|
|
7
|
+
import os
|
|
5
8
|
|
|
6
9
|
#``````````````````Definitions````````````````````````````````````````````````
|
|
7
10
|
# python expressions and functions, used in the spreadsheet
|
|
@@ -15,31 +18,30 @@ def slider(minValue,maxValue):
|
|
|
15
18
|
return {'widget':'hslider','opLimits':[minValue,maxValue],'span':[2,1]}
|
|
16
19
|
|
|
17
20
|
LargeFont = {'color':'light gray', **font(18), 'fgColor':'dark green'}
|
|
18
|
-
ButtonFont = {'font':['Open Sans
|
|
21
|
+
ButtonFont = {'font':['Open Sans Bold,14']}# Comic Sans MS
|
|
19
22
|
LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
|
|
20
23
|
lColor = color('lightGreen')
|
|
21
|
-
|
|
22
|
-
# definition for plotting cell
|
|
23
24
|
PyPath = 'python -m'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#``````````````````PyPage Object``````````````````````````````````````````````
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
Instance = f"multiadc{os.environ['EPICSDEV_MULTIADC']}:"
|
|
28
|
+
except KeyError:
|
|
29
|
+
Instance = 'multiadc0:'
|
|
30
|
+
|
|
31
|
+
#``````````````````Mandatory object PyPage````````````````````````````````````
|
|
32
32
|
class PyPage():
|
|
33
|
-
def __init__(self, instance=
|
|
34
|
-
title=
|
|
33
|
+
def __init__(self, instance=None,
|
|
34
|
+
title=None, channels=6):
|
|
35
35
|
"""instance: unique name of the page.
|
|
36
36
|
For EPICS it is usually device prefix
|
|
37
37
|
"""
|
|
38
|
+
if instance is None:
|
|
39
|
+
instance = Instance
|
|
38
40
|
print(f'Instantiating Page {instance,title} with {channels} channels')
|
|
39
41
|
|
|
40
42
|
#``````````Mandatory class members starts here````````````````````````
|
|
41
43
|
self.namespace = 'PVA'
|
|
42
|
-
self.title = title
|
|
44
|
+
self.title = title if title is not None else instance[:-1]
|
|
43
45
|
|
|
44
46
|
#``````````Page attributes, optional`````````````````````````
|
|
45
47
|
self.page = {**color(240,240,240)}
|
|
@@ -69,18 +71,29 @@ string or device:parameter and the value is dictionary of the features.
|
|
|
69
71
|
|
|
70
72
|
#``````````Abbreviations, used in cell definitions
|
|
71
73
|
def ChLine(suffix):
|
|
72
|
-
return [f'{D}
|
|
73
|
-
|
|
74
|
+
return [f'{D}c{ch+1:02d}{suffix}' for ch in range(channels)]
|
|
75
|
+
PaneP2P = ' '.join([f'c{i+1:02d}Mean c{i+1:02d}Peak2Peak' for i in range(channels)])
|
|
76
|
+
PaneWF = ' '.join([f'c{i+1:02d}Waveform' for i in range(channels)])
|
|
77
|
+
#PaneT = 'timing[1] timing[3]'
|
|
78
|
+
Plot = {'Plot Channels':{'launch':
|
|
79
|
+
f'{PyPath} pvplot Y-5:5 -aV:{instance} -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
|
|
80
|
+
**lColor, **ButtonFont}}
|
|
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}')
|
|
74
84
|
#``````````mandatory member```````````````````````````````````````````
|
|
75
85
|
self.rows = [
|
|
76
|
-
['Device:', D, {D+'
|
|
77
|
-
['State:', D+'server', 'cycle:', D+'cycle',_,_, Plot], # 'Recall:', D+'setup',],
|
|
86
|
+
['Device:',D, D+'server', {D+'channels':just(2)},'chnls, host:',D+'host',D+'version'],
|
|
78
87
|
['Status:', {D+'status': span(8,1)}],
|
|
79
|
-
['
|
|
80
|
-
|
|
88
|
+
['Cycle time:',D+'cycleTime', 'Sleep:',D+'sleep', 'Cycle:',D+'cycle'],
|
|
89
|
+
['nPoints:',D+'recordLength','Noise:',D+'noiseLevel',
|
|
90
|
+
'Throughput:',D+'throughput',Timing],
|
|
81
91
|
[{'ATTRIBUTES':{**color('lightCyan'),**just(1)}},
|
|
82
|
-
|
|
92
|
+
Plot,'CH1','CH2','CH3','CH4','CH5','CH6'],
|
|
83
93
|
['V/div:']+ChLine('VoltsPerDiv'),
|
|
94
|
+
['VoltOffset:']+ChLine('VoltOffset'),
|
|
84
95
|
['Mean:']+ChLine('Mean'),
|
|
85
96
|
['Peak2Peak:']+ChLine('Peak2Peak'),
|
|
97
|
+
#['Waveform:']+ChLine('Waveform'),
|
|
98
|
+
['Timing:',{D+'timing':span(3,1)}],
|
|
86
99
|
]
|
|
Binary file
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"""Skeleton and helper functions for creating EPICS PVAccess server"""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= 'v2.
|
|
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
|
|
|
8
7
|
import sys
|
|
9
|
-
|
|
8
|
+
import time
|
|
9
|
+
from time import perf_counter as timer
|
|
10
10
|
import os
|
|
11
11
|
from socket import gethostname
|
|
12
12
|
from p4p.nt import NTScalar, NTEnum
|
|
@@ -15,6 +15,8 @@ from p4p.server import Server
|
|
|
15
15
|
from p4p.server.thread import SharedPV
|
|
16
16
|
from p4p.client.thread import Context
|
|
17
17
|
|
|
18
|
+
PeriodicUpdateInterval = 10. # seconds
|
|
19
|
+
|
|
18
20
|
#``````````````````Module Storage`````````````````````````````````````````````
|
|
19
21
|
def _serverStateChanged(newState:str):
|
|
20
22
|
"""Dummy serverStateChanged function"""
|
|
@@ -28,6 +30,10 @@ class C_():
|
|
|
28
30
|
PVs = {}
|
|
29
31
|
PVDefs = []
|
|
30
32
|
serverStateChanged = _serverStateChanged
|
|
33
|
+
lastCycleTime = timer()
|
|
34
|
+
lastUpdateTime = 0.
|
|
35
|
+
cycleTimeSum = 0.
|
|
36
|
+
cyclesAfterUpdate = 0
|
|
31
37
|
|
|
32
38
|
#```````````````````Helper methods````````````````````````````````````````````
|
|
33
39
|
def serverState():
|
|
@@ -35,7 +41,7 @@ def serverState():
|
|
|
35
41
|
cached in C_ to avoid unnecessary get() calls."""
|
|
36
42
|
return C_.serverState
|
|
37
43
|
def _printTime():
|
|
38
|
-
return strftime("%m%d:%H%M%S")
|
|
44
|
+
return time.strftime("%m%d:%H%M%S")
|
|
39
45
|
def printi(msg):
|
|
40
46
|
"""Print info message and publish it to status PV."""
|
|
41
47
|
print(f'inf_@{_printTime()}: {msg}')
|
|
@@ -81,7 +87,7 @@ def publish(pvName:str, value, ifChanged=False, t=None):
|
|
|
81
87
|
print(f'WARNING: PV {pvName} not found. Cannot publish value.')
|
|
82
88
|
return
|
|
83
89
|
if t is None:
|
|
84
|
-
t = time()
|
|
90
|
+
t = time.time()
|
|
85
91
|
if not ifChanged or pv.current() != value:
|
|
86
92
|
pv.post(value, timestamp=t)
|
|
87
93
|
|
|
@@ -127,7 +133,7 @@ def SPV(initial, meta='', vtype=None):
|
|
|
127
133
|
|
|
128
134
|
#``````````````````create_PVs()```````````````````````````````````````````````
|
|
129
135
|
def _create_PVs(pvDefs):
|
|
130
|
-
ts = time()
|
|
136
|
+
ts = time.time()
|
|
131
137
|
for defs in pvDefs:
|
|
132
138
|
try:
|
|
133
139
|
pname,desc,spv,extra = defs
|
|
@@ -174,7 +180,7 @@ def _create_PVs(pvDefs):
|
|
|
174
180
|
if spv.writable:
|
|
175
181
|
@spv.put
|
|
176
182
|
def handle(spv, op):
|
|
177
|
-
ct = time()
|
|
183
|
+
ct = time.time()
|
|
178
184
|
vv = op.value()
|
|
179
185
|
vr = vv.raw.value
|
|
180
186
|
current = spv._wrap(spv.current())
|
|
@@ -259,8 +265,12 @@ def create_PVs(pvDefs=None):
|
|
|
259
265
|
{'setter':set_server}],
|
|
260
266
|
['verbose', 'Debugging verbosity', SPV(C_.verbose,'W','u8'),
|
|
261
267
|
{'setter':set_verbose, LL:0,LH:3}],
|
|
262
|
-
['
|
|
263
|
-
|
|
268
|
+
['sleep', 'Pause in the main loop, it could be useful for throttling the data output',
|
|
269
|
+
SPV(1.0,'W'), {U:'S', LL:0.001, LH:10.1}],
|
|
270
|
+
['cycle', 'Cycle number, published every {PeriodicUpdateInterval} S.',
|
|
271
|
+
SPV(0,'','u32'), {}],
|
|
272
|
+
['cycleTime','Average cycle time including sleep, published every {PeriodicUpdateInterval} S',
|
|
273
|
+
SPV(0.), {U:'S'}],
|
|
264
274
|
]
|
|
265
275
|
# append application's PVs, defined in the pvDefs and create map of
|
|
266
276
|
# providers
|
|
@@ -301,7 +311,8 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
|
|
|
301
311
|
host = repr(get_externalPV(prefix+'host')).replace("'",'')
|
|
302
312
|
print(f'ERROR: Server for {prefix} already running at {host}. Exiting.')
|
|
303
313
|
sys.exit(1)
|
|
304
|
-
except TimeoutError:
|
|
314
|
+
except TimeoutError:
|
|
315
|
+
pass
|
|
305
316
|
|
|
306
317
|
# No existing server found. Creating PVs.
|
|
307
318
|
pvs = create_PVs(pvDefs)
|
|
@@ -315,8 +326,33 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
|
|
|
315
326
|
with open(filepath, 'w', encoding="utf-8") as f:
|
|
316
327
|
for _pvname in pvs:
|
|
317
328
|
f.write(_pvname + '\n')
|
|
329
|
+
printi(f'Hosting {len(pvs)} PVs')
|
|
318
330
|
return pvs
|
|
319
331
|
|
|
332
|
+
def sleep():
|
|
333
|
+
"""Sleep function to be called in the main loop. It updates cycleTime PV
|
|
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()
|
|
339
|
+
C_.cycleTimeSum += tnow - C_.lastCycleTime
|
|
340
|
+
C_.lastCycleTime = tnow
|
|
341
|
+
C_.cyclesAfterUpdate += 1
|
|
342
|
+
C_.cycle += 1
|
|
343
|
+
printv(f'cycle {C_.cycle}')
|
|
344
|
+
sleeping = True
|
|
345
|
+
if tnow - C_.lastUpdateTime > PeriodicUpdateInterval:
|
|
346
|
+
avgCycleTime = C_.cycleTimeSum / C_.cyclesAfterUpdate
|
|
347
|
+
printv(f'Average cycle time: {avgCycleTime:.6f} S.')
|
|
348
|
+
publish('cycle', C_.cycle)
|
|
349
|
+
publish('cycleTime', avgCycleTime)
|
|
350
|
+
C_.lastUpdateTime = tnow
|
|
351
|
+
C_.cycleTimeSum = 0.
|
|
352
|
+
C_.cyclesAfterUpdate = 0
|
|
353
|
+
sleeping = False
|
|
354
|
+
return sleeping
|
|
355
|
+
|
|
320
356
|
#``````````````````Demo````````````````````````````````````````````````````````
|
|
321
357
|
if __name__ == "__main__":
|
|
322
358
|
import numpy as np
|
|
@@ -327,20 +363,20 @@ if __name__ == "__main__":
|
|
|
327
363
|
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
328
364
|
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
329
365
|
return [ # device-specific PVs
|
|
330
|
-
['noiseLevel', 'Noise amplitude', SPV(1
|
|
366
|
+
['noiseLevel', 'Noise amplitude', SPV(1.,'W'), {U:'V'}],
|
|
331
367
|
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
332
368
|
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
333
369
|
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
334
|
-
['
|
|
335
|
-
['
|
|
336
|
-
['
|
|
337
|
-
['
|
|
338
|
-
['
|
|
370
|
+
['throughput', 'Performance metrics, points per second', SPV(0.), {U:'Mpts/s'}],
|
|
371
|
+
['c01Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
|
|
372
|
+
['c01VoltsPerDiv', 'Vertical scale', SPV(0.1,'W'), {U:'V/du'}],
|
|
373
|
+
['c01Waveform', 'Waveform array', SPV([0.]), {U:'du'}],
|
|
374
|
+
['c01Mean', 'Mean of the waveform', SPV(0.,'A'), {U:'du'}],
|
|
375
|
+
['c01Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {U:'du',**alarm}],
|
|
339
376
|
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
340
377
|
]
|
|
341
|
-
nPatterns = 100 # number of waveform patterns.
|
|
342
378
|
pargs = None
|
|
343
|
-
rng = np.random.default_rng(
|
|
379
|
+
rng = np.random.default_rng()
|
|
344
380
|
nPoints = 100
|
|
345
381
|
|
|
346
382
|
def set_recordLength(value, *_):
|
|
@@ -349,37 +385,23 @@ if __name__ == "__main__":
|
|
|
349
385
|
printi(f'Setting tAxis to {value}')
|
|
350
386
|
publish('tAxis', np.arange(value)*1.E-6)
|
|
351
387
|
publish('recordLength', value)
|
|
352
|
-
# Re-initialize noise array, because its size depends on recordLength
|
|
353
|
-
set_noise(pvv('noiseLevel'))
|
|
354
|
-
|
|
355
|
-
def set_noise(level, *_):
|
|
356
|
-
"""Noise level have changed. Update noise array."""
|
|
357
|
-
v = float(level)
|
|
358
|
-
recordLength = pvv('recordLength')
|
|
359
|
-
ts = timer()
|
|
360
|
-
pargs.noise = np.random.normal(scale=0.5*level,
|
|
361
|
-
size=recordLength+nPatterns)# 45ms/1e6 points
|
|
362
|
-
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
363
|
-
publish('noiseLevel', level)
|
|
364
388
|
|
|
365
389
|
def init(recordLength):
|
|
366
|
-
"""
|
|
390
|
+
"""Example of device initialization function"""
|
|
367
391
|
set_recordLength(recordLength)
|
|
368
392
|
#set_noise(pvv('noiseLevel')) # already called from set_recordLength
|
|
369
393
|
|
|
370
394
|
def poll():
|
|
371
|
-
"""Example of polling function"""
|
|
372
|
-
#
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
publish('ch1Peak2Peak', np.ptp(wf))
|
|
382
|
-
publish('ch1Mean', np.mean(wf))
|
|
395
|
+
"""Example of polling function. Called every cycle when server is running."""
|
|
396
|
+
#ts = timer()
|
|
397
|
+
wf = rng.random(pvv('recordLength'))*pvv('noiseLevel')# it takes 5ms for 1M points
|
|
398
|
+
wf /= pvv('c01VoltsPerDiv')
|
|
399
|
+
wf += pvv('c01Offset')
|
|
400
|
+
#print(f'Waveform updated in {timer()-ts:.6g} S.')
|
|
401
|
+
publish('c01Waveform', wf)
|
|
402
|
+
publish('c01Peak2Peak', np.ptp(wf))
|
|
403
|
+
publish('c01Mean', np.mean(wf))
|
|
404
|
+
#print(f'Polling completed in {timer()-ts:.6g} S.')
|
|
383
405
|
|
|
384
406
|
# Argument parsing
|
|
385
407
|
parser = argparse.ArgumentParser(description = __doc__,
|
|
@@ -389,7 +411,7 @@ if __name__ == "__main__":
|
|
|
389
411
|
'Device name, the PV name will be <device><index>:')
|
|
390
412
|
parser.add_argument('-i', '--index', default='0', help=
|
|
391
413
|
'Device index, the PV name will be <device><index>:')
|
|
392
|
-
parser.add_argument('-l', '--list',
|
|
414
|
+
parser.add_argument('-l', '--list', nargs='?', help=(
|
|
393
415
|
'Directory to save list of all generated PVs, if no directory is given, '
|
|
394
416
|
'then </tmp/pvlist/><prefix> is assumed.'))
|
|
395
417
|
# The rest of options are not essential, they can be controlled at runtime using PVs.
|
|
@@ -413,12 +435,16 @@ if __name__ == "__main__":
|
|
|
413
435
|
|
|
414
436
|
# Main loop
|
|
415
437
|
server = Server(providers=[PVs])
|
|
416
|
-
printi(f'Server started
|
|
438
|
+
printi(f'Server started. Sleeping per cycle: {repr(pvv("sleep"))} S.')
|
|
417
439
|
while True:
|
|
418
440
|
state = serverState()
|
|
419
441
|
if state.startswith('Exit'):
|
|
420
442
|
break
|
|
421
443
|
if not state.startswith('Stop'):
|
|
422
444
|
poll()
|
|
423
|
-
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')
|
|
424
450
|
printi('Server is exited')
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"""Simulated multi-channel ADC device server using epicsdev module."""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= '
|
|
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
|
-
from .epicsdev import pvv, printi, printv, SPV, set_server
|
|
11
|
+
from .epicsdev import pvv, printi, printv, SPV, set_server, sleep
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def myPVDefs():
|
|
@@ -17,19 +16,23 @@ def myPVDefs():
|
|
|
17
16
|
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
18
17
|
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
19
18
|
pvDefs = [ # device-specific PVs
|
|
19
|
+
['channels', 'Number of device channels', SPV(pargs.channels), {}],
|
|
20
20
|
['externalControl', 'Name of external PV, which controls the server',
|
|
21
21
|
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WD'), {}],
|
|
22
|
-
['noiseLevel', 'Noise amplitude', SPV(
|
|
22
|
+
['noiseLevel', 'Noise amplitude', SPV(0.05,'W'), {U:'V'}],
|
|
23
23
|
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
24
24
|
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
25
25
|
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
26
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'}],
|
|
27
30
|
]
|
|
28
31
|
|
|
29
32
|
# Templates for channel-related PVs. Important: SPV cannot be used in this list!
|
|
30
33
|
ChannelTemplates = [
|
|
31
|
-
['c0$VoltsPerDiv', 'Vertical scale', (
|
|
32
|
-
|
|
34
|
+
['c0$VoltsPerDiv', 'Vertical scale', (0.1,'W'), {U:'V/du'}],
|
|
35
|
+
['c0$VoltOffset', 'Vertical offset', (0.,'W'), {U:'V'}],
|
|
33
36
|
['c0$Waveform', 'Waveform array', ([0.],), {U:'du'}],
|
|
34
37
|
['c0$Mean', 'Mean of the waveform', (0.,'A'), {U:'du'}],
|
|
35
38
|
['c0$Peak2Peak','Peak-to-peak amplitude', (0.,'A'), {U:'du',**alarm}],
|
|
@@ -43,30 +46,20 @@ def myPVDefs():
|
|
|
43
46
|
pvDefs.append(newpvdef)
|
|
44
47
|
return pvDefs
|
|
45
48
|
|
|
46
|
-
#``````````````````Module
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
#``````````````````Module attributes
|
|
50
|
+
rng = np.random.default_rng()
|
|
51
|
+
ElapsedTime = {'waveform': 0., 'publish': 0., 'poll': 0.}
|
|
52
|
+
class C_():
|
|
53
|
+
cyclesSinceUpdate = 0
|
|
49
54
|
|
|
50
55
|
#``````````````````Setter functions for PVs```````````````````````````````````
|
|
51
|
-
def set_recordLength(value):
|
|
56
|
+
def set_recordLength(value, *_):
|
|
52
57
|
"""Record length have changed. The tAxis should be updated accordingly."""
|
|
53
58
|
printi(f'Setting tAxis to {value}')
|
|
54
59
|
publish('tAxis', np.arange(value)*1.E-6)
|
|
55
60
|
publish('recordLength', value)
|
|
56
|
-
# Re-initialize noise array, because its size depends on recordLength
|
|
57
|
-
set_noise(pvv('noiseLevel'))
|
|
58
|
-
|
|
59
|
-
def set_noise(level):
|
|
60
|
-
"""Noise level have changed. Update noise array."""
|
|
61
|
-
v = float(level)
|
|
62
|
-
recordLength = pvv('recordLength')
|
|
63
|
-
ts = timer()
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
67
|
-
publish('noiseLevel', level)
|
|
68
|
-
|
|
69
|
-
def set_externalControl(value):
|
|
62
|
+
def set_externalControl(value, *_):
|
|
70
63
|
"""External control PV have changed. Control the server accordingly."""
|
|
71
64
|
pvname = str(value)
|
|
72
65
|
if pvname in (None,'0'):
|
|
@@ -91,27 +84,45 @@ def serverStateChanged(newState:str):
|
|
|
91
84
|
publish('cycle', 0)
|
|
92
85
|
|
|
93
86
|
def init(recordLength):
|
|
94
|
-
"""
|
|
87
|
+
"""Device initialization function"""
|
|
95
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)
|
|
96
92
|
#set_externalControl(pargs.prefix + pargs.external)
|
|
93
|
+
publish('sleep', pargs.sleep)
|
|
97
94
|
|
|
98
95
|
def poll():
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
printv(f'cycle {repr(cycle)}')
|
|
103
|
-
publish('cycle', cycle + 1)
|
|
96
|
+
"""Device polling function, called every cycle when server is running"""
|
|
97
|
+
C_.cyclesSinceUpdate += 1
|
|
98
|
+
ts0 = timer()
|
|
104
99
|
for ch in range(pargs.channels):
|
|
105
|
-
|
|
100
|
+
ts1 = timer()
|
|
106
101
|
chstr = f'c{ch+1:02}'
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
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)
|
|
113
108
|
publish(f'{chstr}Peak2Peak', np.ptp(wf))
|
|
114
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
|
+
|
|
115
126
|
|
|
116
127
|
# Argument parsing
|
|
117
128
|
parser = argparse.ArgumentParser(description = __doc__,
|
|
@@ -130,6 +141,8 @@ parser.add_argument('-i', '--index', default='0', help=
|
|
|
130
141
|
# The rest of arguments are not essential, they can be changed at runtime using PVs.
|
|
131
142
|
parser.add_argument('-n', '--npoints', type=int, default=100, help=
|
|
132
143
|
'Number of points in the waveform')
|
|
144
|
+
parser.add_argument('-s', '--sleep', type=float, default=1.0, help=
|
|
145
|
+
'Sleep time per cycle')
|
|
133
146
|
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
134
147
|
'Show more log messages (-vv: show even more)')
|
|
135
148
|
pargs = parser.parse_args()
|
|
@@ -139,11 +152,6 @@ print(f'pargs: {pargs}')
|
|
|
139
152
|
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
140
153
|
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose,
|
|
141
154
|
serverStateChanged, pargs.list)
|
|
142
|
-
# if pargs.list != '':
|
|
143
|
-
# print('List of PVs:')
|
|
144
|
-
# for _pvname in PVs:
|
|
145
|
-
# print(_pvname)
|
|
146
|
-
printi(f'Hosting {len(PVs)} PVs')
|
|
147
155
|
|
|
148
156
|
# Initialize the device, using pargs if needed.
|
|
149
157
|
# That can be used to set the number of points in the waveform, for example.
|
|
@@ -154,12 +162,13 @@ set_server('Start')
|
|
|
154
162
|
|
|
155
163
|
#``````````````````Main loop``````````````````````````````````````````````````
|
|
156
164
|
server = Server(providers=[PVs])
|
|
157
|
-
printi(f'Server started
|
|
165
|
+
printi(f'Server started. Sleeping per cycle: {float(pvv("sleep")):.3f} S.')
|
|
158
166
|
while True:
|
|
159
167
|
state = serverState()
|
|
160
168
|
if state.startswith('Exit'):
|
|
161
169
|
break
|
|
162
170
|
if not state.startswith('Stop'):
|
|
163
171
|
poll()
|
|
164
|
-
|
|
165
|
-
|
|
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
|