1
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
57
63
76
78 self.isRunning = False
79
114
144
149
150
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
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):
183
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
205 GPIO.setmode(GPIO.BOARD)
206 GPIO.setwarnings(False)
207
208
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
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
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
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)
238 Tools.debug("Found SMBus for revision 2")
239
240
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
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
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)
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:
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)
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
290 data = [0] * 4
291 self._bus.write_i2c_block_data(addr, data[0], data[1:])
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
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
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
315 if self.sensorThread == None:
316 self.sensorThread = SensorThread()
317 self.sensorThread.start()
318 self.sensorThread.add(sensor)
319
320
357
365
376
378 '''
379 Same as isButtonHit() for compatibility with remote mode.
380 '''
381 return self.isButtonHit()
382
384 '''
385 Empty method for compatibility with remote mode.
386 '''
387 pass
388
390 '''
391 Empty method for compatibility with remote mode.
392 '''
393 pass
394
396 '''
397 Empty method for compatibility with remote mode.
398 '''
399 pass
400
402 '''
403 Empty method for compatibility with remote mode.
404 '''
405 pass
406
408 '''
409 Empty method for compatibility with remote mode.
410 '''
411 pass
412
438
447
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
465 @staticmethod
471
472 @staticmethod
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
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
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
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
515 return False
516 try:
517 pygame.mixer.music.load(soundFile)
518 except:
519 pygame.mixer.quit()
520
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
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
541 '''
542 Starts playing.
543 '''
544 try:
545 pygame.mixer.music.play()
546 except:
547 pass
548
549 @staticmethod
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
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
573 '''
574 Stops playing sound.
575 '''
576 try:
577 pygame.mixer.music.stop()
578 except:
579 pass
580
581 @staticmethod
583 '''
584 Temporarily stops playing at current position.
585 '''
586 try:
587 pygame.mixer.music.pause()
588 except:
589 pass
590
591 @staticmethod
593 '''
594 Resumes playing from stop position.
595 '''
596 try:
597 pygame.mixer.music.unpause()
598 except:
599 pass
600
601 @staticmethod
603 '''
604 Resumes playing from the beginning.
605 '''
606 try:
607 pygame.mixer.music.rewind()
608 except:
609 pass
610
611 @staticmethod
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