1 """
2 The ckbots.logical module provides classes to create representations
3 of modules inside a cluster. It directly includes classes representing
4 modules classes from pololu, dynamixel and hitec.
5 The top of the classes heirarchy for modules and their NodeAdaptor-s
6 are found in ckmodule.
7
8 Main uses of this module:
9 (*) query the object dictionary of a module by its logical name
10 (*) send a position command using a process message
11
12 The top level of this module is cluster. Typically users will create a
13 cluster to represent a set of modules that can communicate on the same
14 Bus. Modules can be addressed through logical names via the Attributes
15 class.
16 """
17 import re
18 from time import sleep, time as now
19 from warnings import warn
20 from traceback import extract_stack
21
22 from ckmodule import *
23
24 import pololu
25 import hitec
26 import dynamixel
27
28 from defaults import *
31 return ",".join(["Nx%02x" % nid for nid in nids])
32
34 """
35 Concrete class with a cluster's attributes.
36
37 The cluster dynamically adds named attributes to instances of this class to
38 provide convenient names for modules
39 """
42
43 - def _add( self, name, value ):
44 setattr( self, name, value )
45 self.__names.add(name)
46
48 delattr( self, name )
49 self.__names.remove(name)
50
52 plan = list(self.__names)
53 plan.sort()
54 return iter(plan)
55
57 """
58 Exception class for discovery failures
59 """
61 """
62 ATTRIBUTES:
63 timeout -- number -- seconds to timeout
64 required -- set -- required node ID-s
65 found -- set -- node ID-s found
66 count -- number -- node count required
67 """
68 StandardError.__init__(self,msg)
69 self.timeout = kw.get('timeout',0)
70 self.count = kw.get('count',0)
71 self.required = kw.get('required',set([]))
72 self.found = kw.get('found',set([]))
73
75 """
76 Callable object returned when setters or getters with
77 are obtained for cluster properties that cannot be (resp.)
78 written or read.
79
80 A DelayedPermissionError stores the stack at its initialization
81 to make it easier for the subsequent error to be traced back to
82 its original source.
83 ]
84 If called, a DelayedPermissionError raises itself.
85 """
89
92
94 """
95 Concrete class representing a CKBot cluster, which is a collection of
96 modules residing on the same bus.
97
98 A Cluster contains a Protocol class to manage communication with the
99 modules.
100
101 A Cluster instance is itself a dictionary of modules, addressed by
102 their node ID-s. This dictionary is populated by the .populate()
103 method. Clusters also implement the reflection iterators itermodules,
104 iterhwaddr, and iterprop.
105
106 Typically, users will use the convenience attribute .at, which
107 provides syntactic sugar for naming modules in a cluster using names
108 defined when the cluster is .populate()-ed. These allow ipython tab
109 completion to be used to quickly explore which modules are available.
110
111 Typical use:
112 >>> c = Cluster()
113 >>> c.populate(3,{ 0x91 : 'left', 0xb2 : 'head',0x5d : 'right'} )
114 >>> for m in c.itervalues():
115 >>> m.get_od(c.p)
116 >>> c.at.head.od.set_pos( 3000 ) # via object dictionary
117 >>> c.at.head.set_pos(4500) # via process message
118 """
119 - def __init__(self,arch=None,port=None,*args,**kwargs):
120 """
121 Create a new cluster. Optionally, also .populate() it
122
123 INPUT:
124 arch -- optional -- python module containing arch.Bus and
125 arch.Protocol to use for low level communication. Defaults
126 to DEFAULT_ARCH
127
128 Can also be a Protocol instance ready to be used.
129
130 Supported architectures include:
131 can -- CAN-bus CKBot 1.4 and earlier
132 hitec -- modified hitec servos
133 dynamixel -- Robotis Dynamixel RX, EX and MX
134 pololu -- pololu Maestro servo controllers
135 nobus -- software simulated modules for no-hardware-needed
136 testing of code.
137
138 port -- specification of the communication port to use, as per
139 ckbot.port2port.newConnection. port defaults to DEFAULT_PORT, and
140 is ignored if arch is an initialized AbstractProtocol instance.
141
142 This can be used to specify serial devices and baudrates, e.g.
143 port = 'tty={glob="/dev/ttyACM1",baudrate=115200}'
144
145 *argc, **kw -- if any additional parameters are given, the
146 .populate(*argc,**kw) method is invoked after initialization
147
148 ATTRIBUTES:
149 p -- instance of Protocol for communication with modules
150 at -- instance of the Attributes class.
151 limit -- float -- heartbeat time limit before considering node dead
152 _updQ -- list -- collection of objects that need update() calls
153 """
154 dict.__init__(self)
155 if arch is None:
156 arch = DEFAULT_ARCH
157 if port is None:
158 port = DEFAULT_PORT
159 if isinstance(arch,AbstractProtocol):
160 self.p = arch
161 else:
162 self.p = arch.Protocol(bus = arch.Bus(port=port))
163 self._updQ = [self.p]
164 self.at = ModulesByName()
165 self.limit = 2.0
166 if args or kwargs:
167 return self.populate(*args,**kwargs)
168
169 - def populate(self, count = None, names = {}, timeout=2, timestep=0.1,
170 required = set(), fillMissing=None, walk=False,
171 autonamer=lambda nid : "Nx%02X" % nid ):
172 """
173 Tries to populate the cluster based on heartbeats observed on the bus.
174 Will terminate when either at least count modules were found or timeout
175 seconds have elapsed. While waiting, checks bus once every timestep seconds.
176 If timed out, raises an IOError exception.
177
178 If the bus already got all the heartbeats needed, populate() should
179 terminate without sleeping.
180
181 If provided, names gives a dictionary of module names based on their node
182 ID. Node IDs that aren't found in the dictionary have a name automatically
183 generated from the ID by calling autonamer.
184
185 INPUT:
186 count, timeout, timestep, required, fillMissing-- see self.discover()
187 fillMissing -- class / bool -- fills in any missing yet required modules
188 with instances of this class. If boolean true, uses MissingModule
189 NOTE: the nobus mechanism bypasses this; use nobus.NID_CLASS
190 names -- dictionary of Modules names based with node id as their key.
191 walk -- bool -- if true, walks each module to indentify its interface
192 autonamer -- names the modules if no names are given.
193 """
194 self.clear()
195 if fillMissing is None:
196 exc = DiscoveryError
197 else:
198 exc = None
199 required = set(required)
200 nids = self.discover(count,timeout,timestep,required,raiseClass = exc)
201 for nid in nids:
202 name = names.get(nid, autonamer(nid))
203 mod = self.newModuleIX( nid, name )
204 self.add(mod)
205 if walk:
206 mod.get_od()
207 if fillMissing:
208 if type(fillMissing) is not type:
209 fillMissing = MissingModule
210 for nid in required - nids:
211 name = names.get(nid, autonamer(nid))
212 mod = fillMissing( nid, name )
213 self.add(mod)
214
216 """
217 Discover which nodes are in the cluster.
218 Termination condition for discovery is that at least count modules were
219 discovered, and that all of the required modules were found.
220 If this hasn't happened after a duration of timeout seconds (+/- a timestep),
221 discover raises a DiscoveryError.
222 In the special case of count==0 and required=set(), discover collects
223 all node ID-s found until timeout, and does not return an error.
224 INPUT:
225 count -- number of modules -- if 0 then collects until timeout
226 timeout -- time to listen in seconds
227 timestep -- how often to read the CAN buffer
228 required -- set -- requires that all these nids are found
229 raiseClass -- class -- exception class to raise on timeout
230 OUTPUT:
231 python set of node ID numbers
232 """
233 required = set(required)
234
235 if not count and not required:
236 progress("Discover: waiting for %g seconds..." % timeout)
237 sleep(timeout)
238 nids = self.getLive(timeout)
239 progress("Discover: done. Found %s" % nids2str(nids))
240 return nids
241 elif required and (count is 0 or len(required)==count):
242
243 self.p.hintNodes( required )
244
245 time_end = now()+timeout
246 nids = self.getLive(timeout)
247 while (len(nids) < count) or not (nids >= required):
248 sleep(timestep)
249 nids = self.getLive(timeout)
250 progress("Discover: found %s" % nids2str(nids))
251 if time_end < now():
252 if raiseClass is None:
253 break
254 raise raiseClass("CAN discovery timeout",
255 timeout=timeout, found=nids, required=required, count=count )
256 return nids
257
259 """Allow stateful members to update; propagates to """
260 for m in self._updQ:
261 m.update()
262
264 """Make all servo or motor modules go slack"""
265 for m in self.itermodules():
266 if hasattr(m,'go_slack') and callable(getattr(m,'go_slack')):
267 m.go_slack()
268
269 - def who( self, t = 10 ):
270 """
271 Show which modules are currently visible on the bus
272 runs for t seconds
273 """
274 t0 = now()
275 while now()-t0<t:
276 lst = []
277 for nid in self.getLive():
278 if self.has_key(nid):
279 lst.append( '%02X:%s' % (nid,self[nid].name) )
280 else:
281 lst.append( '%02X:<?>' % nid )
282 print "%.2g:" % (now()-t0),", ".join(lst)
283 sleep(1)
284
285 - def add( self, *modules ):
286 """
287 Add the specified modules (as returned from .newModuleIX())
288 """
289 for mod in modules:
290 progress("Adding %s %s" % (mod.__class__.__name__,mod.name) )
291
292
293 assert isinstance(mod,Module) or isinstance(mod, pololu2_vv.PololuServoModule) or isinstance(mod, hitec.HitecServoModule)
294 self.at._add(mod.name, mod)
295 if hasattr(mod,"update") and callable(mod.update):
296 self._updQ.append(mod)
297 self[mod.node_id] = mod
298 return self
299
301 """
302 Build the interface for a module
303 INPUTS
304 nid -- int -- node identifier for use on the bus
305 name -- string -- name for the module, or None if regenerating
306 an existing module whose name is already known
307 OUTPUTS
308 mod -- Module subclass representing this node
309 """
310 nid = int(nid)
311 pna = self.p.generatePNA(nid)
312 if name is None:
313 name = self[nid].name
314 tc = pna.get_typecode()
315 mod = Module.newFromDiscovery(nid, tc, pna)
316 mod.name = name
317 return mod
318
322
324 for mnm in self.at:
325 yield getattr(self.at,mnm)
326
333
334 - def iterprop( self, attr=False, perm='' ):
335 for mod in self.itermodules():
336 if attr:
337 for prop in mod.iterattr(perm):
338 yield mod.name + "/@" + prop
339 for prop in mod.iterprop(perm):
340 yield mod.name + "/" + prop
341
343 """
344 Use heartbeats to get set of live node ID-s
345 INPUT:
346 limit -- float -- heartbeats older than limit seconds in
347 the past are ignored
348 OUTPUT:
349 python set of node ID numbers
350 """
351 if limit is None:
352 limit = self.limit
353 t0 = now()
354 self.p.update()
355 s = set( ( nid
356 for nid,(ts,_) in self.p.heartbeats.iteritems()
357 if ts + limit > t0 ) )
358 return s
359
360 @staticmethod
362 "build a hardware address for a property from node and index"
363 return "%02x:%04x" % (nid,index)
364
365 REX_HW = re.compile("([a-fA-F0-9]{2})(:)([a-fA-F0-9]{4})")
366 REX_PROP = re.compile("([a-zA-Z_]\w*)(/)((?:[a-zA-Z_]\w*)|(?:0x[a-fA-F0-9]{4}))")
367 REX_ATTR = re.compile("([a-zA-Z_]\w*)(/@)([a-zA-Z_]\w*)")
368
369 @classmethod
371 """
372 parse a class property name into head and tail parts
373
374 use this method to validate class property name syntax.
375
376 OUTPUT: kind, head, tail
377 where kind is one of ":", "/", "/@"
378 """
379 m = cls.REX_HW.match(clp)
380 if not m: m = cls.REX_PROP.match(clp)
381 if not m: m = cls.REX_ATTR.match(clp)
382 if not m:
383 raise ValueError("'%s' is not a valid cluster property name" % clp )
384 return m.group(2),m.group(1),m.group(3)
385
387 """
388 Find the module containing a given property
389 """
390 kind,head,tail = self.parseClp(clp)
391
392 if kind==":":
393 return self[int(head,16)]
394
395 if kind[:1]=="/":
396 return getattr( self.at, head )
397
399 """(private)
400 Obtain python attribute of a cluster property identified by a clp
401 """
402 kind,head,tail = self.parseClp(clp)
403
404 if kind==":":
405 nid = int(head,16)
406 index = int(tail,16)
407 try:
408 od = self[nid].od
409 except KeyError:
410 raise KeyError("Unknown node ID 0x%02x" % nid)
411 if od is None:
412 raise ValueError("Node %s (ID 0x%02x) was not scanned for properties. Use get_od() or populate(...,walk=1) " % (self[nid].name,nid))
413 try:
414 return getattr(od.index_table[index],attr)
415 except KeyError:
416 raise KeyError("Unknown Object Dictionary index 0x%04x" % index)
417
418
419 if kind[:1]=="/":
420 try:
421 mod = getattr( self.at, head )
422 return mod._getModAttrOfClp( kind+tail, attr )
423 except AttributeError:
424 raise KeyError("'%s' is not the name of a module in .at" % head )
425
427 """
428 Obtain a getter function for a cluster property
429
430 If property is not readable, returns a DelayedPermissionError
431 """
432 if self._getAttrOfClp(clp,'isReadable')():
433 return self._getAttrOfClp(clp,'get_sync')
434 return DelayedPermissionError("Property '%s' is not readable" % clp)
435
437 """
438 Obtain a setter function for a cluster property
439
440 If property is not writeable, returns a DelayedPermissionError
441 """
442 if self._getAttrOfClp(clp,'isWritable')():
443 return self._getAttrOfClp(clp,'set')
444 return DelayedPermissionError("Property '%s' is not writable" % clp)
445
446 Module.Types[MissingModule.TYPECODE] = MissingModule
447 Module.Types['PolServoModule'] = pololu.ServoModule
448 Module.Types['HitecServoModule'] = hitec.ServoModule
449 Module.Types['HitecMotorModule'] = hitec.MotorModule
450
451 Module.Types.update(
452 { tc : mc for tc,(mm,mc) in dynamixel.MODELS.iteritems() }
453 )
454