epicsdev 0.0.0__tar.gz → 1.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-0.0.0 → epicsdev-1.0.1}/PKG-INFO +15 -1
- epicsdev-1.0.1/README.md +16 -0
- epicsdev-1.0.1/config/epicsSimscope_pp.py +112 -0
- epicsdev-1.0.1/config/epicsdev_pp.py +115 -0
- epicsdev-1.0.1/epicsdev/epicsdev.py +337 -0
- {epicsdev-0.0.0 → epicsdev-1.0.1}/pyproject.toml +1 -1
- epicsdev-0.0.0/README.md +0 -2
- epicsdev-0.0.0/epicsdev/epicsdev.py +0 -244
- {epicsdev-0.0.0 → epicsdev-1.0.1}/LICENSE +0 -0
- {epicsdev-0.0.0 → epicsdev-1.0.1}/epicsdev/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: epicsdev
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.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
|
|
@@ -15,3 +15,17 @@ Description-Content-Type: text/markdown
|
|
|
15
15
|
|
|
16
16
|
# epicsdev
|
|
17
17
|
Helper module for creating EPICS PVAccess servers.
|
|
18
|
+
|
|
19
|
+
Demo:
|
|
20
|
+
```
|
|
21
|
+
python pip install epicsdev
|
|
22
|
+
python -m epicsdev.epicsdev -l
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
To control and plot:
|
|
26
|
+
```
|
|
27
|
+
python pip install pypeto,pvplot
|
|
28
|
+
python -m pypeto -c config -f epicsdev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
|
epicsdev-1.0.1/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Pypet page for simulated oscilloscopes epicsScope"""
|
|
2
|
+
# format: pypeto 1.2+
|
|
3
|
+
__version__ = 'v0.0.0 2026-01-15'#
|
|
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
|
+
# Attributes for gray row, it should be in the first cell:
|
|
20
|
+
#GrayRow = {'ATTRIBUTES':{'color':'light gray', **font(12)}}
|
|
21
|
+
LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
|
|
22
|
+
lColor = color('lightGreen')
|
|
23
|
+
|
|
24
|
+
# definition for plotting cell
|
|
25
|
+
PyPath = '~sukhanov/venv/bin/python -m'
|
|
26
|
+
PaneP2P = ' '.join([f'c{i:02d}Peak2Peak' for i in range(1)])
|
|
27
|
+
PaneWF = ' '.join([f'c{i:02d}Waveform' for i in range(1)])
|
|
28
|
+
#PaneT = 'timing[1] timing[3]'
|
|
29
|
+
Plot = {'Plot':{'launch':f'{PyPath} pvplot -aV:simScope0: -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
|
|
30
|
+
**lColor, **ButtonFont}}
|
|
31
|
+
print(f'Plot command: {Plot}')
|
|
32
|
+
#``````````````````PyPage Object``````````````````````````````````````````````
|
|
33
|
+
class PyPage():
|
|
34
|
+
def __init__(self, instance='simScope0:',
|
|
35
|
+
title="Simulated oscilloscope", channels=1):
|
|
36
|
+
"""instance: unique name of the page.
|
|
37
|
+
For EPICS it is usually device prefix
|
|
38
|
+
"""
|
|
39
|
+
print(f'Instantiating Page {instance,title} with {channels} channels')
|
|
40
|
+
|
|
41
|
+
#``````````Mandatory class members starts here````````````````````````
|
|
42
|
+
self.namespace = 'PVA'
|
|
43
|
+
self.title = title
|
|
44
|
+
|
|
45
|
+
#``````````Page attributes, optional`````````````````````````
|
|
46
|
+
self.page = {**color(240,240,240)}# Does not work
|
|
47
|
+
#self.page['editable'] = False
|
|
48
|
+
|
|
49
|
+
#``````````Definition of columns`````````````````````````````
|
|
50
|
+
self.columns = {
|
|
51
|
+
1: {'width': 120, 'justify': 'right'},
|
|
52
|
+
2: {'width': 80},
|
|
53
|
+
3: {'width': 80},
|
|
54
|
+
4: {'width': 80},
|
|
55
|
+
5: {'width': 80},
|
|
56
|
+
6: {'width': 80},
|
|
57
|
+
7: {'width': 80},
|
|
58
|
+
8: {'width': 80},
|
|
59
|
+
9: {'width': 80},
|
|
60
|
+
}
|
|
61
|
+
"""`````````````````Configuration of rows`````````````````````````````
|
|
62
|
+
A row is a list of comma-separated cell definitions.
|
|
63
|
+
The cell definition is one of the following:
|
|
64
|
+
1)string, 2)device:parameters, 3)dictionary.
|
|
65
|
+
The dictionary is used when the cell requires extra features like color, width,
|
|
66
|
+
description etc. The dictionary is single-entry {key:value}, where the key is a
|
|
67
|
+
string or device:parameter and the value is dictionary of the features.
|
|
68
|
+
"""
|
|
69
|
+
D = instance
|
|
70
|
+
|
|
71
|
+
#``````````Abbreviations, used in cell definitions
|
|
72
|
+
def ChLine(suffix):
|
|
73
|
+
return [f'{D}c{ch:02d}{suffix}' for ch in range(channels)]
|
|
74
|
+
#FOption = ' -file '+logreqMap.get(D,'')
|
|
75
|
+
#``````````mandatory member```````````````````````````````````````````
|
|
76
|
+
self.rows = [
|
|
77
|
+
['Device:', D, {D+'version':span(2,1)},_, 'scope time:', {D+'dateTime':span(2,1)},_],
|
|
78
|
+
['State:', D+'server','cycle:',D+'cycle',_,_,Plot], # 'Recall:', D+'setup',],
|
|
79
|
+
['Status:', {D+'status': span(8,1)}],
|
|
80
|
+
['Polling Interval:', D+'polling',_,_,_,],
|
|
81
|
+
#['Triggers recorded:', D+'acqCount', 'Lost:', D+'lostTrigs',
|
|
82
|
+
# 'Acquisitions:',D+'scopeAcqCount'],
|
|
83
|
+
# ['Horizontal scale:', D+'timePerDiv', ' samples:', D+'recLength',
|
|
84
|
+
# 'SamplRate:', {D+'samplingRate':span(2,1)},_],
|
|
85
|
+
# #['Trigger:', D+'trigSourceS', D+'trigCouplingS', D+'trigSlopeS', 'level:', D+'trigLevelS', 'delay:', {D+'trigDelay':span(2,1)},''],
|
|
86
|
+
# ['Trigger state:',D+'trigState',' trigMode:',D+'trigMode',
|
|
87
|
+
# 'TrigLevel','TrigDelay'],
|
|
88
|
+
# [{D+'trigger':color('lightCyan')}, D+'trigSource', D+'trigCoupling',
|
|
89
|
+
# D+'trigSlope', D+'trigLevel', D+'trigDelay'],
|
|
90
|
+
[{'ATTRIBUTES':color('lightGreen')}, 'Channels:','CH1','CH2','CH3','CH4','CH5','CH6'],
|
|
91
|
+
# ['Gain:']+ChLine('VoltsPerDiv'),
|
|
92
|
+
# ['Offset:']+ChLine('Position'),
|
|
93
|
+
# ['Coupling:']+ChLine('Coupling'),
|
|
94
|
+
# ['Termination:']+ChLine('Termination'),
|
|
95
|
+
# ['On/Off:']+ChLine('OnOff'),
|
|
96
|
+
#['Delay:']+ChLine('DelayFromTriggerM'),
|
|
97
|
+
#['Waveform:']+ChLine('WaveforM'),
|
|
98
|
+
['Peak2Peak:']+ChLine('Peak2Peak'),
|
|
99
|
+
#[''],
|
|
100
|
+
# ["Trigger",D+'trigSourceS',D+'trigLevelS',D+'trigSlopeS',D+'trigModeS'],
|
|
101
|
+
# ['',"Setup"],
|
|
102
|
+
# ["Repair:",D+'updateDataA',D+'deviceClearA',D+'resetScopeA',D+'forceTrigA'],
|
|
103
|
+
# ["Session",D+'SaveSession',D+'RecallSession',"folder:",D+'folderS'],
|
|
104
|
+
# [D+'currentSessionS',"<-current",D+'nextSessionS',"out off",D+'sessionsM'],
|
|
105
|
+
#[{'ATTRIBUTES':{'color':'yellow'}},
|
|
106
|
+
#['tAxis:',D+'tAxis'],
|
|
107
|
+
# [LYRow,'',{'For Experts only!':{**span(6,1),**font(14)}}],
|
|
108
|
+
# [LYRow,'Scope command:', {D+'instrCmdS':span(2,1)},_,{D+'instrCmdR':span(4,1)}],
|
|
109
|
+
# [LYRow,'Special commands', {D+'instrCtrl':span(2,1)},_,_,_,_,_,],
|
|
110
|
+
# [LYRow,'Timing:',{D+'timing':span(6,1)}],
|
|
111
|
+
# [LYRow,'ActOnEvent',D+'actOnEvent','AOE_Limit',D+'aOE_Limit',_,_,_],
|
|
112
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Pypet page for epicdev.epicsdev module"""
|
|
2
|
+
# format: pypeto 1.2+
|
|
3
|
+
__version__ = 'v0.0.1 2026-01-16'#
|
|
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
|
+
# Attributes for gray row, it should be in the first cell:
|
|
20
|
+
#GrayRow = {'ATTRIBUTES':{'color':'light gray', **font(12)}}
|
|
21
|
+
LYRow = {'ATTRIBUTES':{'color':'light yellow'}}
|
|
22
|
+
lColor = color('lightGreen')
|
|
23
|
+
|
|
24
|
+
# definition for plotting cell
|
|
25
|
+
PyPath = 'python -m'
|
|
26
|
+
PaneP2P = ' '.join([f'ch{i+1:01d}Mean' for i in range(1)])
|
|
27
|
+
PaneWF = ' '.join([f'ch{i+1:01d}Waveform' for i in range(1)])
|
|
28
|
+
#PaneT = 'timing[1] timing[3]'
|
|
29
|
+
Plot = {'Plot':{'launch':f'{PyPath} pvplot -aV:epicsDev0: -#0"{PaneP2P}" -#1"{PaneWF}"',# -#2"{PaneT}"',
|
|
30
|
+
**lColor, **ButtonFont}}
|
|
31
|
+
print(f'Plot command: {Plot}')
|
|
32
|
+
#``````````````````PyPage Object``````````````````````````````````````````````
|
|
33
|
+
class PyPage():
|
|
34
|
+
def __init__(self, instance='epicsDev0:',
|
|
35
|
+
title="Simulated oscilloscope", channels=1):
|
|
36
|
+
"""instance: unique name of the page.
|
|
37
|
+
For EPICS it is usually device prefix
|
|
38
|
+
"""
|
|
39
|
+
print(f'Instantiating Page {instance,title} with {channels} channels')
|
|
40
|
+
|
|
41
|
+
#``````````Mandatory class members starts here````````````````````````
|
|
42
|
+
self.namespace = 'PVA'
|
|
43
|
+
self.title = title
|
|
44
|
+
|
|
45
|
+
#``````````Page attributes, optional`````````````````````````
|
|
46
|
+
self.page = {**color(240,240,240)}# Does not work
|
|
47
|
+
#self.page['editable'] = False
|
|
48
|
+
|
|
49
|
+
#``````````Definition of columns`````````````````````````````
|
|
50
|
+
self.columns = {
|
|
51
|
+
1: {'width': 120, 'justify': 'right'},
|
|
52
|
+
2: {'width': 80},
|
|
53
|
+
3: {'width': 80},
|
|
54
|
+
4: {'width': 80},
|
|
55
|
+
5: {'width': 80},
|
|
56
|
+
6: {'width': 80},
|
|
57
|
+
7: {'width': 80},
|
|
58
|
+
8: {'width': 80},
|
|
59
|
+
9: {'width': 80},
|
|
60
|
+
}
|
|
61
|
+
"""`````````````````Configuration of rows`````````````````````````````
|
|
62
|
+
A row is a list of comma-separated cell definitions.
|
|
63
|
+
The cell definition is one of the following:
|
|
64
|
+
1)string, 2)device:parameters, 3)dictionary.
|
|
65
|
+
The dictionary is used when the cell requires extra features like color, width,
|
|
66
|
+
description etc. The dictionary is single-entry {key:value}, where the key is a
|
|
67
|
+
string or device:parameter and the value is dictionary of the features.
|
|
68
|
+
"""
|
|
69
|
+
D = instance
|
|
70
|
+
|
|
71
|
+
#``````````Abbreviations, used in cell definitions
|
|
72
|
+
def ChLine(suffix):
|
|
73
|
+
return [f'{D}ch{ch+1:01d}{suffix}' for ch in range(channels)]
|
|
74
|
+
#FOption = ' -file '+logreqMap.get(D,'')
|
|
75
|
+
#``````````mandatory member```````````````````````````````````````````
|
|
76
|
+
self.rows = [
|
|
77
|
+
['Device:', D, {D+'version':span(2,1)}],#_, 'scope time:', #{D+'dateTime':span(2,1)},_],
|
|
78
|
+
['State:', D+'server','cycle:',D+'cycle',_,_,Plot], # 'Recall:', D+'setup',],
|
|
79
|
+
['Status:', {D+'status': span(8,1)}],
|
|
80
|
+
['Polling Interval:', D+'polling','RecLength:',D+'recordLength',
|
|
81
|
+
'V/Div:',D+'ch1VoltsPerDiv'],
|
|
82
|
+
['Noise level:',D+'noiseLevel'],
|
|
83
|
+
#['Triggers recorded:', D+'acqCount', 'Lost:', D+'lostTrigs',
|
|
84
|
+
# 'Acquisitions:',D+'scopeAcqCount'],
|
|
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'),
|
|
100
|
+
['Mean:']+ChLine('Mean'),
|
|
101
|
+
['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
|
+
]
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Skeleton and helper functions for creating EPICS PVAccess server"""
|
|
2
|
+
# pylint: disable=invalid-name
|
|
3
|
+
__version__= 'v1.0.1 26-01-16'# rng range = nPatterns
|
|
4
|
+
#TODO: NTEnums do not have structure display
|
|
5
|
+
#TODO: Add performance counters to demo.
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from p4p.nt import NTScalar, NTEnum
|
|
10
|
+
from p4p.nt.enum import ntenum
|
|
11
|
+
from p4p.server import Server
|
|
12
|
+
from p4p.server.thread import SharedPV
|
|
13
|
+
from p4p.client.thread import Context
|
|
14
|
+
|
|
15
|
+
#``````````````````Module Storage`````````````````````````````````````````````
|
|
16
|
+
class C_():
|
|
17
|
+
"""Storage for module members"""
|
|
18
|
+
prefix = ''
|
|
19
|
+
verbose = 0
|
|
20
|
+
cycle = 0
|
|
21
|
+
serverState = ''
|
|
22
|
+
PVs = {}
|
|
23
|
+
PVDefs = []
|
|
24
|
+
#```````````````````Helper methods````````````````````````````````````````````
|
|
25
|
+
def serverState():
|
|
26
|
+
"""Return current server state. That is the value of the server PV, but cached in C_ to avoid unnecessary get() calls."""
|
|
27
|
+
return C_.serverState
|
|
28
|
+
def _printTime():
|
|
29
|
+
return time.strftime("%m%d:%H%M%S")
|
|
30
|
+
def printi(msg):
|
|
31
|
+
"""Print info message and publish it to status PV."""
|
|
32
|
+
print(f'inf_@{_printTime()}: {msg}')
|
|
33
|
+
def printw(msg):
|
|
34
|
+
"""Print warning message and publish it to status PV."""
|
|
35
|
+
txt = f'WAR_@{_printTime()}: {msg}'
|
|
36
|
+
print(txt)
|
|
37
|
+
publish('status',txt)
|
|
38
|
+
def printe(msg):
|
|
39
|
+
"""Print error message and publish it to status PV."""
|
|
40
|
+
txt = f'ERR_{_printTime()}: {msg}'
|
|
41
|
+
print(txt)
|
|
42
|
+
publish('status',txt)
|
|
43
|
+
def _printv(msg, level):
|
|
44
|
+
if C_.verbose >= level:
|
|
45
|
+
print(f'DBG{level}: {msg}')
|
|
46
|
+
def printv(msg):
|
|
47
|
+
"""Print debug message if verbosity level >=1."""
|
|
48
|
+
_printv(msg, 1)
|
|
49
|
+
def printvv(msg):
|
|
50
|
+
"""Print debug message if verbosity level >=2."""
|
|
51
|
+
_printv(msg, 2)
|
|
52
|
+
def printv3(msg):
|
|
53
|
+
"""Print debug message if verbosity level >=3."""
|
|
54
|
+
_printv(msg, 3)
|
|
55
|
+
|
|
56
|
+
def pvobj(pvName):
|
|
57
|
+
"""Return PV with given name"""
|
|
58
|
+
return C_.PVs[C_.prefix+pvName]
|
|
59
|
+
|
|
60
|
+
def pvv(pvName:str):
|
|
61
|
+
"""Return PV value"""
|
|
62
|
+
return pvobj(pvName).current()
|
|
63
|
+
|
|
64
|
+
def publish(pvName:str, value, ifChanged=False, t=None):
|
|
65
|
+
"""Publish value to PV. If ifChanged is True, then publish only if the value is different from the current value. If t is not None, then use it as timestamp, otherwise use current time."""
|
|
66
|
+
try:
|
|
67
|
+
pv = pvobj(pvName)
|
|
68
|
+
except KeyError:
|
|
69
|
+
printw(f'PV {pvName} not found. Cannot publish value.')
|
|
70
|
+
return
|
|
71
|
+
if t is None:
|
|
72
|
+
t = time.time()
|
|
73
|
+
if not ifChanged or pv.current() != value:
|
|
74
|
+
pv.post(value, timestamp=t)
|
|
75
|
+
|
|
76
|
+
def SPV(initial, meta='', vtype=None):
|
|
77
|
+
"""Construct SharedPV.
|
|
78
|
+
meta is a string with characters W,A,E indicating if the PV is writable, has alarm or it is NTEnum.
|
|
79
|
+
vtype should be one of the p4p.nt type definitions (see https://epics-base.github.io/p4p/values.html).
|
|
80
|
+
if vtype is None then the nominal type will be determined automatically.
|
|
81
|
+
"""
|
|
82
|
+
typeCode = {
|
|
83
|
+
's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
|
|
84
|
+
'u64':'L', 'f32':'f', 'f64':'d', str:'s',
|
|
85
|
+
}
|
|
86
|
+
iterable = type(initial) not in (int,float,str)
|
|
87
|
+
if vtype is None:
|
|
88
|
+
firstItem = initial[0] if iterable else initial
|
|
89
|
+
itype = type(firstItem)
|
|
90
|
+
vtype = {int: 'i32', float: 'f32'}.get(itype,itype)
|
|
91
|
+
tcode = typeCode[vtype]
|
|
92
|
+
if 'E' in meta:
|
|
93
|
+
initial = {'choices': initial, 'index': 0}
|
|
94
|
+
nt = NTEnum(display=True, control='W' in meta)
|
|
95
|
+
else:
|
|
96
|
+
prefix = 'a' if iterable else ''
|
|
97
|
+
nt = NTScalar(prefix+tcode, display=True, control='W' in meta, valueAlarm='A' in meta)
|
|
98
|
+
pv = SharedPV(nt=nt, initial=initial)
|
|
99
|
+
pv.writable = 'W' in meta
|
|
100
|
+
return pv
|
|
101
|
+
|
|
102
|
+
#``````````````````create_PVs()```````````````````````````````````````````````
|
|
103
|
+
def _create_PVs(pvDefs):
|
|
104
|
+
"""Create PVs, using definitions from pvDEfs list. Each definition is a list of the form:
|
|
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()
|
|
107
|
+
for defs in pvDefs:
|
|
108
|
+
pname,desc,spv,extra = defs
|
|
109
|
+
ivalue = spv.current()
|
|
110
|
+
printv(f'created pv {pname}, initial: {type(ivalue),ivalue}, extra: {extra}')
|
|
111
|
+
C_.PVs[C_.prefix+pname] = spv
|
|
112
|
+
v = spv._wrap(ivalue, timestamp=ts)
|
|
113
|
+
if spv.writable:
|
|
114
|
+
try:
|
|
115
|
+
# To indicate that the PV is writable, set control limits to (0,0). Not very elegant, but it works for numerics and enums, not for strings.
|
|
116
|
+
v['control.limitLow'] = 0
|
|
117
|
+
v['control.limitHigh'] = 0
|
|
118
|
+
except KeyError as e:
|
|
119
|
+
#print(f'control not set for {pname}: {e}')
|
|
120
|
+
pass
|
|
121
|
+
if 'ntenum' in str(type(ivalue)):
|
|
122
|
+
spv.post(ivalue, timestamp=ts)
|
|
123
|
+
else:
|
|
124
|
+
v['display.description'] = desc
|
|
125
|
+
for field in extra.keys():
|
|
126
|
+
if field in ['limitLow','limitHigh','format','units']:
|
|
127
|
+
v[f'display.{field}'] = extra[field]
|
|
128
|
+
if field.startswith('limit'):
|
|
129
|
+
v[f'control.{field}'] = extra[field]
|
|
130
|
+
if field == 'valueAlarm':
|
|
131
|
+
for key,value in extra[field].items():
|
|
132
|
+
v[f'valueAlarm.{key}'] = value
|
|
133
|
+
spv.post(v)
|
|
134
|
+
|
|
135
|
+
# add new attributes. To my surprise that works!
|
|
136
|
+
spv.name = pname
|
|
137
|
+
spv.setter = extra.get('setter')
|
|
138
|
+
|
|
139
|
+
if spv.writable:
|
|
140
|
+
@spv.put
|
|
141
|
+
def handle(spv, op):
|
|
142
|
+
ct = time.time()
|
|
143
|
+
vv = op.value()
|
|
144
|
+
vr = vv.raw.value
|
|
145
|
+
current = spv._wrap(spv.current())
|
|
146
|
+
# check limits, if they are defined. That will be a good example of using control structure and valueAlarm.
|
|
147
|
+
try:
|
|
148
|
+
limitLow = current['control.limitLow']
|
|
149
|
+
limitHigh = current['control.limitHigh']
|
|
150
|
+
if limitLow != limitHigh and not (limitLow <= vr <= limitHigh):
|
|
151
|
+
printw(f'Value {vr} is out of limits [{limitLow}, {limitHigh}]. Ignoring.')
|
|
152
|
+
op.done(error=f'Value out of limits [{limitLow}, {limitHigh}]')
|
|
153
|
+
return
|
|
154
|
+
except KeyError:
|
|
155
|
+
pass
|
|
156
|
+
if isinstance(vv, ntenum):
|
|
157
|
+
vr = vv
|
|
158
|
+
if spv.setter:
|
|
159
|
+
spv.setter(vr)
|
|
160
|
+
# value will be updated by the setter, so get it again
|
|
161
|
+
vr = pvv(spv.name)
|
|
162
|
+
printv(f'putting {spv.name} = {vr}')
|
|
163
|
+
spv.post(vr, timestamp=ct) # update subscribers
|
|
164
|
+
op.done()
|
|
165
|
+
#print(f'PV {pv.name} created: {spv}')
|
|
166
|
+
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
167
|
+
#``````````````````Setters
|
|
168
|
+
def set_verbosity(level):
|
|
169
|
+
"""Set verbosity level for debugging"""
|
|
170
|
+
C_.verbose = level
|
|
171
|
+
publish('verbosity',level)
|
|
172
|
+
|
|
173
|
+
def set_server(state=None):
|
|
174
|
+
"""Example of the setter for the server PV."""
|
|
175
|
+
#printv(f'>set_server({state}), {type(state)}')
|
|
176
|
+
if state is None:
|
|
177
|
+
state = pvv('server')
|
|
178
|
+
printi(f'Setting server state to {state}')
|
|
179
|
+
state = str(state)
|
|
180
|
+
if state == 'Start':
|
|
181
|
+
printi('Starting the server')
|
|
182
|
+
# configure_instrument()
|
|
183
|
+
# adopt_local_setting()
|
|
184
|
+
publish('server','Started')
|
|
185
|
+
publish('status','Started')
|
|
186
|
+
elif state == 'Stop':
|
|
187
|
+
printi('server stopped')
|
|
188
|
+
publish('server','Stopped')
|
|
189
|
+
publish('status','Stopped')
|
|
190
|
+
elif state == 'Exit':
|
|
191
|
+
printi('server is exiting')
|
|
192
|
+
publish('server','Exited')
|
|
193
|
+
publish('status','Exited')
|
|
194
|
+
elif state == 'Clear':
|
|
195
|
+
publish('acqCount', 0)
|
|
196
|
+
publish('status','Cleared')
|
|
197
|
+
# set server to previous state
|
|
198
|
+
set_server(C_.serverState)
|
|
199
|
+
C_.serverState = state
|
|
200
|
+
|
|
201
|
+
def create_PVs(pvDefs=None):
|
|
202
|
+
"""Creates manadatory PVs and adds PVs specified in pvDefs list"""
|
|
203
|
+
U,LL,LH = 'units','limitLow','limitHigh'
|
|
204
|
+
C_.PVDefs = [
|
|
205
|
+
['version', 'Program version', SPV(__version__), {}],
|
|
206
|
+
['status', 'Server status', SPV('?','W'), {}],
|
|
207
|
+
['server', 'Server control',
|
|
208
|
+
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WE'),
|
|
209
|
+
{'setter':set_server}],
|
|
210
|
+
['verbosity', 'Debugging verbosity', SPV(0,'W','u8'),
|
|
211
|
+
{'setter':set_verbosity}],
|
|
212
|
+
['polling', 'Polling interval', SPV(1.0,'W'), {U:'S', LL:0.001, LH:10.1}],
|
|
213
|
+
['cycle', 'Cycle number', SPV(0,'','u32'), {}],
|
|
214
|
+
]
|
|
215
|
+
# append application's PVs, defined in the pvDefs and create map of providers
|
|
216
|
+
if pvDefs is not None:
|
|
217
|
+
C_.PVDefs += pvDefs
|
|
218
|
+
_create_PVs(C_.PVDefs)
|
|
219
|
+
return C_.PVs
|
|
220
|
+
|
|
221
|
+
def get_externalPV(pvName, timeout=0.5):
|
|
222
|
+
"""Get value of PV from another server. That can be used to check if the server is already running, or to get values from other servers."""
|
|
223
|
+
ctxt = Context('pva')
|
|
224
|
+
return ctxt.get(pvName, timeout=timeout)
|
|
225
|
+
|
|
226
|
+
def init_epicsdev(prefix, pvDefs, verbose=0):
|
|
227
|
+
"""Check if no other server is running with the same prefix, create PVs and return them as a dictionary."""
|
|
228
|
+
C_.prefix = prefix
|
|
229
|
+
C_.verbose = verbose
|
|
230
|
+
try:
|
|
231
|
+
get_externalPV(prefix+'version')
|
|
232
|
+
print(f'Server for {prefix} already running. Exiting.')
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
except TimeoutError:
|
|
235
|
+
pass
|
|
236
|
+
pvs = create_PVs(pvDefs)
|
|
237
|
+
return pvs
|
|
238
|
+
|
|
239
|
+
#``````````````````Demo````````````````````````````````````````````````````````
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
import numpy as np
|
|
242
|
+
import argparse
|
|
243
|
+
|
|
244
|
+
def myPVDefs():
|
|
245
|
+
"""Example of PV definitions"""
|
|
246
|
+
SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
|
|
247
|
+
alarm = {'valueAlarm':{'lowAlarmLimit':0, 'highAlarmLimit':100}}
|
|
248
|
+
return [ # device-specific PVs
|
|
249
|
+
['noiseLevel', 'Noise amplitude', SPV(1.E-6,'W'), {SET:set_noise}],
|
|
250
|
+
['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
|
|
251
|
+
['recordLength','Max number of points', SPV(100,'W','u32'),
|
|
252
|
+
{LL:4,LH:1000000, SET:set_recordLength}],
|
|
253
|
+
['ch1Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
|
|
254
|
+
['ch1VoltsPerDiv', 'Vertical scale', SPV(1E-3,'W'), {U:'V/du'}],
|
|
255
|
+
['timePerDiv', 'Horizontal scale', SPV(1.E-6,'W'), {U:'S/du'}],
|
|
256
|
+
['ch1Waveform', 'Waveform array', SPV([0.]), {}],
|
|
257
|
+
['ch1Mean', 'Mean of the waveform', SPV(0.,'A'), {}],
|
|
258
|
+
['ch1Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {}],
|
|
259
|
+
['alarm', 'PV with alarm', SPV(0,'WA'), alarm],
|
|
260
|
+
]
|
|
261
|
+
nPatterns = 100 # number of waveform patterns.
|
|
262
|
+
pargs = None
|
|
263
|
+
nDivs = 10 # number of divisions on the oscilloscope screen. That is needed to set tAxis when recordLength is changed.
|
|
264
|
+
rng = np.random.default_rng(nPatterns)
|
|
265
|
+
|
|
266
|
+
def set_recordLength(value):
|
|
267
|
+
"""Record length have changed. The tAxis should be updated accordingly."""
|
|
268
|
+
printi(f'Setting tAxis to {value}')
|
|
269
|
+
publish('tAxis', np.arange(value)*pvv('timePerDiv')/nDivs)
|
|
270
|
+
publish('recordLength', value)
|
|
271
|
+
set_noise(pvv('noiseLevel')) # Re-initialize noise array, because its size depends on recordLength
|
|
272
|
+
|
|
273
|
+
def set_noise(level):
|
|
274
|
+
"""Noise level have changed. Update noise array."""
|
|
275
|
+
printi(f'Setting noise level to {repr(level)}')
|
|
276
|
+
recordLength = pvv('recordLength')
|
|
277
|
+
pargs.noise = np.random.normal(scale=0.5*level, size=recordLength+nPatterns)
|
|
278
|
+
print(f'Noise array {len(pargs.noise)} updated with level {repr(level)}')
|
|
279
|
+
publish('noiseLevel', level)
|
|
280
|
+
|
|
281
|
+
def init(recordLength):
|
|
282
|
+
"""Testing function. Do not use in production code."""
|
|
283
|
+
set_recordLength(recordLength)
|
|
284
|
+
set_noise(pvv('noiseLevel'))
|
|
285
|
+
|
|
286
|
+
def poll():
|
|
287
|
+
"""Example of polling function"""
|
|
288
|
+
#pattern = C_.cycle % nPatterns# produces sliding
|
|
289
|
+
pattern = rng.integers(0, nPatterns)
|
|
290
|
+
C_.cycle += 1
|
|
291
|
+
printv(f'cycle {C_.cycle}')
|
|
292
|
+
publish('cycle', C_.cycle)
|
|
293
|
+
wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
|
|
294
|
+
wf *= pvv('ch1VoltsPerDiv')*nDivs
|
|
295
|
+
wf += pvv('ch1Offset')
|
|
296
|
+
publish('ch1Waveform', wf)
|
|
297
|
+
publish('ch1Peak2Peak', np.ptp(wf))
|
|
298
|
+
publish('ch1Mean', np.mean(wf))
|
|
299
|
+
|
|
300
|
+
# Argument parsing
|
|
301
|
+
parser = argparse.ArgumentParser(description = __doc__,
|
|
302
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
303
|
+
epilog=f'{__version__}')
|
|
304
|
+
parser.add_argument('-l', '--listPVs', action='store_true', help=
|
|
305
|
+
'List all generated PVs')
|
|
306
|
+
parser.add_argument('-p', '--prefix', default='epicsDev0:', help=
|
|
307
|
+
'Prefix to be prepended to all PVs')
|
|
308
|
+
parser.add_argument('-n', '--npoints', type=int, default=100, help=
|
|
309
|
+
'Number of points in the waveform')
|
|
310
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help=
|
|
311
|
+
'Show more log messages (-vv: show even more)')
|
|
312
|
+
pargs = parser.parse_args()
|
|
313
|
+
|
|
314
|
+
# Initialize epicsdev and PVs
|
|
315
|
+
PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose)
|
|
316
|
+
if pargs.listPVs:
|
|
317
|
+
print('List of PVs:')
|
|
318
|
+
for _pvname in PVs:
|
|
319
|
+
print(_pvname)
|
|
320
|
+
|
|
321
|
+
# Initialize the device, using pargs if needed. That can be used to set the number of points in the waveform, for example.
|
|
322
|
+
init(pargs.npoints)
|
|
323
|
+
|
|
324
|
+
# Start the Server. Use your set_server, if needed.
|
|
325
|
+
set_server('Start')
|
|
326
|
+
|
|
327
|
+
# Main loop
|
|
328
|
+
server = Server(providers=[PVs])
|
|
329
|
+
printi(f'Server started with polling interval {repr(pvv("polling"))} S.')
|
|
330
|
+
while True:
|
|
331
|
+
state = serverState()
|
|
332
|
+
if state.startswith('Exit'):
|
|
333
|
+
break
|
|
334
|
+
if not state.startswith('Stop'):
|
|
335
|
+
poll()
|
|
336
|
+
time.sleep(pvv("polling"))
|
|
337
|
+
printi('Server is exited')
|
epicsdev-0.0.0/README.md
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
"""Skeleton and helper functions for creating EPICS PVAccess server"""
|
|
2
|
-
# pylint: disable=invalid-name
|
|
3
|
-
__version__= 'v0.0.0 26-01-14'# Created
|
|
4
|
-
#TODO: Do not start if another device is already running
|
|
5
|
-
#TODO: NTEnums do not have structure display
|
|
6
|
-
#TODO: Find a way to indicate that a PV is writable.
|
|
7
|
-
# Options:
|
|
8
|
-
# 1) add structure control with (0,0) limits as indication of Writable.
|
|
9
|
-
# 2) use an extra field of the NTScalar.
|
|
10
|
-
|
|
11
|
-
import argparse
|
|
12
|
-
import time
|
|
13
|
-
from p4p.nt import NTScalar, NTEnum
|
|
14
|
-
from p4p.nt.enum import ntenum
|
|
15
|
-
from p4p.server import Server
|
|
16
|
-
from p4p.server.thread import SharedPV
|
|
17
|
-
|
|
18
|
-
#``````````````````Module Storage`````````````````````````````````````````````
|
|
19
|
-
class C_():
|
|
20
|
-
"""Storage for module members"""
|
|
21
|
-
AppName = 'epicsDevLecroyScope'
|
|
22
|
-
cycle = 0
|
|
23
|
-
lastRareUpdate = 0.
|
|
24
|
-
server = None
|
|
25
|
-
serverState = ''
|
|
26
|
-
PVs = {}
|
|
27
|
-
PVDefs = []
|
|
28
|
-
#```````````````````Helper methods````````````````````````````````````````````
|
|
29
|
-
def printTime(): return time.strftime("%m%d:%H%M%S")
|
|
30
|
-
def printi(msg): print(f'inf_@{printTime()}: {msg}')
|
|
31
|
-
def printw(msg):
|
|
32
|
-
txt = f'WAR_@{printTime()}: {msg}'
|
|
33
|
-
print(txt)
|
|
34
|
-
#publish('status',txt)
|
|
35
|
-
def printe(msg):
|
|
36
|
-
txt = f'ERR_{printTime()}: {msg}'
|
|
37
|
-
print(txt)
|
|
38
|
-
#publish('status',txt)
|
|
39
|
-
def _printv(msg, level):
|
|
40
|
-
if pargs.verbose >= level: print(f'DBG{level}: {msg}')
|
|
41
|
-
def printv(msg): _printv(msg, 1)
|
|
42
|
-
def printvv(msg): _printv(msg, 2)
|
|
43
|
-
def printv3(msg): _printv(msg, 3)
|
|
44
|
-
|
|
45
|
-
def pvobj(pvname):
|
|
46
|
-
"""Return PV with given name"""
|
|
47
|
-
return C_.PVs[pargs.prefix+pvname]
|
|
48
|
-
|
|
49
|
-
def pvv(pvname:str):
|
|
50
|
-
"""Return PV value"""
|
|
51
|
-
return pvobj(pvname).current()
|
|
52
|
-
|
|
53
|
-
def publish(pvname:str, value, ifChanged=False, t=None):
|
|
54
|
-
"""Post PV with new value"""
|
|
55
|
-
try:
|
|
56
|
-
pv = pvobj(pvname)
|
|
57
|
-
except KeyError:
|
|
58
|
-
return
|
|
59
|
-
if t is None:
|
|
60
|
-
t = time.time()
|
|
61
|
-
if not ifChanged or pv.current() != value:
|
|
62
|
-
pv.post(value, timestamp=t)
|
|
63
|
-
|
|
64
|
-
def SPV(initial, vtype=None):
|
|
65
|
-
"""Construct SharedPV, vtype should be one of typeCode keys,
|
|
66
|
-
if vtype is None then the nominal type will be determined automatically
|
|
67
|
-
"""
|
|
68
|
-
typeCode = {
|
|
69
|
-
'F64':'d', 'F32':'f', 'I64':'l', 'I8':'b', 'U8':'B', 'I16':'h',
|
|
70
|
-
'U16':'H', 'I32':'i', 'U32':'I', str:'s', 'enum':'enum',
|
|
71
|
-
}
|
|
72
|
-
iterable = type(initial) not in (int,float,str)
|
|
73
|
-
if vtype is None:
|
|
74
|
-
firstItem = initial[0] if iterable else initial
|
|
75
|
-
itype = type(firstItem)
|
|
76
|
-
vtype = {int: 'I32', float: 'F32'}.get(itype,itype)
|
|
77
|
-
tcode = typeCode[vtype]
|
|
78
|
-
if tcode == 'enum':
|
|
79
|
-
initial = {'choices': initial, 'index': 0}
|
|
80
|
-
nt = NTEnum(display=True)#TODO: that does not work
|
|
81
|
-
else:
|
|
82
|
-
prefix = 'a' if iterable else ''
|
|
83
|
-
nt = NTScalar(prefix+tcode, display=True, control=True, valueAlarm=True)
|
|
84
|
-
return SharedPV(nt=nt, initial=initial)
|
|
85
|
-
|
|
86
|
-
#``````````````````Definition of PVs``````````````````````````````````````````
|
|
87
|
-
def _define_PVs():
|
|
88
|
-
"""Example of PV definitions"""
|
|
89
|
-
R,W,SET,U,ENUM,LL,LH = 'R','W','setter','units','enum','limitLow','limitHigh'
|
|
90
|
-
alarm = {'valueAlarm':{'lowAlarmLimit':0, 'highAlarmLimit':100}}
|
|
91
|
-
return [
|
|
92
|
-
# device-specific PVs
|
|
93
|
-
['VoltOffset', 'Offset', SPV(0.), W, {U:'V'}],
|
|
94
|
-
['VoltPerDiv', 'Vertical scale', SPV(0.), W, {U:'V/du'}],
|
|
95
|
-
['TimePerDiv', 'Horizontal scale', SPV('0.01 0.02 0.05 0.1 0.2 0.5 1 2 5'.split(),ENUM), W, {U:'S/du'}],
|
|
96
|
-
['trigDelay', 'Trigger delay', SPV(0.), W, {U:'S'}],
|
|
97
|
-
['Waveform', 'Waveform array', SPV([0.]), R, {}],
|
|
98
|
-
['tAxis', 'Full scale of horizontal axis', SPV([0.]), R, {}],
|
|
99
|
-
['recordLength','Max number of points', SPV(100,'U32'), W, {}],
|
|
100
|
-
['peak2peak', 'Peak-to-peak amplitude', SPV(0.), R, {}],
|
|
101
|
-
['alarm', 'PV with alarm', SPV(0), 'WA', alarm],
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
#``````````````````create_PVs()```````````````````````````````````````````````
|
|
105
|
-
def _create_PVs():
|
|
106
|
-
"""Create PVs"""
|
|
107
|
-
ts = time.time()
|
|
108
|
-
for defs in C_.PVDefs:
|
|
109
|
-
pname,desc,spv,features,extra = defs
|
|
110
|
-
pv = spv
|
|
111
|
-
ivalue = pv.current()
|
|
112
|
-
printv(f'created pv {pname}, initial: {type(ivalue),ivalue}, extra: {extra}')
|
|
113
|
-
C_.PVs[pargs.prefix+pname] = pv
|
|
114
|
-
#if isinstance(ivalue,dict):# NTEnum
|
|
115
|
-
if 'ntenum' in str(type(ivalue)):
|
|
116
|
-
pv.post(ivalue, timestamp=ts)
|
|
117
|
-
else:
|
|
118
|
-
v = pv._wrap(ivalue, timestamp=ts)
|
|
119
|
-
v['display.description'] = desc
|
|
120
|
-
for field in extra.keys():
|
|
121
|
-
if field in ['limitLow','limitHigh','format','units']:
|
|
122
|
-
v[f'display.{field}'] = extra[field]
|
|
123
|
-
if field.startswith('limit'):
|
|
124
|
-
v[f'control.{field}'] = extra[field]
|
|
125
|
-
if field == 'valueAlarm':
|
|
126
|
-
for key,value in extra[field].items():
|
|
127
|
-
v[f'valueAlarm.{key}'] = value
|
|
128
|
-
pv.post(v)
|
|
129
|
-
|
|
130
|
-
# add new attributes. To my surprise that works!
|
|
131
|
-
pv.name = pname
|
|
132
|
-
pv.setter = extra.get('setter')
|
|
133
|
-
|
|
134
|
-
writable = 'W' in features
|
|
135
|
-
if writable:
|
|
136
|
-
@pv.put
|
|
137
|
-
def handle(pv, op):
|
|
138
|
-
ct = time.time()
|
|
139
|
-
vv = op.value()
|
|
140
|
-
vr = vv.raw.value
|
|
141
|
-
if isinstance(vv, ntenum):
|
|
142
|
-
vr = vv
|
|
143
|
-
if pv.setter:
|
|
144
|
-
pv.setter(vr)
|
|
145
|
-
# value could change by the setter
|
|
146
|
-
vr = pvv(pv.name)
|
|
147
|
-
printv(f'putting {pv.name} = {vr}')
|
|
148
|
-
pv.post(vr, timestamp=ct) # update subscribers
|
|
149
|
-
op.done()
|
|
150
|
-
#print(f'PV {pv.name} created: {pv}')
|
|
151
|
-
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
|
152
|
-
#``````````````````Setters
|
|
153
|
-
def set_verbosity(level):
|
|
154
|
-
"""Set verbosity level for debugging"""
|
|
155
|
-
pargs.verbose = level
|
|
156
|
-
publish('verbosity',level)
|
|
157
|
-
|
|
158
|
-
def set_server(state=None):
|
|
159
|
-
"""Example of the setter for the server PV."""
|
|
160
|
-
#printv(f'>set_server({state}), {type(state)}')
|
|
161
|
-
if state is None:
|
|
162
|
-
state = pvv('server')
|
|
163
|
-
printi(f'Setting server state to {state}')
|
|
164
|
-
state = str(state)
|
|
165
|
-
if state == 'Start':
|
|
166
|
-
printi('Starting the server')
|
|
167
|
-
#configure_scope()
|
|
168
|
-
#adopt_local_setting()
|
|
169
|
-
publish('server','Started')
|
|
170
|
-
elif state == 'Stop':
|
|
171
|
-
printi('server stopped')
|
|
172
|
-
publish('server','Stopped')
|
|
173
|
-
elif state == 'Exit':
|
|
174
|
-
printi('server is exiting')
|
|
175
|
-
publish('server','Exited')
|
|
176
|
-
elif state == 'Clear':
|
|
177
|
-
publish('acqCount', 0)
|
|
178
|
-
#publish('lostTrigs', 0)
|
|
179
|
-
#C_.triggersLost = 0
|
|
180
|
-
publish('status','Cleared')
|
|
181
|
-
# set server to previous state
|
|
182
|
-
set_server(C_.serverState)
|
|
183
|
-
C_.serverState = state
|
|
184
|
-
|
|
185
|
-
def poll():
|
|
186
|
-
"""Example of polling function"""
|
|
187
|
-
C_.cycle += 1
|
|
188
|
-
printv(f'cycle {C_.cycle}')
|
|
189
|
-
publish('cycle', C_.cycle)
|
|
190
|
-
|
|
191
|
-
def create_PVs(pvDefs:list):
|
|
192
|
-
"""Creates manadatory PVs and adds PVs, using definitions from pvDEfs list"""
|
|
193
|
-
U,LL,LH = 'units','limitLow','limitHigh'
|
|
194
|
-
C_.PVDefs = [
|
|
195
|
-
['version', 'Program version', SPV(__version__), 'R', {}],
|
|
196
|
-
['status', 'Server status', SPV('?'), 'W', {}],
|
|
197
|
-
['server', 'Server control',
|
|
198
|
-
SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'enum'),
|
|
199
|
-
'W', {'setter':set_server}],
|
|
200
|
-
['verbosity', 'Debugging verbosity', SPV(0,'U8'), 'W',
|
|
201
|
-
{'setter':set_verbosity}],
|
|
202
|
-
['polling', 'Polling interval', SPV(1.0), 'W', {U:'S', LL:0.001, LH:10.1}],
|
|
203
|
-
['cycle', 'Cycle number', SPV(0,'U32'), 'R', {}],
|
|
204
|
-
]
|
|
205
|
-
# append application PVs, defined in define_PVs()
|
|
206
|
-
C_.PVDefs += pvDefs
|
|
207
|
-
_create_PVs()
|
|
208
|
-
return C_.PVs
|
|
209
|
-
|
|
210
|
-
#``````````````````Example of the Main() function````````````````````````````
|
|
211
|
-
if __name__ == "__main__":
|
|
212
|
-
# Argument parsing
|
|
213
|
-
parser = argparse.ArgumentParser(description = __doc__,
|
|
214
|
-
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
215
|
-
epilog=f'{__version__}')
|
|
216
|
-
parser.add_argument('-c','--channels', type=int, default=4, help=
|
|
217
|
-
'Number of channels in the scope')
|
|
218
|
-
parser.add_argument('-p', '--prefix', default='epicsDev:', help=
|
|
219
|
-
'Prefix to be prepended to all PVs')
|
|
220
|
-
parser.add_argument('-l', '--listPVs', action='store_true', help=\
|
|
221
|
-
'List all generated PVs')
|
|
222
|
-
parser.add_argument('-v', '--verbose', action='count', default=0, help=\
|
|
223
|
-
'Show more log messages (-vv: show even more)')
|
|
224
|
-
pargs = parser.parse_args()
|
|
225
|
-
|
|
226
|
-
PVs = create_PVs(_define_PVs())# Provide your PV definitions instead of _define_PVs()
|
|
227
|
-
|
|
228
|
-
# List the PVs
|
|
229
|
-
if pargs.listPVs:
|
|
230
|
-
print(f'List of PVs:')
|
|
231
|
-
for pvname in PVs:
|
|
232
|
-
print(pvname)
|
|
233
|
-
|
|
234
|
-
# Start the Server. Use your set_server, if needed.
|
|
235
|
-
set_server('Start')
|
|
236
|
-
|
|
237
|
-
# Main loop
|
|
238
|
-
server = Server(providers=[PVs])
|
|
239
|
-
printi(f'Server started with polling interval {repr(pvv("polling"))} S.')
|
|
240
|
-
while not C_.serverState.startswith('Exit'):
|
|
241
|
-
time.sleep(pvv("polling"))
|
|
242
|
-
if not C_.serverState.startswith('Stop'):
|
|
243
|
-
poll()
|
|
244
|
-
printi('Server is exited')
|
|
File without changes
|
|
File without changes
|