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 | '''@note: watch EPICS process variables and write them to an XML file |
---|
13 | |
---|
14 | @version: |
---|
15 | ########### SVN repository information ################### |
---|
16 | # $Date: 2009-11-06 19:15:10 +0000 (Fri, 06 Nov 2009) $ |
---|
17 | # $Author: jemian $ |
---|
18 | # $Revision: 85 $ |
---|
19 | # $URL$ |
---|
20 | # $Id: epics2xml.py 85 2009-11-06 19:15:10Z jemian $ |
---|
21 | ########### SVN repository information ################### |
---|
22 | |
---|
23 | @note: for help with xml.dom, see http://docs.python.org/library/xml.dom.html |
---|
24 | ''' |
---|
25 | |
---|
26 | |
---|
27 | from xml.dom import minidom |
---|
28 | import datetime |
---|
29 | #import copy |
---|
30 | import pvConnect |
---|
31 | |
---|
32 | |
---|
33 | class Epics2Xml: |
---|
34 | '''watch PVs and write to XMl''' |
---|
35 | |
---|
36 | def __init__(self, configFile=None): |
---|
37 | '''prepare the settings file |
---|
38 | @param configFile: [string] name of XML file with settings''' |
---|
39 | self.rootElementConfig = 'epics2xml' |
---|
40 | self.rootElementPvdata = 'pvdata' |
---|
41 | self.Clear() |
---|
42 | self.SetConfigFile(configFile) |
---|
43 | |
---|
44 | def GetDb(self): |
---|
45 | '''@return: database''' |
---|
46 | return self.db |
---|
47 | |
---|
48 | def GetConfigFile(self): |
---|
49 | '''@return: name of XML configuration file''' |
---|
50 | return self.configFile |
---|
51 | |
---|
52 | def SetConfigFile(self, thefile): |
---|
53 | '''set the name of XML configuration file |
---|
54 | @param thefile: [string] name of XML file with the configuration''' |
---|
55 | self.configFile = thefile |
---|
56 | |
---|
57 | def Clear(self): |
---|
58 | '''reset the internal data representations (db, pv) to empty''' |
---|
59 | self.db = {} |
---|
60 | self.pv = {} |
---|
61 | self.newData = False |
---|
62 | |
---|
63 | def ReadConfigFile(self): |
---|
64 | '''read the configuration from an XML file into an internal dictionary (self.db) |
---|
65 | |
---|
66 | @note: this method uses xml.dom.minidom (built into all Pythons) |
---|
67 | @see: http://docs.python.org/library/xml.dom.minidom.html |
---|
68 | ''' |
---|
69 | try: |
---|
70 | doc = minidom.parse(self.configFile) # parse an XML file by name |
---|
71 | assert doc.documentElement.tagName == self.rootElementConfig |
---|
72 | except IOError: |
---|
73 | return 'Could not read the XML file: ' + self.configFile |
---|
74 | except AssertionError: |
---|
75 | return 'XML root element is not ' + self.rootElementConfig |
---|
76 | #... read all attributes from the root element |
---|
77 | docElem = doc.documentElement |
---|
78 | self.Clear() |
---|
79 | version = self._get_attribute(docElem, "version", "not-specified") |
---|
80 | try: |
---|
81 | # only handle v1.0 resource configuration files |
---|
82 | assert version == u'1.0' |
---|
83 | except AssertionError: |
---|
84 | doc.unlink() |
---|
85 | return 'Cannot handle file version:', version |
---|
86 | # file verified now |
---|
87 | self.db['title'] = self._getFirstChildData(docElem, "title", "default title") |
---|
88 | tfNode = docElem.getElementsByTagName("targetFile")[0] |
---|
89 | tf_db = {} |
---|
90 | tf_db['name'] = self._get_attribute(tfNode, "name", "").strip() |
---|
91 | tf_db['refreshMinimum'] = self._getFirstChildData(tfNode, "refreshMinimum", "60") |
---|
92 | tf_db['stylesheet'] = self._getFirstChildData(tfNode, "stylesheet", "basic-table.xsl") |
---|
93 | # refreshMinimum |
---|
94 | self.db['targetFile'] = tf_db |
---|
95 | pvlist_db = [] |
---|
96 | for elementNode in docElem.getElementsByTagName("pv"): |
---|
97 | pv_db = {} |
---|
98 | pv_db['name'] = self._get_attribute(elementNode, "name", "").strip() |
---|
99 | pv_db['rate'] = self._get_attribute(elementNode, "rate", "monitor").strip() |
---|
100 | pv_db['description'] = self._getFirstChildData(elementNode, "description", "") |
---|
101 | pvlist_db.append(pv_db) |
---|
102 | self.db['pvList'] = pvlist_db |
---|
103 | |
---|
104 | def _getFirstChildData(self, parent, tag, default): |
---|
105 | '''get the full content of the node or return the default |
---|
106 | @param parent: XML Node object |
---|
107 | @param tag: [string] name of element to find |
---|
108 | @param default: [string] default value to return''' |
---|
109 | str = default |
---|
110 | # need some error checking here |
---|
111 | nodeList = parent.getElementsByTagName(tag) |
---|
112 | node = nodeList[0] |
---|
113 | child = node.firstChild |
---|
114 | data = child.data |
---|
115 | str = data.strip() |
---|
116 | return str |
---|
117 | |
---|
118 | def _get_attribute(self, node, key, default): |
---|
119 | '''get a specific attribute or return the default |
---|
120 | @param node: XML Node object |
---|
121 | @param key: [string] name of attribute to find |
---|
122 | @param default: [string] default value to return''' |
---|
123 | value = default |
---|
124 | if node.attributes.has_key(key): |
---|
125 | value = node.attributes[key].value |
---|
126 | return value |
---|
127 | |
---|
128 | def SaveXmlFile(self): |
---|
129 | '''save the internal data (self.__repr__()) to the target XML file''' |
---|
130 | out = open(self.db['targetFile']['name'], 'w') |
---|
131 | out.write(repr(self)) |
---|
132 | out.close() |
---|
133 | return 'Saved settings to ' + self.configFile |
---|
134 | |
---|
135 | def _SetAttr(self, node, attribute, value): |
---|
136 | '''add attributes that are not empty (but do not strip the whitespace) |
---|
137 | @param node: XML Node object |
---|
138 | @param attribute: [string] name of attribute |
---|
139 | @param value: [string] value of attribute''' |
---|
140 | if len(value) > 0: |
---|
141 | node.setAttribute(attribute, value) |
---|
142 | |
---|
143 | def _makeTextNode(self, doc, tag, value): |
---|
144 | '''create a text node for the XML file |
---|
145 | @param doc: [xml.dom.minidom documentElement object] |
---|
146 | @param tag: [string] element name |
---|
147 | @param value: [string] element text''' |
---|
148 | node = doc.createElement(tag) |
---|
149 | text = doc.createTextNode(value) |
---|
150 | node.appendChild(text) |
---|
151 | return node |
---|
152 | |
---|
153 | def _getDateTime(self): |
---|
154 | '''return current date and time as a tuple |
---|
155 | @return: yyyy-mm-dd hh:mm:ss |
---|
156 | ''' |
---|
157 | t = datetime.datetime.now() |
---|
158 | yyyymmdd = t.strftime("%Y-%m-%d") |
---|
159 | hhmmss = t.strftime("%H:%M:%S") |
---|
160 | return yyyymmdd, hhmmss |
---|
161 | |
---|
162 | def getPvKey(self, name, key, default): |
---|
163 | '''get the internal database representation of the PV key |
---|
164 | or the default |
---|
165 | @param name: EPICS PV name |
---|
166 | @param key: value | date | time |
---|
167 | @param default: use if key not defined''' |
---|
168 | if self.pv.has_key(name): |
---|
169 | pv = self.pv[name] |
---|
170 | if pv.has_key(key): |
---|
171 | content = pv[key] |
---|
172 | else: |
---|
173 | content = default |
---|
174 | else: |
---|
175 | content = default |
---|
176 | return content |
---|
177 | |
---|
178 | def setPvKey(self, name, key, value): |
---|
179 | '''set the internal database representation of the PV key |
---|
180 | @param name: EPICS PV name |
---|
181 | @param key: value | date | time |
---|
182 | @param value: new content from this EPICS PV |
---|
183 | ''' |
---|
184 | if not self.pv.has_key(name): |
---|
185 | self.pv[name] = {} |
---|
186 | pv = self.pv[name] |
---|
187 | pv[key] = value |
---|
188 | self.newData = True |
---|
189 | |
---|
190 | def setPv(self, name, value): |
---|
191 | '''save this PV now |
---|
192 | @param name: EPICS PV name |
---|
193 | @param value: new content from this EPICS PV |
---|
194 | ''' |
---|
195 | yyyymmdd, hhmmss = self._getDateTime() |
---|
196 | self.setPvKey(name, 'value', value) |
---|
197 | self.setPvKey(name, 'date', yyyymmdd) |
---|
198 | self.setPvKey(name, 'time', hhmmss) |
---|
199 | |
---|
200 | def __repr__(self): |
---|
201 | '''default representation of this structure is XML |
---|
202 | @return: XML representation of internal database (db) |
---|
203 | @note: What about a default stylesheet? |
---|
204 | ''' |
---|
205 | # Create the minidom document |
---|
206 | doc = minidom.Document() |
---|
207 | |
---|
208 | # Use a stylesheet |
---|
209 | target = "xml-stylesheet" |
---|
210 | instructions = 'type="text/xsl" href="%s" ' % (self.db['targetFile']['stylesheet']) |
---|
211 | # |
---|
212 | xsltNode = doc.createProcessingInstruction(target, instructions) |
---|
213 | doc.appendChild(xsltNode) |
---|
214 | |
---|
215 | # Create the root element |
---|
216 | root = doc.createElement(self.rootElementPvdata) |
---|
217 | self._SetAttr(root, "version", "1.0") |
---|
218 | yyyymmdd, hhmmss = self._getDateTime() |
---|
219 | self._SetAttr(root, "date", yyyymmdd) |
---|
220 | self._SetAttr(root, "time", hhmmss) |
---|
221 | doc.appendChild(root) |
---|
222 | titleNode = self._makeTextNode(doc, "title", self.db['title']) |
---|
223 | root.appendChild(titleNode) |
---|
224 | for pv in self.db['pvList']: |
---|
225 | pvNode = doc.createElement('pv') |
---|
226 | name = pv['name'] |
---|
227 | pv_value = self.getPvKey(name, "value", "") |
---|
228 | pv_date = self.getPvKey(name, "date", "") |
---|
229 | pv_time = self.getPvKey(name, "time", "") |
---|
230 | self._SetAttr(pvNode, "name", pv['name']) |
---|
231 | self._SetAttr(pvNode, "rate", pv['rate']) |
---|
232 | self._SetAttr(pvNode, "date", pv_date) |
---|
233 | self._SetAttr(pvNode, "time", pv_time) |
---|
234 | pvNode.appendChild(self._makeTextNode(doc, "description", pv['description'])) |
---|
235 | pvNode.appendChild(self._makeTextNode(doc, "value", pv_value)) |
---|
236 | root.appendChild(pvNode) |
---|
237 | return doc.toprettyxml(indent=" ") |
---|
238 | |
---|
239 | def receiver(self, epics_args, user_args): |
---|
240 | '''Response to an EPICS monitor on the channel. Expects to have the PV name as the first user_args. |
---|
241 | @param value: str(epics_args['pv_value'])''' |
---|
242 | value = epics_args['pv_value'] |
---|
243 | pv = user_args[0] |
---|
244 | self.setPv(pv, str(value)) |
---|
245 | #print 'receiver: ', pv, ' = ', str(value) |
---|
246 | |
---|
247 | |
---|
248 | if __name__ == '__main__': |
---|
249 | cf = Epics2Xml("config.xml") |
---|
250 | cf.ReadConfigFile() |
---|
251 | # connect with EPICS here and start waiting for events |
---|
252 | if pvConnect.IMPORTED_CACHANNEL: |
---|
253 | for item in cf.db['pvList']: |
---|
254 | pv = item['name'] |
---|
255 | if pvConnect.testConnect(pv): |
---|
256 | #print pv |
---|
257 | ch = pvConnect.EpicsPv(pv) |
---|
258 | ch.SetUserArgs(pv) |
---|
259 | ch.connectw() |
---|
260 | ch.SetUserCallback(cf.receiver) |
---|
261 | ch.monitor() |
---|
262 | ch.chan.pend_event() |
---|
263 | import time |
---|
264 | while True: |
---|
265 | # wait for the minimum refresh period |
---|
266 | refreshSeconds = int(cf.db['targetFile'] ['refreshMinimum']) |
---|
267 | for seconds in range(refreshSeconds): |
---|
268 | time.sleep(1) |
---|
269 | ch.chan.pend_event() |
---|
270 | # write only if there is new data |
---|
271 | if cf.newData: |
---|
272 | #print cf.__repr__() |
---|
273 | cf.SaveXmlFile() |
---|
274 | # not likely to get here |
---|
275 | pvConnect.on_exit() |
---|
276 | else: |
---|
277 | #---------------------------- |
---|
278 | # load some artificial data to test |
---|
279 | cf.setPv("S:SRcurrent:AI", '99.9853') |
---|
280 | cf.setPv("APS:BarometricPressure:MBR", '992.083') |
---|
281 | print cf.__repr__() |
---|
282 | #---------------------------- |
---|