epicsdev 1.0.2__tar.gz → 2.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epicsdev-2.0.1/.github/copilot-instructions.md +28 -0
- {epicsdev-1.0.2 → epicsdev-2.0.1}/PKG-INFO +16 -2
- epicsdev-2.0.1/README.md +30 -0
- {epicsdev-1.0.2 → epicsdev-2.0.1}/config/epicsdev_pp.py +8 -8
- epicsdev-2.0.1/config/multiadc_pp.py +87 -0
- epicsdev-2.0.1/docs/epicsdev_pvplot.jpg +0 -0
- epicsdev-2.0.1/docs/epicsdev_pypet.png +0 -0
- {epicsdev-1.0.2 → epicsdev-2.0.1}/epicsdev/epicsdev.py +137 -72
- epicsdev-2.0.1/epicsdev/multiadc.py +165 -0
- epicsdev-2.0.1/fallback/multiadc.py +151 -0
- {epicsdev-1.0.2 → epicsdev-2.0.1}/pyproject.toml +1 -1
- epicsdev-1.0.2/README.md +0 -16
- {epicsdev-1.0.2 → epicsdev-2.0.1}/LICENSE +0 -0
- {epicsdev-1.0.2 → epicsdev-2.0.1}/config/epicsSimscope_pp.py +0 -0
- {epicsdev-1.0.2 → epicsdev-2.0.1}/epicsdev/__init__.py +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
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`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epicsdev
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Helper module for creating EPICS PVAccess servers using p4p
|
|
5
5
|
Project-URL: Homepage, https://github.com/ASukhanov/epicsdev
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/ASukhanov/epicsdev
|
|
@@ -19,7 +19,7 @@ Helper module for creating EPICS PVAccess servers.
|
|
|
19
19
|
Demo:
|
|
20
20
|
```
|
|
21
21
|
python pip install epicsdev
|
|
22
|
-
python -m epicsdev.epicsdev
|
|
22
|
+
python -m epicsdev.epicsdev
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
To control and plot:
|
|
@@ -28,4 +28,18 @@ python pip install pypeto,pvplot
|
|
|
28
28
|
python -m pypeto -c config -f epicsdev
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Multi-channel waveform generator
|
|
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.
|
|
35
|
+
```
|
|
36
|
+
python -m epicsdev.multiadc -c100 -n1000
|
|
37
|
+
```
|
|
38
|
+
The GUI for monitoring:<br>
|
|
39
|
+
```python -m pypeto -c config -f multiadc```
|
|
40
|
+
|
|
41
|
+
The graphs should look like this:
|
|
42
|
+
[control page](docs/epicsdev_pypet.png),
|
|
43
|
+
[plots](docs/epicsdev_pvplot.jpg).
|
|
44
|
+
|
|
31
45
|
|
epicsdev-2.0.1/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
|
|
@@ -49,9 +49,9 @@ class PyPage():
|
|
|
49
49
|
self.columns = {
|
|
50
50
|
1: {'width': 120, 'justify': 'right'},
|
|
51
51
|
2: {'width': 80},
|
|
52
|
-
3: {'width': 80},
|
|
52
|
+
3: {'width': 80, 'justify': 'right'},
|
|
53
53
|
4: {'width': 80},
|
|
54
|
-
5: {'width': 80},
|
|
54
|
+
5: {'width': 80, 'justify': 'right'},
|
|
55
55
|
6: {'width': 80},
|
|
56
56
|
7: {'width': 80},
|
|
57
57
|
8: {'width': 80},
|
|
@@ -73,13 +73,13 @@ string or device:parameter and the value is dictionary of the features.
|
|
|
73
73
|
#FOption = ' -file '+logreqMap.get(D,'')
|
|
74
74
|
#``````````mandatory member```````````````````````````````````````````
|
|
75
75
|
self.rows = [
|
|
76
|
-
['Device:', D, {D+'version':span(2,1)},_,
|
|
77
|
-
['State:', D+'server','cycle:',D+'cycle',_,_,Plot], # 'Recall:', D+'setup',],
|
|
76
|
+
['Device:', D, {D+'version':span(2,1)},_, 'host:', D+'host',_],
|
|
77
|
+
['State:', D+'server', 'cycle:', D+'cycle',_,_, Plot], # 'Recall:', D+'setup',],
|
|
78
78
|
['Status:', {D+'status': span(8,1)}],
|
|
79
|
-
['Polling Interval:', D+'polling','nPoints:',D+'recordLength',
|
|
80
|
-
'Noise:',D+'noiseLevel',_],
|
|
81
|
-
[{'ATTRIBUTES':color('lightCyan')},
|
|
82
|
-
'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
|
|
79
|
+
['Polling Interval:', D+'polling', 'nPoints:', D+'recordLength',
|
|
80
|
+
'Noise:', D+'noiseLevel',_],
|
|
81
|
+
[{'ATTRIBUTES':{**color('lightCyan'),**just(1)}},
|
|
82
|
+
'Channels:', 'CH1', 'CH2', 'CH3', 'CH4', 'CH5', 'CH6'],
|
|
83
83
|
['V/div:']+ChLine('VoltsPerDiv'),
|
|
84
84
|
['Mean:']+ChLine('Mean'),
|
|
85
85
|
['Peak2Peak:']+ChLine('Peak2Peak'),
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Pypet page for epicdev.multiadc module"""
|
|
2
|
+
# format: pypeto 1.2+
|
|
3
|
+
__version__ = 'v0.0.1 2026-01-23'#
|
|
4
|
+
print(f'epicsScope {__version__}')
|
|
5
|
+
|
|
6
|
+
#``````````````````Definitions````````````````````````````````````````````````
|
|
7
|
+
# python expressions and functions, used in the spreadsheet
|
|
8
|
+
_ = ''
|
|
9
|
+
def span(x,y=1): return {'span':[x,y]}
|
|
10
|
+
def color(*v): return {'color':v[0]} if len(v)==1 else {'color':list(v)}
|
|
11
|
+
def font(size): return {'font':['Arial',size]}
|
|
12
|
+
def just(i): return {'justify':{0:'left',1:'center',2:'right'}[i]}
|
|
13
|
+
def slider(minValue,maxValue):
|
|
14
|
+
"""Definition of the GUI element: horizontal slider with flexible range"""
|
|
15
|
+
return {'widget':'hslider','opLimits':[minValue,maxValue],'span':[2,1]}
|
|
16
|
+
|
|
17
|
+
LargeFont = {'color':'light gray', **font(18), 'fgColor':'dark green'}
|
|
18
|
+
ButtonFont = {'font':['Open Sans Extrabold',14]}# Comic Sans MS
|
|
19
|
+
LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
|
|
20
|
+
lColor = color('lightGreen')
|
|
21
|
+
|
|
22
|
+
# definition for plotting cell
|
|
23
|
+
PyPath = 'python -m'
|
|
24
|
+
PaneP2P = ' '.join([f'c{i+1:02d}Mean c{i+1:02d}Peak2Peak' for i in range(1)])
|
|
25
|
+
PaneWF = ' '.join([f'c{i+1:02d}Waveform' for i in range(1)])
|
|
26
|
+
#PaneT = 'timing[1] timing[3]'
|
|
27
|
+
Plot = {'Plot':{'launch':
|
|
28
|
+
f'{PyPath} pvplot Y-5:5 -aV:multiadc0: -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
|
|
29
|
+
**lColor, **ButtonFont}}
|
|
30
|
+
print(f'Plot command: {Plot}')
|
|
31
|
+
#``````````````````PyPage Object``````````````````````````````````````````````
|
|
32
|
+
class PyPage():
|
|
33
|
+
def __init__(self, instance='multiadc0:',
|
|
34
|
+
title="Simulated oscilloscope", channels=6):
|
|
35
|
+
"""instance: unique name of the page.
|
|
36
|
+
For EPICS it is usually device prefix
|
|
37
|
+
"""
|
|
38
|
+
print(f'Instantiating Page {instance,title} with {channels} channels')
|
|
39
|
+
|
|
40
|
+
#``````````Mandatory class members starts here````````````````````````
|
|
41
|
+
self.namespace = 'PVA'
|
|
42
|
+
self.title = title
|
|
43
|
+
|
|
44
|
+
#``````````Page attributes, optional`````````````````````````
|
|
45
|
+
self.page = {**color(240,240,240)}
|
|
46
|
+
#self.page['editable'] = False
|
|
47
|
+
|
|
48
|
+
#``````````Definition of columns`````````````````````````````
|
|
49
|
+
self.columns = {
|
|
50
|
+
1: {'width': 120, 'justify': 'right'},
|
|
51
|
+
2: {'width': 80},
|
|
52
|
+
3: {'width': 80, 'justify': 'right'},
|
|
53
|
+
4: {'width': 80},
|
|
54
|
+
5: {'width': 80, 'justify': 'right'},
|
|
55
|
+
6: {'width': 80},
|
|
56
|
+
7: {'width': 80},
|
|
57
|
+
8: {'width': 80},
|
|
58
|
+
9: {'width': 80},
|
|
59
|
+
}
|
|
60
|
+
"""`````````````````Configuration of rows`````````````````````````````
|
|
61
|
+
A row is a list of comma-separated cell definitions.
|
|
62
|
+
The cell definition is one of the following:
|
|
63
|
+
1)string, 2)device:parameters, 3)dictionary.
|
|
64
|
+
The dictionary is used when the cell requires extra features like color, width,
|
|
65
|
+
description etc. The dictionary is single-entry {key:value}, where the key is a
|
|
66
|
+
string or device:parameter and the value is dictionary of the features.
|
|
67
|
+
"""
|
|
68
|
+
D = instance
|
|
69
|
+
|
|
70
|
+
#``````````Abbreviations, used in cell definitions
|
|
71
|
+
def ChLine(suffix):
|
|
72
|
+
return [f'{D}c{ch+1:02d}{suffix}' for ch in range(channels)]
|
|
73
|
+
#FOption = ' -file '+logreqMap.get(D,'')
|
|
74
|
+
#``````````mandatory member```````````````````````````````````````````
|
|
75
|
+
self.rows = [
|
|
76
|
+
['Device:', D, {D+'version':span(2,1)},_, 'host:', D+'host',_],
|
|
77
|
+
['State:', D+'server','cycle:', D+'cycle',_,_,Plot], # 'Recall:', D+'setup',],
|
|
78
|
+
['Status:', {D+'status': span(8,1)}],
|
|
79
|
+
['Polling Interval:', D+'polling', 'nPoints:', D+'recordLength',
|
|
80
|
+
'Noise:', D+'noiseLevel',_],
|
|
81
|
+
[{'ATTRIBUTES':{**color('lightCyan'),**just(1)}},
|
|
82
|
+
'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
|
|
83
|
+
['V/div:']+ChLine('VoltsPerDiv'),
|
|
84
|
+
['Mean:']+ChLine('Mean'),
|
|
85
|
+
['Peak2Peak:']+ChLine('Peak2Peak'),
|
|
86
|
+
#['Waveform:']+ChLine('Waveform'),
|
|
87
|
+
]
|
|
Binary file
|
|
Binary file
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"""Skeleton and helper functions for creating EPICS PVAccess server"""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= '
|
|
4
|
-
#TODO:
|
|
5
|
-
#TODO: Add performance counters to demo.
|
|
3
|
+
__version__= 'v2.0.1 26-01-30'# added mandatory host PV
|
|
4
|
+
#TODO add mandatory PV: host, to identify the server host.
|
|
6
5
|
#Issue: There is no way in PVAccess to specify if string PV is writable.
|
|
7
6
|
# As a workaround we append description with suffix ' Features: W' to indicate that.
|
|
8
7
|
|
|
9
8
|
import sys
|
|
10
|
-
import time
|
|
11
|
-
from time import perf_counter as timer
|
|
9
|
+
from time import time, sleep, strftime, perf_counter as timer
|
|
12
10
|
import os
|
|
11
|
+
from socket import gethostname
|
|
13
12
|
from p4p.nt import NTScalar, NTEnum
|
|
14
13
|
from p4p.nt.enum import ntenum
|
|
15
14
|
from p4p.server import Server
|
|
@@ -17,6 +16,9 @@ from p4p.server.thread import SharedPV
|
|
|
17
16
|
from p4p.client.thread import Context
|
|
18
17
|
|
|
19
18
|
#``````````````````Module Storage`````````````````````````````````````````````
|
|
19
|
+
def _serverStateChanged(newState:str):
|
|
20
|
+
"""Dummy serverStateChanged function"""
|
|
21
|
+
return
|
|
20
22
|
class C_():
|
|
21
23
|
"""Storage for module members"""
|
|
22
24
|
prefix = ''
|
|
@@ -24,13 +26,16 @@ class C_():
|
|
|
24
26
|
cycle = 0
|
|
25
27
|
serverState = ''
|
|
26
28
|
PVs = {}
|
|
27
|
-
PVDefs = []
|
|
29
|
+
PVDefs = []
|
|
30
|
+
serverStateChanged = _serverStateChanged
|
|
31
|
+
|
|
28
32
|
#```````````````````Helper methods````````````````````````````````````````````
|
|
29
33
|
def serverState():
|
|
30
|
-
"""Return current server state. That is the value of the server PV, but
|
|
34
|
+
"""Return current server state. That is the value of the server PV, but
|
|
35
|
+
cached in C_ to avoid unnecessary get() calls."""
|
|
31
36
|
return C_.serverState
|
|
32
37
|
def _printTime():
|
|
33
|
-
return
|
|
38
|
+
return strftime("%m%d:%H%M%S")
|
|
34
39
|
def printi(msg):
|
|
35
40
|
"""Print info message and publish it to status PV."""
|
|
36
41
|
print(f'inf_@{_printTime()}: {msg}')
|
|
@@ -66,24 +71,31 @@ def pvv(pvName:str):
|
|
|
66
71
|
return pvobj(pvName).current()
|
|
67
72
|
|
|
68
73
|
def publish(pvName:str, value, ifChanged=False, t=None):
|
|
69
|
-
"""Publish value to PV. If ifChanged is True, then publish only if the
|
|
74
|
+
"""Publish value to PV. If ifChanged is True, then publish only if the
|
|
75
|
+
value is different from the current value. If t is not None, then use
|
|
76
|
+
it as timestamp, otherwise use current time."""
|
|
77
|
+
#print(f'Publishing {pvName}')
|
|
70
78
|
try:
|
|
71
79
|
pv = pvobj(pvName)
|
|
72
80
|
except KeyError:
|
|
73
|
-
|
|
81
|
+
print(f'WARNING: PV {pvName} not found. Cannot publish value.')
|
|
74
82
|
return
|
|
75
83
|
if t is None:
|
|
76
|
-
t = time
|
|
84
|
+
t = time()
|
|
77
85
|
if not ifChanged or pv.current() != value:
|
|
78
86
|
pv.post(value, timestamp=t)
|
|
79
87
|
|
|
80
88
|
def SPV(initial, meta='', vtype=None):
|
|
81
89
|
"""Construct SharedPV.
|
|
82
|
-
meta is a string with characters W,A,
|
|
83
|
-
|
|
90
|
+
meta is a string with characters W,R,A,D indicating if the PV is writable,
|
|
91
|
+
has alarm or it is discrete (ENUM).
|
|
92
|
+
vtype should be one of the p4p.nt type definitions
|
|
93
|
+
(see https://epics-base.github.io/p4p/values.html).
|
|
84
94
|
if vtype is None then the nominal type will be determined automatically.
|
|
95
|
+
initial is the initial value of the PV. It can be a single value or
|
|
96
|
+
a list/array of values (for array PVs).
|
|
85
97
|
"""
|
|
86
|
-
typeCode = {
|
|
98
|
+
typeCode = {# mapping from vtype to p4p type code
|
|
87
99
|
's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
|
|
88
100
|
'u64':'L', 'f32':'f', 'f64':'d', str:'s',
|
|
89
101
|
}
|
|
@@ -93,21 +105,29 @@ def SPV(initial, meta='', vtype=None):
|
|
|
93
105
|
itype = type(firstItem)
|
|
94
106
|
vtype = {int: 'i32', float: 'f32'}.get(itype,itype)
|
|
95
107
|
tcode = typeCode[vtype]
|
|
96
|
-
|
|
108
|
+
allowed_chars = 'WRAD'
|
|
109
|
+
discrete = False
|
|
110
|
+
for ch in meta:
|
|
111
|
+
if ch not in allowed_chars:
|
|
112
|
+
printe(f'Unknown meta character {ch} in SPV definition')
|
|
113
|
+
sys.exit(1)
|
|
114
|
+
if 'D' in meta:
|
|
115
|
+
discrete = True
|
|
97
116
|
initial = {'choices': initial, 'index': 0}
|
|
98
117
|
nt = NTEnum(display=True, control='W' in meta)
|
|
99
118
|
else:
|
|
100
119
|
prefix = 'a' if iterable else ''
|
|
101
|
-
nt = NTScalar(prefix+tcode, display=True, control='W' in meta,
|
|
120
|
+
nt = NTScalar(prefix+tcode, display=True, control='W' in meta,
|
|
121
|
+
valueAlarm='A' in meta)
|
|
102
122
|
pv = SharedPV(nt=nt, initial=initial)
|
|
123
|
+
# add new attributes.
|
|
103
124
|
pv.writable = 'W' in meta
|
|
125
|
+
pv.discrete = discrete
|
|
104
126
|
return pv
|
|
105
127
|
|
|
106
128
|
#``````````````````create_PVs()```````````````````````````````````````````````
|
|
107
129
|
def _create_PVs(pvDefs):
|
|
108
|
-
|
|
109
|
-
[pvname, description, SPV object, extra], where extra is a dictionary of extra parameters, like setter, units, limits etc. Setter is a function, that will be called when"""
|
|
110
|
-
ts = time.time()
|
|
130
|
+
ts = time()
|
|
111
131
|
for defs in pvDefs:
|
|
112
132
|
try:
|
|
113
133
|
pname,desc,spv,extra = defs
|
|
@@ -115,15 +135,22 @@ def _create_PVs(pvDefs):
|
|
|
115
135
|
printe(f'Invalid PV definition of {defs[0]}')
|
|
116
136
|
sys.exit(1)
|
|
117
137
|
ivalue = spv.current()
|
|
118
|
-
printv(f'created pv {pname}, initial: {type(ivalue),ivalue},
|
|
138
|
+
printv((f'created pv {pname}, initial: {type(ivalue),ivalue},'
|
|
139
|
+
f'extra: {extra}'))
|
|
140
|
+
key = C_.prefix + pname
|
|
141
|
+
if key in C_.PVs:
|
|
142
|
+
printe(f'Duplicate PV name: {pname}')
|
|
143
|
+
sys.exit(1)
|
|
119
144
|
C_.PVs[C_.prefix+pname] = spv
|
|
120
145
|
v = spv._wrap(ivalue, timestamp=ts)
|
|
121
146
|
if spv.writable:
|
|
122
147
|
try:
|
|
123
|
-
# To indicate that the PV is writable, set control limits to
|
|
148
|
+
# To indicate that the PV is writable, set control limits to
|
|
149
|
+
# (0,0). Not very elegant, but it works for numerics and enums,
|
|
150
|
+
# not for strings.
|
|
124
151
|
v['control.limitLow'] = 0
|
|
125
152
|
v['control.limitHigh'] = 0
|
|
126
|
-
except KeyError
|
|
153
|
+
except KeyError:
|
|
127
154
|
#print(f'control not set for {pname}: {e}')
|
|
128
155
|
pass
|
|
129
156
|
if 'ntenum' in str(type(ivalue)):
|
|
@@ -140,18 +167,19 @@ def _create_PVs(pvDefs):
|
|
|
140
167
|
v[f'valueAlarm.{key}'] = value
|
|
141
168
|
spv.post(v)
|
|
142
169
|
|
|
143
|
-
# add new attributes.
|
|
170
|
+
# add new attributes.
|
|
144
171
|
spv.name = pname
|
|
145
172
|
spv.setter = extra.get('setter')
|
|
146
173
|
|
|
147
174
|
if spv.writable:
|
|
148
175
|
@spv.put
|
|
149
176
|
def handle(spv, op):
|
|
150
|
-
ct = time
|
|
177
|
+
ct = time()
|
|
151
178
|
vv = op.value()
|
|
152
179
|
vr = vv.raw.value
|
|
153
180
|
current = spv._wrap(spv.current())
|
|
154
|
-
# check limits, if they are defined. That will be a good
|
|
181
|
+
# check limits, if they are defined. That will be a good
|
|
182
|
+
# example of using control structure and valueAlarm.
|
|
155
183
|
try:
|
|
156
184
|
limitLow = current['control.limitLow']
|
|
157
185
|
limitHigh = current['control.limitHigh']
|
|
@@ -162,65 +190,80 @@ def _create_PVs(pvDefs):
|
|
|
162
190
|
except KeyError:
|
|
163
191
|
pass
|
|
164
192
|
if isinstance(vv, ntenum):
|
|
165
|
-
vr = vv
|
|
193
|
+
vr = str(vv)
|
|
166
194
|
if spv.setter:
|
|
167
|
-
spv.setter(vr)
|
|
195
|
+
spv.setter(vr, spv)
|
|
168
196
|
# value will be updated by the setter, so get it again
|
|
169
197
|
vr = pvv(spv.name)
|
|
170
198
|
printv(f'putting {spv.name} = {vr}')
|
|
171
199
|
spv.post(vr, timestamp=ct) # update subscribers
|
|
172
200
|
op.done()
|
|
173
|
-
#print(f'PV {pv.name} created: {spv}')
|
|
174
201
|
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
175
202
|
#``````````````````Setters
|
|
176
|
-
def
|
|
203
|
+
def set_verbose(level, *_):
|
|
177
204
|
"""Set verbosity level for debugging"""
|
|
178
205
|
C_.verbose = level
|
|
179
|
-
|
|
206
|
+
printi(f'Setting verbose to {level}')
|
|
207
|
+
publish('verbose',level)
|
|
180
208
|
|
|
181
|
-
def set_server(
|
|
182
|
-
"""Example of the setter for the server PV.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
def set_server(servState, *_):
|
|
210
|
+
"""Example of the setter for the server PV.
|
|
211
|
+
servState can be 'Start', 'Stop', 'Exit' or 'Clear'. If servState is None,
|
|
212
|
+
then get the desired state from the server PV."""
|
|
213
|
+
#printv(f'>set_server({servState}), {type(servState)}')
|
|
214
|
+
if servState is None:
|
|
215
|
+
servState = pvv('server')
|
|
216
|
+
printi(f'Setting server state to {servState}')
|
|
217
|
+
servState = str(servState)
|
|
218
|
+
C_.serverStateChanged(servState)
|
|
219
|
+
if servState == 'Start':
|
|
189
220
|
printi('Starting the server')
|
|
190
|
-
# configure_instrument()
|
|
191
|
-
# adopt_local_setting()
|
|
192
221
|
publish('server','Started')
|
|
193
222
|
publish('status','Started')
|
|
194
|
-
elif
|
|
223
|
+
elif servState == 'Stop':
|
|
195
224
|
printi('server stopped')
|
|
196
225
|
publish('server','Stopped')
|
|
197
226
|
publish('status','Stopped')
|
|
198
|
-
elif
|
|
227
|
+
elif servState == 'Exit':
|
|
199
228
|
printi('server is exiting')
|
|
200
229
|
publish('server','Exited')
|
|
201
230
|
publish('status','Exited')
|
|
202
|
-
elif
|
|
203
|
-
publish('acqCount', 0)
|
|
231
|
+
elif servState == 'Clear':
|
|
204
232
|
publish('status','Cleared')
|
|
205
|
-
# set server to previous
|
|
233
|
+
# set server to previous servState
|
|
206
234
|
set_server(C_.serverState)
|
|
207
|
-
|
|
235
|
+
return
|
|
236
|
+
C_.serverState = servState
|
|
208
237
|
|
|
209
238
|
def create_PVs(pvDefs=None):
|
|
210
|
-
"""Creates manadatory PVs and adds PVs specified in pvDefs list
|
|
239
|
+
"""Creates manadatory PVs and adds PVs specified in pvDefs list.
|
|
240
|
+
Returns dictionary of created PVs.
|
|
241
|
+
Each definition is a list of the form:
|
|
242
|
+
[pvname, description, SPV object, extra], where extra is a dictionary of
|
|
243
|
+
extra parameters.
|
|
244
|
+
Extra parameters can include:
|
|
245
|
+
'setter' : function to be called on put
|
|
246
|
+
'units' : string with units
|
|
247
|
+
'limitLow' : low control limit
|
|
248
|
+
'limitHigh' : high control limit
|
|
249
|
+
'format' : format string
|
|
250
|
+
'valueAlarm': dictionary with valueAlarm parameters, like
|
|
251
|
+
'lowAlarmLimit', 'highAlarmLimit', etc."""
|
|
211
252
|
U,LL,LH = 'units','limitLow','limitHigh'
|
|
212
253
|
C_.PVDefs = [
|
|
254
|
+
['host', 'Server host name', SPV(gethostname()), {}],
|
|
213
255
|
['version', 'Program version', SPV(__version__), {}],
|
|
214
|
-
['status', 'Server status. Features: RWE', SPV('
|
|
215
|
-
['server', 'Server control',
|
|
216
|
-
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), '
|
|
256
|
+
['status', 'Server status. Features: RWE', SPV('','W'), {}],
|
|
257
|
+
['server', 'Server control',
|
|
258
|
+
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WD'),
|
|
217
259
|
{'setter':set_server}],
|
|
218
|
-
['
|
|
219
|
-
{'setter':
|
|
260
|
+
['verbose', 'Debugging verbosity', SPV(C_.verbose,'W','u8'),
|
|
261
|
+
{'setter':set_verbose, LL:0,LH:3}],
|
|
220
262
|
['polling', 'Polling interval', SPV(1.0,'W'), {U:'S', LL:0.001, LH:10.1}],
|
|
221
263
|
['cycle', 'Cycle number', SPV(0,'','u32'), {}],
|
|
222
264
|
]
|
|
223
|
-
# append application's PVs, defined in the pvDefs and create map of
|
|
265
|
+
# append application's PVs, defined in the pvDefs and create map of
|
|
266
|
+
# providers
|
|
224
267
|
if pvDefs is not None:
|
|
225
268
|
C_.PVDefs += pvDefs
|
|
226
269
|
_create_PVs(C_.PVDefs)
|
|
@@ -232,20 +275,35 @@ def get_externalPV(pvName:str, timeout=0.5):
|
|
|
232
275
|
ctxt = Context('pva')
|
|
233
276
|
return ctxt.get(pvName, timeout=timeout)
|
|
234
277
|
|
|
235
|
-
def init_epicsdev(prefix:str, pvDefs:list,
|
|
278
|
+
def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
|
|
279
|
+
serverStateChanged=None, listDir=None):
|
|
236
280
|
"""Check if no other server is running with the same prefix.
|
|
237
281
|
Create PVs and return them as a dictionary.
|
|
282
|
+
prefix is a string to be prepended to all PV names.
|
|
283
|
+
pvDefs is a list of PV definitions (see create_PVs()).
|
|
284
|
+
verbose is the verbosity level for debug messages.
|
|
285
|
+
serverStateChanged is a function to be called when the server PV changes.
|
|
286
|
+
The function should have the signature:
|
|
287
|
+
def serverStateChanged(newStatus:str):
|
|
288
|
+
If serverStateChanged is None, then a dummy function is used.
|
|
238
289
|
The listDir is a directory to save list of all generated PVs,
|
|
239
290
|
if no directory is given, then </tmp/pvlist/><prefix> is assumed.
|
|
240
291
|
"""
|
|
292
|
+
if not isinstance(verbose, int) or verbose < 0:
|
|
293
|
+
printe('init_epicsdev arguments should be (prefix:str, pvDefs:list, verbose:int, listDir:str)')
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
printi(f'Initializing epicsdev with prefix {prefix}')
|
|
241
296
|
C_.prefix = prefix
|
|
242
297
|
C_.verbose = verbose
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
298
|
+
if serverStateChanged is not None:# set custom serverStateChanged function
|
|
299
|
+
C_.serverStateChanged = serverStateChanged
|
|
300
|
+
try: # check if server is already running
|
|
301
|
+
host = repr(get_externalPV(prefix+'host')).replace("'",'')
|
|
302
|
+
print(f'ERROR: Server for {prefix} already running at {host}. Exiting.')
|
|
246
303
|
sys.exit(1)
|
|
247
|
-
except TimeoutError:
|
|
248
|
-
|
|
304
|
+
except TimeoutError: pass
|
|
305
|
+
|
|
306
|
+
# No existing server found. Creating PVs.
|
|
249
307
|
pvs = create_PVs(pvDefs)
|
|
250
308
|
# Save list of PVs to a file, if requested
|
|
251
309
|
if listDir != '':
|
|
@@ -261,7 +319,6 @@ def init_epicsdev(prefix:str, pvDefs:list, listDir:str, verbose:str=0):
|
|
|
261
319
|
|
|
262
320
|
#``````````````````Demo````````````````````````````````````````````````````````
|
|
263
321
|
if __name__ == "__main__":
|
|
264
|
-
print(f'epicsdev multiadc demo server {__version__}')
|
|
265
322
|
import numpy as np
|
|
266
323
|
import argparse
|
|
267
324
|
|
|
@@ -286,19 +343,22 @@ if __name__ == "__main__":
|
|
|
286
343
|
rng = np.random.default_rng(nPatterns)
|
|
287
344
|
nPoints = 100
|
|
288
345
|
|
|
289
|
-
def set_recordLength(value):
|
|
290
|
-
"""Record length have changed. The tAxis should be updated
|
|
346
|
+
def set_recordLength(value, *_):
|
|
347
|
+
"""Record length have changed. The tAxis should be updated
|
|
348
|
+
accordingly."""
|
|
291
349
|
printi(f'Setting tAxis to {value}')
|
|
292
350
|
publish('tAxis', np.arange(value)*1.E-6)
|
|
293
351
|
publish('recordLength', value)
|
|
294
|
-
|
|
352
|
+
# Re-initialize noise array, because its size depends on recordLength
|
|
353
|
+
set_noise(pvv('noiseLevel'))
|
|
295
354
|
|
|
296
|
-
def set_noise(level):
|
|
355
|
+
def set_noise(level, *_):
|
|
297
356
|
"""Noise level have changed. Update noise array."""
|
|
298
357
|
v = float(level)
|
|
299
358
|
recordLength = pvv('recordLength')
|
|
300
359
|
ts = timer()
|
|
301
|
-
pargs.noise = np.random.normal(scale=0.5*level,
|
|
360
|
+
pargs.noise = np.random.normal(scale=0.5*level,
|
|
361
|
+
size=recordLength+nPatterns)# 45ms/1e6 points
|
|
302
362
|
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
303
363
|
publish('noiseLevel', level)
|
|
304
364
|
|
|
@@ -325,10 +385,13 @@ if __name__ == "__main__":
|
|
|
325
385
|
parser = argparse.ArgumentParser(description = __doc__,
|
|
326
386
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
327
387
|
epilog=f'{__version__}')
|
|
328
|
-
parser.add_argument('-
|
|
329
|
-
'
|
|
330
|
-
parser.add_argument('-
|
|
331
|
-
'
|
|
388
|
+
parser.add_argument('-d', '--device', default='epicsDev', help=
|
|
389
|
+
'Device name, the PV name will be <device><index>:')
|
|
390
|
+
parser.add_argument('-i', '--index', default='0', help=
|
|
391
|
+
'Device index, the PV name will be <device><index>:')
|
|
392
|
+
parser.add_argument('-l', '--list', default='', nargs='?', help=(
|
|
393
|
+
'Directory to save list of all generated PVs, if no directory is given, '
|
|
394
|
+
'then </tmp/pvlist/><prefix> is assumed.'))
|
|
332
395
|
# The rest of options are not essential, they can be controlled at runtime using PVs.
|
|
333
396
|
parser.add_argument('-n', '--npoints', type=int, default=nPoints, help=
|
|
334
397
|
'Number of points in the waveform')
|
|
@@ -338,9 +401,11 @@ if __name__ == "__main__":
|
|
|
338
401
|
print(pargs)
|
|
339
402
|
|
|
340
403
|
# Initialize epicsdev and PVs
|
|
341
|
-
|
|
404
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
405
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose, None, pargs.list)
|
|
342
406
|
|
|
343
|
-
# Initialize the device
|
|
407
|
+
# Initialize the device using pargs if needed. That can be used to set
|
|
408
|
+
# the number of points in the waveform, for example.
|
|
344
409
|
init(pargs.npoints)
|
|
345
410
|
|
|
346
411
|
# Start the Server. Use your set_server, if needed.
|
|
@@ -355,5 +420,5 @@ if __name__ == "__main__":
|
|
|
355
420
|
break
|
|
356
421
|
if not state.startswith('Stop'):
|
|
357
422
|
poll()
|
|
358
|
-
|
|
423
|
+
sleep(pvv("polling"))
|
|
359
424
|
printi('Server is exited')
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Simulated multi-channel ADC device server using epicsdev module."""
|
|
2
|
+
# pylint: disable=invalid-name
|
|
3
|
+
__version__= 'v0.0.2 26-01-23'# refactored, adjusted for epicdev 2.0.1
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from time import perf_counter as timer
|
|
8
|
+
import numpy as np
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
from .epicsdev import Server, Context, init_epicsdev, serverState, publish
|
|
12
|
+
from .epicsdev import pvv, printi, printv, SPV, set_server
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def myPVDefs():
|
|
16
|
+
"""Example of PV definitions"""
|
|
17
|
+
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
18
|
+
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
19
|
+
pvDefs = [ # device-specific PVs
|
|
20
|
+
['externalControl', 'Name of external PV, which controls the server',
|
|
21
|
+
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WD'), {}],
|
|
22
|
+
['noiseLevel', 'Noise amplitude', SPV(1.E-4,'W'), {SET:set_noise, U:'V'}],
|
|
23
|
+
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
24
|
+
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
25
|
+
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
26
|
+
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Templates for channel-related PVs. Important: SPV cannot be used in this list!
|
|
30
|
+
ChannelTemplates = [
|
|
31
|
+
['c0$VoltsPerDiv', 'Vertical scale', (1E-3,'W'), {U:'V/du'}],
|
|
32
|
+
#['c0$VoltOffset', 'Vertical offset', (1E-3,), {U:'V/du'}],
|
|
33
|
+
['c0$Waveform', 'Waveform array', ([0.],), {U:'du'}],
|
|
34
|
+
['c0$Mean', 'Mean of the waveform', (0.,'A'), {U:'du'}],
|
|
35
|
+
['c0$Peak2Peak','Peak-to-peak amplitude', (0.,'A'), {U:'du',**alarm}],
|
|
36
|
+
]
|
|
37
|
+
# extend PvDefs with channel-related PVs
|
|
38
|
+
for ch in range(pargs.channels):
|
|
39
|
+
for pvdef in ChannelTemplates:
|
|
40
|
+
newpvdef = pvdef.copy()
|
|
41
|
+
newpvdef[0] = pvdef[0].replace('0$',f'{ch+1:02}')
|
|
42
|
+
newpvdef[2] = SPV(*pvdef[2])
|
|
43
|
+
pvDefs.append(newpvdef)
|
|
44
|
+
return pvDefs
|
|
45
|
+
|
|
46
|
+
#``````````````````Module constants
|
|
47
|
+
nPatterns = 100 # number of waveform patterns.
|
|
48
|
+
rng = np.random.default_rng(nPatterns)
|
|
49
|
+
|
|
50
|
+
#``````````````````Setter functions for PVs```````````````````````````````````
|
|
51
|
+
def set_recordLength(value):
|
|
52
|
+
"""Record length have changed. The tAxis should be updated accordingly."""
|
|
53
|
+
printi(f'Setting tAxis to {value}')
|
|
54
|
+
publish('tAxis', np.arange(value)*1.E-6)
|
|
55
|
+
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
|
+
|
|
65
|
+
pargs.noise = np.random.normal(scale=0.5*level, size=recordLength+nPatterns)# 45ms/1e6 points
|
|
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):
|
|
70
|
+
"""External control PV have changed. Control the server accordingly."""
|
|
71
|
+
pvname = str(value)
|
|
72
|
+
if pvname in (None,'0'):
|
|
73
|
+
print('External control is not activated.')
|
|
74
|
+
return
|
|
75
|
+
printi(f'External control PV: {pvname}')
|
|
76
|
+
ctxt = Context('pva')
|
|
77
|
+
try:
|
|
78
|
+
r = ctxt.get(pvname, timeout=0.5)
|
|
79
|
+
except TimeoutError:
|
|
80
|
+
printi(f'Cannot connect to external control PV {pvname}.')
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
83
|
+
def serverStateChanged(newState:str):
|
|
84
|
+
"""Start device function called when server is started"""
|
|
85
|
+
if newState == 'Start':
|
|
86
|
+
printi('start_device called')
|
|
87
|
+
elif newState == 'Stop':
|
|
88
|
+
printi('stop_device called')
|
|
89
|
+
elif newState == 'Clear':
|
|
90
|
+
printi('clear_device called')
|
|
91
|
+
publish('cycle', 0)
|
|
92
|
+
|
|
93
|
+
def init(recordLength):
|
|
94
|
+
"""Testing function. Do not use in production code."""
|
|
95
|
+
set_recordLength(recordLength)
|
|
96
|
+
#set_externalControl(pargs.prefix + pargs.external)
|
|
97
|
+
|
|
98
|
+
def poll():
|
|
99
|
+
"""Example of polling function"""
|
|
100
|
+
#pattern = C_.cycle % nPatterns# produces sliding
|
|
101
|
+
cycle = pvv('cycle')
|
|
102
|
+
printv(f'cycle {repr(cycle)}')
|
|
103
|
+
publish('cycle', cycle + 1)
|
|
104
|
+
for ch in range(pargs.channels):
|
|
105
|
+
pattern = rng.integers(0, nPatterns)
|
|
106
|
+
chstr = f'c{ch+1:02}'
|
|
107
|
+
wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
|
|
108
|
+
#print(f'ch{ch}, {pattern}: {wf[0], wf.sum(), wf.mean(), np.mean(wf)}')
|
|
109
|
+
wf /= pvv(f'{chstr}VoltsPerDiv')
|
|
110
|
+
#wf += pvv(f'{chstr}Offset')
|
|
111
|
+
wf += ch
|
|
112
|
+
publish(f'{chstr}Waveform', list(wf))
|
|
113
|
+
publish(f'{chstr}Peak2Peak', np.ptp(wf))
|
|
114
|
+
publish(f'{chstr}Mean', np.mean(wf))
|
|
115
|
+
|
|
116
|
+
# Argument parsing
|
|
117
|
+
parser = argparse.ArgumentParser(description = __doc__,
|
|
118
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
119
|
+
epilog=f'{__version__}')
|
|
120
|
+
parser.add_argument('-c', '--channels', type=int, default=6, help=
|
|
121
|
+
'Number of channels per device')
|
|
122
|
+
parser.add_argument('-e', '--external', help=
|
|
123
|
+
'Name of external PV, which controls the server, if 0 then it will be <device>0:')
|
|
124
|
+
parser.add_argument('-l', '--list', default=None, nargs='?', help=
|
|
125
|
+
'Directory to save list of all generated PVs, if None, then </tmp/pvlist/><prefix> is assumed.')
|
|
126
|
+
parser.add_argument('-d', '--device', default='multiadc', help=
|
|
127
|
+
'Device name, the PV name will be <device><index>:')
|
|
128
|
+
parser.add_argument('-i', '--index', default='0', help=
|
|
129
|
+
'Device index, the PV name will be <device><index>:')
|
|
130
|
+
# The rest of arguments are not essential, they can be changed at runtime using PVs.
|
|
131
|
+
parser.add_argument('-n', '--npoints', type=int, default=100, help=
|
|
132
|
+
'Number of points in the waveform')
|
|
133
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
134
|
+
'Show more log messages (-vv: show even more)')
|
|
135
|
+
pargs = parser.parse_args()
|
|
136
|
+
print(f'pargs: {pargs}')
|
|
137
|
+
|
|
138
|
+
# Initialize epicsdev and PVs
|
|
139
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
140
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose,
|
|
141
|
+
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
|
+
|
|
148
|
+
# Initialize the device, using pargs if needed.
|
|
149
|
+
# That can be used to set the number of points in the waveform, for example.
|
|
150
|
+
init(pargs.npoints)
|
|
151
|
+
|
|
152
|
+
# Start the Server. Use your set_server, if needed.
|
|
153
|
+
set_server('Start')
|
|
154
|
+
|
|
155
|
+
#``````````````````Main loop``````````````````````````````````````````````````
|
|
156
|
+
server = Server(providers=[PVs])
|
|
157
|
+
printi(f'Server started with polling interval {repr(pvv("polling"))} S.')
|
|
158
|
+
while True:
|
|
159
|
+
state = serverState()
|
|
160
|
+
if state.startswith('Exit'):
|
|
161
|
+
break
|
|
162
|
+
if not state.startswith('Stop'):
|
|
163
|
+
poll()
|
|
164
|
+
time.sleep(pvv("polling"))
|
|
165
|
+
printi('Server is exited')
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Simulated multi-channel ADC device server using epicsdev module."""
|
|
2
|
+
# pylint: disable=invalid-name
|
|
3
|
+
__version__= 'v0.0.1 26-01-18'#
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from copy import copy
|
|
8
|
+
from time import perf_counter as timer
|
|
9
|
+
from .epicsdev import Server, Context, init_epicsdev, serverState, publish
|
|
10
|
+
from .epicsdev import pvv, printi, printv, SPV, set_server
|
|
11
|
+
|
|
12
|
+
if True: # to enable code folding in some editors
|
|
13
|
+
import numpy as np
|
|
14
|
+
import argparse
|
|
15
|
+
|
|
16
|
+
def myPVDefs():
|
|
17
|
+
"""Example of PV definitions"""
|
|
18
|
+
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
19
|
+
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
20
|
+
pvDefs = [ # device-specific PVs
|
|
21
|
+
['externalControl', 'Name of external PV, which controls the server',
|
|
22
|
+
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WE'), {}],
|
|
23
|
+
['noiseLevel', 'Noise amplitude', SPV(1.E-4,'W'), {SET:set_noise, U:'V'}],
|
|
24
|
+
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
25
|
+
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
26
|
+
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
27
|
+
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Templates for channel-related PVs. Important: SPV cannot be used in this list!
|
|
31
|
+
ChannelTemplates = [
|
|
32
|
+
['c0$VoltsPerDiv', 'Vertical scale', (1E-3,'W'), {U:'V/du'}],
|
|
33
|
+
#['c0$VoltOffset', 'Vertical offset', (1E-3,), {U:'V/du'}],
|
|
34
|
+
['c0$Waveform', 'Waveform array', ([0.],), {U:'du'}],
|
|
35
|
+
['c0$Mean', 'Mean of the waveform', (0.,'A'), {U:'du'}],
|
|
36
|
+
['c0$Peak2Peak','Peak-to-peak amplitude', (0.,'A'), {U:'du',**alarm}],
|
|
37
|
+
]
|
|
38
|
+
# extend PvDefs with channel-related PVs
|
|
39
|
+
for ch in range(pargs.channels):
|
|
40
|
+
for pvdef in ChannelTemplates:
|
|
41
|
+
newpvdef = pvdef.copy()
|
|
42
|
+
newpvdef[0] = pvdef[0].replace('0$',f'{ch+1:02}')
|
|
43
|
+
newpvdef[2] = SPV(*pvdef[2])
|
|
44
|
+
pvDefs.append(newpvdef)
|
|
45
|
+
return pvDefs
|
|
46
|
+
|
|
47
|
+
nPatterns = 100 # number of waveform patterns.
|
|
48
|
+
rng = np.random.default_rng(nPatterns)
|
|
49
|
+
|
|
50
|
+
def set_recordLength(value):
|
|
51
|
+
"""Record length have changed. The tAxis should be updated accordingly."""
|
|
52
|
+
printi(f'Setting tAxis to {value}')
|
|
53
|
+
publish('tAxis', np.arange(value)*1.E-6)
|
|
54
|
+
publish('recordLength', value)
|
|
55
|
+
set_noise(pvv('noiseLevel')) # Re-initialize noise array, because its size depends on recordLength
|
|
56
|
+
|
|
57
|
+
def set_noise(level):
|
|
58
|
+
"""Noise level have changed. Update noise array."""
|
|
59
|
+
v = float(level)
|
|
60
|
+
recordLength = pvv('recordLength')
|
|
61
|
+
ts = timer()
|
|
62
|
+
|
|
63
|
+
pargs.noise = np.random.normal(scale=0.5*level, size=recordLength+nPatterns)# 45ms/1e6 points
|
|
64
|
+
printi(f'Noise array[{len(pargs.noise)}] updated with level {v:.4g} V. in {timer()-ts:.4g} S.')
|
|
65
|
+
publish('noiseLevel', level)
|
|
66
|
+
|
|
67
|
+
def set_externalControl(value):
|
|
68
|
+
"""External control PV have changed. Control the server accordingly."""
|
|
69
|
+
pvname = str(value)
|
|
70
|
+
if pvname in (None,'0'):
|
|
71
|
+
print('External control is not activated.')
|
|
72
|
+
return
|
|
73
|
+
printi(f'External control PV: {pvname}')
|
|
74
|
+
ctxt = Context('pva')
|
|
75
|
+
try:
|
|
76
|
+
r = ctxt.get(pvname, timeout=0.5)
|
|
77
|
+
except TimeoutError:
|
|
78
|
+
printi(f'Cannot connect to external control PV {pvname}.')
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
|
|
81
|
+
def init(recordLength):
|
|
82
|
+
"""Testing function. Do not use in production code."""
|
|
83
|
+
set_recordLength(recordLength)
|
|
84
|
+
#set_externalControl(pargs.prefix + pargs.external)
|
|
85
|
+
|
|
86
|
+
def poll():
|
|
87
|
+
"""Example of polling function"""
|
|
88
|
+
#pattern = C_.cycle % nPatterns# produces sliding
|
|
89
|
+
cycle = pvv('cycle')
|
|
90
|
+
printv(f'cycle {repr(cycle)}')
|
|
91
|
+
publish('cycle', cycle + 1)
|
|
92
|
+
for ch in range(pargs.channels):
|
|
93
|
+
pattern = rng.integers(0, nPatterns)
|
|
94
|
+
chstr = f'c{ch+1:02}'
|
|
95
|
+
wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
|
|
96
|
+
#print(f'ch{ch}, {pattern}: {wf[0], wf.sum(), wf.mean(), np.mean(wf)}')
|
|
97
|
+
wf /= pvv(f'{chstr}VoltsPerDiv')
|
|
98
|
+
#wf += pvv(f'{chstr}Offset')
|
|
99
|
+
wf += ch
|
|
100
|
+
publish(f'{chstr}Waveform', list(wf))
|
|
101
|
+
publish(f'{chstr}Peak2Peak', np.ptp(wf))
|
|
102
|
+
publish(f'{chstr}Mean', np.mean(wf))
|
|
103
|
+
|
|
104
|
+
# Argument parsing
|
|
105
|
+
parser = argparse.ArgumentParser(description = __doc__,
|
|
106
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
107
|
+
epilog=f'{__version__}')
|
|
108
|
+
parser.add_argument('-c', '--channels', type=int, default=6, help=
|
|
109
|
+
'Number of channels per device')
|
|
110
|
+
parser.add_argument('-e', '--external', help=
|
|
111
|
+
'Name of external PV, which controls the server, if 0 then it will be <device>0:')
|
|
112
|
+
parser.add_argument('-l', '--list', default=None, nargs='?', help=
|
|
113
|
+
'Directory to save list of all generated PVs, if None, then </tmp/pvlist/><prefix> is assumed.')
|
|
114
|
+
parser.add_argument('-d', '--device', default='multiadc', help=
|
|
115
|
+
'Device name, the PV name will be <device><index>:')
|
|
116
|
+
parser.add_argument('-i', '--index', default='0', help=
|
|
117
|
+
'Device index, the PV name will be <device><index>:')
|
|
118
|
+
# The rest of arguments are not essential, they can be changed at runtime using PVs.
|
|
119
|
+
parser.add_argument('-n', '--npoints', type=int, default=100, help=
|
|
120
|
+
'Number of points in the waveform')
|
|
121
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
122
|
+
'Show more log messages (-vv: show even more)')
|
|
123
|
+
pargs = parser.parse_args()
|
|
124
|
+
print(f'pargs: {pargs}')
|
|
125
|
+
|
|
126
|
+
# Initialize epicsdev and PVs
|
|
127
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
128
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.list, pargs.verbose)
|
|
129
|
+
# if pargs.list != '':
|
|
130
|
+
# print('List of PVs:')
|
|
131
|
+
# for _pvname in PVs:
|
|
132
|
+
# print(_pvname)
|
|
133
|
+
printi(f'Hosting {len(PVs)} PVs')
|
|
134
|
+
|
|
135
|
+
# Initialize the device, using pargs if needed. That can be used to set the number of points in the waveform, for example.
|
|
136
|
+
init(pargs.npoints)
|
|
137
|
+
|
|
138
|
+
# Start the Server. Use your set_server, if needed.
|
|
139
|
+
set_server('Start')
|
|
140
|
+
|
|
141
|
+
# Main loop
|
|
142
|
+
server = Server(providers=[PVs])
|
|
143
|
+
printi(f'Server started with polling interval {repr(pvv("polling"))} S.')
|
|
144
|
+
while True:
|
|
145
|
+
state = serverState()
|
|
146
|
+
if state.startswith('Exit'):
|
|
147
|
+
break
|
|
148
|
+
if not state.startswith('Stop'):
|
|
149
|
+
poll()
|
|
150
|
+
time.sleep(pvv("polling"))
|
|
151
|
+
printi('Server is exited')
|
epicsdev-1.0.2/README.md
DELETED
|
@@ -1,16 +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 -l
|
|
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
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|