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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: epicsdev
3
- Version: 0.0.0
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
+
@@ -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')
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "epicsdev"
7
- version = "0.0.0"
7
+ version = "1.0.1"
8
8
  authors = [
9
9
  { name="Andrey Sukhanov", email="sukhanov@bnl.gov" },
10
10
  ]
epicsdev-0.0.0/README.md DELETED
@@ -1,2 +0,0 @@
1
- # epicsdev
2
- Helper module for creating EPICS PVAccess servers.
@@ -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