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
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
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 object.__setattr
__(self
, '_ser', ser
)
40 object.__setattr
__(self
, 'node_id', node_id
)
41 object.__setattr
__(self
, 'name', name
)
47 jsonconfig
= self
._read
_config
()
50 except TimeoutException
as e
:
52 except ValueError as e
:
53 print('That device threw some nasty ValueError\'ing JSON!', e
)
56 raise serial
.serialutil
.SerialException('Could not connect, giving up after 20 tries')
58 object.__setattr
__(self
, 'members', {})
59 for name
, member
in jsonconfig
.get('members', {}).items():
60 self
.members
[name
] = Ganglion(node_id
, jsonconfig
=member
, ser
=self
._ser
, name
=self
.name
+'/'+name
)
61 object.__setattr
__(self
, 'properties', {})
62 for name
, prop
in jsonconfig
.get('properties', {}).items():
63 self
.properties
[name
] = (prop
['id'], prop
['fmt'], prop
.get('access', 'rw'))
64 object.__setattr
__(self
, 'functions', {})
65 for name
, func
in jsonconfig
.get('functions', {}).items():
66 def proxy_method(*args
):
67 return self
._callfunc
(func
["id"], func
.get("args", ""), args
, func
.get("returns", ""))
68 self
.functions
[name
] = proxy_method
69 object.__setattr
__(self
, 'type', jsonconfig
.get('type', None))
70 object.__setattr
__(self
, 'config', { k
: v
for k
,v
in jsonconfig
.items() if not k
in ['members', 'properties', 'functions'] })
73 """Construct an iterator to iterate over *all* (direct or not) child nodes of this node."""
74 return GanglionIter(self
)
76 def _read_config(self
):
77 """Fetch the device configuration descriptor from the device."""
79 s
.write(b
'\\#' + escape(struct
.pack(">H", self
.node_id
)) + b
'\x00\x00\x00\x00')
80 (clen
,) = struct
.unpack(">H", s
.read(2))
82 #decide whether cbytes contains lzma or json depending on the first byte (which is used as a magic here)
83 if cbytes
[0] is ord('#'):
84 return json
.JSONDecoder().decode(str(lzma
.decompress(cbytes
[1:]), "utf-8"))
86 return json
.JSONDecoder().decode(str(cbytes
, "utf-8"))
88 def _callfunc(self
, fid
, argsfmt
, args
, retfmt
):
89 """Call a function on the device by id, directly passing argument/return format parameters."""
90 # Make a list out of the arguments if they are none
91 #print('calling function No. {}, args({}) {}, returning {}'.format(fid, argsfmt, args, retfmt))
92 if not (isinstance(args
, tuple) or isinstance(args
, list)):
95 # Send the encoded data
96 cmd
= b
'\\#' + escape(struct
.pack(">HHH", self
.node_id
, fid
, struct
.calcsize(argsfmt
)) + struct
.pack(argsfmt
, *args
))
99 (clen
,) = struct
.unpack(">H", s
.read(2))
101 cbytes
= s
.read(clen
)
102 if clen
!= struct
.calcsize(retfmt
):
103 # CAUTION! This error is thrown not because the user supplied a wrong value but because the device answered in an unexpected manner.
104 # FIXME raise an error here or let the whole operation just fail in the following struct.unpack?
105 raise AttributeError("Device response format problem: Length mismatch: {} != {}".format(clen
, struct
.calcsize(retfmt
)))
106 rv
= struct
.unpack(retfmt
, cbytes
)
107 # Try to interpret the return value in a useful manner
116 """Get a list of all attributes of this object. This includes virtual Cerebrum stuff like members, properties and functions."""
117 return list(self
.members
.keys()) + list(self
.properties
.keys()) + list(self
.functions
.keys()) + list(self
.__dict
__.keys())
119 # Only now add the setattr magic method so it does not interfere with the above code
120 def __setattr__(self
, name
, value
):
121 """Magic method to set an attribute. This one even handles remote Cerebrum properties."""
122 #check if the name is a known property
123 if name
in self
.properties
:
124 #call the property's Cerebrum setter function
125 varid
, varfmt
, access
= self
.properties
[name
]
126 if not "w" in access
:
127 raise TypeError("{} is a read-only property".format(name
))
128 return self
._callfunc
(varid
+1, varfmt
, value
, "")
129 #if the above code falls through, do a normal __dict__ lookup.
130 self
.__dict
__[name
] = value
133 def __getattr__(self
, name
):
134 """Magic method to get an attribute of this object, considering Cerebrum members, properties and functions.
136 At this point a hierarchy is imposed upon the members/properties/functions that is not present in the implementation:
138 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.
141 if name
in self
.members
:
142 return self
.members
[name
]
144 if name
in self
.properties
:
146 self
.__setattr
__(name
, newx
)
147 varid
, varfmt
, access
= self
.properties
[name
]
148 rv
= self
._callfunc
(varid
, "", (), varfmt
)
149 # If the return value is a list, construct an auto-updating thingy from it.
150 if isinstance(rv
, list):
151 return NotifyList(rv
, callbacks
=[cb
])
155 if name
in self
.functions
:
156 return self
.functions
[name
]
158 #If all of the above falls through...
159 raise AttributeError(name
)
162 """Iterator class for ganglions that recursively iterates over all (direct or indirect) child nodes of a given Ganglion"""
164 def __init__(self
, g
):
166 self
.keyiter
= g
.members
.__iter
__()
174 return self
.miter
.__next
__()
175 except StopIteration:
177 except AttributeError:
179 foo
= self
.g
.__getattr
__(self
.keyiter
.__next
__())
180 self
.miter
= foo
.__iter
__()