epicsdev 2.1.0__tar.gz → 2.1.2__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.
@@ -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.0
3
+ Version: 2.1.2
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 100 of
34
- 1000-pont noisy waveforms and 300 of scalar parameters.
33
+ the EPICS environment. For example the following command will generate 10000 of
34
+ 100-point noisy waveforms and 40000 of scalar parameters per second.
35
35
  ```
36
- python -m epicsdev.multiadc -c100 -n1000
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,17 @@ The graphs should look like this:
42
42
  [control page](docs/epicsdev_pypet.png),
43
43
  [plots](docs/epicsdev_pvplot.jpg).
44
44
 
45
+ Example of [Phoebus display](docs/phoebus_epicsdev.jpg), as defined in config/epicsdev.bob.
46
+
47
+ ## Using AI to generate PVAccess server for arbitrary instruments.
48
+ The epicsdev module is designed to be suitable for automatic development using AI agents.<br>
49
+ The roadmap to create a server for new instrument using copilot at github:
50
+ - Create new repository.
51
+ - In the prompt section enter something like this:<br>
52
+ 'Build device support for Tektronix MSO oscilloscopes using epicsdev_rigol_scope as a template and programming manual at < link to a pdf file >.'
53
+ - In 20-40 minutes the copilot will create a pull request.
54
+ - Follow instructions to review, commit and merge.
55
+
56
+ As an example, the generated server for Tektronix MSO oscilloscope was 99% correct and it reqiured very minor modifications.
57
+
45
58
 
@@ -0,0 +1,43 @@
1
+ # epicsdev
2
+ Helper module for creating EPICS PVAccess servers.
3
+
4
+ Demo:
5
+ ```
6
+ python pip install epicsdev
7
+ python -m epicsdev.epicsdev
8
+ ```
9
+
10
+ To control and plot:
11
+ ```
12
+ python pip install pypeto,pvplot
13
+ python -m pypeto -c config -f epicsdev
14
+ ```
15
+
16
+ ## Multi-channel waveform generator
17
+ Module **epicdev.multiadc** can generate large amount of data for stress-testing
18
+ the EPICS environment. For example the following command will generate 10000 of
19
+ 100-point noisy waveforms and 40000 of scalar parameters per second.
20
+ ```
21
+ python -m epicsdev.multiadc -s0.1 -c10000 -n100
22
+ ```
23
+ The GUI for monitoring:<br>
24
+ ```python -m pypeto -c config -f multiadc```
25
+
26
+ The graphs should look like this:
27
+ [control page](docs/epicsdev_pypet.png),
28
+ [plots](docs/epicsdev_pvplot.jpg).
29
+
30
+ Example of [Phoebus display](docs/phoebus_epicsdev.jpg), as defined in config/epicsdev.bob.
31
+
32
+ ## Using AI to generate PVAccess server for arbitrary instruments.
33
+ The epicsdev module is designed to be suitable for automatic development using AI agents.<br>
34
+ The roadmap to create a server for new instrument using copilot at github:
35
+ - Create new repository.
36
+ - In the prompt section enter something like this:<br>
37
+ 'Build device support for Tektronix MSO oscilloscopes using epicsdev_rigol_scope as a template and programming manual at < link to a pdf file >.'
38
+ - In 20-40 minutes the copilot will create a pull request.
39
+ - Follow instructions to review, commit and merge.
40
+
41
+ As an example, the generated server for Tektronix MSO oscilloscope was 99% correct and it reqiured very minor modifications.
42
+
43
+
@@ -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 Extrabold',14]}# Comic Sans MS
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 command: {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+'version', 'host:',D+'host',_],
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', Plot],
87
- ['nPoints:',D+'recordLength','Noise:',D+'noiseLevel', 'Channels:',D+'channels',_],
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
- 'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
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.0 26-01-31'# polling renamed to sleep. Sleep function added.
4
- #TODO add mandatory PV: host, to identify the server host.
3
+ __version__= 'v2.1.2 26-02-07'# do nothing in sleep() if stopped.
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 = time.time()
33
+ lastCycleTime = timer()
35
34
  lastUpdateTime = 0.
36
35
  cycleTimeSum = 0.
37
36
  cyclesAfterUpdate = 0
@@ -164,14 +163,18 @@ def _create_PVs(pvDefs):
164
163
  spv.post(ivalue, timestamp=ts)
165
164
  else:
166
165
  v['display.description'] = desc
167
- for field in extra.keys():
168
- if field in ['limitLow','limitHigh','format','units']:
169
- v[f'display.{field}'] = extra[field]
170
- if field.startswith('limit'):
171
- v[f'control.{field}'] = extra[field]
172
- if field == 'valueAlarm':
173
- for key,value in extra[field].items():
174
- v[f'valueAlarm.{key}'] = value
166
+ for field in extra.keys():
167
+ try:
168
+ if field in ['limitLow','limitHigh','format','units']:
169
+ v[f'display.{field}'] = extra[field]
170
+ if field.startswith('limit'):
171
+ v[f'control.{field}'] = extra[field]
172
+ if field == 'valueAlarm':
173
+ for key,value in extra[field].items():
174
+ v[f'valueAlarm.{key}'] = value
175
+ except KeyError as e:
176
+ print(f'Cannot set {field} for {pname}: {e}')
177
+ sys.exit(1)
175
178
  spv.post(v)
176
179
 
177
180
  # add new attributes.
@@ -332,8 +335,14 @@ def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
332
335
 
333
336
  def sleep():
334
337
  """Sleep function to be called in the main loop. It updates cycleTime PV
335
- and sleeps for the time specified in sleep PV."""
336
- tnow = time.time()
338
+ and sleeps for the time specified in sleep PV.
339
+ Returns False if a periodic update occurred.
340
+ """
341
+ time.sleep(pvv('sleep'))
342
+ sleeping = True
343
+ if serverState().startswith('Stop'):
344
+ return sleeping
345
+ tnow = timer()
337
346
  C_.cycleTimeSum += tnow - C_.lastCycleTime
338
347
  C_.lastCycleTime = tnow
339
348
  C_.cyclesAfterUpdate += 1
@@ -347,7 +356,8 @@ def sleep():
347
356
  C_.lastUpdateTime = tnow
348
357
  C_.cycleTimeSum = 0.
349
358
  C_.cyclesAfterUpdate = 0
350
- time.sleep(pvv('sleep'))
359
+ sleeping = False
360
+ return sleeping
351
361
 
352
362
  #``````````````````Demo````````````````````````````````````````````````````````
353
363
  if __name__ == "__main__":
@@ -359,20 +369,20 @@ if __name__ == "__main__":
359
369
  SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
360
370
  alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
361
371
  return [ # device-specific PVs
362
- ['noiseLevel', 'Noise amplitude', SPV(1.E-6,'W'), {SET:set_noise, U:'V'}],
372
+ ['noiseLevel', 'Noise amplitude', SPV(1.,'W'), {U:'V'}],
363
373
  ['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
364
374
  ['recordLength','Max number of points', SPV(100,'W','u32'),
365
375
  {LL:4,LH:1000000, SET:set_recordLength}],
376
+ ['throughput', 'Performance metrics, points per second', SPV(0.), {U:'Mpts/s'}],
366
377
  ['c01Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
367
- ['c01VoltsPerDiv', 'Vertical scale', SPV(1E-3,'W'), {U:'V/du'}],
378
+ ['c01VoltsPerDiv', 'Vertical scale', SPV(0.1,'W'), {U:'V/du'}],
368
379
  ['c01Waveform', 'Waveform array', SPV([0.]), {U:'du'}],
369
380
  ['c01Mean', 'Mean of the waveform', SPV(0.,'A'), {U:'du'}],
370
381
  ['c01Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {U:'du',**alarm}],
371
382
  ['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
372
383
  ]
373
- nPatterns = 100 # number of waveform patterns.
374
384
  pargs = None
375
- rng = np.random.default_rng(nPatterns)
385
+ rng = np.random.default_rng()
376
386
  nPoints = 100
377
387
 
378
388
  def set_recordLength(value, *_):
@@ -381,18 +391,6 @@ if __name__ == "__main__":
381
391
  printi(f'Setting tAxis to {value}')
382
392
  publish('tAxis', np.arange(value)*1.E-6)
383
393
  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
394
 
397
395
  def init(recordLength):
398
396
  """Example of device initialization function"""
@@ -401,14 +399,15 @@ if __name__ == "__main__":
401
399
 
402
400
  def poll():
403
401
  """Example of polling function. Called every cycle when server is running."""
404
- #pattern = C_.cycle % nPatterns# produces sliding
405
- pattern = rng.integers(0, nPatterns)
406
- wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
402
+ #ts = timer()
403
+ wf = rng.random(pvv('recordLength'))*pvv('noiseLevel')# it takes 5ms for 1M points
407
404
  wf /= pvv('c01VoltsPerDiv')
408
405
  wf += pvv('c01Offset')
406
+ #print(f'Waveform updated in {timer()-ts:.6g} S.')
409
407
  publish('c01Waveform', wf)
410
408
  publish('c01Peak2Peak', np.ptp(wf))
411
409
  publish('c01Mean', np.mean(wf))
410
+ #print(f'Polling completed in {timer()-ts:.6g} S.')
412
411
 
413
412
  # Argument parsing
414
413
  parser = argparse.ArgumentParser(description = __doc__,
@@ -449,5 +448,9 @@ if __name__ == "__main__":
449
448
  break
450
449
  if not state.startswith('Stop'):
451
450
  poll()
452
- sleep()
451
+ if not sleep():# Sleep and update performance metrics periodically
452
+ if not state.startswith('Stop'):
453
+ pointsPerSecond = len(pvv('c01Waveform'))/(pvv('cycleTime')-pvv('sleep'))/1.E6
454
+ publish('throughput', round(pointsPerSecond,6))
455
+ printv(f'periodic update. Performance: {pointsPerSecond:.3g} Mpts/s')
453
456
  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.0 26-01-31'# updated for epicsdev v2.1.0
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(1.E-4,'W'), {SET:set_noise, U:'V'}],
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', (1E-3,'W'), {U:'V/du'}],
33
- #['c0$VoltOffset', 'Vertical offset', (1E-3,), {U:'V/du'}],
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 constants
48
- nPatterns = 100 # number of waveform patterns.
49
- rng = np.random.default_rng(nPatterns)
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
- pattern = rng.integers(0, nPatterns)
100
+ ts1 = timer()
103
101
  chstr = f'c{ch+1:02}'
104
- wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
105
- #print(f'ch{ch}, {pattern}: {wf[0], wf.sum(), wf.mean(), np.mean(wf)}')
106
- wf /= pvv(f'{chstr}VoltsPerDiv')
107
- #wf += pvv(f'{chstr}Offset')
108
- wf += ch
109
- publish(f'{chstr}Waveform', list(wf))
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: {repr(pvv("sleep"))} S.')
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
- printi('Server is exited')
172
+ if not sleep():
173
+ periodic_update()
174
+ printi('Server has exited')
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "epicsdev"
7
- version = "2.1.0"
7
+ version = "2.1.2"
8
8
  authors = [
9
9
  { name="Andrey Sukhanov", email="sukhanov@bnl.gov" },
10
10
  ]
@@ -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`
epicsdev-2.1.0/README.md DELETED
@@ -1,30 +0,0 @@
1
- # epicsdev
2
- Helper module for creating EPICS PVAccess servers.
3
-
4
- Demo:
5
- ```
6
- python pip install epicsdev
7
- python -m epicsdev.epicsdev
8
- ```
9
-
10
- To control and plot:
11
- ```
12
- python pip install pypeto,pvplot
13
- python -m pypeto -c config -f epicsdev
14
- ```
15
-
16
- ## Multi-channel waveform generator
17
- Module **epicdev.multiadc** can generate large amount of data for stress-testing
18
- the EPICS environment. For example the following command will generate 100 of
19
- 1000-pont noisy waveforms and 300 of scalar parameters.
20
- ```
21
- python -m epicsdev.multiadc -c100 -n1000
22
- ```
23
- The GUI for monitoring:<br>
24
- ```python -m pypeto -c config -f multiadc```
25
-
26
- The graphs should look like this:
27
- [control page](docs/epicsdev_pypet.png),
28
- [plots](docs/epicsdev_pvplot.jpg).
29
-
30
-
File without changes
File without changes
File without changes