source: moxy/trunk/src/moxy/moxy_xml.py @ 635

Last change on this file since 635 was 635, checked in by jemian, 12 years ago

fix the imports, add some TODO items - more to come

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Date Revision Author HeadURL Id
File size: 22.4 KB
Line 
1#!/usr/bin/env python
2
3#*************************************************************************
4# Copyright (c) 2009-2010 The University of Chicago, as Operator of Argonne
5#     National Laboratory.
6# Copyright (c) 2009-2010 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: support the XML settings file for the moxy application
13
14This Python file provides routines to read and write XML settings
15files for the moxy application.  An example of the XML file is
16shown below.  The routines manage the settings internally with a
17Python dictionary.  Interface routines are used to read and write
18the various components of the file.  *HOWEVER*, the EPICS configuration
19is communicated in a Python dictionary.  An example of the Python
20dictionary with the EPICS configuration is shown below.
21
22@version:
23########### SVN repository information ###################
24# $Date: 2011-09-09 17:29:58 +0000 (Fri, 09 Sep 2011) $
25# $Author: jemian $
26# $Revision: 635 $
27# $URL: moxy/trunk/src/moxy/moxy_xml.py $
28# $Id: moxy_xml.py 635 2011-09-09 17:29:58Z jemian $
29########### SVN repository information ###################
30
31@note: for help with xml.dom, see http://docs.python.org/library/xml.dom.html
32
33@note: Here is an example XML file:
34<?xml version="1.0" ?>
35<moxy date="2009-04-09" time="10:27:00" version="1.0">
36  <XYpair name="example" selected="True">
37    <EPICS_configuration>
38      <axis name="x">
39        <flag isMotorRec="False" />
40        <field name="VAL" pv="32idbLAX:float1" />
41        <field name="RBV" pv="32idbLAX:float2" />
42        <field name="DESC" pv="32idbLAX:string1" />
43        <field name="EGU" pv="32idbLAX:string2" />
44        <field name="DMOV" pv="32idbLAX:bit1" />
45        <field name="STOP" pv="32idbLAX:bit2" />
46      </axis>
47      <axis name="y">
48        <flag isMotorRec="True" />
49        <field name="VAL" pv="32idbLAX:m58:c1:m1" /><!-- USAXS a1y -->
50      </axis>
51    </EPICS_configuration>
52    <tab name="page 1">
53      <row name="page 1, row 0" x="1.0" y="-1.0" selected="True"/>
54      <row name="page 1, row 1" x="1.1" y="-1.1"/>
55      <row/>
56    </tab>
57    <tab name="page 2" selected="True">
58      <row name="page 2, row 0" x="2.0" y="-2.0"/>
59      <row name="page 2, row 1" x="2.1" y="-2.1"/>
60      <row name="page 2, row 2" x="2.2" y="-2.2" selected="True"/>
61      <row name="page 2, row 3" x="2.3" y="-2.3"/>
62      <row name="page 2, row 4" x="2.4" y="-2.4"/>
63      <row name="page 2, row 5" x="2.5" y="-2.5"/>
64      <row name="page 2, row 6" x="2.6" y="-2.6"/>
65    </tab>
66    <tab name="empty page"/>
67    <tab name="page 3">
68  </XYpair>
69</moxy>
70
71@note: Here is an example Python dictionary of the EPICS configuration above:
72    example_dictionary = {
73        'x': {
74            'isMotorRec': False,
75            'VAL': '32idbLAX:float1',
76            'RBV': '32idbLAX:float2',
77            'DESC': '32idbLAX:string1',
78            'EGU': '32idbLAX:string2',
79            'DMOV': '32idbLAX:bit1',
80            'STOP': '32idbLAX:bit2'
81        },
82        'y': {
83            'isMotorRec': True,
84            'VAL': '32idbLAX:m58:c1:m1.VAL'
85        }
86    }
87'''
88
89
90from xml.dom import minidom
91import datetime
92import copy
93import moxy_axis
94
95
96class Settings:
97    '''handle the XML settings file'''
98
99    def __init__(self, settingsFile=None):
100        '''prepare the settings file
101            @param settingsFile: [string] name of XML file with settings'''
102        self.rootElement = 'moxy'
103        # TODO: consider supporting legacy 'wxmtxy' root elements, too
104        self.Clear()
105        self.SetSettingsFile(settingsFile)
106
107    def GetDb(self):
108        '''@return: database'''
109        return self.db
110
111    def GetSettingsFile(self):
112        '''@return: name of XML settings file'''
113        return self.settingsFile
114
115    def SetSettingsFile(self, thefile):
116        '''set the name of XML settings file
117            @param thefile: [string] name of XML file with settings'''
118        self.settingsFile = thefile
119
120    def Clear(self):
121        '''reset the internal data representation (db) to empty'''
122        self.db = {}
123
124    def NewPair(self, title=''):
125        ''' create space in the database (db) for a new pair
126            and sets defaults for fields
127
128            @param title: [string] the title of the XY_pair set (default="")
129            @return: the index number'''
130        if self.CountPairs() == -1:
131            self.db[u"pairs"] = []
132        pairdb = {}
133        pairdb[u"@name"] = title
134        pairdb[u"@selected"] = False
135        self.db[u'pairs'].append(pairdb)
136        return len(self.db[u'pairs'])-1
137
138    def GetPairTitle(self, pairnum):
139        '''return the name of the XY_pair
140            @param pairnum: [int] index number of the XY_pair'''
141        return self.db[u"pairs"][pairnum][u"@name"]
142
143    def SetPairTitle(self, pairnum, title):
144        '''set the name of the XY_pair
145            @param pairnum: [int] index number of the XY_pair'
146            @param title: [string] name of the XY_pair'''
147        self.db[u"pairs"][pairnum][u"@name"] = title
148
149    def SelectPair(self, pairnum):
150        '''set the "selected" attribute of the pair
151            @param pairnum: [int] index number of the XY_pair'''
152        for pair in self.db[u"pairs"]:   # first, deselect all pairs
153            pair[u"@selected"] = False
154        self.db[u"pairs"][pairnum][u"@selected"] = True
155
156    def GetSelectedPair(self):
157        '''@return: index number of the "selected" pair (-1 if none selected)'''
158        selected = -1
159        try:
160            pairs = self.db[u"pairs"]
161            for pairnum in range(len(pairs)):
162                if pairs[pairnum][u"@selected"]:
163                    selected = pairnum
164                    break
165        except:
166            pass
167        return selected
168
169    def CountPairs(self):
170        '''@return: number of pairs'''
171        try:
172            return len(self.db[u"pairs"])
173        except:
174            return -1
175
176    def NewEpicsConfig(self, pairnum):
177        '''Create internal space for a new EPICS configuration
178            @param pairnum: [int] index number of the XY_pair'''
179        pairdb = self.db[u"pairs"][pairnum]
180        pairdb[u"epics"] = {}
181        epicsdb = pairdb[u"epics"]
182        for axis in ['x', 'y']:
183            epicsdb[axis] = {}
184            axisdb = epicsdb[axis]
185            for field in moxy_axis.field_list:
186                axisdb[field] = ""
187            axisdb[u"isMotorRec"] = False
188
189    def GetEpicsConfig(self, pairnum):
190        '''Get a deep copy Python dictionary of the current EPICS PV config.
191            @param pairnum: [int] index number of the XY_pair
192            @return: the current EPICS configuration'''
193        return copy.deepcopy(self.db[u"pairs"][pairnum][u"epics"])
194
195    def SetEpicsConfig(self, pairnum, config):
196        '''set the current EPICS configuration
197            @param pairnum: [int] index number of the XY_pair
198            @param config: Python dictionary of EPICS PV configuration'''
199        pairdb = self.db[u"pairs"][pairnum]
200        deep = copy.deepcopy(config)
201        pairdb[u"epics"] = deep
202
203    def SetEpicsField(self, pairnum, axis, field, value):
204        '''Define the EPICS config for a specific field
205            @param pairnum: [int] index number of the XY_pair'
206            @param axis: [string] "x" or "y"'
207            @param field: [string] member of moxy_axis.field_list'
208            @param value: [string] value of this field'''
209        try:
210            axisdb = self.db[u"pairs"][pairnum][u"epics"][axis]
211            axisdb[field] = value
212        except:
213            print "Could not assign EPICS field", pairnum, axis, field, value
214
215
216    def NewTab(self, pairnum, title=''):
217        ''' create space in the database (db) pair for a new tab
218            and sets defaults for fields
219
220            @param pairnum: [int] index number of the XY_pair
221            @param title: the title of the pair set (default="")
222            @return the index number'''
223        pairdb = self.db[u"pairs"][pairnum]
224        if not pairdb.has_key(u"tabs"):
225            pairdb[u"tabs"] = []
226        tabdb = {}
227        tabdb[u"@name"] = title
228        tabdb[u"@selected"] = False
229        pairdb[u"tabs"].append(tabdb)
230        return len(pairdb[u"tabs"])-1
231
232    def GetTabTitle(self, pairnum, tabnum):
233        '''return the name of the tab
234            @param pairnum: [int] index number of the XY_pair
235            @param tabnum: [int] index number of the Tab object'''
236        return self.db[u"pairs"][pairnum][u"tabs"][tabnum][u"@name"]
237
238    def SetTabTitle(self, pairnum, tabnum, title):
239        '''set the name attribute of the tab
240            @param pairnum: [int] index number of the XY_pair
241            @param tabnum: [int] index number of the Tab object
242            @param title: [string] title the Tab object'''
243        self.db[u"pairs"][pairnum][u"tabs"][tabnum]["@name"] = title
244
245    def SelectTab(self, pairnum, tabnum):
246        '''set the selected attribute of the pair
247            @param pairnum: [int] index number of the XY_pair
248            @param tabnum: [int] index number of the Tab object'''
249        pairdb = self.db[u"pairs"][pairnum]
250        for tab in pairdb[u"tabs"]:   # first, deselect all tabs
251            tab[u"@selected"] = False
252        pairdb[u"tabs"][tabnum][u"@selected"] = True
253
254    def GetSelectedTab(self, pairnum):
255        '''return the index number of the selected tab
256            @param pairnum: [int] index number of the XY_pair'''
257        selected = -1
258        try:
259            tabs = self.db[u"pairs"][pairnum][u"tabs"]
260            for tabnum in range(len(tabs)):
261                if tabs[tabnum][u"@selected"]:
262                    selected = tabnum
263                    break
264        except:
265            pass
266        return selected
267
268    def CountTabs(self, pairnum):
269        '''return the number of tabs
270            @param pairnum: [int] index number of the XY_pair'''
271        try:
272            return len(self.db[u"pairs"][pairnum][u"tabs"])
273        except:
274            return -1
275
276    def NewRow(self, pairnum, tabnum, title=''):
277        ''' create space in the database (db) pair for a new tab
278            and sets defaults for fields
279
280            @param pairnum: [int] index number of the XY_pair
281            @param tabnum: [int] index number of the Tab object
282            @param title: the title of the Tab object (default="")
283            @return the index number'''
284        tabdb = self.db[u"pairs"][pairnum][u"tabs"][tabnum]
285        if not tabdb.has_key(u"rows"):
286            tabdb[u"rows"] = []
287        rowdb = {}
288        rowdb[u"@name"] = title
289        rowdb[u"@selected"] = False
290        rowdb[u"@x"] = ""
291        rowdb[u"@y"] = ""
292        tabdb[u"rows"].append(rowdb)
293        return len(tabdb[u"rows"])-1
294
295    def GetRowTitle(self, pairnum, tabnum, rownum):
296        '''return the name of the row
297            @param pairnum: [int] index number of the XY_pair
298            @param tabnum: [int] index number of the Tab object
299            @param rownum: [int] index number of the Row object'''
300        tabdb = self.db[u"pairs"][pairnum][u"tabs"][tabnum]
301        return tabdb[u"rows"][rownum][u"@name"]
302
303    def SetRowTitle(self, pairnum, tabnum, rownum, title):
304        '''set the name attribute of the row
305            @param pairnum: [int] index number of the XY_pair
306            @param tabnum: [int] index number of the Tab object
307            @param rownum: [int] index number of the Row object
308            @param title: [string] title the Tab object'''
309        tabdb = self.db[u"pairs"][pairnum][u"tabs"][tabnum]
310        tabdb[u"rows"][rownum][u"@name"] = title
311
312    def GetRowXY(self, pairnum, tabnum, rownum):
313        '''return the name of the row
314            @param pairnum: [int] index number of the XY_pair
315            @param tabnum: [int] index number of the Tab object
316            @param rownum: [int] index number of the Row object'''
317        tabdb = self.db[u"pairs"][pairnum][u"tabs"][tabnum]
318        x = tabdb[u"rows"][rownum][u"@x"]
319        y = tabdb[u"rows"][rownum][u"@y"]
320        return x, y
321
322    def SetRowXY(self, pairnum, tabnum, rownum, x, y):
323        '''set the name attribute of the row
324            @param pairnum: [int] index number of the XY_pair
325            @param tabnum: [int] index number of the Tab object
326            @param x: [float] X axis position
327            @param y: [float] Y axis position'''
328        tabdb = self.db[u"pairs"][pairnum][u"tabs"][tabnum]
329        tabdb[u"rows"][rownum][u"@x"] = x
330        tabdb[u"rows"][rownum][u"@y"] = y
331
332    def CountRows(self, pairnum, tabnum):
333        '''return the number of rows
334            @param pairnum: [int] index number of the XY_pair
335            @param tabnum: [int] index number of the Tab object'''
336        try:
337            return len(self.db[u"pairs"][pairnum][u"tabs"][tabnum][u"rows"])
338        except:
339            return -1
340
341    def GetSelectedRow(self, pairnum, tabnum):
342        '''return the index number of the selected row
343            @param pairnum: [int] index number of the XY_pair
344            @param tabnum: [int] index number of the Tab object'''
345        selected = -1
346        try:
347            rows = self.db[u"pairs"][pairnum][u"tabs"][tabnum][u"rows"]
348            for rownum in range(len(rows)):
349                if rows[rownum][u"@selected"]:
350                    selected = rownum
351                    break
352        except:
353            pass
354        return selected
355
356    def SelectRow(self, pairnum, tabnum, rownum):
357        '''set the selected attribute of the pair
358            @param pairnum: [int] index number of the XY_pair
359            @param tabnum: [int] index number of the Tab object
360            @param rownum: [int] index number of the Row object'''
361        tabdb = self.db[u"pairs"][pairnum][u"tabs"][tabnum]
362        for row in tabdb[u"rows"]:   # first, deselect all rows
363            row[u"@selected"] = False
364        tabdb[u"rows"][rownum][u"@selected"] = True
365
366    def ReadXmlFile(self):
367        '''read the settings from a file into an internal dictionary (self.db)
368
369            @note: this method uses xml.dom.minidom (built into all Pythons)
370            @see: http://docs.python.org/library/xml.dom.minidom.html
371        '''
372        try:
373            doc = minidom.parse(self.settingsFile) # parse an XML file by name
374            assert doc.documentElement.tagName == self.rootElement
375        except IOError:
376            return 'Could not read the XML file: ' + self.settingsFile
377        except AssertionError:
378            return 'XML root element is not ' + self.rootElement
379        #... read all attributes from the root element
380        docElem = doc.documentElement
381        self.Clear()
382        version = self._get_attribute(docElem, "version", "not-specified")
383        try:
384            # only handle v1.0 resource configuration files
385            assert version == u'1.0'
386        except AssertionError:
387            doc.unlink()
388            return 'Cannot handle file version:', version
389        # file verified now
390        for pairNode in docElem.getElementsByTagName("XYpair"):
391            title = self._get_attribute(pairNode, "name", "")
392            selected = self._get_attribute(pairNode, "selected", "")
393            pairnum = self.NewPair(title)
394            if selected.lower() == "true":
395                self.SelectPair(pairnum)
396            self.NewEpicsConfig(pairnum)
397            for EpicsNode in pairNode.getElementsByTagName("EPICS_configuration"):
398                for axisNode in EpicsNode.getElementsByTagName("axis"):
399                    axis = self._get_attribute(axisNode, "name", "")
400                    #isMotorRec
401                    for flagNode in axisNode.getElementsByTagName("flag"):
402                        text = self._get_attribute(flagNode, "isMotorRec", "False")
403                        self.SetEpicsField(pairnum, axis, "isMotorRec", (text == "True"))
404                    for fieldNode in axisNode.getElementsByTagName("field"):
405                        name = self._get_attribute(fieldNode, "name", "")
406                        pv = self._get_attribute(fieldNode, "pv", "")
407                        if (len(name)>0) and (len(pv)>0):
408                            #print pairnum, axis, name, pv
409                            self.SetEpicsField(pairnum, axis, name, pv)
410            for tabNode in pairNode.getElementsByTagName("tab"):
411                title = self._get_attribute(tabNode, "name", "")
412                selected = self._get_attribute(tabNode, "selected", "")
413                tabnum = self.NewTab(pairnum, title)
414                if selected.lower() == "true":
415                    self.SelectTab(pairnum, tabnum)
416                # EPICS settings here
417                for rowNode in tabNode.getElementsByTagName("row"):
418                    title = self._get_attribute(rowNode, "name", "")
419                    selected = self._get_attribute(rowNode, "selected", "")
420                    x = self._get_attribute(rowNode, "x", "")
421                    y = self._get_attribute(rowNode, "y", "")
422                    rownum = self.NewRow(pairnum, tabnum, title)
423                    self.SetRowXY(pairnum, tabnum, rownum, x, y)
424        doc.unlink()  # ensures XML document is disposed cleanly
425        return None
426
427    def _get_attribute(self, node, key, default):
428        '''get a specific attribute or return the default
429            @param node: XML Node object
430            @param key: [string] name of attribute to find
431            @param default: [string] default value to return'''
432        value = default
433        if node.attributes.has_key(key):
434            value = node.attributes[key].value
435        return value
436
437    def SaveXmlFile(self):
438        '''save the internal dictionary (self.db) to an XML file
439           @note: What about using/saving a default stylesheet?
440           @see: http://www.boddie.org.uk/python/XML_intro.html
441        '''
442        out = open(self.settingsFile, 'w')
443        out.write(repr(self))
444        out.close()
445        #
446        # What about a default stylesheet?
447        #
448       
449        return 'Saved settings to ' + self.settingsFile
450
451    def _SetAttr(self, node, attribute, value):
452        '''add attributes that are not empty (but do not strip the whitespace)
453            @param node: XML Node object
454            @param attribute: [string] name of attribute
455            @param value: [string] value of attribute'''
456        if len(value) > 0:
457            node.setAttribute(attribute, value)
458
459    def _makeTextNode(self, doc, tag, value):
460        '''create a text node for the XML file
461            @param doc: [xml.dom.minidom documentElement object]
462            @param tag: [string] element name
463            @param value: [string] element text'''
464        node = doc.createElement(tag)
465        text = doc.createTextNode(value)
466        node.appendChild(text)
467        return node
468
469    def __repr__(self):
470        '''default representation of this structure is XML
471            @return: XML representation of internal database (db)
472            @note: What about a default stylesheet?
473        '''
474        t = datetime.datetime.now()
475        yyyymmdd = t.strftime("%Y-%m-%d")
476        hhmmss = t.strftime("%H:%M:%S")
477
478        # Create the minidom document
479        doc = minidom.Document()
480
481        # Create the root element
482        root = doc.createElement(self.rootElement)
483        self._SetAttr(root, "version", "1.0")
484        self._SetAttr(root, "date", yyyymmdd)
485        self._SetAttr(root, "time", hhmmss)
486        doc.appendChild(root)
487        selectedpairnum = self.GetSelectedPair()
488        for pairnum in range(self.CountPairs()):
489            pairnode = doc.createElement("XYpair")
490            self._SetAttr(pairnode, "name", 
491                   self.GetPairTitle(pairnum))
492            if selectedpairnum == pairnum:
493                self._SetAttr(pairnode, "selected", "True")
494            if self.db[u"pairs"][pairnum].has_key(u"epics"):
495                epicsnode = doc.createElement("EPICS_configuration")
496                epicsdb = self.db[u"pairs"][pairnum][u"epics"]
497                for axis in epicsdb:
498                    axisnode = doc.createElement("axis")
499                    self._SetAttr(axisnode, "name", axis)
500                    field = "isMotorRec"
501                    if field in epicsdb[axis]:
502                        node = doc.createElement("flag")
503                        self._SetAttr(node, field, str(epicsdb[axis][field]))
504                        axisnode.appendChild(node)
505                    for field in moxy_axis.field_list:
506                        if field in epicsdb[axis]:
507                            if len(epicsdb[axis][field])>0:
508                                node = doc.createElement("field")
509                                self._SetAttr(node, "name", field)
510                                self._SetAttr(node, "pv", str(epicsdb[axis][field]))
511                                axisnode.appendChild(node)
512                    epicsnode.appendChild(axisnode)
513                pairnode.appendChild(epicsnode)
514            selectedtabnum = self.GetSelectedTab(pairnum)
515            for tabnum in range(self.CountTabs(pairnum)):
516                tabnode = doc.createElement("tab")
517                self._SetAttr(tabnode, "name", self.GetTabTitle(pairnum, tabnum))
518                if selectedtabnum == tabnum:
519                    self._SetAttr(tabnode, "selected", "True")
520                selectedrownum = self.GetSelectedRow(pairnum, tabnum)
521                for rownum in range(self.CountRows(pairnum, tabnum)):
522                    rownode = doc.createElement("row")
523                    label = self.GetRowTitle(pairnum, tabnum, rownum)
524                    x, y = self.GetRowXY(pairnum, tabnum, rownum)
525                    self._SetAttr(rownode, "name", label)
526                    self._SetAttr(rownode, "x", x)
527                    self._SetAttr(rownode, "y", y)
528                    if selectedrownum == rownum:
529                        self._SetAttr(rownode, "selected", "True")
530                    tabnode.appendChild(rownode)
531                pairnode.appendChild(tabnode)
532            root.appendChild(pairnode)
533        return doc.toprettyxml(indent="  ")
534
535
536if __name__ == '__main__':
537    rc = Settings("examples/test-settings.xml")
538    rc.ReadXmlFile()
539    rc.SetSettingsFile('output-test.xml')
540    rc.SaveXmlFile()
541
542    pj = Settings()
543    pairnum = pj.NewPair("my test pair")
544    tabnum = pj.NewTab(pairnum, title='my test tab')
545    tabnum = pj.NewTab(pairnum, title='another')
546    pj.SelectTab(pairnum, tabnum)
547    tabnum = pj.NewTab(pairnum, title='another')
548    tabnum = pj.NewTab(pairnum, title='another')
549    pairnum = pj.NewPair("another")
550    pj.SelectPair(pairnum)
551    tabnum = pj.NewTab(pairnum, title='another')
552    rownum = pj.NewRow(pairnum, tabnum, "beam center")
553    pj.SelectRow(pairnum, tabnum, rownum)
554    tabnum = pj.NewTab(pairnum, title='another')
555    pairnum = pj.NewPair("another")
556    print str(pj)
Note: See TracBrowser for help on using the repository browser.