summaryrefslogtreecommitdiffstats
path: root/xddlib.py
blob: 6c90e737bd74bd8011b46ec507241239e98e3996 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/python
# vim: set fileencoding=utf-8 tabstop=8 expandtab shiftwidth=4 softtabstop=4 :

""" xddlib.py
Python-library to read the CANopenObject-Part of XDD files
Florian Bruhin
"""

import re
import logging
import warnings
import sys

import canlib

from lxml import etree

class XDDWarning(UserWarning):
    """ Base class for warning in this module. """
    pass

class InvalidXDDWarning(XDDWarning):
    """ Warning raised when the input does not seem to be an XDD file """
    pass

class UnknownDatatypeWarning(XDDWarning):
    """ Warning raised when a datatype is unknown """
    pass

class NoCastConvertFunctionWarning(XDDWarning):
    """ Warning raised when we don't know how to convert a datatype """
    pass

class CastConvertWarning(XDDWarning):
    """ Warning raised when there was an error while casting a datatype """
    pass

class UnknownAttributeWarning(XDDWarning):
    """ Warning raised when there is an attribute which is unknown """
    pass

class XDDFile:
    """ Represents the CANopenObjects in an XDD file """
    
    def __init__(self):
        """ Parses the file, converts units and dumps to objlist """
        self.objlist = [] # The objects will land here

    def parse(self, filename):
        """ Parses the XML and fills objlist """
        try:
            tree = etree.parse(filename) # XML parsing
        except IOError:
            logging.critical("Could not read input file \"{}\"!".format(
                             filename))
            sys.exit(1)
        except etree.XMLSyntaxError:
            logging.critical("Input file is not valid XML!")
            sys.exit(1)
        if tree.getroot().tag != 'ISO15745ProfileContainer':
            warnings.warn("Input file \"{}\" does not seem to be an XDD "
                          "file!".format(filename), InvalidXDDWarning)
        self._setobjects(tree) # Fill objlist
        for obj in self._deepiterate():
            # Convert datatype-numbers
            _convert_datatypes(obj)
            # Convert numeric object types to datatype objects
            _convert_objecttypes(obj)
            # Cast strings to ints/...
            _convert_cast(obj)

    def shift(self, rangestart, rangeend, offset):
        """ Shifts a certain range of objects """
        for obj in self.objlist:
            # Shift indexes
            index = obj.index
            if rangestart <= index <= rangeend:
                newindex = index + offset
                obj.index = newindex

    def sortobjs(self):
        """ Sorts objects (only top-level) """
        self.objlist.sort(key=lambda obj: obj.index)

    def dump(self):
        """ Dumps the object list """
        print(self.objlist)

    def deepdump(self):
        """ Gives back all objects, even subobjects """
        for elem in self._deepiterate():
            print(elem)

    def _deepiterate(self):
        """ Iterates over the object and all subobjects """
        for obj in self.objlist:
            yield obj
            for subobj in obj.subobjects:
                yield subobj

    def _setobjects(self, tree):
        """ Fill objlist with the objects """
        # XPath to extract the objects
        xpath = ("/ISO15745ProfileContainer/ISO15745Profile[2]/ProfileBody/"
                 "ApplicationLayers/CANopenObjectList/CANopenObject")
        self.objlist = []
        xmlobjects = tree.xpath(xpath)
        for xmlobject in xmlobjects:
            canobject = canlib.CanObject()
            attributes = dict(xmlobject.attrib)
            _setattributes(canobject, attributes)
            if len(xmlobject) > 0: # check if the object has subobjects
                for child in _getchildren(xmlobject):
                    subobject = canobject.newchild()
                    _setattributes(subobject, child)
            self.objlist.append(canobject)

def _is_reserved(objdict):
    """ Returns true if an object is reserved """
    match = re.match('^reserved[0-9a-fA_F]*$', objdict['name'])
    return bool(match)

def _setattributes(canobj, attributes):
    """ Sets object attributes """
    xddmapping = {
        # XDD: CanObject
        'index':        'index',
        'subIndex':     'subindex',
        'name':         'name',
        'dataType':     'datatype',
        'accessType':   'accesstype',
        'objectType':   'objecttype',
        'PDOmapping':   'pdomapping',
        'defaultValue': 'default',
        'lowLimit':     'minimum',
        'highLimit':    'maximum',
        'objFlags':     'objflags',
    }
    for label in attributes:
        try:
            canobjectlabel = xddmapping[label]
        except KeyError:
            warnings.warn("The attribute {} is unknown and will be ignored!".
                           format(label), UnknownAttributeWarning)
        else:
            value = attributes[label]
            setattr(canobj, canobjectlabel, value)
    canobj.convertstate = 'xdd'

def _getchildren(xmlobject):
    """ Gets the children of an object """
    children = []
    for child in xmlobject:
        childattr = dict(child.attrib)
        if not _is_reserved(childattr):
            children.append(childattr)
    return children

def _set_string_length(obj):
    """ Sets the correct string length in a datatype """
    if obj.datatype.type_ == 'string':
        basesize = 8
    elif obj.datatype.type_ == 'unicode':
        basesize = 16
    default = obj.default
    length = len(default) + 1
    size = basesize * length
    obj.datatype.length = length
    obj.datatype.size = size

def _set_octet_length(obj):
    """ Sets the correct octet string length """
    # This converts a string like 0x0000 to it's size in bytes. (here: 2).
    # First we remove 2 from the length (the "0x").
    # Then we add 1 again to round up, then we integer-divide (which rounds
    # down). This way, if the length is dividable by 2, adding 1 won't hurt.
    # If it isn't, we will round up.
    #
    # Examples:
    # 0x000000 -> (8 - 2 + 1) // 2 == 3
    # 0x000    -> 2
    # 0x00     -> 1
    # 0x0      -> 1
    default = obj.default
    length = (len(default) - 2 + 1) // 2
    size = 8 * length
    obj.datatype.length = length
    obj.datatype.size = size

def _get_py_datatype(obj):
    """ Returns the Datatype of a CAN datatype """
    # signed: If the type is signed (True) or unsigned (False). For strings,
    #         None is used.
    # type:   Type (int / str)
    # size:   Size in bytes - String: (len("x") + 1) * 8
    # name:   Name like shown in screenshot
    datatypes = {
        0x01: (None,  'boolean', 1,  'BOOLEAN'),
        0x02: (True,  'integer', 8,  'INTEGER8'),
        0x03: (True,  'integer', 16, 'INTEGER16'),
        0x04: (True,  'integer', 32, 'INTEGER32'),
        0x05: (False, 'integer', 8,  'UNSIGNED8'),
        0x06: (False, 'integer', 16, 'UNSIGNED16'),
        0x07: (False, 'integer', 32, 'UNSIGNED32'),
        0x08: (None,  'float',   32, 'REAL32'),
        0x09: (None,  'string',  0,  'VISIBLE_STRING'),
        0x0A: (None,  'octet',   0,  'OCTET_STRING'),
        0x0B: (None,  'unicode', 0,  'UNICODE_STRING'),
       #0x0F: (None,  'domain',  0,  'DOMAIN'),
        0x10: (True,  'integer', 24, 'INTEGER24'),
        0x11: (None,  'float',   64, 'REAL64'),
        0x16: (False, 'integer', 24, 'UNSIGNED24'),
        0x18: (False, 'integer', 40, 'UNSIGNED40'),
        0x19: (False, 'integer', 48, 'UNSIGNED48'),
        0x1A: (False, 'integer', 56, 'UNSIGNED56'),
        0x1B: (False, 'integer', 64, 'UNSIGNED64'),
    }
    xdd_datatype = int(obj.datatype, 16)
    try:
        list_datatype = datatypes[xdd_datatype]
    except KeyError:
        warnings.warn("Datatype {} is not fully implemented yet!".format(
                       xdd_datatype), UnknownDatatypeWarning)
        list_datatype = [None, None, None, 'UNKNOWN ({})'.format(
                         xdd_datatype)]
    py_datatype = canlib.Datatype(*list_datatype)
    return py_datatype

def _convert_objecttypes(obj):
    """ Converts the numeric objecttype to a more meaningful string """
    objecttypes = {
        '7': 'var',
        '8': 'arr',
        '9': 'rec',
    }
    if obj.objecttype is not None:
        obj.objecttype = objecttypes[obj.objecttype]

def _convert_datatypes(obj):
    """ Converts numeric XDD datatypes to datatype-objects """
    # toplevel-objects of subobjects don't have a dataType
    if obj.datatype is not None:
        py_datatype = _get_py_datatype(obj)
        obj.datatype = py_datatype
        special_handlers = {
            'string':  _set_string_length,
            'octet':   _set_octet_length,
            'unicode': _set_string_length,
        }
        try:
            handler = special_handlers[py_datatype.type_]
        except KeyError:
            pass
        else:
            handler(obj)

def _convert_cast(obj):
    """ Converts XML-strings to the right types """
    # Available types:
    #
    # 'truefalse': Convert to a Python boolean
    #              'true' - True / 'false' - False
    # 'integer':   Convert to an Integer
    #              int('1')
    # 'hexint':    Convert to an Integer, original value hex
    #              int('1', 16)
    # 'dt':        Type depends on the datatype
    units = {
        'pdomapping':   'truefalse',
        'index':        'hexint',
        'subindex':     'hexint',
        'default':      'dt',
        'maximum':      'dt',
        'minimum':      'dt',
    } 
    for label in units:  # Get all the unit definitions and see if the key
        if getattr(obj, label) is not None: # is in the active object
            _convert_cast_attribute(obj, label, units)

def _convert_cast_attribute(obj, label, units):
    """ Converts an attribute of an object to the right type """
    value = getattr(obj, label)
    unit = _convert_cast_get_unit(obj, units[label], value)
    convfuncs = {
        'truefalse': _convert_cast_truefalse,
        'boolean':   _convert_cast_hexint,
        'integer':   _convert_cast_int,
        'float':     _convert_cast_hexint,
        'hexint':    _convert_cast_hexint,
        'string':    _convert_cast_nop,
        'octet':     _convert_cast_nop,
        'unicode':   _convert_cast_nop,
    }
    convfunc = convfuncs.get(unit)
    try:
        newvalue = convfunc(value)
    except (ValueError, KeyError):
        warnings.warn("{}={} in {} is not a valid {}!".format(label, value,
                       obj, unit), CastConvertWarning)
    except TypeError: # None
        warnings.warn("Can't convert {}={} to {}!".format(label, value, unit),
                       NoCastConvertFunctionWarning)
        newvalue = value
    setattr(obj, label, newvalue)

def _convert_cast_get_unit(obj, unit, value):
    """ Gets the unit of an object attribute (from the datatype if needed) """
    if unit == 'dt': # If the unit is 'dt', get the real unit from the
                     # datatype
        newunit = obj.datatype.type_
        # The datatype doesn't know hexint, but if an integer starts with '0x'
        # it's hexadecimal.
        if (newunit == 'integer') and value.startswith('0x'):
            newunit = 'hexint'
    else: # unit is already right
        newunit = unit
    return newunit

def _convert_cast_truefalse(value):
    """ Converts to a python boolean """
    conv = { 'true': True, 'false': False }
    newvalue = conv[value]
    return newvalue

def _convert_cast_int(value):
    """ Converts to an integer """
    newvalue = int(value, 10)
    return newvalue

def _convert_cast_hexint(value):
    """ Converts from a hexadecimal integer-string to an integer """
    newvalue = int(value, 16)
    return newvalue

def _convert_cast_nop(value):
    """ Doesn't convert anything """
    return value