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

Source Code for Module ckbot.ckmodule

  1  """ 
  2  The ckmodule class defines the abstract superclass of all ckbot modules 
  3  and those Module subclasses that are entirely hardware independent, 
  4  such as GenericModule and MissingModule. In addition, it defines the 
  5  AbstractNodeAdaptor and AbstractProtocol classes, which are used for 
  6  organizational purposes to allow all Protocol-s and  
  7  ProtocolNodeAdaptor-s to inherit from a single superclass 
  8   
  9  These classes were separated out of logical.py to allow non-CAN module 
 10  classes to inherit functionality from the Module base class. 
 11   
 12  The users of ckmodule are logical.py, which import *-s everything, 
 13  and python software modules which define Module subclasses. 
 14  """ 
 15   
 16  from sys import platform as SYS_PLATFORM, stdout 
 17  from time import time as now 
 18  from warnings import warn 
 19  from glob import glob as GLOB_GLOB 
 20  from os import sep, getenv 
 21   
 22  # Support functions ################################################### 
 23   
 24  _T0 = now() 
25 -def progress(msg):
26 """Display a progress message""" 27 stdout.write("%7.3f %s\n" % (now()-_T0,msg)) 28 stdout.flush()
29 30 # Library location specified by PYCKBOTPATH environment var 31 32 PYCKBOTPATH = getenv('PYCKBOTPATH',None) 33 if PYCKBOTPATH is None: 34 if __file__.rfind('py%sckbot'%sep)>-1: 35 PYCKBOTPATH = __file__[:__file__.rfind('py%sckbot'%sep)] 36 elif PYCKBOTPATH[-1:] != sep: 37 PYCKBOTPATH=PYCKBOTPATH+sep 38 if not GLOB_GLOB(PYCKBOTPATH+"py%sckbot%sckmodule.py" % (sep,sep)): 39 progress("WARNING: PYCKBOTPATH='%s' does not point to the right place" % PYCKBOTPATH )
40 41 # Organizational superclasses ######################################### 42 43 -class AbstractProtocol( object ):
44 """abstract superclass of all Protocol classes 45 46 Protocol classes are responsible for generating the Protocol Node 47 Adaptors for each module on the bus, when the bus is scanned. 48 Protocols implement the transactions that the underlying communication 49 infrastructure supports, and typically maintain state representing any 50 incomplete transactions. 51 52 AbstractProtocol subclasses must implement the following methods: 53 p.update() 54 p.hintNodes(nodes) 55 p.generatePNA( nid ) 56 57 AbstractProtocol instances must have the following data attributes: 58 p.heartbeats -- dict -- nid to last heartbeat 59 """
60 - def __init__(self,bus=None):
61 """ 62 Allow a bus parameter to be passed to all AbstractProtocol subclass 63 constructors 64 """ 65 self.heartbeats = {}
66
67 - def update( self ):
68 """ 69 *PURE* perform Protocol housekeeping operations 70 """ 71 raise TypeError,"pure method called"
72
73 - def hintNodes( self, nodes ):
74 """ 75 *PURE* use hint that specified nodes are available 76 """ 77 raise TypeError,"pure method called"
78
79 - def generatePNA( self, nid ):
80 """ 81 *PURE* Generate a ProtocolNodeAdaptor for the specified nid 82 """ 83 raise TypeError,"pure method called"
84
85 -class AbstractBus( object ):
86 """abstract superclass of all Bus classes 87 88 Bus classes are responsible for wrapping OS hardware drivers and 89 presenting a pythonically correct, human readable interface. 90 91 Bus instances should be (mostly) stateless, and bus methods should 92 return as fast as possible. 93 """
94 - def __init__(self,port=None):
95 """ 96 Allow a port parameter to be passed to all AbstractBus subclass 97 constructors 98 """ 99 pass
100
101 -class AbstractNodeAdaptor( object ):
102 """abstract superclass of all ProtocolNodeAdaptor classes 103 104 AbstractNodeAdaptor subclasses must implement the get_typecode() 105 method, returning the module's type identification string 106 """ 107 pass
108
109 -class PermissionError( RuntimeError ):
110 "(organizational) abstract superclass of all PermissionError classes"
111 - def __init__(self,*arg,**kw):
112 RuntimeError.__init__(self,*arg,**kw)
113
114 -class AbstractBusError( StandardError ):
115 "(organizational) abstract superclass of all BusError classes"
116 - def __init__(self,*arg,**kw):
117 StandardError.__init__(self,*arg,**kw)
118
119 -class AbstractProtocolError( StandardError ):
120 "(organizational) abstract superclass of all ProtocolError classes"
121 - def __init__(self,*arg,**kw):
122 StandardError.__init__(self,*arg,**kw)
123
124 -class AttributeGetter( object ):
125 """ 126 Callable wrapper for providing access to object properties via 127 a getter function 128 129 AttributeGetter(obj,attr)() is getattr(obj,attr) 130 """
131 - def __init__(self,obj,attr):
132 self.obj = obj 133 self.attr = attr
134
135 - def __repr__( self ):
136 return "<%s at 0x%x for %s of %s>" % ( 137 self.__class__.__name__, id(self), self.attr, repr(self.obj) )
138
139 - def __call__(self):
140 return getattr(self.obj,self.attr)
141
142 -class AttributeSetter( object ):
143 """ 144 Callable wrapper for providing access to object properties via 145 a setter function 146 147 AttributeSetter(obj,attr)(value) is setattr(obj,attr,value) 148 """
149 - def __init__(self,obj,attr):
150 self.obj = obj 151 self.attr = attr
152
153 - def __repr__( self ):
154 return "<%s at 0x%x for %s of %s>" % ( 155 self.__class__.__name__, id(self), self.attr, repr(self.obj) )
156
157 - def __call__(self,value):
158 setattr(self.obj,self.attr,value)
159
160 -class Module(object):
161 """ 162 Abstract superclass representing a CKBot module. 163 Cluster creates the appropriate Module subclass by calling 164 Module.newFromDiscovery 165 """ 166 # dictionary mapping module type-codes (as read from CAN via object 167 # dictionary to the appropriate Module subclass 168 Types = {} 169 170 @classmethod
171 - def newFromDiscovery(cls, nid, typecode, pna):
172 """ 173 Factory method for instantiating Module subclasses given a node id and typecode 174 INPUTS: 175 nid -- int -- the node id 176 typecode -- string -- the typecode string, which is looked up in 177 the Module.Types dictionary 178 pna -- a ProtocolNodeAdaptor -- typically created using a 179 Protocol instance's .generatePNA(nid) factory method 180 """ 181 subclass = cls.Types.get(typecode,GenericModule) 182 m = subclass(nid, typecode, pna) 183 m.code_version = typecode 184 return m
185
186 - def __init__(self, node_id, typecode, pna ):
187 """ 188 Concrete constructor. 189 190 ATTRIBUTES: 191 node_id -- 7 bit number to address a module uniquely 192 typecode -- version number of module code 193 pna -- ProtocolNodeAdaptor -- specialized for this node_id 194 """ 195 self.node_id = int(node_id) 196 self.od = None 197 self.code_version = None 198 assert pna is None or isinstance(pna,AbstractNodeAdaptor) 199 assert pna is None or (pna.nid & 0xFF) == (node_id & 0xFF) 200 self.pna = pna 201 self.name = None 202 self._attr = {} 203 #these lines added from mem classes 204 self.mcu = None 205 self.mem = None 206 self.od = None
207
208 - def get_od(self):
209 """ 210 This method creates an Object dictionary for this module. 211 """ 212 if self.od is None: 213 self.od = self.pna.get_od(progress=progress)
214
215 - def iterhwaddr(self):
216 """ 217 Iterator for all Object Dictionary index addresses in this module 218 219 The addresses are returned as integers 220 """ 221 if self.od is None: 222 return iter([]) 223 return self.od.index_table.iterkeys()
224
225 - def iterprop(self, perm=''):
226 """ 227 Iterator for Object Dictionary properties exposed by this module 228 229 INPUTS: 230 perm -- string -- ''(default) all properties; 231 'R' readable/gettable 232 'W' writable/settable 233 OUTPUT: 234 property names returned as strings 235 """ 236 if self.od is None or perm not in 'RW': 237 return iter([]) 238 idx = self.od.name_table.iterkeys() 239 if perm=='R': 240 return (nm for nm in idx if self.od.name_table[nm].isReadable()) 241 elif perm=='W': 242 return (nm for nm in idx if self.od.name_table[nm].isWritable()) 243 return idx
244
245 - def iterattr(self,perm=''):
246 """ 247 Iterator for module attributes exposed by this module class 248 249 INPUTS: 250 perm -- string -- ''(default) all properties; 'R' readable/gettable 251 'W' writable/settable 252 OUTPUT: 253 property names returned as strings 254 """ 255 plan = self._attr.iteritems() 256 if perm: 257 return (nm for nm,acc in plan if perm in acc) 258 return (nm for nm,_ in plan)
259
260 - def _getAttrProperty( self, prop, req ):
261 """(private) 262 Access python attributes exposed as properties 263 ('/@' property names) 264 """ 265 def boolambda( b ): 266 if b: 267 return lambda : True 268 return lambda : False
269 # Check what access is provided to this attribute 270 perm = self._attr.get(prop,None) 271 if perm is None: 272 raise KeyError("Property '@%s' was not found in class %s" 273 % (prop,self.__class__.__name__) ) 274 # Try to access attribute; generates error on failure 275 val = getattr( self, prop ) 276 ## Attribute permissions: 277 # R -- readable; by default using AttributeGetter 278 # W -- writeable; by default using AttributeSetter 279 # 1 -- method with no parameters, treated as a getter 280 # 2 -- method with 1 parameter, treated as a setter 281 if req == 'isReadable': return boolambda( "R" in perm ) 282 elif req == 'isWritable': return boolambda( "W" in perm ) 283 elif req == 'get_sync': 284 if "1" in perm: return val 285 if "R" in perm: return AttributeGetter(self,prop) 286 elif req == 'set': 287 if "2" in perm: return val 288 if "W" in perm: return AttributeSetter(self,prop) 289 raise TypeError("Property '@%s' does not provide '%s'" % (prop,req))
290
291 - def _getModAttrOfClp( self, clp, attr ):
292 """ 293 Obtain a module attribute from a clp 294 295 At the module level, the clp is expected to start with '/' 296 297 This is the Module superclass implementation. 298 clp-s starting with /@ expose module attrbiutes 299 """ 300 assert clp[0]=="/", "Logical clp-s only" 301 if clp[:2]=="/@": 302 return self._getAttrProperty(clp[2:],attr) 303 # Must be an OD property access 304 if self.od is None: 305 raise KeyError('ObjectDictionary was not initialized; use .get_od() or populate(...,walk=1)') 306 # Look for OD name 307 odo = self.od.name_table[clp[1:]] 308 return getattr( odo, attr )
309
310 - def start(self):
311 """ 312 This method sends a CAN Message to start this module. 313 """ 314 self.pna.start()
315
316 - def stop(self):
317 """ 318 This method sends a CAN Message to stop this module. 319 """ 320 self.pna.stop()
321
322 - def reset(self):
323 """ 324 This method sends a CAN Message to reset this module. 325 """ 326 self.pna.reset()
327
328 -class AbstractServoModule( Module ):
329 """ 330 Abstract superclass of servo modules. These support additional functionality 331 associated with position servos: 332 333 .set_pos -- sets position of the module. -- units in 100s of degrees between 334 -9000 and 9000. This is a "safe" version, which only accepts legal values 335 .set_pos_UNSAFE -- sets position of module, without any validity checking 336 .get_pos -- reads current position (may be at offset from set_pos, even without load) 337 .go_slack -- makes the servo go limp 338 .is_slack -- return True if and only if module is slack 339 """ 340 341 # Upper limit for positions 342 POS_UPPER = 9000 343 344 # Lower limit for positions 345 POS_LOWER = -9000 346
347 - def __init__(self, *argv, **kwarg):
348 Module.__init__(self, *argv, **kwarg) 349 self._attr=dict( 350 go_slack="1R", 351 is_slack="1R", 352 get_pos="1R", 353 set_pos="2W", 354 set_pos_UNSAFE="2W" 355 )
356
357 - def go_slack(self):
358 """ 359 Makes the servo go limp 360 """ 361 raise RuntimeError("PURE")
362
363 - def set_pos_UNSAFE(self, val):
364 """ 365 Sets position of the module, without any validity checking 366 367 INPUT: 368 val -- units in 100s of degrees between -9000 and 9000 369 """ 370 raise RuntimeError("PURE")
371
372 - def set_pos(self,val):
373 """ 374 Sets position of the module, with safety checks. 375 376 INPUT: 377 val -- units in 100s of degrees between -9000 and 9000 378 """ 379 raise RuntimeError("PURE")
380 381
382 - def get_pos(self):
383 """ 384 Gets the actual position of the module 385 """ 386 raise RuntimeError("PURE")
387
388 - def get_pos_async(self):
389 """ 390 Asynchronously gets the actual position of the module 391 """ 392 raise RuntimeError("PURE")
393
394 - def is_slack(self):
395 """ 396 Gets the actual position of the module 397 """ 398 raise RuntimeError("PURE")
399
400 -class MemInterface( object ):
401 """ 402 Implementation of the .mem sub-object for modules that provide the generic IO memory interface 403 404 Instances of this class are created by the module subclass constructor, for those module software 405 versions that actually implement the interface. 406 407 Typical usage, assuming m is a module: 408 >>> m.mem[0xBAD] = 0xC0ED 409 >>> print m.mem[0xDEAD] 410 >>> m.mem[[0xBAD, 0xD00D]] = [0xBE, 0xEF] 411 >>> print m.mem[[0xC0ED, 0xBABE]] 412 >>> m.mem[0xF00] |= (1<<5) # set bit 5 413 >>> m.mem[0xF00] &= ~(1<<5) # clear bit 5 414 >>> m.mem[0xF00] ^= (1<<5) # toggle bit 5 415 416 If the module also has a .mcu sub-object, it will typically provide the constants needed instead of 417 the literals in the example above. 418 """
419 - def __init__(self, module):
420 "Attach memory sub-object to this module" 421 self.set = module.mem_write 422 self.get = module.mem_read
423
424 - def __getitem__( self, key ):
425 """ 426 Read memory from microcontroller. 427 428 key can either be an integer memory address, or a list of such addresses. 429 When a list is used, returns a list of the results 430 """ 431 if type(key) is list: 432 return [ self.get(k) for k in key ] 433 #elif type(key) is tuple: 434 # addr, bit = key 435 # return (self.get(addr) & (1<<bit)) != 0 436 return self.get(key)
437
438 - def __setitem__(self, key, val):
439 """ 440 Write memory values in microcontroller 441 442 key can be an address and val a byte value, or key is a list of addresses, and val a sequence of 443 values (i.e. val can also be a tuple, or any other sequence type). 444 """ 445 if type(key) is list: 446 for k,v in zip(key,val): 447 self[k]=v 448 else: 449 self.set(key,val)
450
451 -class MemIxMixin( object ):
452 """ 453 Module mixin class implementing memory interface 454 455 This adds mem_read and mem_write methods, and a get_mem 456 """ 457
458 - def mem_write( self, addr, val ):
459 "Write a byte to a memory address in the module's microcontroller" 460 self.pna.set( self.GIO_W_VAL, "B", val ) 461 self.pna.set( self.GIO_W_ADDR, "H", addr )
462
463 - def mem_read( self, addr ):
464 "Read a memory address from the module's microncontroller" 465 self.pna.set( self.GIO_R_ADDR, "H", addr ) 466 return self.pna.get_sync( self.GIO_R_VAL, "B" )
467
468 - def mem_getterOf( self, addr ):
469 "Return a getter function for a memory address" 470 # Create the closure 471 def getter(): 472 return self.mem_read(addr)
473 return getter
474
475 - def mem_setterOf( self, addr ):
476 "Return a setter function for a memory address" 477 # Create the closure 478 def setter(val): 479 return self.mem_write(addr,val)
480 return setter 481
482 -class GenericModule( Module ):
483 """ 484 Concrete subclass of Module used for modules whose typecode was not 485 recognized 486 """
487 - def __init__(self, nid, typecode , pna):
488 Module.__init__(self,nid,typecode,pna) 489 warn( 490 RuntimeWarning("CAN node 0x%02X reported unknown typecode '%s'. Falling back on GenericModule class"% (nid, str(typecode))) 491 )
492
493 -class MissingModule( Module ):
494 """ 495 Concrete class representing required modules that were not discovered during 496 the call to Cluster.populate 497 498 Instances of this class "fake" servo and motor modules, and implement the 499 MemIx interface. 500 501 The .msg attribute may contain a callable. If present, this callable 502 will be called with messages describing any API calls made on the 503 MissingModule. Typically, one may set: m.msg = ckbot.logical.progress 504 505 """ 506 TYPECODE = "<<missing>>"
507 - def __init__(self, nid, name, pna=None):
508 Module.__init__(self, nid, MissingModule.TYPECODE, None) 509 self.mem = MemInterface(self) 510 self.mcu = object() 511 self._mem = {} 512 self.od = None 513 self.msg = None 514 self.nid = nid 515 self.name = name 516 self.slack = True 517 self.pos = 0 518 self.speed = 0 519 self._attr=dict( 520 go_slack="1R", 521 is_slack="1R", 522 get_pos="1R", 523 set_pos="2W", 524 get_speed="1R", 525 set_speed="2W", 526 )
527
528 - def get_od( self ):
529 """get_od is not supported on MissingModule-s""" 530 pass
531
532 - def mem_read( self, addr ):
533 val = self._mem.get(addr,None) 534 if val is None: 535 self._mem[addr] = 0 536 val = 0 537 if self.msg is not None: 538 self.msg("%s.mem[0x%04X] --> %d" % (self.name,addr,val)) 539 return val
540
541 - def mem_write( self, addr, val ):
542 self._mem[addr] = val 543 if self.msg is not None: 544 self.msg("%s.mem[0x%04X] <-- %d" % (self.name,addr,val))
545
546 - def go_slack(self):
547 self.slack = True 548 self.speed = 0 549 if self.msg is not None: 550 self.msg("%s.go_slack()" % self.name )
551
552 - def is_slack(self):
553 if self.msg is not None: 554 self.msg("%s.is_slack() --> %s" % (self.name,str(self.slack))) 555 return self.slack
556
557 - def set_pos( self, val ):
558 self.slack = False 559 self.pos = val 560 if self.msg is not None: 561 self.msg("%s.set_pos(%d)" % (self.name,val))
562
563 - def set_speed( self, val ):
564 self.slack = False 565 self.speed = val 566 if self.msg is not None: 567 self.msg("%s.set_speed(%d)" % (self.name,val))
568
569 - def set_torque( self, val ):
570 self.slack = False 571 self.speed = val 572 if self.msg is not None: 573 self.msg("%s.set_torque(%d) [alias for speed]" % (self.name,val))
574
575 - def get_pos(self):
576 if self.msg is not None: 577 self.msg("%s.get_pos() --> %d" % (self.name,self.pos)) 578 return self.pos
579
580 - def get_speed(self):
581 if self.msg is not None: 582 self.msg("%s.get_speed() --> %d" % (self.name,self.speed)) 583 return self.speed
584