Merge branch 'master' of c-leuse:cerebrum
[cerebrum.git] / pylibcerebrum / ganglion.py
blob612ca1b8796c86e56231c31060d8d8f6ef4993e9
1 #!/usr/bin/env python3
3 #Copyright (C) 2012 jaseg <s@jaseg.de>
5 #This program is free software; you can redistribute it and/or
6 #modify it under the terms of the GNU General Public License
7 #version 3 as published by the Free Software Foundation.
9 import json
10 import struct
11 try:
12 import lzma
13 except:
14 import pylzma as lzma
15 import time
16 import serial
17 from pylibcerebrum.NotifyList import NotifyList
18 from pylibcerebrum.timeout_exception import TimeoutException
20 escape = lambda s: s.replace(b'\\', b'\\\\')
22 """Call RPC functions on serially connected devices over the Cerebrum protocol."""
24 class Ganglion(object):
25 """Proxy class for calling remote methods on hardware connected through a serial port using the Cerebrum protocol"""
27 # NOTE: the device config is *not* the stuff from the config "dev" section but
28 #read from the device. It can also be found in that [devicename].config.json
29 #file created by the code generator
30 def __init__(self, node_id, jsonconfig=None, ser=None, name=''):
31 """Ganglion constructor
33 Keyword arguments:
34 device -- the device file to connect to
35 baudrate -- the baudrate to use (default 115200)
36 The other keyword arguments are for internal use only.
38 """
39 object.__setattr__(self, '_ser', ser)
40 object.__setattr__(self, 'node_id', node_id)
41 if not jsonconfig:
42 # get a config
43 i=0
44 while True:
45 try:
46 jsonconfig = self._read_config()
47 time.sleep(0.1)
48 break
49 except TimeoutException as e:
50 print('Timeout', e)
51 except ValueError as e:
52 print('That device threw some nasty ValueError\'ing JSON!', e)
53 i += 1
54 if i > 20:
55 raise serial.serialutil.SerialException('Could not connect, giving up after 20 tries')
56 if not name:
57 name = jsonconfig.get('name')
58 object.__setattr__(self, 'name', name)
59 # populate the object
60 object.__setattr__(self, 'members', {})
61 for name, member in jsonconfig.get('members', {}).items():
62 self.members[name] = Ganglion(node_id, jsonconfig=member, ser=self._ser, name=name)
63 object.__setattr__(self, 'properties', {})
64 for name, prop in jsonconfig.get('properties', {}).items():
65 self.properties[name] = (prop['id'], prop['fmt'], prop.get('access', 'rw'))
66 object.__setattr__(self, 'functions', {})
67 for name, func in jsonconfig.get('functions', {}).items():
68 def proxy_method(*args):
69 return self._callfunc(func["id"], func.get("args", ""), args, func.get("returns", ""))
70 self.functions[name] = proxy_method
71 object.__setattr__(self, 'type', jsonconfig.get('type', None))
72 object.__setattr__(self, 'config', { k: v for k,v in jsonconfig.items() if not k in ['members', 'properties', 'functions'] })
74 def __iter__(self):
75 """Construct an iterator to iterate over *all* (direct or not) child nodes of this node."""
76 return GanglionIter(self)
78 def _read_config(self):
79 """Fetch the device configuration descriptor from the device."""
80 with self._ser as s:
81 s.write(b'\\#' + escape(struct.pack(">H", self.node_id)) + b'\x00\x00\x00\x00')
82 (clen,) = struct.unpack(">H", s.read(2))
83 cbytes = s.read(clen)
84 #decide whether cbytes contains lzma or json depending on the first byte (which is used as a magic here)
85 if cbytes[0] is ord('#'):
86 return json.JSONDecoder().decode(str(lzma.decompress(cbytes[1:]), "utf-8"))
87 else:
88 return json.JSONDecoder().decode(str(cbytes, "utf-8"))
90 def _callfunc(self, fid, argsfmt, args, retfmt):
91 """Call a function on the device by id, directly passing argument/return format parameters."""
92 # Make a list out of the arguments if they are none
93 #print('calling function No. {}, args({}) {}, returning {}'.format(fid, argsfmt, args, retfmt))
94 if not (isinstance(args, tuple) or isinstance(args, list)):
95 args = [args]
96 with self._ser as s:
97 # Send the encoded data
98 cmd = b'\\#' + escape(struct.pack(">HHH", self.node_id, fid, struct.calcsize(argsfmt)) + struct.pack(argsfmt, *args))
99 s.write(cmd)
100 # payload length
101 (clen,) = struct.unpack(">H", s.read(2))
102 # payload data
103 cbytes = s.read(clen)
104 if clen != struct.calcsize(retfmt):
105 # CAUTION! This error is thrown not because the user supplied a wrong value but because the device answered in an unexpected manner.
106 # FIXME raise an error here or let the whole operation just fail in the following struct.unpack?
107 raise AttributeError("Device response format problem: Length mismatch: {} != {}".format(clen, struct.calcsize(retfmt)))
108 rv = struct.unpack(retfmt, cbytes)
109 # Try to interpret the return value in a useful manner
110 if len(rv) == 0:
111 return None
112 elif len(rv) == 1:
113 return rv[0]
114 else:
115 return list(rv)
117 def __dir__(self):
118 """Get a list of all attributes of this object. This includes virtual Cerebrum stuff like members, properties and functions."""
119 return list(self.members.keys()) + list(self.properties.keys()) + list(self.functions.keys()) + list(self.__dict__.keys())
121 # Only now add the setattr magic method so it does not interfere with the above code
122 def __setattr__(self, name, value):
123 """Magic method to set an attribute. This one even handles remote Cerebrum properties."""
124 #check if the name is a known property
125 if name in self.properties:
126 #call the property's Cerebrum setter function
127 varid, varfmt, access = self.properties[name]
128 if not "w" in access:
129 raise TypeError("{} is a read-only property".format(name))
130 return self._callfunc(varid+1, varfmt, value, "")
131 #if the above code falls through, do a normal __dict__ lookup.
132 self.__dict__[name] = value
135 def __getattr__(self, name):
136 """Magic method to get an attribute of this object, considering Cerebrum members, properties and functions.
138 At this point a hierarchy is imposed upon the members/properties/functions that is not present in the implementation:
140 Between a member, a property and a function of the same name the member will be preferred over the property and the property will be preferred over the function. If you should manage to make device have such colliding names, consider using _callfunc(...) directly.
143 if name in self.members:
144 return self.members[name]
146 if name in self.properties:
147 def cb(newx):
148 self.__setattr__(name, newx)
149 varid, varfmt, access = self.properties[name]
150 rv = self._callfunc(varid, "", (), varfmt)
151 # If the return value is a list, construct an auto-updating thingy from it.
152 if isinstance(rv, list):
153 return NotifyList(rv, callbacks=[cb])
154 else:
155 return rv
157 if name in self.functions:
158 return self.functions[name]
160 #If all of the above falls through...
161 raise AttributeError(name)
163 class GanglionIter:
164 """Iterator class for ganglions that recursively iterates over all (direct or indirect) child nodes of a given Ganglion"""
166 def __init__(self, g):
167 self.g = g
168 self.keyiter = g.members.__iter__()
169 self.miter = None
171 def __iter__(self):
172 return self
174 def __next__(self):
175 try:
176 return self.miter.__next__()
177 except StopIteration:
178 pass
179 except AttributeError:
180 pass
181 foo = self.g.__getattr__(self.keyiter.__next__())
182 self.miter = foo.__iter__()
183 return foo