epicsdev 1.0.1__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.1 → epicsdev-2.0.1}/PKG-INFO +16 -2
- epicsdev-2.0.1/README.md +30 -0
- {epicsdev-1.0.1 → epicsdev-2.0.1}/config/epicsdev_pp.py +14 -43
- 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.1 → epicsdev-2.0.1}/epicsdev/epicsdev.py +183 -96
- epicsdev-2.0.1/epicsdev/multiadc.py +165 -0
- epicsdev-2.0.1/fallback/multiadc.py +151 -0
- {epicsdev-1.0.1 → epicsdev-2.0.1}/pyproject.toml +1 -1
- epicsdev-1.0.1/README.md +0 -16
- {epicsdev-1.0.1 → epicsdev-2.0.1}/LICENSE +0 -0
- {epicsdev-1.0.1 → epicsdev-2.0.1}/config/epicsSimscope_pp.py +0 -0
- {epicsdev-1.0.1 → 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
|
+
|
|
@@ -16,18 +16,17 @@ def slider(minValue,maxValue):
|
|
|
16
16
|
|
|
17
17
|
LargeFont = {'color':'light gray', **font(18), 'fgColor':'dark green'}
|
|
18
18
|
ButtonFont = {'font':['Open Sans Extrabold',14]}# Comic Sans MS
|
|
19
|
-
# Attributes for gray row, it should be in the first cell:
|
|
20
|
-
#GrayRow = {'ATTRIBUTES':{'color':'light gray', **font(12)}}
|
|
21
19
|
LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
|
|
22
20
|
lColor = color('lightGreen')
|
|
23
21
|
|
|
24
22
|
# definition for plotting cell
|
|
25
23
|
PyPath = 'python -m'
|
|
26
|
-
PaneP2P = ' '.join([f'ch{i+1:01d}Mean' for i in range(1)])
|
|
24
|
+
PaneP2P = ' '.join([f'ch{i+1:01d}Mean ch{i+1:01d}Peak2Peak' for i in range(1)])
|
|
27
25
|
PaneWF = ' '.join([f'ch{i+1:01d}Waveform' for i in range(1)])
|
|
28
26
|
#PaneT = 'timing[1] timing[3]'
|
|
29
|
-
Plot = {'Plot':{'launch':
|
|
30
|
-
|
|
27
|
+
Plot = {'Plot':{'launch':
|
|
28
|
+
f'{PyPath} pvplot Y-5:5 -aV:epicsDev0: -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
|
|
29
|
+
**lColor, **ButtonFont}}
|
|
31
30
|
print(f'Plot command: {Plot}')
|
|
32
31
|
#``````````````````PyPage Object``````````````````````````````````````````````
|
|
33
32
|
class PyPage():
|
|
@@ -43,16 +42,16 @@ class PyPage():
|
|
|
43
42
|
self.title = title
|
|
44
43
|
|
|
45
44
|
#``````````Page attributes, optional`````````````````````````
|
|
46
|
-
self.page = {**color(240,240,240)}
|
|
45
|
+
self.page = {**color(240,240,240)}
|
|
47
46
|
#self.page['editable'] = False
|
|
48
47
|
|
|
49
48
|
#``````````Definition of columns`````````````````````````````
|
|
50
49
|
self.columns = {
|
|
51
50
|
1: {'width': 120, 'justify': 'right'},
|
|
52
51
|
2: {'width': 80},
|
|
53
|
-
3: {'width': 80},
|
|
52
|
+
3: {'width': 80, 'justify': 'right'},
|
|
54
53
|
4: {'width': 80},
|
|
55
|
-
5: {'width': 80},
|
|
54
|
+
5: {'width': 80, 'justify': 'right'},
|
|
56
55
|
6: {'width': 80},
|
|
57
56
|
7: {'width': 80},
|
|
58
57
|
8: {'width': 80},
|
|
@@ -74,42 +73,14 @@ string or device:parameter and the value is dictionary of the features.
|
|
|
74
73
|
#FOption = ' -file '+logreqMap.get(D,'')
|
|
75
74
|
#``````````mandatory member```````````````````````````````````````````
|
|
76
75
|
self.rows = [
|
|
77
|
-
['Device:', D, {D+'version':span(2,1)}
|
|
78
|
-
['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',],
|
|
79
78
|
['Status:', {D+'status': span(8,1)}],
|
|
80
|
-
['Polling Interval:', D+'polling','
|
|
81
|
-
'
|
|
82
|
-
['
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# ['Horizontal scale:', D+'timePerDiv', ' samples:', D+'recLength',
|
|
86
|
-
# 'SamplRate:', {D+'samplingRate':span(2,1)},_],
|
|
87
|
-
# #['Trigger:', D+'trigSourceS', D+'trigCouplingS', D+'trigSlopeS', 'level:', D+'trigLevelS', 'delay:', {D+'trigDelay':span(2,1)},''],
|
|
88
|
-
# ['Trigger state:',D+'trigState',' trigMode:',D+'trigMode',
|
|
89
|
-
# 'TrigLevel','TrigDelay'],
|
|
90
|
-
# [{D+'trigger':color('lightCyan')}, D+'trigSource', D+'trigCoupling',
|
|
91
|
-
# D+'trigSlope', D+'trigLevel', D+'trigDelay'],
|
|
92
|
-
[{'ATTRIBUTES':color('lightGreen')}, 'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
|
|
93
|
-
# ['Gain:']+ChLine('VoltsPerDiv'),
|
|
94
|
-
# ['Offset:']+ChLine('Position'),
|
|
95
|
-
# ['Coupling:']+ChLine('Coupling'),
|
|
96
|
-
# ['Termination:']+ChLine('Termination'),
|
|
97
|
-
# ['On/Off:']+ChLine('OnOff'),
|
|
98
|
-
#['Delay:']+ChLine('DelayFromTriggerM'),
|
|
99
|
-
#['Waveform:']+ChLine('WaveforM'),
|
|
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'),
|
|
100
84
|
['Mean:']+ChLine('Mean'),
|
|
101
85
|
['Peak2Peak:']+ChLine('Peak2Peak'),
|
|
102
|
-
#[''],
|
|
103
|
-
# ["Trigger",D+'trigSourceS',D+'trigLevelS',D+'trigSlopeS',D+'trigModeS'],
|
|
104
|
-
# ['',"Setup"],
|
|
105
|
-
# ["Repair:",D+'updateDataA',D+'deviceClearA',D+'resetScopeA',D+'forceTrigA'],
|
|
106
|
-
# ["Session",D+'SaveSession',D+'RecallSession',"folder:",D+'folderS'],
|
|
107
|
-
# [D+'currentSessionS',"<-current",D+'nextSessionS',"out off",D+'sessionsM'],
|
|
108
|
-
#[{'ATTRIBUTES':{'color':'yellow'}},
|
|
109
|
-
#['tAxis:',D+'tAxis'],
|
|
110
|
-
# [LYRow,'',{'For Experts only!':{**span(6,1),**font(14)}}],
|
|
111
|
-
# [LYRow,'Scope command:', {D+'instrCmdS':span(2,1)},_,{D+'instrCmdR':span(4,1)}],
|
|
112
|
-
# [LYRow,'Special commands', {D+'instrCtrl':span(2,1)},_,_,_,_,_,],
|
|
113
|
-
# [LYRow,'Timing:',{D+'timing':span(6,1)}],
|
|
114
|
-
# [LYRow,'ActOnEvent',D+'actOnEvent','AOE_Limit',D+'aOE_Limit',_,_,_],
|
|
115
86
|
]
|
|
@@ -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,11 +1,14 @@
|
|
|
1
1
|
"""Skeleton and helper functions for creating EPICS PVAccess server"""
|
|
2
2
|
# pylint: disable=invalid-name
|
|
3
|
-
__version__= '
|
|
4
|
-
#TODO:
|
|
5
|
-
#
|
|
3
|
+
__version__= 'v2.0.1 26-01-30'# added mandatory host PV
|
|
4
|
+
#TODO add mandatory PV: host, to identify the server host.
|
|
5
|
+
#Issue: There is no way in PVAccess to specify if string PV is writable.
|
|
6
|
+
# As a workaround we append description with suffix ' Features: W' to indicate that.
|
|
6
7
|
|
|
7
8
|
import sys
|
|
8
|
-
import time
|
|
9
|
+
from time import time, sleep, strftime, perf_counter as timer
|
|
10
|
+
import os
|
|
11
|
+
from socket import gethostname
|
|
9
12
|
from p4p.nt import NTScalar, NTEnum
|
|
10
13
|
from p4p.nt.enum import ntenum
|
|
11
14
|
from p4p.server import Server
|
|
@@ -13,6 +16,9 @@ from p4p.server.thread import SharedPV
|
|
|
13
16
|
from p4p.client.thread import Context
|
|
14
17
|
|
|
15
18
|
#``````````````````Module Storage`````````````````````````````````````````````
|
|
19
|
+
def _serverStateChanged(newState:str):
|
|
20
|
+
"""Dummy serverStateChanged function"""
|
|
21
|
+
return
|
|
16
22
|
class C_():
|
|
17
23
|
"""Storage for module members"""
|
|
18
24
|
prefix = ''
|
|
@@ -20,13 +26,16 @@ class C_():
|
|
|
20
26
|
cycle = 0
|
|
21
27
|
serverState = ''
|
|
22
28
|
PVs = {}
|
|
23
|
-
PVDefs = []
|
|
29
|
+
PVDefs = []
|
|
30
|
+
serverStateChanged = _serverStateChanged
|
|
31
|
+
|
|
24
32
|
#```````````````````Helper methods````````````````````````````````````````````
|
|
25
33
|
def serverState():
|
|
26
|
-
"""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."""
|
|
27
36
|
return C_.serverState
|
|
28
37
|
def _printTime():
|
|
29
|
-
return
|
|
38
|
+
return strftime("%m%d:%H%M%S")
|
|
30
39
|
def printi(msg):
|
|
31
40
|
"""Print info message and publish it to status PV."""
|
|
32
41
|
print(f'inf_@{_printTime()}: {msg}')
|
|
@@ -62,24 +71,31 @@ def pvv(pvName:str):
|
|
|
62
71
|
return pvobj(pvName).current()
|
|
63
72
|
|
|
64
73
|
def publish(pvName:str, value, ifChanged=False, t=None):
|
|
65
|
-
"""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}')
|
|
66
78
|
try:
|
|
67
79
|
pv = pvobj(pvName)
|
|
68
80
|
except KeyError:
|
|
69
|
-
|
|
81
|
+
print(f'WARNING: PV {pvName} not found. Cannot publish value.')
|
|
70
82
|
return
|
|
71
83
|
if t is None:
|
|
72
|
-
t = time
|
|
84
|
+
t = time()
|
|
73
85
|
if not ifChanged or pv.current() != value:
|
|
74
86
|
pv.post(value, timestamp=t)
|
|
75
87
|
|
|
76
88
|
def SPV(initial, meta='', vtype=None):
|
|
77
89
|
"""Construct SharedPV.
|
|
78
|
-
meta is a string with characters W,A,
|
|
79
|
-
|
|
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).
|
|
80
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).
|
|
81
97
|
"""
|
|
82
|
-
typeCode = {
|
|
98
|
+
typeCode = {# mapping from vtype to p4p type code
|
|
83
99
|
's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
|
|
84
100
|
'u64':'L', 'f32':'f', 'f64':'d', str:'s',
|
|
85
101
|
}
|
|
@@ -89,33 +105,52 @@ def SPV(initial, meta='', vtype=None):
|
|
|
89
105
|
itype = type(firstItem)
|
|
90
106
|
vtype = {int: 'i32', float: 'f32'}.get(itype,itype)
|
|
91
107
|
tcode = typeCode[vtype]
|
|
92
|
-
|
|
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
|
|
93
116
|
initial = {'choices': initial, 'index': 0}
|
|
94
117
|
nt = NTEnum(display=True, control='W' in meta)
|
|
95
118
|
else:
|
|
96
119
|
prefix = 'a' if iterable else ''
|
|
97
|
-
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)
|
|
98
122
|
pv = SharedPV(nt=nt, initial=initial)
|
|
123
|
+
# add new attributes.
|
|
99
124
|
pv.writable = 'W' in meta
|
|
125
|
+
pv.discrete = discrete
|
|
100
126
|
return pv
|
|
101
127
|
|
|
102
128
|
#``````````````````create_PVs()```````````````````````````````````````````````
|
|
103
129
|
def _create_PVs(pvDefs):
|
|
104
|
-
|
|
105
|
-
[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"""
|
|
106
|
-
ts = time.time()
|
|
130
|
+
ts = time()
|
|
107
131
|
for defs in pvDefs:
|
|
108
|
-
|
|
132
|
+
try:
|
|
133
|
+
pname,desc,spv,extra = defs
|
|
134
|
+
except ValueError:
|
|
135
|
+
printe(f'Invalid PV definition of {defs[0]}')
|
|
136
|
+
sys.exit(1)
|
|
109
137
|
ivalue = spv.current()
|
|
110
|
-
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)
|
|
111
144
|
C_.PVs[C_.prefix+pname] = spv
|
|
112
145
|
v = spv._wrap(ivalue, timestamp=ts)
|
|
113
146
|
if spv.writable:
|
|
114
147
|
try:
|
|
115
|
-
# 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.
|
|
116
151
|
v['control.limitLow'] = 0
|
|
117
152
|
v['control.limitHigh'] = 0
|
|
118
|
-
except KeyError
|
|
153
|
+
except KeyError:
|
|
119
154
|
#print(f'control not set for {pname}: {e}')
|
|
120
155
|
pass
|
|
121
156
|
if 'ntenum' in str(type(ivalue)):
|
|
@@ -132,18 +167,19 @@ def _create_PVs(pvDefs):
|
|
|
132
167
|
v[f'valueAlarm.{key}'] = value
|
|
133
168
|
spv.post(v)
|
|
134
169
|
|
|
135
|
-
# add new attributes.
|
|
170
|
+
# add new attributes.
|
|
136
171
|
spv.name = pname
|
|
137
172
|
spv.setter = extra.get('setter')
|
|
138
173
|
|
|
139
174
|
if spv.writable:
|
|
140
175
|
@spv.put
|
|
141
176
|
def handle(spv, op):
|
|
142
|
-
ct = time
|
|
177
|
+
ct = time()
|
|
143
178
|
vv = op.value()
|
|
144
179
|
vr = vv.raw.value
|
|
145
180
|
current = spv._wrap(spv.current())
|
|
146
|
-
# 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.
|
|
147
183
|
try:
|
|
148
184
|
limitLow = current['control.limitLow']
|
|
149
185
|
limitHigh = current['control.limitHigh']
|
|
@@ -154,86 +190,131 @@ def _create_PVs(pvDefs):
|
|
|
154
190
|
except KeyError:
|
|
155
191
|
pass
|
|
156
192
|
if isinstance(vv, ntenum):
|
|
157
|
-
vr = vv
|
|
193
|
+
vr = str(vv)
|
|
158
194
|
if spv.setter:
|
|
159
|
-
spv.setter(vr)
|
|
195
|
+
spv.setter(vr, spv)
|
|
160
196
|
# value will be updated by the setter, so get it again
|
|
161
197
|
vr = pvv(spv.name)
|
|
162
198
|
printv(f'putting {spv.name} = {vr}')
|
|
163
199
|
spv.post(vr, timestamp=ct) # update subscribers
|
|
164
200
|
op.done()
|
|
165
|
-
#print(f'PV {pv.name} created: {spv}')
|
|
166
201
|
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
167
202
|
#``````````````````Setters
|
|
168
|
-
def
|
|
203
|
+
def set_verbose(level, *_):
|
|
169
204
|
"""Set verbosity level for debugging"""
|
|
170
205
|
C_.verbose = level
|
|
171
|
-
|
|
206
|
+
printi(f'Setting verbose to {level}')
|
|
207
|
+
publish('verbose',level)
|
|
172
208
|
|
|
173
|
-
def set_server(
|
|
174
|
-
"""Example of the setter for the server PV.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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':
|
|
181
220
|
printi('Starting the server')
|
|
182
|
-
# configure_instrument()
|
|
183
|
-
# adopt_local_setting()
|
|
184
221
|
publish('server','Started')
|
|
185
222
|
publish('status','Started')
|
|
186
|
-
elif
|
|
223
|
+
elif servState == 'Stop':
|
|
187
224
|
printi('server stopped')
|
|
188
225
|
publish('server','Stopped')
|
|
189
226
|
publish('status','Stopped')
|
|
190
|
-
elif
|
|
227
|
+
elif servState == 'Exit':
|
|
191
228
|
printi('server is exiting')
|
|
192
229
|
publish('server','Exited')
|
|
193
230
|
publish('status','Exited')
|
|
194
|
-
elif
|
|
195
|
-
publish('acqCount', 0)
|
|
231
|
+
elif servState == 'Clear':
|
|
196
232
|
publish('status','Cleared')
|
|
197
|
-
# set server to previous
|
|
233
|
+
# set server to previous servState
|
|
198
234
|
set_server(C_.serverState)
|
|
199
|
-
|
|
235
|
+
return
|
|
236
|
+
C_.serverState = servState
|
|
200
237
|
|
|
201
238
|
def create_PVs(pvDefs=None):
|
|
202
|
-
"""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."""
|
|
203
252
|
U,LL,LH = 'units','limitLow','limitHigh'
|
|
204
253
|
C_.PVDefs = [
|
|
254
|
+
['host', 'Server host name', SPV(gethostname()), {}],
|
|
205
255
|
['version', 'Program version', SPV(__version__), {}],
|
|
206
|
-
['status', 'Server status', SPV('
|
|
207
|
-
['server', 'Server control',
|
|
208
|
-
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'),
|
|
209
259
|
{'setter':set_server}],
|
|
210
|
-
['
|
|
211
|
-
{'setter':
|
|
260
|
+
['verbose', 'Debugging verbosity', SPV(C_.verbose,'W','u8'),
|
|
261
|
+
{'setter':set_verbose, LL:0,LH:3}],
|
|
212
262
|
['polling', 'Polling interval', SPV(1.0,'W'), {U:'S', LL:0.001, LH:10.1}],
|
|
213
263
|
['cycle', 'Cycle number', SPV(0,'','u32'), {}],
|
|
214
264
|
]
|
|
215
|
-
# 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
|
|
216
267
|
if pvDefs is not None:
|
|
217
268
|
C_.PVDefs += pvDefs
|
|
218
269
|
_create_PVs(C_.PVDefs)
|
|
219
270
|
return C_.PVs
|
|
220
271
|
|
|
221
|
-
def get_externalPV(pvName, timeout=0.5):
|
|
222
|
-
"""Get value of PV from another server. That can be used to check if the
|
|
272
|
+
def get_externalPV(pvName:str, timeout=0.5):
|
|
273
|
+
"""Get value of PV from another server. That can be used to check if the
|
|
274
|
+
server is already running, or to get values from other servers."""
|
|
223
275
|
ctxt = Context('pva')
|
|
224
276
|
return ctxt.get(pvName, timeout=timeout)
|
|
225
277
|
|
|
226
|
-
def init_epicsdev(prefix, pvDefs, verbose=0
|
|
227
|
-
|
|
278
|
+
def init_epicsdev(prefix:str, pvDefs:list, verbose=0,
|
|
279
|
+
serverStateChanged=None, listDir=None):
|
|
280
|
+
"""Check if no other server is running with the same prefix.
|
|
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.
|
|
289
|
+
The listDir is a directory to save list of all generated PVs,
|
|
290
|
+
if no directory is given, then </tmp/pvlist/><prefix> is assumed.
|
|
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}')
|
|
228
296
|
C_.prefix = prefix
|
|
229
297
|
C_.verbose = verbose
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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.')
|
|
233
303
|
sys.exit(1)
|
|
234
|
-
except TimeoutError:
|
|
235
|
-
|
|
304
|
+
except TimeoutError: pass
|
|
305
|
+
|
|
306
|
+
# No existing server found. Creating PVs.
|
|
236
307
|
pvs = create_PVs(pvDefs)
|
|
308
|
+
# Save list of PVs to a file, if requested
|
|
309
|
+
if listDir != '':
|
|
310
|
+
listDir = '/tmp/pvlist/' if listDir is None else listDir
|
|
311
|
+
if not os.path.exists(listDir):
|
|
312
|
+
os.makedirs(listDir)
|
|
313
|
+
filepath = f'{listDir}{prefix[:-1]}.txt'
|
|
314
|
+
print(f'Writing list of PVs to {filepath}')
|
|
315
|
+
with open(filepath, 'w', encoding="utf-8") as f:
|
|
316
|
+
for _pvname in pvs:
|
|
317
|
+
f.write(_pvname + '\n')
|
|
237
318
|
return pvs
|
|
238
319
|
|
|
239
320
|
#``````````````````Demo````````````````````````````````````````````````````````
|
|
@@ -244,54 +325,57 @@ if __name__ == "__main__":
|
|
|
244
325
|
def myPVDefs():
|
|
245
326
|
"""Example of PV definitions"""
|
|
246
327
|
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
247
|
-
alarm = {'valueAlarm':{'lowAlarmLimit'
|
|
328
|
+
alarm = {'valueAlarm':{'lowAlarmLimit':-9., 'highAlarmLimit':9.}}
|
|
248
329
|
return [ # device-specific PVs
|
|
249
|
-
['noiseLevel', 'Noise amplitude', SPV(1.E-6,'W'), {SET:set_noise}],
|
|
330
|
+
['noiseLevel', 'Noise amplitude', SPV(1.E-6,'W'), {SET:set_noise, U:'V'}],
|
|
250
331
|
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
251
332
|
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
252
333
|
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
253
334
|
['ch1Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
|
|
254
335
|
['ch1VoltsPerDiv', 'Vertical scale', SPV(1E-3,'W'), {U:'V/du'}],
|
|
255
|
-
['
|
|
256
|
-
['
|
|
257
|
-
['
|
|
258
|
-
['
|
|
259
|
-
['alarm', 'PV with alarm', SPV(0,'WA'), alarm],
|
|
336
|
+
['ch1Waveform', 'Waveform array', SPV([0.]), {U:'du'}],
|
|
337
|
+
['ch1Mean', 'Mean of the waveform', SPV(0.,'A'), {U:'du'}],
|
|
338
|
+
['ch1Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {U:'du',**alarm}],
|
|
339
|
+
['alarm', 'PV with alarm', SPV(0,'WA'), {U:'du',**alarm}],
|
|
260
340
|
]
|
|
261
341
|
nPatterns = 100 # number of waveform patterns.
|
|
262
342
|
pargs = None
|
|
263
|
-
nDivs = 10 # number of divisions on the oscilloscope screen. That is needed to set tAxis when recordLength is changed.
|
|
264
343
|
rng = np.random.default_rng(nPatterns)
|
|
344
|
+
nPoints = 100
|
|
265
345
|
|
|
266
|
-
def set_recordLength(value):
|
|
267
|
-
"""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."""
|
|
268
349
|
printi(f'Setting tAxis to {value}')
|
|
269
|
-
publish('tAxis', np.arange(value)*
|
|
350
|
+
publish('tAxis', np.arange(value)*1.E-6)
|
|
270
351
|
publish('recordLength', value)
|
|
271
|
-
|
|
352
|
+
# Re-initialize noise array, because its size depends on recordLength
|
|
353
|
+
set_noise(pvv('noiseLevel'))
|
|
272
354
|
|
|
273
|
-
def set_noise(level):
|
|
355
|
+
def set_noise(level, *_):
|
|
274
356
|
"""Noise level have changed. Update noise array."""
|
|
275
|
-
|
|
357
|
+
v = float(level)
|
|
276
358
|
recordLength = pvv('recordLength')
|
|
277
|
-
|
|
278
|
-
|
|
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.')
|
|
279
363
|
publish('noiseLevel', level)
|
|
280
364
|
|
|
281
365
|
def init(recordLength):
|
|
282
366
|
"""Testing function. Do not use in production code."""
|
|
283
367
|
set_recordLength(recordLength)
|
|
284
|
-
set_noise(pvv('noiseLevel'))
|
|
368
|
+
#set_noise(pvv('noiseLevel')) # already called from set_recordLength
|
|
285
369
|
|
|
286
370
|
def poll():
|
|
287
371
|
"""Example of polling function"""
|
|
288
372
|
#pattern = C_.cycle % nPatterns# produces sliding
|
|
289
373
|
pattern = rng.integers(0, nPatterns)
|
|
290
|
-
|
|
291
|
-
printv(f'cycle {
|
|
292
|
-
publish('cycle',
|
|
374
|
+
cycle = pvv('cycle')
|
|
375
|
+
printv(f'cycle {repr(cycle)}')
|
|
376
|
+
publish('cycle', cycle + 1)
|
|
293
377
|
wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
|
|
294
|
-
wf
|
|
378
|
+
wf /= pvv('ch1VoltsPerDiv')
|
|
295
379
|
wf += pvv('ch1Offset')
|
|
296
380
|
publish('ch1Waveform', wf)
|
|
297
381
|
publish('ch1Peak2Peak', np.ptp(wf))
|
|
@@ -301,24 +385,27 @@ if __name__ == "__main__":
|
|
|
301
385
|
parser = argparse.ArgumentParser(description = __doc__,
|
|
302
386
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
303
387
|
epilog=f'{__version__}')
|
|
304
|
-
parser.add_argument('-
|
|
305
|
-
'
|
|
306
|
-
parser.add_argument('-
|
|
307
|
-
'
|
|
308
|
-
parser.add_argument('-
|
|
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.'))
|
|
395
|
+
# The rest of options are not essential, they can be controlled at runtime using PVs.
|
|
396
|
+
parser.add_argument('-n', '--npoints', type=int, default=nPoints, help=
|
|
309
397
|
'Number of points in the waveform')
|
|
310
398
|
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
311
|
-
'Show more log messages (-vv: show even more)')
|
|
399
|
+
'Show more log messages (-vv: show even more)')
|
|
312
400
|
pargs = parser.parse_args()
|
|
401
|
+
print(pargs)
|
|
313
402
|
|
|
314
403
|
# Initialize epicsdev and PVs
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
print('List of PVs:')
|
|
318
|
-
for _pvname in PVs:
|
|
319
|
-
print(_pvname)
|
|
404
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
405
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose, None, pargs.list)
|
|
320
406
|
|
|
321
|
-
# 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.
|
|
322
409
|
init(pargs.npoints)
|
|
323
410
|
|
|
324
411
|
# Start the Server. Use your set_server, if needed.
|
|
@@ -333,5 +420,5 @@ if __name__ == "__main__":
|
|
|
333
420
|
break
|
|
334
421
|
if not state.startswith('Stop'):
|
|
335
422
|
poll()
|
|
336
|
-
|
|
423
|
+
sleep(pvv("polling"))
|
|
337
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.1/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
|