1 | #!/usr/bin/env python |
---|
2 | |
---|
3 | #************************************************************************* |
---|
4 | # Copyright (c) 2009 The University of Chicago, as Operator of Argonne |
---|
5 | # National Laboratory. |
---|
6 | # Copyright (c) 2009 The Regents of the University of California, as |
---|
7 | # Operator of Los Alamos National Laboratory. |
---|
8 | # This file is distributed subject to a Software License Agreement found |
---|
9 | # in the file LICENSE that is included with this distribution. |
---|
10 | #************************************************************************* |
---|
11 | |
---|
12 | ''' |
---|
13 | simplified connections to an EPICS PV using CaChannel |
---|
14 | |
---|
15 | Provides these classes: |
---|
16 | |
---|
17 | CaPollWx |
---|
18 | Use in WX-based GUIs to call ca.poll() in the background |
---|
19 | @param interval_s: [float] interval between calls to ca.poll() |
---|
20 | |
---|
21 | EpicsPv |
---|
22 | manage a CaChannel connection with an EPICS PV |
---|
23 | @param name: [string] EPICS PV to connect |
---|
24 | |
---|
25 | Provides these utility routines: |
---|
26 | |
---|
27 | on_exit(timer) |
---|
28 | Exit handler to stop the ca.poll() |
---|
29 | @param timer: CaPollWx object |
---|
30 | |
---|
31 | CaPoll() |
---|
32 | Use in non-GUI scripts to call ca.poll() in the background |
---|
33 | |
---|
34 | GetRTYP(pv) |
---|
35 | Returns the record type of "pv" |
---|
36 | @param pv:[string] |
---|
37 | @return: [string] EPICS record type or None if cannot connect |
---|
38 | |
---|
39 | testConnect(pv) |
---|
40 | Tests if a CaChannel connection can be established to "pv" |
---|
41 | @param pv:[string] |
---|
42 | @return: True if can connect, otherwise False |
---|
43 | |
---|
44 | receiver(value) |
---|
45 | Example response to an EPICS monitor on the channel |
---|
46 | @param value: str(epics_args['pv_value']) |
---|
47 | |
---|
48 | @version: |
---|
49 | ########### SVN repository information ################### |
---|
50 | # $Date: 2009-11-06 15:49:21 +0000 (Fri, 06 Nov 2009) $ |
---|
51 | # $Author: jemian $ |
---|
52 | # $Revision: 75 $ |
---|
53 | # $URL$ |
---|
54 | # $Id: pvConnect.py 75 2009-11-06 15:49:21Z jemian $ |
---|
55 | ########### SVN repository information ################### |
---|
56 | ''' |
---|
57 | |
---|
58 | |
---|
59 | import sys |
---|
60 | import time |
---|
61 | |
---|
62 | |
---|
63 | try: |
---|
64 | # CaChannel provides access to the EPICS PVs |
---|
65 | import CaChannel |
---|
66 | IMPORTED_CACHANNEL = True |
---|
67 | except: |
---|
68 | IMPORTED_CACHANNEL = False |
---|
69 | |
---|
70 | |
---|
71 | try: |
---|
72 | # wx is needed for the timer to call CaChannel.ca.poll() |
---|
73 | # only use this with a wx-based GUI |
---|
74 | import wx |
---|
75 | IMPORTED_WX = True |
---|
76 | except: |
---|
77 | IMPORTED_WX = False |
---|
78 | |
---|
79 | |
---|
80 | class CaPollWx: |
---|
81 | '''Use in WX-based GUIs to call ca.poll() in the background |
---|
82 | |
---|
83 | Set up a separate thread to trigger periodic calls to the |
---|
84 | EPICS CaChannel.ca.poll() connection. Awaiting (a.k.a., |
---|
85 | outstanding or pending) channel access background |
---|
86 | activity executes during the poll. Calls pend_event() |
---|
87 | with a timeout short enough to poll. |
---|
88 | |
---|
89 | The default polling interval is 0.1 second. |
---|
90 | |
---|
91 | @note: The code will silently do nothing if wx was not imported. |
---|
92 | This routine use the wx.PyTimer() to call ca.poll() frequently |
---|
93 | during the main WX event loop. |
---|
94 | |
---|
95 | @warning: Only use this in a routine that has already called |
---|
96 | wx.App() or an exception will occur. |
---|
97 | Command line code will need to call ca.poll() using a different |
---|
98 | method (such as CaPoll() below). |
---|
99 | ''' |
---|
100 | |
---|
101 | def __init__(self, interval_s = 0.1): |
---|
102 | '''@param interval_s: [float] interval between calls to ca.poll()''' |
---|
103 | if IMPORTED_WX: # only if wx was imported |
---|
104 | self.running = False |
---|
105 | self.interval_s = interval_s |
---|
106 | self.TIMER_ID = 100 |
---|
107 | self.timer = wx.PyTimer(self.poll) |
---|
108 | self.timer.Start(self.TIMER_ID) |
---|
109 | |
---|
110 | def start(self): |
---|
111 | '''start polling''' |
---|
112 | if IMPORTED_WX: # only if wx was imported |
---|
113 | self.running = True |
---|
114 | self.timer.Start(self.TIMER_ID) |
---|
115 | |
---|
116 | def stop(self): |
---|
117 | '''stop polling''' |
---|
118 | if IMPORTED_WX: # only if wx was imported |
---|
119 | self.running = False |
---|
120 | self.timer.Stop() |
---|
121 | |
---|
122 | def poll(self): |
---|
123 | '''Poll for changes in Channel''' |
---|
124 | if IMPORTED_WX: # only if wx was imported |
---|
125 | CaChannel.ca.poll() |
---|
126 | |
---|
127 | def GetInterval(self): |
---|
128 | '''return the current interval between calls to CaChannel.ca.poll()''' |
---|
129 | if IMPORTED_WX: # only if wx was imported |
---|
130 | return self.interval_s |
---|
131 | |
---|
132 | def SetInterval(self, interval_s): |
---|
133 | '''set the next interval between calls to CaChannel.ca.poll()''' |
---|
134 | if IMPORTED_WX: # only if wx was imported |
---|
135 | self.interval_s = interval_s |
---|
136 | |
---|
137 | |
---|
138 | class EpicsPv: |
---|
139 | '''manage a connection with an EPICS PV''' |
---|
140 | |
---|
141 | def __init__(self, name): |
---|
142 | '''initialize the class and set default values |
---|
143 | @param name: [string] EPICS PV to connect''' |
---|
144 | self.pv = name |
---|
145 | self.chan = None |
---|
146 | self.value = None |
---|
147 | self.user_callback = None |
---|
148 | self.epics_args = None |
---|
149 | self.user_args = None |
---|
150 | self.mask = None |
---|
151 | if IMPORTED_CACHANNEL: |
---|
152 | self.mask = CaChannel.ca.DBE_VALUE |
---|
153 | |
---|
154 | def callback(self, epics_args, user_args): |
---|
155 | '''receive an EPICS callback, copy epics_args and user_args, then call user''' |
---|
156 | self.epics_args = epics_args |
---|
157 | self.user_args = user_args |
---|
158 | self.value = epics_args['pv_value'] |
---|
159 | if self.user_callback != None: |
---|
160 | self.user_callback(epics_args, user_args) |
---|
161 | |
---|
162 | def connect(self): |
---|
163 | '''initiate the connection with EPICS''' |
---|
164 | if IMPORTED_CACHANNEL: |
---|
165 | if len(self.pv) > 0: |
---|
166 | self.chan = CaChannel.CaChannel() |
---|
167 | self.chan.search(str(self.pv)) |
---|
168 | |
---|
169 | def connectw(self): |
---|
170 | '''initiate the connection with EPICS, standard wait for the connection''' |
---|
171 | if IMPORTED_CACHANNEL: |
---|
172 | if len(self.pv) > 0: |
---|
173 | self.chan = CaChannel.CaChannel() |
---|
174 | self.chan.searchw(str(self.pv)) |
---|
175 | |
---|
176 | def release(self): |
---|
177 | '''release the connection with EPICS |
---|
178 | @note: Release ALL channels before calling on_exit()''' |
---|
179 | if self.chan != None: |
---|
180 | del self.chan |
---|
181 | self.chan = None |
---|
182 | |
---|
183 | def monitor(self): |
---|
184 | '''Initiate a monitor on the EPICS channel, delivering the |
---|
185 | CaChannel callback to the supplied function. |
---|
186 | |
---|
187 | @note: Example: |
---|
188 | ch = EpicsPv(test_pv) |
---|
189 | ch.connectw() |
---|
190 | uargs = test_pv, widget.SetLabel |
---|
191 | ch.SetUserArgs(uargs) |
---|
192 | ch.SetUserCallback(myCallback) |
---|
193 | ch.monitor() |
---|
194 | |
---|
195 | @warning: At this time, there is not an easy way to turn off monitors. |
---|
196 | Instead, ch.release() the channel (which will set self.chan = None), |
---|
197 | To re-start a monitor after a ch.release(), connect as usual and start |
---|
198 | the monitor again, as the first time. |
---|
199 | ''' |
---|
200 | if IMPORTED_CACHANNEL and self.chan != None: |
---|
201 | type = CaChannel.ca.dbf_type_to_DBR_GR(self.chan.field_type()) |
---|
202 | # call supplied callback routine with default argument list |
---|
203 | # self.user_callback(epics_args, user_args) |
---|
204 | self.chan.add_masked_array_event(type, |
---|
205 | None, self.mask, self.callback, self.user_args) |
---|
206 | |
---|
207 | def GetPv(self): |
---|
208 | '''@return: PV name''' |
---|
209 | return self.pv |
---|
210 | |
---|
211 | def GetValue(self): |
---|
212 | '''@return: value from EPICS from the most recent monitor event''' |
---|
213 | return self.value |
---|
214 | |
---|
215 | def SetPv(self, name): |
---|
216 | '''redefine the PV name only if there is no connection |
---|
217 | @param name: valid EPICS PV name''' |
---|
218 | if self.chan == None: |
---|
219 | self.pv = name |
---|
220 | |
---|
221 | def GetChan(self): |
---|
222 | '''@return: CaChannel channel''' |
---|
223 | return self.chan |
---|
224 | |
---|
225 | def GetEpicsArgs(self): |
---|
226 | '''@return: epics_args from the most recent monitor event''' |
---|
227 | return self.epics_args |
---|
228 | |
---|
229 | def GetUserArgs(self): |
---|
230 | '''@return: user_args from the most recent monitor event''' |
---|
231 | return self.user_args |
---|
232 | |
---|
233 | def SetUserArgs(self, user_args): |
---|
234 | '''define the user_args tuple to use when monitoring |
---|
235 | @param user_args: tuple of user data (for use in user_callback function)''' |
---|
236 | self.user_args = user_args |
---|
237 | |
---|
238 | def SetMask(self, mask): |
---|
239 | '''Define the mask used when applying a channel monitor. |
---|
240 | The default is: self.mask = CaChannel.ca.DBE_VALUE |
---|
241 | @param mask: as defined in the CaChannel manual''' |
---|
242 | self.pv = mask |
---|
243 | |
---|
244 | def GetUserCallback(self): |
---|
245 | '''return the callback function supplied by the caller |
---|
246 | values will be set by self.user_callback(value) |
---|
247 | @return: function object''' |
---|
248 | return self.user_callback |
---|
249 | |
---|
250 | def SetUserCallback(self, user_callback): |
---|
251 | '''Set the callback function supplied by the caller |
---|
252 | values will be set by self.user_callback(value) |
---|
253 | @param user_callback: function object''' |
---|
254 | self.user_callback = user_callback |
---|
255 | |
---|
256 | |
---|
257 | def on_exit(timer = None): |
---|
258 | '''Exit handler to stop the ca.poll() |
---|
259 | @param timer: CaPollWx object |
---|
260 | |
---|
261 | Call this to cleanup when program is exiting. |
---|
262 | ONLY call this function during a program's exit handling. |
---|
263 | If ca.task_exit() is not called, then expect to see the errors: |
---|
264 | FATAL: exception not rethrown |
---|
265 | Abort |
---|
266 | ''' |
---|
267 | if IMPORTED_CACHANNEL: # only if ca binding was loaded |
---|
268 | CaChannel.ca.task_exit() |
---|
269 | try: # fail no matter what |
---|
270 | if timer != None: |
---|
271 | timer.stop() |
---|
272 | except: |
---|
273 | pass |
---|
274 | |
---|
275 | |
---|
276 | def CaPoll(): |
---|
277 | '''Use in non-GUI scripts to call ca.poll() in the background''' |
---|
278 | if IMPORTED_CACHANNEL: |
---|
279 | CaChannel.ca.poll() |
---|
280 | |
---|
281 | |
---|
282 | def GetRTYP(pv): |
---|
283 | '''Returns the record type of "pv" |
---|
284 | @param pv:[string] |
---|
285 | @return: [string] EPICS record type or None if cannot connect''' |
---|
286 | if not IMPORTED_CACHANNEL: |
---|
287 | return None |
---|
288 | if len(pv) == 0: |
---|
289 | return None |
---|
290 | base = pv.split('.')[0] |
---|
291 | if testConnect(base): |
---|
292 | rtyp_pv = base + '.RTYP' |
---|
293 | ch = EpicsPv(rtyp_pv) |
---|
294 | ch.connectw() |
---|
295 | rtyp = ch.chan.getw() |
---|
296 | del ch |
---|
297 | return rtyp |
---|
298 | else: |
---|
299 | return None |
---|
300 | |
---|
301 | |
---|
302 | def testConnect(pv): |
---|
303 | '''Tests if a CaChannel connection can be established to "pv" |
---|
304 | @param pv:[string] |
---|
305 | @return: True if can connect, otherwise False''' |
---|
306 | result = False |
---|
307 | if not IMPORTED_CACHANNEL: |
---|
308 | return result |
---|
309 | try: |
---|
310 | #print 'testConnect:', type(pv), pv |
---|
311 | chan = CaChannel.CaChannel() |
---|
312 | chan.searchw(str(pv)) |
---|
313 | val = chan.getw() |
---|
314 | del chan |
---|
315 | result = True |
---|
316 | except (TypeError, CaChannel.CaChannelException), status: |
---|
317 | # python3: (TypeError, CaChannel.CaChannelException) as status |
---|
318 | #print 'testConnect:', status |
---|
319 | pass |
---|
320 | return result |
---|
321 | |
---|
322 | |
---|
323 | def receiver(epics_args, user_args): |
---|
324 | '''Example response to an EPICS monitor on the channel |
---|
325 | @param value: str(epics_args['pv_value'])''' |
---|
326 | value = epics_args['pv_value'] |
---|
327 | print 'receiver', 'updated value:', str(value) |
---|
328 | |
---|
329 | |
---|
330 | if __name__ == '__main__': |
---|
331 | if IMPORTED_CACHANNEL: |
---|
332 | test_pv = 'S:SRcurrentAI' |
---|
333 | if testConnect(test_pv): |
---|
334 | print "recordType(%s) = %s" % (test_pv, GetRTYP(test_pv)) |
---|
335 | ch = EpicsPv(test_pv) |
---|
336 | ch.connectw() |
---|
337 | ch.SetUserCallback(receiver) |
---|
338 | ch.monitor() |
---|
339 | ch.chan.pend_event() |
---|
340 | import time |
---|
341 | count = 5 |
---|
342 | for seconds in range(count): |
---|
343 | time.sleep(1) |
---|
344 | ch.chan.pend_event() |
---|
345 | print count - seconds - 1, ch.GetPv(), '=', ch.GetValue() |
---|
346 | ch.release() |
---|
347 | on_exit() |
---|
348 | else: |
---|
349 | print "CaChannel is missing, cannot run" |
---|