Added a more natural list interface
[cerebrum.git] / pylibcerebrum / ganglion.py
blob88999de53dfc349d58245504775d6fb592dec3bb
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 serial
10 import json
11 import struct
12 try:
13 import lzma
14 except:
15 import pylzma as lzma
16 import time
17 from pylibcerebrum.NotifyList import NotifyList
19 """Call RPC functions on serially connected devices over the Cerebrum protocol."""
21 class TimeoutException(Exception):
22 pass
24 class Ganglion:
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, device=None, baudrate=115200, config=None, ser=None):
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 #FIXME HACK to et the initialization go smooth despite the __*__ special functions and "config" not yet being set
40 self._config = None
41 if ser is None:
42 assert(config is None)
43 s = serial.Serial(port=device, baudrate=baudrate, timeout=1)
44 #Trust me, without the following two lines it *wont* *work*. Fuck serial ports.
45 s.setXonXoff(True)
46 s.setXonXoff(False)
47 s.setDTR(True)
48 s.setDTR(False)
49 self._opened_ser = self._ser = s
50 i=0
51 while True:
52 try:
53 self._config = self._read_config()
54 time.sleep(0.1)
55 break
56 except TimeoutException as e:
57 print('Timeout', e)
58 except ValueError as e:
59 print('That device threw some nasty ValueError\'ing JSON!', e)
60 i += 1
61 if i > 20:
62 raise serial.serialutil.SerialException('Could not connect, giving up after 20 tries')
63 else:
64 assert(device is None)
65 self._config = config
66 self._ser = ser
68 def __del__(self):
69 self.close()
71 def __exit__(self, exception_type, exception_val, trace):
72 self.close()
74 def close(self):
75 """Close the serial port."""
76 try:
77 self._opened_ser.close()
78 except AttributeError:
79 pass
81 def type(self):
82 """Return the cerebrum type of this node.
84 If this method returns None, that means the node has no type as it is the
85 case in pure group nodes. For leaf nodes the leaf type is returned, the
86 root node returns the device type (e.g. "avr")
87 """
88 return self._config.get("type")
90 def __iter__(self):
91 """Construct an iterator to iterate over *all* (direct or not) child nodes of this node."""
92 return GanglionIter(self)
94 @property
95 def members(self):
96 """Return a list of child node names of this node."""
97 if "members" in self._config:
98 return list(self._config["members"].keys())
99 return []
101 @property
102 def properties(self):
103 """Return a list of property names of this node."""
104 if "properties" in self._config:
105 return list(self._config["properties"].keys())
106 return []
108 @property
109 def functions(self):
110 """Return a list of function names of this node."""
111 if "functions" in self._config:
112 return list(self._config["functions"].keys())
113 return []
115 def _my_ser_read(self, n):
116 """Read n bytes from the serial device and raise a TimeoutException in case of a timeout."""
117 data = self._ser.read(n)
118 if len(data) != n:
119 raise TimeoutException('Read {} bytes trying to read {}'.format(len(data), n))
120 return data
122 def _read_config(self):
123 """Fetch the device configuration descriptor from the device."""
124 self._ser.write(b'\\#\x00\x00\x00\x00')
125 (clen,) = struct.unpack(">H", self._my_ser_read(2))
126 cbytes = self._my_ser_read(clen)
127 #decide whether cbytes contains lzma or json depending on the first byte (which is used as a magic here)
128 if cbytes[0] is ord('#'):
129 return json.JSONDecoder().decode(str(lzma.decompress(cbytes[1:]), "utf-8"))
130 else:
131 return json.JSONDecoder().decode(str(cbytes, "utf-8"))
133 def _callfunc(self, fid, argsfmt, args, retfmt):
134 """Call a function on the device by id, directly passing argument/return format parameters."""
135 if not (isinstance(args, tuple) or isinstance(args, list)):
136 args = [args]
137 cmd = b'\\#' + struct.pack("<HH", fid, struct.calcsize(argsfmt)) + struct.pack(argsfmt, *args)
138 self._ser.write(cmd)
139 #payload length
140 (clen,) = struct.unpack(">H", self._my_ser_read(2))
141 #payload data
142 cbytes = self._my_ser_read(clen)
143 if clen != struct.calcsize(retfmt):
144 #CAUTION! This error is thrown not because the user supplied a wrong value but because the device answered in an unexpected manner.
145 #FIXME raise an error here or let the whole operation just fail in the following struct.unpack?
146 raise AttributeError("Device response format problem: Length mismatch: {} != {}".format(clen, struct.calcsize(retfmt)))
147 rv = struct.unpack(retfmt, cbytes)
148 if len(rv) == 0:
149 return None
150 elif len(rv) == 1:
151 return rv[0]
152 else:
153 return list(rv)
155 def __dir__(self):
156 """Get a list of all attributes of this object. This includes virtual Cerebrum stuff like members, properties and functions."""
157 return self.members + self.properties + self.functions + list(self.__dict__.keys())
159 def __setattr__(self, name, value):
160 """Magic method to set an attribute. This one even handles remote Cerebrum properties."""
161 if name is not "_config": #Guard against all too infinite recursion
162 #check if the name is a known property
163 if self._config and "properties" in self._config and name in self._config["properties"]:
164 #call the property's Cerebrum setter function
165 var = self._config["properties"][name]
166 if not "w" in var.get("access", "rw"):
167 raise TypeError("{} is a read-only property".format(name))
168 return self._callfunc(var["id"]+1, var["fmt"], value, "")
169 #if the above code falls through, do a normal __dict__ lookup.
170 self.__dict__[name] = value
172 def __getattr__(self, name):
173 """Magic method to get an attribute of this object, considering Cerebrum members, properties and functions.
175 At this point a hierarchy is imposed upon the members/properties/functions that is not present in the implementation:
177 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.
180 if not self._config: #Guard against all too infinite recursion
181 raise AttributeError(name)
183 if "members" in self._config and name in self._config["members"]:
184 g = Ganglion(config=self._config["members"][name], ser=self._ser)
185 self.__dict__[name] = g
186 return g
188 if "properties" in self._config and name in self._config["properties"]:
189 var = self._config["properties"][name]
190 def cb(newx):
191 self.__setattr__(name, newx)
192 rv = self._callfunc(var["id"], "", (), var["fmt"])
193 if isinstance(rv, list):
194 return NotifyList(rv, callbacks=[cb])
195 else:
196 return rv
198 if "functions" in self._config and name in self._config["functions"]:
199 fun = self._config["functions"][name]
200 def proxy_method(*args):
201 return self._callfunc(fun["id"], fun.get("args", ""), args, fun.get("returns", ""))
202 return proxy_method
204 #If all of the above falls through...
205 raise AttributeError(name)
207 class GanglionIter:
208 """Iterator class for ganglions that recursively iterates over all (direct or indirect) child nodes of a given Ganglion"""
210 def __init__(self, g):
211 self.g = g
212 self.keyiter = g.members.__iter__()
213 self.miter = None
215 def __iter__(self):
216 return self
218 def __next__(self):
219 try:
220 return self.miter.__next__()
221 except StopIteration:
222 pass
223 except AttributeError:
224 pass
225 foo = self.g.__getattr__(self.keyiter.__next__())
226 self.miter = foo.__iter__()
227 return foo