epicsdev 0.0.0__tar.gz → 1.0.0__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.0
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,113 @@
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',_,_,_,],
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
+ ['Mean:']+ChLine('Mean'),
99
+ ['Peak2Peak:']+ChLine('Peak2Peak'),
100
+ #[''],
101
+ # ["Trigger",D+'trigSourceS',D+'trigLevelS',D+'trigSlopeS',D+'trigModeS'],
102
+ # ['',"Setup"],
103
+ # ["Repair:",D+'updateDataA',D+'deviceClearA',D+'resetScopeA',D+'forceTrigA'],
104
+ # ["Session",D+'SaveSession',D+'RecallSession',"folder:",D+'folderS'],
105
+ # [D+'currentSessionS',"<-current",D+'nextSessionS',"out off",D+'sessionsM'],
106
+ #[{'ATTRIBUTES':{'color':'yellow'}},
107
+ #['tAxis:',D+'tAxis'],
108
+ # [LYRow,'',{'For Experts only!':{**span(6,1),**font(14)}}],
109
+ # [LYRow,'Scope command:', {D+'instrCmdS':span(2,1)},_,{D+'instrCmdR':span(4,1)}],
110
+ # [LYRow,'Special commands', {D+'instrCtrl':span(2,1)},_,_,_,_,_,],
111
+ # [LYRow,'Timing:',{D+'timing':span(6,1)}],
112
+ # [LYRow,'ActOnEvent',D+'actOnEvent','AOE_Limit',D+'aOE_Limit',_,_,_],
113
+ ]
@@ -0,0 +1,332 @@
1
+ """Skeleton and helper functions for creating EPICS PVAccess server"""
2
+ # pylint: disable=invalid-name
3
+ __version__= 'v1.0.0 26-01-16'# re-factored and simplified, comments added. Main() re-writtedn.
4
+ #TODO: NTEnums do not have structure display
5
+
6
+ import sys
7
+ import time
8
+ from p4p.nt import NTScalar, NTEnum
9
+ from p4p.nt.enum import ntenum
10
+ from p4p.server import Server
11
+ from p4p.server.thread import SharedPV
12
+ from p4p.client.thread import Context
13
+
14
+ #``````````````````Module Storage`````````````````````````````````````````````
15
+ class C_():
16
+ """Storage for module members"""
17
+ prefix = ''
18
+ verbose = 0
19
+ cycle = 0
20
+ serverState = ''
21
+ PVs = {}
22
+ PVDefs = []
23
+ #```````````````````Helper methods````````````````````````````````````````````
24
+ def serverState():
25
+ """Return current server state. That is the value of the server PV, but cached in C_ to avoid unnecessary get() calls."""
26
+ return C_.serverState
27
+ def _printTime():
28
+ return time.strftime("%m%d:%H%M%S")
29
+ def printi(msg):
30
+ """Print info message and publish it to status PV."""
31
+ print(f'inf_@{_printTime()}: {msg}')
32
+ def printw(msg):
33
+ """Print warning message and publish it to status PV."""
34
+ txt = f'WAR_@{_printTime()}: {msg}'
35
+ print(txt)
36
+ publish('status',txt)
37
+ def printe(msg):
38
+ """Print error message and publish it to status PV."""
39
+ txt = f'ERR_{_printTime()}: {msg}'
40
+ print(txt)
41
+ publish('status',txt)
42
+ def _printv(msg, level):
43
+ if C_.verbose >= level:
44
+ print(f'DBG{level}: {msg}')
45
+ def printv(msg):
46
+ """Print debug message if verbosity level >=1."""
47
+ _printv(msg, 1)
48
+ def printvv(msg):
49
+ """Print debug message if verbosity level >=2."""
50
+ _printv(msg, 2)
51
+ def printv3(msg):
52
+ """Print debug message if verbosity level >=3."""
53
+ _printv(msg, 3)
54
+
55
+ def pvobj(pvName):
56
+ """Return PV with given name"""
57
+ return C_.PVs[C_.prefix+pvName]
58
+
59
+ def pvv(pvName:str):
60
+ """Return PV value"""
61
+ return pvobj(pvName).current()
62
+
63
+ def publish(pvName:str, value, ifChanged=False, t=None):
64
+ """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."""
65
+ try:
66
+ pv = pvobj(pvName)
67
+ except KeyError:
68
+ printw(f'PV {pvName} not found. Cannot publish value.')
69
+ return
70
+ if t is None:
71
+ t = time.time()
72
+ if not ifChanged or pv.current() != value:
73
+ pv.post(value, timestamp=t)
74
+
75
+ def SPV(initial, meta='', vtype=None):
76
+ """Construct SharedPV.
77
+ meta is a string with characters W,A,E indicating if the PV is writable, has alarm or it is NTEnum.
78
+ vtype should be one of the p4p.nt type definitions (see https://epics-base.github.io/p4p/values.html).
79
+ if vtype is None then the nominal type will be determined automatically.
80
+ """
81
+ typeCode = {
82
+ 's8':'b', 'u8':'B', 's16':'h', 'u16':'H', 'i32':'i', 'u32':'I', 'i64':'l',
83
+ 'u64':'L', 'f32':'f', 'f64':'d', str:'s',
84
+ }
85
+ iterable = type(initial) not in (int,float,str)
86
+ if vtype is None:
87
+ firstItem = initial[0] if iterable else initial
88
+ itype = type(firstItem)
89
+ vtype = {int: 'i32', float: 'f32'}.get(itype,itype)
90
+ tcode = typeCode[vtype]
91
+ if 'E' in meta:
92
+ initial = {'choices': initial, 'index': 0}
93
+ nt = NTEnum(display=True, control='W' in meta)
94
+ else:
95
+ prefix = 'a' if iterable else ''
96
+ nt = NTScalar(prefix+tcode, display=True, control='W' in meta, valueAlarm='A' in meta)
97
+ pv = SharedPV(nt=nt, initial=initial)
98
+ pv.writable = 'W' in meta
99
+ return pv
100
+
101
+ #``````````````````create_PVs()```````````````````````````````````````````````
102
+ def _create_PVs(pvDefs):
103
+ """Create PVs, using definitions from pvDEfs list. Each definition is a list of the form:
104
+ [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"""
105
+ ts = time.time()
106
+ for defs in pvDefs:
107
+ pname,desc,spv,extra = defs
108
+ ivalue = spv.current()
109
+ printv(f'created pv {pname}, initial: {type(ivalue),ivalue}, extra: {extra}')
110
+ C_.PVs[C_.prefix+pname] = spv
111
+ v = spv._wrap(ivalue, timestamp=ts)
112
+ if spv.writable:
113
+ try:
114
+ # 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.
115
+ v['control.limitLow'] = 0
116
+ v['control.limitHigh'] = 0
117
+ except KeyError as e:
118
+ #print(f'control not set for {pname}: {e}')
119
+ pass
120
+ if 'ntenum' in str(type(ivalue)):
121
+ spv.post(ivalue, timestamp=ts)
122
+ else:
123
+ v['display.description'] = desc
124
+ for field in extra.keys():
125
+ if field in ['limitLow','limitHigh','format','units']:
126
+ v[f'display.{field}'] = extra[field]
127
+ if field.startswith('limit'):
128
+ v[f'control.{field}'] = extra[field]
129
+ if field == 'valueAlarm':
130
+ for key,value in extra[field].items():
131
+ v[f'valueAlarm.{key}'] = value
132
+ spv.post(v)
133
+
134
+ # add new attributes. To my surprise that works!
135
+ spv.name = pname
136
+ spv.setter = extra.get('setter')
137
+
138
+ if spv.writable:
139
+ @spv.put
140
+ def handle(spv, op):
141
+ ct = time.time()
142
+ vv = op.value()
143
+ vr = vv.raw.value
144
+ current = spv._wrap(spv.current())
145
+ # check limits, if they are defined. That will be a good example of using control structure and valueAlarm.
146
+ try:
147
+ limitLow = current['control.limitLow']
148
+ limitHigh = current['control.limitHigh']
149
+ if limitLow != limitHigh and not (limitLow <= vr <= limitHigh):
150
+ printw(f'Value {vr} is out of limits [{limitLow}, {limitHigh}]. Ignoring.')
151
+ op.done(error=f'Value out of limits [{limitLow}, {limitHigh}]')
152
+ return
153
+ except KeyError:
154
+ pass
155
+ if isinstance(vv, ntenum):
156
+ vr = vv
157
+ if spv.setter:
158
+ spv.setter(vr)
159
+ # value will be updated by the setter, so get it again
160
+ vr = pvv(spv.name)
161
+ printv(f'putting {spv.name} = {vr}')
162
+ spv.post(vr, timestamp=ct) # update subscribers
163
+ op.done()
164
+ #print(f'PV {pv.name} created: {spv}')
165
+ #,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
166
+ #``````````````````Setters
167
+ def set_verbosity(level):
168
+ """Set verbosity level for debugging"""
169
+ C_.verbose = level
170
+ publish('verbosity',level)
171
+
172
+ def set_server(state=None):
173
+ """Example of the setter for the server PV."""
174
+ #printv(f'>set_server({state}), {type(state)}')
175
+ if state is None:
176
+ state = pvv('server')
177
+ printi(f'Setting server state to {state}')
178
+ state = str(state)
179
+ if state == 'Start':
180
+ printi('Starting the server')
181
+ # configure_instrument()
182
+ # adopt_local_setting()
183
+ publish('server','Started')
184
+ publish('status','Started')
185
+ elif state == 'Stop':
186
+ printi('server stopped')
187
+ publish('server','Stopped')
188
+ publish('status','Stopped')
189
+ elif state == 'Exit':
190
+ printi('server is exiting')
191
+ publish('server','Exited')
192
+ publish('status','Exited')
193
+ elif state == 'Clear':
194
+ publish('acqCount', 0)
195
+ publish('status','Cleared')
196
+ # set server to previous state
197
+ set_server(C_.serverState)
198
+ C_.serverState = state
199
+
200
+ def create_PVs(pvDefs=None):
201
+ """Creates manadatory PVs and adds PVs specified in pvDefs list"""
202
+ U,LL,LH = 'units','limitLow','limitHigh'
203
+ C_.PVDefs = [
204
+ ['version', 'Program version', SPV(__version__), {}],
205
+ ['status', 'Server status', SPV('?','W'), {}],
206
+ ['server', 'Server control',
207
+ SPV('Start Stop Clear Exit Started Stopped Exited'.split(), 'WE'),
208
+ {'setter':set_server}],
209
+ ['verbosity', 'Debugging verbosity', SPV(0,'W','u8'),
210
+ {'setter':set_verbosity}],
211
+ ['polling', 'Polling interval', SPV(1.0,'W'), {U:'S', LL:0.001, LH:10.1}],
212
+ ['cycle', 'Cycle number', SPV(0,'','u32'), {}],
213
+ ]
214
+ # append application's PVs, defined in the pvDefs and create map of providers
215
+ if pvDefs is not None:
216
+ C_.PVDefs += pvDefs
217
+ _create_PVs(C_.PVDefs)
218
+ return C_.PVs
219
+
220
+ def get_externalPV(pvName, timeout=0.5):
221
+ """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."""
222
+ ctxt = Context('pva')
223
+ return ctxt.get(pvName, timeout=timeout)
224
+
225
+ def init_epicsdev(prefix, pvDefs, verbose=0):
226
+ """Check if no other server is running with the same prefix, create PVs and return them as a dictionary."""
227
+ C_.prefix = prefix
228
+ C_.verbose = verbose
229
+ try:
230
+ get_externalPV(prefix+'version')
231
+ print(f'Server for {prefix} already running. Exiting.')
232
+ sys.exit(1)
233
+ except TimeoutError:
234
+ pass
235
+ pvs = create_PVs(pvDefs)
236
+ return pvs
237
+
238
+ #``````````````````Testing stuff``````````````````````````````````````````````
239
+ if __name__ == "__main__":
240
+ import numpy as np
241
+ import argparse
242
+
243
+ def myPVDefs():
244
+ """Example of PV definitions"""
245
+ SET,U,LL,LH = 'setter','units','limitLow','limitHigh'
246
+ alarm = {'valueAlarm':{'lowAlarmLimit':0, 'highAlarmLimit':100}}
247
+ return [ # device-specific PVs
248
+ ['noiseLevel', 'Noise amplitude', SPV(1.E-6,'W'), {SET:set_noise}],
249
+ ['tAxis', 'Full scale of horizontal axis', SPV([0.]), {U:'S'}],
250
+ ['recordLength','Max number of points', SPV(100,'W','u32'),
251
+ {LL:4,LH:1000000, SET:set_recordLength}],
252
+ ['ch1Offset', 'Offset', SPV(0.,'W'), {U:'du'}],
253
+ ['ch1VoltsPerDiv', 'Vertical scale', SPV(1E-3,'W'), {U:'V/du'}],
254
+ ['timePerDiv', 'Horizontal scale', SPV(1.E-6,'W'), {U:'S/du'}],
255
+ ['ch1Waveform', 'Waveform array', SPV([0.]), {}],
256
+ ['ch1Mean', 'Mean of the waveform', SPV(0.,'A'), {}],
257
+ ['ch1Peak2Peak','Peak-to-peak amplitude', SPV(0.,'A'), {}],
258
+ ['alarm', 'PV with alarm', SPV(0,'WA'), alarm],
259
+ ]
260
+ nPatterns = 100 # number of patterns in the waveform.
261
+ pargs = None
262
+ nDivs = 10 # number of divisions on the oscilloscope screen. That is needed to set tAxis when recordLength is changed.
263
+
264
+ def set_recordLength(value):
265
+ """Record length have changed. The tAxis should be updated accordingly."""
266
+ printi(f'Setting tAxis to {value}')
267
+ publish('tAxis', np.arange(value)*pvv('timePerDiv')/nDivs)
268
+ publish('recordLength', value)
269
+
270
+ def set_noise(level):
271
+ """Noise level have changed. Update noise array."""
272
+ printi(f'Setting noise level to {level}')
273
+ pargs.noise = np.random.normal(scale=0.5*level, size=pargs.npoints+nPatterns)
274
+ #printv(f'Noise array updated with level {level}: {pargs.noise}')
275
+ publish('noiseLevel', level)
276
+
277
+ def init(recordLength):
278
+ """Testing function. Do not use in production code."""
279
+ set_recordLength(recordLength)
280
+ set_noise(pvv('noiseLevel'))
281
+
282
+ def poll():
283
+ """Example of polling function"""
284
+ pattern = C_.cycle % nPatterns
285
+ C_.cycle += 1
286
+ printv(f'cycle {C_.cycle}')
287
+ publish('cycle', C_.cycle)
288
+ wf = pargs.noise[pattern:pattern+pvv('recordLength')].copy()
289
+ wf *= pvv('ch1VoltsPerDiv')*nDivs
290
+ wf += pvv('ch1Offset')
291
+ publish('ch1Waveform', wf)
292
+ publish('ch1Peak2Peak', np.ptp(wf))
293
+ publish('ch1Mean', np.mean(wf))
294
+
295
+ # Argument parsing
296
+ parser = argparse.ArgumentParser(description = __doc__,
297
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
298
+ epilog=f'{__version__}')
299
+ parser.add_argument('-l', '--listPVs', action='store_true', help=
300
+ 'List all generated PVs')
301
+ parser.add_argument('-p', '--prefix', default='epicsDev0:', help=
302
+ 'Prefix to be prepended to all PVs')
303
+ parser.add_argument('-n', '--npoints', type=int, default=100, help=
304
+ 'Number of points in the waveform')
305
+ parser.add_argument('-v', '--verbose', action='count', default=0, help=
306
+ 'Show more log messages (-vv: show even more)')
307
+ pargs = parser.parse_args()
308
+
309
+ # Initialize epicsdev and PVs
310
+ PVs = init_epicsdev(pargs.prefix, myPVDefs(), pargs.verbose)
311
+ if pargs.listPVs:
312
+ print('List of PVs:')
313
+ for _pvname in PVs:
314
+ print(_pvname)
315
+
316
+ # Initialize the device, using pargs if needed. That can be used to set the number of points in the waveform, for example.
317
+ init(pargs.npoints)
318
+
319
+ # Start the Server. Use your set_server, if needed.
320
+ set_server('Start')
321
+
322
+ # Main loop
323
+ server = Server(providers=[PVs])
324
+ printi(f'Server started with polling interval {repr(pvv("polling"))} S.')
325
+ while True:
326
+ state = serverState()
327
+ if state.startswith('Exit'):
328
+ break
329
+ if not state.startswith('Stop'):
330
+ poll()
331
+ time.sleep(pvv("polling"))
332
+ 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.0"
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