Module Robot
[hide private]
[frames] | no frames]

Source Code for Module Robot

  1  # Robot.py 
  2   
  3  ''' 
  4  Abstraction of a robot based on Pi2Go (full version) from 4tronix. 
  5   
  6   This software is part of the raspibrick module. 
  7   It is Open Source Free Software, so you may 
  8   - run the code for any purpose 
  9   - study how the code works and adapt it to your needs 
 10   - integrate all or parts of the code in your own programs 
 11   - redistribute copies of the code 
 12   - improve the code and release your improvements to the public 
 13   However the use of the code is entirely your responsibility. 
 14   ''' 
 15   
 16   
 17  from RobotInstance import RobotInstance 
 18  from smbus import SMBus 
 19  import RPi.GPIO as GPIO 
 20  import os, sys 
 21  import time 
 22  from Tools import Tools 
 23  import SharedConstants 
 24  from Display import Display 
 25  from DgTell import DgTell 
 26  from DgTell1 import DgTell1 
 27  from Disp4tronix import Disp4tronix 
 28  from OLED1306 import OLED1306 
 29  from SensorThread import SensorThread 
 30  from Led import Led 
 31  from PCA9685 import PWM 
 32  from threading import Thread 
 33  from subprocess import Popen, PIPE 
 34  import re 
 35  import smbus 
 36  import pygame 
37 38 # --------------------------- class ButtonThread ------------ 39 -class ButtonThread(Thread):
40 - def __init__(self):
41 Thread.__init__(self) 42 self.isRunning = False
43
44 - def run(self):
45 Tools.debug("===>ButtonThread started") 46 self.isRunning = True 47 startTime = time.time() 48 while self.isRunning and (time.time() - startTime < SharedConstants.BUTTON_LONGPRESS_DURATION): 49 time.sleep(0.1) 50 if self.isRunning: 51 if _buttonListener != None: 52 _buttonListener(SharedConstants.BUTTON_LONGPRESSED) 53 Tools.debug("===>ButtonThread terminated")
54
55 - def stop(self):
56 self.isRunning = False
57
58 # --------------------------- class ClickThread ------------- 59 -class ClickThread(Thread):
60 - def __init__(self):
61 Thread.__init__(self) 62 self.start()
63
64 - def run(self):
65 Tools.debug("===>ClickThread started") 66 global _clickThread 67 self.isRunning = True 68 startTime = time.time() 69 while self.isRunning and (time.time() - startTime < SharedConstants.BUTTON_DOUBLECLICK_TIME): 70 time.sleep(0.1) 71 if _clickCount == 1 and not _isLongPressEvent: 72 if _xButtonListener != None: 73 _xButtonListener(SharedConstants.BUTTON_CLICKED) 74 _clickThread = None 75 Tools.debug("===>ClickThread terminated")
76
77 - def stop(self):
78 self.isRunning = False
79
80 81 # --------------------------- Global functions ------------------------ 82 -def _onXButtonEvent(event):
83 global _clickThread, _clickCount, _isLongPressEvent 84 if event == SharedConstants.BUTTON_PRESSED: 85 if _xButtonListener != None: 86 _xButtonListener(SharedConstants.BUTTON_PRESSED) 87 _isLongPressEvent = False 88 if _clickThread == None: 89 _clickCount = 0 90 _clickThread = ClickThread() 91 92 elif event == SharedConstants.BUTTON_RELEASED: 93 if _xButtonListener != None: 94 _xButtonListener(SharedConstants.BUTTON_RELEASED) 95 if _isLongPressEvent: 96 _clickThread.stop() 97 _clickThread = None 98 return 99 _clickCount += 1 100 if _clickThread != None: 101 if _clickCount == 2: 102 _clickThread.stop() 103 _clickThread = None 104 if _xButtonListener != None: 105 _xButtonListener(SharedConstants.BUTTON_DOUBLECLICKED) 106 else: 107 if _xButtonListener != None: 108 _xButtonListener(SharedConstants.BUTTON_CLICKED) 109 110 elif event == SharedConstants.BUTTON_LONGPRESSED: 111 _isLongPressEvent = True 112 if _xButtonListener != None: 113 _xButtonListener(SharedConstants.BUTTON_LONGPRESSED)
114
115 -def _onButtonEvent(channel):
116 # switch may bounce: down-up-up, down-up-down, down-down-up etc. in fast sequence 117 global _isBtnHit, _buttonThread, _inCallback 118 if _inCallback: 119 return # inhibit reentrance 120 _inCallback = True 121 if not _isButtonEnabled: 122 Tools.debug("Button event detected, but button disabled") 123 return 124 try: 125 if GPIO.input(SharedConstants.P_BUTTON) == GPIO.LOW: 126 if _buttonThread == None: # down-down is suppressed 127 Tools.debug("ButtonDown event on channel " + str(channel)) 128 _isBtnHit = True 129 _buttonThread = ButtonThread() 130 _buttonThread.start() 131 if _buttonListener != None: 132 _buttonListener(SharedConstants.BUTTON_PRESSED) 133 else: 134 if _buttonThread != None: # up-up is suppressed 135 Tools.debug("ButtonUp event on channel " + str(channel)) 136 _buttonThread.stop() 137 _buttonThread.join(200) # wait until finished 138 _buttonThread = None 139 if _buttonListener != None: 140 _buttonListener(SharedConstants.BUTTON_RELEASED) 141 except: # NoneType error when program is already terminated 142 pass 143 _inCallback = False
144
145 -def _onBatteryDown(channel):
146 Tools.debug("onBatteryDown event on channel " + str(channel)) 147 if _batteryListener != None: 148 _batteryListener()
149 150 # --------------------------- Global variables ------------------------ 151 _buttonThread = None 152 _inCallback = False 153 _clickThread = None 154 _doubleClickTime = SharedConstants.BUTTON_DOUBLECLICK_TIME 155 _buttonListener = None 156 _xButtonListener = None 157 _batteryListener = None 158 _isBtnHit = False 159 _isButtonEnabled = False
160 161 # ------------------------ Class Robot ------------------------------------------------- 162 -class Robot(object):
163 ''' 164 Class that creates or returns a single MyRobot instance. 165 Signature for the butten event callback: buttonEvent(int). 166 (BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_LONGPRESSED defined in ShareConstants.) 167 @param ipAddress the IP address (default: None for autonomous mode) 168 @param buttonEvent the callback function for pushbutton events (default: None) 169 '''
170 - def __new__(cls, ipAddress = "", buttonEvent = None):
171 global _isBtnHit 172 if RobotInstance.getRobot() == None: 173 r = MyRobot(ipAddress, buttonEvent) 174 r.isEscapeHit() # Dummy to clear button hit flag 175 RobotInstance.setRobot(r) 176 for sensor in RobotInstance._sensorsToRegister: 177 r.registerSensor(sensor) 178 return r 179 else: 180 r = RobotInstance.getRobot() 181 r.isEscapeHit() # Dummy to clear button hit flag 182 return RobotInstance.getRobot()
183
184 # ------------------------ Class MyRobot ----------------------------------------------- 185 -class MyRobot(object):
186 ''' 187 Singleton class that represents a robot. 188 Signature for the butten event callback: buttonEvent(int). 189 (BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_LONGPRESSED defined in ShareConstants.) 190 @param ipAddress the IP address (default: None for autonomous mode) 191 @param buttonEvent the callback function for pushbutton events (default: None) 192 ''' 193 _myInstance = None
194 - def __init__(self, ipAddress = "", buttonEvent = None):
195 ''' 196 Creates an instance of MyRobot and initalizes the GPIO. 197 ''' 198 if MyRobot._myInstance != None: 199 raise Exception("Only one instance of MyRobot allowed") 200 global _isButtonEnabled, _buttonListener 201 202 _buttonListener = buttonEvent 203 204 # Use physical pin numbers 205 GPIO.setmode(GPIO.BOARD) 206 GPIO.setwarnings(False) 207 208 # Left motor 209 GPIO.setup(SharedConstants.P_LEFT_FORWARD, GPIO.OUT) 210 SharedConstants.LEFT_MOTOR_PWM[0] = GPIO.PWM(SharedConstants.P_LEFT_FORWARD, SharedConstants.MOTOR_PWM_FREQ) 211 SharedConstants.LEFT_MOTOR_PWM[0].start(0) 212 GPIO.setup(SharedConstants.P_LEFT_BACKWARD, GPIO.OUT) 213 SharedConstants.LEFT_MOTOR_PWM[1] = GPIO.PWM(SharedConstants.P_LEFT_BACKWARD, SharedConstants.MOTOR_PWM_FREQ) 214 SharedConstants.LEFT_MOTOR_PWM[1].start(0) 215 216 # Right motor 217 GPIO.setup(SharedConstants.P_RIGHT_FORWARD, GPIO.OUT) 218 SharedConstants.RIGHT_MOTOR_PWM[0] = GPIO.PWM(SharedConstants.P_RIGHT_FORWARD, SharedConstants.MOTOR_PWM_FREQ) 219 SharedConstants.RIGHT_MOTOR_PWM[0].start(0) 220 GPIO.setup(SharedConstants.P_RIGHT_BACKWARD, GPIO.OUT) 221 SharedConstants.RIGHT_MOTOR_PWM[1] = GPIO.PWM(SharedConstants.P_RIGHT_BACKWARD, SharedConstants.MOTOR_PWM_FREQ) 222 SharedConstants.RIGHT_MOTOR_PWM[1].start(0) 223 224 # IR sensors 225 GPIO.setup(SharedConstants.P_FRONT_LEFT, GPIO.IN, GPIO.PUD_UP) 226 GPIO.setup(SharedConstants.P_FRONT_CENTER, GPIO.IN, GPIO.PUD_UP) 227 GPIO.setup(SharedConstants.P_FRONT_RIGHT, GPIO.IN, GPIO.PUD_UP) 228 GPIO.setup(SharedConstants.P_LINE_LEFT, GPIO.IN, GPIO.PUD_UP) 229 GPIO.setup(SharedConstants.P_LINE_RIGHT, GPIO.IN, GPIO.PUD_UP) 230 231 # Establish event recognition from battery monitor 232 GPIO.setup(SharedConstants.P_BATTERY_MONITOR, GPIO.IN, GPIO.PUD_UP) 233 GPIO.add_event_detect(SharedConstants.P_BATTERY_MONITOR, GPIO.RISING, _onBatteryDown) 234 235 236 Tools.debug("Trying to detect I2C bus") 237 self._bus = smbus.SMBus(1) # For revision 2 Raspberry Pi 238 Tools.debug("Found SMBus for revision 2") 239 240 # I2C PWM chip PCM9685 for LEDs and servos 241 self.pwm = PWM(self._bus, SharedConstants.PWM_I2C_ADDRESS) 242 self.pwm.setFreq(SharedConstants.PWM_FREQ) 243 self.isPCA9685Available = self.pwm._isAvailable 244 if self.isPCA9685Available: 245 # clear all LEDs 246 for id in range(3): 247 self.pwm.setDuty(3 * id, 0) 248 self.pwm.setDuty(3 * id + 1, 0) 249 self.pwm.setDuty(3 * id + 2, 0) 250 # I2C analog extender chip 251 Tools.debug("Trying to detect PCF8591P I2C expander") 252 channel = 0 253 try: 254 self._bus.write_byte(SharedConstants.ADC_I2C_ADDRESS, channel) 255 self._bus.read_byte(SharedConstants.ADC_I2C_ADDRESS) # ignore reply 256 data = self._bus.read_byte(SharedConstants.ADC_I2C_ADDRESS) 257 Tools.debug("Found PCF8591P I2C expander") 258 except: 259 Tools.debug("PCF8591P I2C expander not found") 260 261 self.displayType = "none" 262 Tools.debug("Trying to detect OLED display") 263 self.oled = OLED1306() 264 if self.oled.isDeviceAvailable(): 265 self.displayType= "oled" 266 else: 267 Tools.debug("'oled' display not found") 268 self.oled = None 269 Tools.debug("Trying to detect 7-segment display") 270 try: 271 addr = 0x20 272 rc = self._bus.read_byte_data(addr, 0) 273 if rc != 0xA0: # returns 255 for 4tronix 274 raise Exception() 275 Tools.delay(100) 276 self.displayType = "didel1" 277 except: 278 Tools.debug("'didel1' display not found") 279 if self.displayType == "none": 280 try: 281 addr = 0x20 282 self._bus.write_byte_data(addr, 0x00, 0x00) # Set all of bank 0 to outputs 283 Tools.delay(100) 284 self.displayType = "4tronix" 285 except: 286 Tools.debug("'4tronix' display not found") 287 if self.displayType == "none": 288 try: 289 addr = 0x24 # old dgTell from didel 290 data = [0] * 4 291 self._bus.write_i2c_block_data(addr, data[0], data[1:]) # trying to clear display 292 self.displayType = "didel" 293 except: 294 Tools.debug("'didel (old type)' display not found") 295 296 Tools.debug("Display type '" + self.displayType + "'") 297 298 # Initializing (clear) display, if available 299 if self.displayType == "4tronix": 300 Disp4tronix().clear() 301 elif self.displayType == "didel": 302 DgTell().clear() 303 elif self.displayType == "didel1": 304 DgTell1().clear() 305 306 GPIO.setup(SharedConstants.P_BUTTON, GPIO.IN, GPIO.PUD_UP) 307 # Establish event recognition from button event 308 GPIO.add_event_detect(SharedConstants.P_BUTTON, GPIO.BOTH, _onButtonEvent) 309 _isButtonEnabled = True 310 Tools.debug("MyRobot instance created. Lib Version: " + SharedConstants.VERSION) 311 self.sensorThread = None 312 MyRobot._myInstance = self
313
314 - def registerSensor(self, sensor):
315 if self.sensorThread == None: 316 self.sensorThread = SensorThread() 317 self.sensorThread.start() 318 self.sensorThread.add(sensor)
319 320
321 - def exit(self):
322 """ 323 Cleans-up and releases all resources. 324 """ 325 global _isButtonEnabled 326 Tools.debug("Calling Robot.exit()") 327 self.setButtonEnabled(False) 328 329 # Stop motors 330 SharedConstants.LEFT_MOTOR_PWM[0].ChangeDutyCycle(0) 331 SharedConstants.LEFT_MOTOR_PWM[1].ChangeDutyCycle(0) 332 SharedConstants.RIGHT_MOTOR_PWM[0].ChangeDutyCycle(0) 333 SharedConstants.RIGHT_MOTOR_PWM[1].ChangeDutyCycle(0) 334 335 # Stop button thread, if necessary 336 if _buttonThread != None: 337 _buttonThread.stop() 338 339 # Stop display 340 display = Display._myInstance 341 if display != None: 342 display.stopTicker() 343 display.clear() 344 345 if self.isPCA9685Available: 346 Led.clearAll() 347 MyRobot.closeSound() 348 349 if self.sensorThread != None: 350 self.sensorThread.stop() 351 self.sensorThread.join(2000) 352 353 # GPIO.cleanup() 354 # Do not cleanup, otherwise button will not work any more when coming back from remote execution 355 356 Tools.delay(2000) # avoid "sys.excepthook is missing"
357
358 - def isButtonDown(self):
359 ''' 360 Checks if button is currently pressed. 361 @return: True, if the button is actually pressed 362 ''' 363 Tools.delay(1); 364 return GPIO.input(SharedConstants.P_BUTTON) == GPIO.LOW
365
366 - def isButtonHit(self):
367 ''' 368 Checks, if the button was ever hit or hit since the last invocation. 369 @return: True, if the button was hit; otherwise False 370 ''' 371 global _isBtnHit 372 Tools.delay(1) 373 hit = _isBtnHit 374 _isBtnHit = False 375 return hit
376
377 - def isEscapeHit(self):
378 ''' 379 Same as isButtonHit() for compatibility with remote mode. 380 ''' 381 return self.isButtonHit()
382
383 - def isEnterHit(self):
384 ''' 385 Empty method for compatibility with remote mode. 386 ''' 387 pass
388
389 - def isUpHit(self):
390 ''' 391 Empty method for compatibility with remote mode. 392 ''' 393 pass
394
395 - def isDownHit(self):
396 ''' 397 Empty method for compatibility with remote mode. 398 ''' 399 pass
400
401 - def isLeftHit(self):
402 ''' 403 Empty method for compatibility with remote mode. 404 ''' 405 pass
406
407 - def isRightHit(self):
408 ''' 409 Empty method for compatibility with remote mode. 410 ''' 411 pass
412
413 - def addButtonListener(self, listener, enableClick = False, 414 doubleClickTime = SharedConstants.BUTTON_DOUBLECLICK_TIME):
415 ''' 416 Registers a listener function to get notifications when the pushbutton is pressed, released or long pressed. 417 If enableClick = True, in addition click and double-click events are reported. The click event not immediately 418 reported, but only if within the doubleClickTime no other click is gererated. 419 The value are defined as ShareConstants.BUTTON_PRESSED, ShareConstants.BUTTON_LONGPRESSED, ShareConstants.BUTTON_RELEASED, 420 ShareConstants.BUTTON_CLICKED, ShareConstants.BUTTON_DOUBLECLICKED. 421 With enableClick = False and the button is long pressed and released the sequence is: BUTTON_PRESSED, BUTTON_LONGPRESSED, BUTTON_RELEASED. 422 With enableClick = True the sequences are the following: 423 click: BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_CLICKED 424 double-click: BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_PRESSED, BUTTON_RELEASED, BUTTON_DOUBLECLICKED 425 long pressed: BUTTON_PRESSED, BUTTON_LONGPRESSED, BUTTON_RELEASED 426 @param listener: the listener function (with boolean parameter event) to register. 427 @param enableClick: if True, the click/double-click is also registered (default: False) 428 @param doubleClickTime: the time (in seconds) to wait for a double click (default: set in SharedContants) 429 ''' 430 if enableClick: 431 global _xButtonListener, _doubleClickTime 432 _doubleClickTime = doubleClickTime 433 self.addButtonListener(_onXButtonEvent) 434 _xButtonListener = listener 435 else: 436 global _buttonListener 437 _buttonListener = listener
438
439 - def setButtonEnabled(self, enable):
440 ''' 441 Enables/disables the push button. The button is enabled, when the Robot is created. 442 @param enable: if True, the button is enabled; otherwise disabled 443 ''' 444 Tools.debug("Calling setButtonEnabled() with enable = " + str(enable)) 445 global _isButtonEnabled 446 _isButtonEnabled = enable
447
448 - def addBatteryMonitor(self, listener):
449 ''' 450 There is a small processor on the PCB (an STM8S003F3P6) which handles the voltage monitoring, 451 as well as trying to reduce the impact of direct light on the IR sensors. 452 It has 2 threshold voltages: At about 6.5V (3 consecutive readings) it flashes the red LED and 453 disables the motor drivers. At about 6.2V it turns the red LED on permanently and 454 sends a signal on GPIO24 pin 18 to the Pi. The software on the Pi can monitor this and 455 shut down gracefully if required. If the voltage goes back above 7.0V then the system resets 456 to Green LED and all enabled. 457 458 Registers a listener function to get notifications when battery voltage is getting low. 459 @param listener: the listener function (with no parameter) to register 460 ''' 461 batteryListener = listener
462 463 464 # ---------------------------------------------- static methods ----------------------------------- 465 @staticmethod
466 - def getVersion():
467 ''' 468 @return: the module library version 469 ''' 470 return SharedConstants.VERSION
471 472 @staticmethod
473 - def setSoundVolume(volume):
474 ''' 475 Sets the sound volume. Value is kept when the program exits. 476 @param volume: the sound volume (0..100) 477 ''' 478 os.system("amixer sset PCM,0 " + str(volume)+ "% >/dev/null")
479 480 @staticmethod
481 - def playTone(frequency, duration):
482 ''' 483 Plays a single sine tone with given frequency and duration. 484 @param frequency: the frequency in Hz 485 @param duration: the duration in ms 486 ''' 487 os.system("speaker-test -t sine -f " + str(frequency) 488 + " >/dev/null & pid=$! ; sleep " + str(duration / 1000.0) + "s ; kill -9 $pid")
489 490 @staticmethod
491 - def getIPAddresses():
492 ''' 493 @return: List of all IP addresses of machine 494 ''' 495 p = Popen(["ifconfig"], stdout = PIPE) 496 ifc_resp = p.communicate() 497 patt = re.compile(r'inet\s*\w*\S*:\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})') 498 resp = patt.findall(ifc_resp[0]) 499 return resp
500 501 @staticmethod
502 - def initSound(soundFile, volume):
503 ''' 504 Prepares the given wav or mp3 sound file for playing with given volume (0..100). The sound 505 sound channel is opened and a background noise is emitted. 506 @param soundFile: the sound file in the local file system 507 @volume: the sound volume (0..100) 508 @returns: True, if successful; False if the sound system is not available or the sound file 509 cannot be loaded 510 ''' 511 try: 512 pygame.mixer.init() 513 except: 514 # print "Error while initializing sound system" 515 return False 516 try: 517 pygame.mixer.music.load(soundFile) 518 except: 519 pygame.mixer.quit() 520 # print "Error while loading sound file", soundFile 521 return False 522 try: 523 pygame.mixer.music.set_volume(volume / 100.0) 524 except: 525 return False 526 return True
527 528 @staticmethod
529 - def closeSound():
530 ''' 531 Stops any playing sound and closes the sound channel. 532 ''' 533 try: 534 pygame.mixer.stop() 535 pygame.mixer.quit() 536 except: 537 pass
538 539 @staticmethod
540 - def playSound():
541 ''' 542 Starts playing. 543 ''' 544 try: 545 pygame.mixer.music.play() 546 except: 547 pass
548 549 @staticmethod
550 - def fadeoutSound(time):
551 ''' 552 Decreases the volume slowly and stops playing. 553 @param time: the fade out time in ms 554 ''' 555 try: 556 pygame.mixer.music.fadeout(time) 557 except: 558 pass
559 560 @staticmethod
561 - def setSoundVolume(volume):
562 ''' 563 Sets the volume while the sound is playing. 564 @param volume: the sound volume (0..100) 565 ''' 566 try: 567 pygame.mixer.music.set_volume(volume / 100.0) 568 except: 569 pass
570 571 @staticmethod
572 - def stopSound():
573 ''' 574 Stops playing sound. 575 ''' 576 try: 577 pygame.mixer.music.stop() 578 except: 579 pass
580 581 @staticmethod
582 - def pauseSound():
583 ''' 584 Temporarily stops playing at current position. 585 ''' 586 try: 587 pygame.mixer.music.pause() 588 except: 589 pass
590 591 @staticmethod
592 - def resumeSound():
593 ''' 594 Resumes playing from stop position. 595 ''' 596 try: 597 pygame.mixer.music.unpause() 598 except: 599 pass
600 601 @staticmethod
602 - def rewindSound():
603 ''' 604 Resumes playing from the beginning. 605 ''' 606 try: 607 pygame.mixer.music.rewind() 608 except: 609 pass
610 611 @staticmethod
612 - def isSoundPlaying():
613 ''' 614 @return: True, if the sound is playing; otherwise False 615 ''' 616 try: 617 return pygame.mixer.music.get_busy() 618 except: 619 return False
620