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.
17 from pylibcerebrum
.NotifyList
import NotifyList
19 """Call RPC functions on serially connected devices over the Cerebrum protocol."""
21 class TimeoutException(Exception):
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
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.
39 #FIXME HACK to et the initialization go smooth despite the __*__ special functions and "config" not yet being set
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.
49 self
._opened
_ser
= self
._ser
= s
53 self
._config
= self
._read
_config
()
56 except TimeoutException
as e
:
58 except ValueError as e
:
59 print('That device threw some nasty ValueError\'ing JSON!', e
)
62 raise serial
.serialutil
.SerialException('Could not connect, giving up after 20 tries')
64 assert(device
is None)
71 def __exit__(self
, exception_type
, exception_val
, trace
):
75 """Close the serial port."""
77 self
._opened
_ser
.close()
78 except AttributeError:
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")
88 return self
._config
.get("type")
91 """Construct an iterator to iterate over *all* (direct or not) child nodes of this node."""
92 return GanglionIter(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())
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())
110 """Return a list of function names of this node."""
111 if "functions" in self
._config
:
112 return list(self
._config
["functions"].keys())
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
)
119 raise TimeoutException('Read {} bytes trying to read {}'.format(len(data
), n
))
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"))
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)):
137 cmd
= b
'\\#' + struct
.pack("<HH", fid
, struct
.calcsize(argsfmt
)) + struct
.pack(argsfmt
, *args
)
140 (clen
,) = struct
.unpack(">H", self
._my
_ser
_read
(2))
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
)
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
188 if "properties" in self
._config
and name
in self
._config
["properties"]:
189 var
= self
._config
["properties"][name
]
191 self
.__setattr
__(name
, newx
)
192 rv
= self
._callfunc
(var
["id"], "", (), var
["fmt"])
193 if isinstance(rv
, list):
194 return NotifyList(rv
, callbacks
=[cb
])
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", ""))
204 #If all of the above falls through...
205 raise AttributeError(name
)
208 """Iterator class for ganglions that recursively iterates over all (direct or indirect) child nodes of a given Ganglion"""
210 def __init__(self
, g
):
212 self
.keyiter
= g
.members
.__iter
__()
220 return self
.miter
.__next
__()
221 except StopIteration:
223 except AttributeError:
225 foo
= self
.g
.__getattr
__(self
.keyiter
.__next
__())
226 self
.miter
= foo
.__iter
__()