Package ckbot :: Module logical
[hide private]
[frames] | no frames]

Source Code for Module ckbot.logical

  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 * 
29 30 -def nids2str( nids ):
31 return ",".join(["Nx%02x" % nid for nid in nids])
32
33 -class ModulesByName(object):
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 """
40 - def __init__(self):
41 self.__names = set()
42
43 - def _add( self, name, value ):
44 setattr( self, name, value ) 45 self.__names.add(name)
46
47 - def _remove( self, name ):
48 delattr( self, name ) 49 self.__names.remove(name)
50
51 - def __iter__(self):
52 plan = list(self.__names) 53 plan.sort() 54 return iter(plan)
55
56 -class DiscoveryError( StandardError ):
57 """ 58 Exception class for discovery failures 59 """
60 - def __init__(self,msg,**kw):
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
74 -class DelayedPermissionError( PermissionError ):
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 """
86 - def __init__(self, *arg, **kw):
87 PermissionError.__init__(self,*arg,**kw) 88 self.init_stack = extract_stack()
89
90 - def __call__( self, *arg, **kw ):
91 raise self
92
93 -class Cluster(dict):
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
215 - def discover( self, count = 0, timeout=2, timestep=0.1, required=set(), raiseClass=DiscoveryError ):
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 # If any number of nodes is acceptable --> wait and collect them 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 # If we only want the required nodes, hint them to protocol 243 self.p.hintNodes( required ) 244 # else --> collect nodes with count limit, timeout, required 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
258 - def update(self):
259 """Allow stateful members to update; propagates to """ 260 for m in self._updQ: 261 m.update()
262
263 - def off( self ):
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 ##V: How to properly do this? 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
300 - def newModuleIX(self,nid,name=None):
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
319 - def __delitem__( self, nid ):
320 self.at._remove( self[nid].name ) 321 dict.__delitem__(self,nid)
322
323 - def itermodules( self ):
324 for mnm in self.at: 325 yield getattr(self.at,mnm)
326
327 - def iterhwaddr( self ):
328 nids = self.keys() 329 nids.sort() 330 for nid in nids: 331 for index in self[nid].iterhwaddr(): 332 yield Cluster.build_hwaddr( nid, index )
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
342 - def getLive( self, limit=None ):
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
361 - def build_hwaddr( nid, index ):
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
370 - def parseClp( cls, clp ):
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
386 - def modOfClp(self, clp ):
387 """ 388 Find the module containing a given property 389 """ 390 kind,head,tail = self.parseClp(clp) 391 # Hardware names 392 if kind==":": 393 return self[int(head,16)] 394 # Property or Attribute 395 if kind[:1]=="/": 396 return getattr( self.at, head )
397
398 - def _getAttrOfClp( self, clp, attr ):
399 """(private) 400 Obtain python attribute of a cluster property identified by a clp 401 """ 402 kind,head,tail = self.parseClp(clp) 403 # Hardware names 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 # Property or Attribute 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
426 - def getterOf( self, clp ):
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
436 - def setterOf( self, clp ):
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 # Inherit all module ID strings defined in dynamixel.MODELS 451 Module.Types.update( 452 { tc : mc for tc,(mm,mc) in dynamixel.MODELS.iteritems() } 453 ) 454