├── .gitignore ├── src ├── examples │ ├── __init__.py │ ├── mip_test_get_battery_level.py │ ├── mip_get_volume.py │ ├── mip_test_get_odometer.py │ ├── mip_set_volume.py │ ├── mip_test_get_chest_led.py │ ├── mip_test_radar_continuous_drive.py │ ├── turtle_example.py │ ├── mip_test_get_orientation.py │ ├── mip_test_get_weight.py │ ├── mip_test_clap.py │ ├── mip_test_odometer.py │ ├── mip_test_eyes.py │ ├── mip_test_continuous_drive_gui2.py │ ├── mip_test_continuous_drive.py │ ├── mip_test_gui.py │ ├── mip_test_sound.py │ ├── mip_test_continuous_drive_gui.py │ ├── movement_canvas.py │ └── mip_explorer_gui.py ├── setup.py └── mippy │ └── __init__.py ├── doc └── mods │ ├── lipo │ ├── wheel.jpg │ ├── back_plate.jpg │ ├── end_result.jpg │ ├── battery_box.jpg │ ├── battery_pack.jpg │ ├── hole_location.jpg │ ├── hotglued_board.jpg │ ├── protection_circuit.jpg │ ├── internal_power_connection.jpg │ └── battery_box_with_terminated_cable.jpg │ └── lipo_mk2 │ ├── battery_box.jpg │ ├── battery_box_no_terminals.jpg │ └── battery_box_after_cutting.jpg ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /src/examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/mods/lipo/wheel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/wheel.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/back_plate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/back_plate.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/end_result.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/end_result.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/battery_box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/battery_box.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/battery_pack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/battery_pack.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/hole_location.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/hole_location.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/hotglued_board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/hotglued_board.jpg -------------------------------------------------------------------------------- /doc/mods/lipo_mk2/battery_box.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo_mk2/battery_box.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/protection_circuit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/protection_circuit.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/internal_power_connection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/internal_power_connection.jpg -------------------------------------------------------------------------------- /doc/mods/lipo_mk2/battery_box_no_terminals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo_mk2/battery_box_no_terminals.jpg -------------------------------------------------------------------------------- /doc/mods/lipo_mk2/battery_box_after_cutting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo_mk2/battery_box_after_cutting.jpg -------------------------------------------------------------------------------- /doc/mods/lipo/battery_box_with_terminated_cable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vlimit/mip/HEAD/doc/mods/lipo/battery_box_with_terminated_cable.jpg -------------------------------------------------------------------------------- /src/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='mippy', 6 | version='0.1', 7 | description='Wowwee MIP library', 8 | author='Stuart Warren', 9 | author_email='devel@rachandstu.com', 10 | url='https://github.com/vlimit/mip', 11 | packages=['mippy'] 12 | ) 13 | 14 | -------------------------------------------------------------------------------- /src/examples/mip_test_get_battery_level.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to get the battery level 5 | To Use: 6 | mip_test_get_battery_level.py -i hci0 -b D0:39:72:C4:7A:01 7 | 8 | """ 9 | 10 | import logging 11 | import mippy 12 | import argparse 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Get MiPs battery level.') 17 | mippy.add_arguments(parser) 18 | args = parser.parse_args() 19 | 20 | logging.basicConfig(level=logging.DEBUG) 21 | gt = mippy.GattTool(args.adaptor, args.device) 22 | 23 | mip = mippy.Mip(gt) 24 | voltage = mip.getBatteryLevel() 25 | print 'Battery Voltage (4.0v-6.4v): %f' % (voltage) 26 | -------------------------------------------------------------------------------- /src/examples/mip_get_volume.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Sound tester. 5 | To Use: 6 | mip_get_volume.py -i hci0 -b D0:39:72:C4:7A:01 7 | Prints current volume level : should be between 0 and 7 8 | """ 9 | 10 | import mippy 11 | import argparse 12 | import time 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Get MiP Volume.') 17 | mippy.add_arguments(parser) 18 | args = parser.parse_args() 19 | 20 | gt = mippy.GattTool(args.adaptor, args.device) 21 | 22 | mip = mippy.Mip(gt) 23 | volume = mip.getVolume() 24 | time.sleep(1) 25 | # play roam sound 26 | mip.playSound(0xfd) 27 | print 'Volume (0..7): %d' % (volume) 28 | -------------------------------------------------------------------------------- /src/examples/mip_test_get_odometer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to get an angle representing the amount of weight MiP is carrying 5 | To Use: 6 | mip_test_get_odometer.py -i hci0 -b D0:39:72:C4:7A:01 7 | 8 | """ 9 | 10 | import logging 11 | import mippy 12 | import argparse 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Get MiPs odometer.') 17 | mippy.add_arguments(parser) 18 | args = parser.parse_args() 19 | 20 | logging.basicConfig(level=logging.DEBUG) 21 | gt = mippy.GattTool(args.adaptor, args.device) 22 | 23 | mip = mippy.Mip(gt) 24 | distance = mip.getOdometer() 25 | print 'Distane (cm): %f' % (distance) 26 | -------------------------------------------------------------------------------- /src/examples/mip_set_volume.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Volume control 5 | To Use: 6 | mip_set_volume.py -i hci0 -b D0:39:72:C4:7A:01 -v 7 | volume should be between 0 and 7 8 | """ 9 | 10 | import mippy 11 | import argparse 12 | import time 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='MiP Volume control.') 17 | mippy.add_arguments(parser) 18 | parser.add_argument( 19 | '-v', 20 | '--volume', 21 | default=0x1, 22 | help='Specify volume (0..7)', type=int) 23 | args = parser.parse_args() 24 | 25 | gt = mippy.GattTool(args.adaptor, args.device) 26 | 27 | mip = mippy.Mip(gt) 28 | mip.setVolume(args.volume) 29 | time.sleep(1) 30 | # play roam sound at new volume level 31 | mip.playSound(0xfd) 32 | -------------------------------------------------------------------------------- /src/examples/mip_test_get_chest_led.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to get the chest LED colour 5 | To Use: 6 | mip_test_get_chest_led.py -i hci0 -b D0:39:72:C4:7A:01 7 | 8 | """ 9 | 10 | import logging 11 | import mippy 12 | import argparse 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Get MiPs chest LED colour.') 17 | mippy.add_arguments(parser) 18 | args = parser.parse_args() 19 | 20 | logging.basicConfig(level=logging.DEBUG) 21 | gt = mippy.GattTool(args.adaptor, args.device) 22 | 23 | mip = mippy.Mip(gt) 24 | # get Chest LE colour 25 | colourVals = [] 26 | colourVals = mip.getChestLed() 27 | print 'Red: %d' % (colourVals[0]) 28 | print 'Green: %d' % (colourVals[1]) 29 | print 'Blue: %d' % (colourVals[2]) 30 | -------------------------------------------------------------------------------- /src/examples/mip_test_radar_continuous_drive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to drive forwards in continuous drive mode until the radar says stop 5 | To Use: 6 | mip_test_radar_continuous_drive.py -i hci0 -b D0:39:72:C4:7A:01 -s 7 | 8 | """ 9 | 10 | import logging 11 | import mippy 12 | import argparse 13 | import time 14 | 15 | if __name__ == '__main__': 16 | 17 | parser = argparse.ArgumentParser(description='Test MiPs radar.') 18 | mippy.add_arguments(parser) 19 | parser.add_argument( 20 | '-s', 21 | '--speed', 22 | default=0x1, 23 | help='Specify speed (0..32)', type=int) 24 | args = parser.parse_args() 25 | 26 | logging.basicConfig(level=logging.DEBUG) 27 | 28 | gt = mippy.GattTool(args.adaptor, args.device) 29 | mip = mippy.Mip(gt) 30 | mip.continuousDriveForwardUntilRadar(args.speed) 31 | -------------------------------------------------------------------------------- /src/examples/turtle_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Logo-style turtle example. 5 | """ 6 | 7 | import argparse 8 | import os.path 9 | import sys 10 | import time 11 | 12 | sys.path.append(os.path.join(os.path.split(__file__)[0], '..')) 13 | 14 | import mippy 15 | 16 | if __name__ == '__main__': 17 | 18 | parser = argparse.ArgumentParser(description='Turtle example.') 19 | mippy.add_arguments(parser) 20 | args = parser.parse_args() 21 | 22 | gt = mippy.GattTool(args.adaptor, args.device) 23 | 24 | mip = mippy.Mip(gt) 25 | mip.playSound(0x4d) 26 | 27 | turtle = mippy.Turtle(mip) 28 | 29 | for i in range(4): 30 | turtle.forward(0.4) 31 | turtle.right(90) 32 | 33 | for i in range(2): 34 | turtle.right(720) 35 | turtle.left(720) 36 | 37 | mip.setChestLed(0.5, 0.0, 0.0) 38 | 39 | time.sleep(2) 40 | 41 | -------------------------------------------------------------------------------- /src/examples/mip_test_get_orientation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to get the battery level 5 | To Use: 6 | mip_test_get_orientation.py -i hci0 -b D0:39:72:C4:7A:01 7 | 8 | """ 9 | 10 | import logging 11 | import mippy 12 | import argparse 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Get MiPs orientation.') 17 | mippy.add_arguments(parser) 18 | args = parser.parse_args() 19 | 20 | logging.basicConfig(level=logging.DEBUG) 21 | gt = mippy.GattTool(args.adaptor, args.device) 22 | 23 | mip = mippy.Mip(gt) 24 | orientation = mip.getMiPOrientationStatus() 25 | orientationString = ['on back' , 'face down' , 'upright' , 'picked up', 26 | 'hand stand' , 'face down on tray' , 27 | 'on back with kickstand' ] 28 | print 'Orientation: %s' % (orientationString[orientation]) 29 | -------------------------------------------------------------------------------- /src/examples/mip_test_get_weight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to get an angle representing the amount of weight MiP is carrying 5 | To Use: 6 | mip_test_get_weight.py -i hci0 -b D0:39:72:C4:7A:01 7 | 8 | """ 9 | 10 | import logging 11 | import mippy 12 | import argparse 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Get MiPs weight angle.') 17 | mippy.add_arguments(parser) 18 | args = parser.parse_args() 19 | 20 | logging.basicConfig(level=logging.DEBUG) 21 | gt = mippy.GattTool(args.adaptor, args.device) 22 | 23 | mip = mippy.Mip(gt) 24 | weightAngle = mip.getWeight() 25 | orientationString = ['on back' , 'face down' , 'upright' , 'picked up', 26 | 'hand stand' , 'face down on tray' , 27 | 'on back with kickstand' ] 28 | print 'Weight Angle (+ve = weight on back): %f' % (weightAngle) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 vlimit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/examples/mip_test_clap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to test clap recognition software 5 | To Use: 6 | ./mip_test_clap.py -i hci0 -b D0:39:72:C4:7A:01 7 | 8 | """ 9 | import logging 10 | import mippy 11 | import argparse 12 | import time 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Test MiPs clap recognition.') 17 | mippy.add_arguments(parser) 18 | args = parser.parse_args() 19 | 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | gt = mippy.GattTool(args.adaptor, args.device) 23 | mip = mippy.Mip(gt) 24 | # enable clap recognition 25 | logging.debug('Enable clap') 26 | mip.clapEnable(0x1) 27 | clapStatus = mip.requestClapStatus() 28 | logging.debug('Clap status %x.' % (clapStatus)) 29 | logging.debug('Entering loop requesting clap status: Ctrl-C to exit.') 30 | done = 0 31 | while done == 0: 32 | logging.debug('Requesting clap times.') 33 | retval = mip.getClapTimes() 34 | if retval > 0: 35 | logging.debug('Clap detected') 36 | else: 37 | logging.debug('NO Clap detected') 38 | -------------------------------------------------------------------------------- /src/examples/mip_test_odometer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to test problems getting odometer readings 5 | To Use: 6 | mip_test_odometer.py -i hci0 -b D0:39:72:C4:7A:01 -d 7 | 8 | """ 9 | 10 | import logging 11 | import mippy 12 | import argparse 13 | 14 | if __name__ == '__main__': 15 | 16 | parser = argparse.ArgumentParser(description='Test MiPs odometer.') 17 | mippy.add_arguments(parser) 18 | parser.add_argument( 19 | '-d', 20 | '--distance', 21 | default=0.1, 22 | help='Distance to move in metres', type=float) 23 | args = parser.parse_args() 24 | 25 | logging.basicConfig(level=logging.DEBUG) 26 | gt = mippy.GattTool(args.adaptor, args.device) 27 | 28 | mip = mippy.Mip(gt) 29 | mip.resetOdomemeter() 30 | distance = mip.getOdometer() 31 | print 'Distane (cm): %f' % (distance) 32 | mip.distanceDrive(args.distance) 33 | distance = mip.getOdometer() 34 | print 'Distane (cm): %f' % (distance) 35 | # 0.1m = 1485 = 30cm @ 48.5/cm 36 | # 0.2m = 2128 = 43cm @ 48.5/cm 37 | # 0.3m = 2089 = 43cm @ 48.5/cm 38 | # 0.5m = 3400 = 70cm @ 48.5/cmm 39 | # 1.0m = 6253 = 129cm @ 48.5/cm 40 | -------------------------------------------------------------------------------- /src/examples/mip_test_eyes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Eye control test program 5 | To Use: 6 | mip_test_eyes.py -i hci0 -b D0:39:72:C4:7A:01 -e1 -e2 -e3 -e4 7 | Each e parameter controls one half of one of MiPs eyes: 8 | eye1 left hand side left eye 9 | eye2 right hand side left eye 10 | eye3 left hand side right eye 11 | eye4 right hand side right eye 12 | 13 | 0 = off 14 | 1 = on 15 | 2 = blink slow 16 | 3 = blink fast 17 | """ 18 | 19 | import mippy 20 | import argparse 21 | 22 | if __name__ == '__main__': 23 | 24 | parser = argparse.ArgumentParser(description='Test MiP Sounds.') 25 | mippy.add_arguments(parser) 26 | parser.add_argument('-e1','--eye1',default=0x1, 27 | help='Eye 1 control. 0 = off, 1 = on, 2 = blink slow, 3 = blink fast', type=int) 28 | parser.add_argument('-e2','--eye2',default=0x1, 29 | help='Eye 2 control. 0 = off, 1 = on, 2 = blink slow, 3 = blink fast', type=int) 30 | parser.add_argument('-e3','--eye3',default=0x1, 31 | help='Eye 3 control. 0 = off, 1 = on, 2 = blink slow, 3 = blink fast', type=int) 32 | parser.add_argument('-e4','--eye4',default=0x1, 33 | help='Eye 4 control. 0 = off, 1 = on, 2 = blink slow, 3 = blink fast', type=int) 34 | args = parser.parse_args() 35 | 36 | gt = mippy.GattTool(args.adaptor, args.device) 37 | 38 | mip = mippy.Mip(gt) 39 | mip.setHeadLed(args.eye1,args.eye2,args.eye3,args.eye4) 40 | # sleep a bit to display results 41 | time.sleep(10.0) 42 | -------------------------------------------------------------------------------- /src/examples/mip_test_continuous_drive_gui2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Setup a movement GUI. 5 | Move Mip based on the GUI 6 | mip_test_continuous_drive_gui2.py -i hci0 -b D0:39:72:C4:7A:01 [-c|--carzy} 7 | """ 8 | 9 | #from Tkinter import * 10 | import Tkinter 11 | from movement_canvas import MovementCanvas 12 | import logging 13 | import mippy 14 | import argparse 15 | 16 | carzy = 0 17 | 18 | def updateMovement(): 19 | # movementCanvas.positionX,movementCanvas.positionY, (-50 - 50) 20 | # movementCanvas.positionAngle,movementCanvas.positionMagnitude 21 | if movementCanvas.positionMagnitude < 10: 22 | logging.debug('updateMovement : stopping') 23 | mip.continuousDriveForward(0x0) 24 | else: 25 | forwardSpeed = int((-movementCanvas.positionY * 0x20)/50) 26 | turnSpeed = int((movementCanvas.positionX * 0x20)/50) 27 | logging.debug('updateMovement : forwardSpeed %d : turnSpeed %d' % (forwardSpeed,turnSpeed)) 28 | if carzy > 0: 29 | mip.continuousCarzyDrive(forwardSpeed,turnSpeed) 30 | else: 31 | mip.continuousDrive(forwardSpeed,turnSpeed) 32 | top.after(50,updateMovement) 33 | 34 | 35 | if __name__ == '__main__': 36 | parser = argparse.ArgumentParser(description='Continuous Drive GUI.') 37 | mippy.add_arguments(parser) 38 | parser.add_argument( 39 | '-c', 40 | '--carzy', 41 | default=0x0,action='count', 42 | help='Carzy mode') 43 | args = parser.parse_args() 44 | 45 | if args.carzy > 0: 46 | carzy = 1 47 | else: 48 | carzy = 0 49 | print ("Carzy Mode = %d " % (carzy)) 50 | 51 | logging.basicConfig(level=logging.DEBUG) 52 | gt = mippy.GattTool(args.adaptor, args.device) 53 | mip = mippy.Mip(gt) 54 | # start gui 55 | top = Tkinter.Tk() 56 | movementCanvas = MovementCanvas(top,300,300) 57 | movementCanvas.setBindings() 58 | #movementCanvas.addUpdateCallback(movement) 59 | movementCanvas.pack() 60 | top.after(50,updateMovement) 61 | top.mainloop() 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Experiments with Wowee's MIP 2 | 3 | ##Introduction 4 | This started out as a reverse engineering effort to work out the protocol used by Wowwee's MIP robot. I used an Ubertooth One to sniff the Bluetooth Low Energy traffic and worked out some of the commands to control the robot. After posting a script on Git Hub, I emailed Wowwee asking about additional documentation. Andy, the R&D Software Manager at Wowwee got back to me and within a few days they posted [documentation] (https://github.com/WowWeeLabs/MiP-BLE-Protocol/blob/master/MiP-Protocol.md) on their Git Hub site. Fantastic! Thanks Wowwee! 5 | 6 | So this is no longer about reverse engineering the MIP and more about providing an easy way to drive the MIP using Python. 7 | 8 | ##Getting Started - Linux 9 | You need a computer with Bluetooth 4.0 capability, or a Bluetooth 4.0 dongle. I'm using a $4 USB dongle I picked up on ebay. It shows up like this in Linux: 10 | 11 | ``` 12 | $ lsusb 13 | Bus 003 Device 002: ID 0a12:0001 Cambridge Silicon Radio, Ltd Bluetooth Dongle (HCI mode) 14 | ``` 15 | 16 | You need a recent version of bluez installed. In particular, you need hcitool and gatttool. Install bluez 5.23 or greater. If you're on Fedora 20 or 21, you'll need to build it yourself. This [bug report] (https://bugzilla.redhat.com/show_bug.cgi?id=1141909) is probably of interest. 17 | 18 | Use hcitool to confirm that the interface is ready to go: 19 | 20 | ``` 21 | $ hcitool dev 22 | Devices: 23 | hci0 00:B4:7D:E2:A1:FE 24 | ``` 25 | 26 | ... so I'm going to use the bluetooth interface hci0 to look for MIP. 27 | 28 | Turn MIP on and search for it (you'll probably have to sudo): 29 | 30 | ``` 31 | $ sudo hcitool -i hci0 lescan 32 | LE Scan ... 33 | D0:39:72:B8:C5:84 (unknown) 34 | D0:39:72:B8:C5:84 WowWee-MiP-33506 35 | ``` 36 | 37 | Grab the bluetooth address of MIP (in this case, D0:39:72:B8:C5:84). Time to start the script: 38 | 39 | ``` 40 | $ ./src/examples/turtle_example.py -i hci0 -b D0:39:72:B8:C5:84 41 | ``` 42 | 43 | You should see MIPs chest light turn green when the bluetooth connection is made. MIP will make a sound and move around. 44 | 45 | -------------------------------------------------------------------------------- /src/examples/mip_test_continuous_drive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Test program to drive forwards in continuous drive mode 5 | To Use: 6 | mip_test_continuous_drive.py -i hci0 -b D0:39:72:C4:7A:01 -s 7 | Fw:0x01(slow)~-0x20(fast) Buffer = 0 8 | OR Bw:0x21(slow)~0x40(fast) This command is for single drive or turn 9 | right spin:0x41(slow)~0x60(fast) Note:Sending per 50ms if held 10 | OR Left spin:0x61(slow)~0x80(fast) 11 | Carzy Fw:0x81(slow)~-0xA0(fast) 12 | OR Carzy Bw:0x81(slow)~0xC0(fast) 13 | Carzy right spin:0xC1(slow)~0xE0(fast) 14 | OR Carzy Left spin:0xE1(slow)~0xFF(fast) 15 | """ 16 | 17 | import logging 18 | import mippy 19 | import argparse 20 | import time 21 | import pexpect 22 | 23 | if __name__ == '__main__': 24 | 25 | parser = argparse.ArgumentParser(description='Test MiPs radar.') 26 | mippy.add_arguments(parser) 27 | def auto_int(x): 28 | return int(x,0) 29 | 30 | parser.add_argument( 31 | '-s', 32 | '--speed', 33 | default=0x1, 34 | help='Specify speed (0..32)', type=auto_int) 35 | parser.add_argument( 36 | '-t', 37 | '--turnspeed', 38 | default=0x0, 39 | help='Specify number (0..32)', type=auto_int) 40 | args = parser.parse_args() 41 | 42 | logging.basicConfig(level=logging.DEBUG) 43 | 44 | # low level gattool test 45 | gt = mippy.GattTool(args.adaptor, args.device) 46 | gt.connect() 47 | time.sleep(2) 48 | # turn radar mode on 49 | logging.debug('Writing MiP Set Gesture Radar Mode ON: 0x0c 0x04 .') 50 | gt.charWriteCmd(0x13, [0x0c, 0x04]) 51 | done = 0 52 | while done == 0: 53 | #gt.charWriteCmd(0x13, [0x78, args.speed]) 54 | gt.charWriteCmd(0x13, [0x78, args.speed, args.turnspeed]) 55 | try: 56 | returnVals = gt.charReadReply(0x13, -1 ,timeout=0.02) 57 | if returnVals[0] == 0x0c: # radar response 58 | radarResponse = returnVals[1] 59 | elif returnVals[0] == 0x79: # MiPStatus response 60 | # returnVals[1] is battery level 61 | orientation = returnVals[2] 62 | except pexpect.TIMEOUT: 63 | radarResponse = 0 64 | orientation = 2 65 | # radarResponse value 0 - no radar response 66 | # radarResponse value 1 - no object 67 | # radarResponse value 2 - object between 10cm-30cm 68 | # radarResponse value 3 - object less than 10cm 69 | # orientation 0 on back 70 | # orientation 2 upright 71 | # Stop if we detect an object or we are not upright 72 | done = (radarResponse > 1) or (orientation != 2) 73 | 74 | -------------------------------------------------------------------------------- /src/examples/mip_test_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Basic Test GUI. 5 | To Use: 6 | mip_test_gui.py -i hci0 -b D0:39:72:C4:7A:01 7 | """ 8 | 9 | from Tkinter import * 10 | import mippy 11 | import argparse 12 | 13 | if __name__ == '__main__': 14 | 15 | parser = argparse.ArgumentParser(description='Basic MiP GUI.') 16 | mippy.add_arguments(parser) 17 | args = parser.parse_args() 18 | 19 | gt = mippy.GattTool(args.adaptor, args.device) 20 | 21 | mip = mippy.Mip(gt) 22 | # Create window 23 | root = Tk() 24 | root.title("MiP Control Panel") 25 | root.geometry("200x200") 26 | 27 | rootFrame = Frame(root) 28 | rootFrame.grid(column=0,row=0) 29 | 30 | configFrame = Frame(rootFrame) 31 | configFrame.grid(column=0,row=0) 32 | 33 | labeld = Label(configFrame, text = "Distance(m):") 34 | labeld.grid(column=0,row=0) 35 | 36 | distanceStringVar = StringVar() 37 | #distanceStringVar = "0.1" 38 | distanceEntry = Entry(configFrame, textvariable = distanceStringVar) 39 | distanceEntry.insert(0,"0.1") 40 | distanceEntry.grid(column=1,row=0) 41 | #distanceEntry.configure(text="1.0") 42 | #distanceEntry["text"] = "2.0" 43 | 44 | labela = Label(configFrame, text = "Angle(deg):") 45 | labela.grid(column=0,row=1) 46 | 47 | angleStringVar = StringVar() 48 | #angleStringVar = "90.0" 49 | angleEntry = Entry(configFrame, textvariable = angleStringVar ) 50 | angleEntry.insert(0,"90") 51 | angleEntry.grid(column=1,row=1) 52 | #angleEntry.configure(text="1.0") 53 | #angleEntry["text"] = "2.0" 54 | 55 | controlFrame = Frame(rootFrame) 56 | controlFrame.grid(column=0,row=1) 57 | 58 | def getDistance(): 59 | distanceString = distanceEntry.get() 60 | distance = float(distanceString) 61 | # distance = 3.0 causes MiP to spin instead - number overflow? 62 | if distance > 2.5: 63 | distance = 2.5 64 | return distance 65 | 66 | def getAngle(): 67 | angleString = angleEntry.get() 68 | angle = int(angleString) 69 | return angle 70 | 71 | def forwardCallBack(): 72 | distance = getDistance() 73 | mip.distanceDrive(distance=distance) 74 | 75 | buttonf = Button(controlFrame, text="F", command=forwardCallBack) 76 | buttonf.grid(column=1,row=0) 77 | 78 | def backwardCallBack(): 79 | distance = getDistance() 80 | mip.distanceDrive(distance=-distance) 81 | 82 | buttonb = Button(controlFrame, text="B", command=backwardCallBack) 83 | buttonb.grid(column=1,row=2) 84 | 85 | def leftCallBack(): 86 | angle = getAngle() 87 | mip.turnByAngle(angle=-angle) 88 | 89 | buttonl = Button(controlFrame, text="L", command=leftCallBack) 90 | buttonl.grid(column=0,row=1) 91 | 92 | def rightCallBack(): 93 | angle = getAngle() 94 | mip.turnByAngle(angle=angle) 95 | 96 | button9 = Button(controlFrame, text="R", command=rightCallBack) 97 | button9.grid(column=2,row=1) 98 | 99 | quitButton = Button(controlFrame, text='Quit', command=root.destroy) 100 | quitButton.grid(column=1,row=3) 101 | 102 | # kick off GUI event loop 103 | root.mainloop() 104 | 105 | -------------------------------------------------------------------------------- /src/examples/mip_test_sound.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MiP Sound tester. 5 | To Use: 6 | mip_test_sound.py -i hci0 -b D0:39:72:C4:7A:01 -s 7 | 1 - 106 8 | 1 = beep 9 | 2 = burp 10 | 3 = ewwp - ah 11 | 4 = la la la la (lower) 12 | 5 = small raspberry? 13 | 6 = rerrr 14 | 7 = punching sound 15 | 8 = punching sound 16 | 9 = harder punching sound 17 | 10 = lep 18 | 11 = lep 19 | 12 = lep 20 | 13 = lep 21 | 14 = ahhh! (interested) 22 | 15 = arhhh! (disapointed) 23 | 16 = oh yeah 24 | 17 = meh (derogatory?) 25 | 18 = beh 26 | 19 = see yah? 27 | 20 = bad a bad a bad a (MiP talking to himself?) 28 | 21 = bad a bad a bad a (MiP talking to himself?) 29 | 22 = stop? 30 | 23 = goodnight? 31 | 24 = bang of drum 32 | 25 = bang of drum (different) 33 | 26 = Hi Yah! 34 | 27 = some word.. gay? 35 | 28 = Ha Ha Ha lep 36 | 29 = lets go! 37 | 30 = bah bah bah (low) 38 | 31 = her (low) 39 | 32 = eigh (something horrible) 40 | 33 = narrrh 41 | 34 = lets go it? 42 | 35 = hellllooo (sexy) 43 | 36 = bah? (questioning) 44 | 37 = ohaye 45 | 38 = huh? 46 | 39 = dur dur dur dur dooo (humming to himself) 47 | 40 = la la la la laaa (humming to himself) 48 | 41 = hah ha hah, hah hah hahaha... 49 | 42 = heaaahhh 50 | 43 = harp sound plus he says something 51 | 44 = lets MiP? 52 | 45 = talks to himself 53 | 46 = 'kay (as when in training mode) 54 | 47 = Music (part one) 55 | 48 = Music (part two) 56 | 49 = Out of power sound 57 | 50 = Happy! 58 | 51 = yeuh (collision warning sound in roam mode) 59 | 52 = Yah ha ha ha 60 | 53 = Music (MiP says music not plays it) 61 | 54 = oh ah (collision warning sound in roam mode) 62 | 55 = Oh Oh (something bad) (part of power down noise?) 63 | 56 = Oh yeah! 64 | 57 = high pitch 'Happy!' 65 | 58 = howell (sound when MiP sees a wall in cage mode?) 66 | 59 = howell (higher pitch) 67 | 60 = play 68 | 61 = lets fish? 69 | 62 = fire? 70 | 63 = click click 71 | 64 = rar 72 | 65 = la la la la la (derogatory sound) 73 | 66 = ah-choo (sneeze?) 74 | 67 = snoring 75 | 68 = feck? 76 | 69 = whish (sound made when recognising a gesture) 77 | 70 = whish (sound made when recognising a gesture) 78 | 71 = X? 79 | 72 = lets trick 80 | 73 = duh duh duh duh duh duh (cage escape sound) 81 | 74 = waaaah 82 | 75 = wakey wakey? 83 | 76 = yay 84 | 0xfd = 77 = roam whistle 85 | 78 = waaaaahhhh 86 | 79 = wuuuy (higher pitch) 87 | 80 = yeuh 88 | 81 = Yeah! 89 | 82 = You (low pitch) 90 | 83 = happy/snappy? (low pitch) 91 | 84 = oooee (low pitch) 92 | 85 = aaeeeh (higher pitch) 93 | 86 = ribit 94 | 87 = Boring 95 | 88 = errr (low pich) 96 | 89 = lets go 97 | 90 = yipppee! (higher pitch) 98 | 91 = ho ho ho ho ho 99 | 92 = crafteee? 100 | 93 = crafty 101 | 94 = ha ha 102 | 95 = this is mip (low pitch) 103 | 96 = sigharhhh 104 | 97 = MiP crying (lost the cage game?) 105 | 98 = nuh (low pitch) 106 | 99 = snifty? 107 | 100 = Aaahhhh (large sigh) 108 | 101 = funny little beeping sound 109 | 102 = drum 110 | 103 = laser beam 111 | 104 = swanny whistle sound 112 | 105 = No sound - stop sound playing 113 | 106 = mip 114 | """ 115 | 116 | import mippy 117 | import argparse 118 | 119 | if __name__ == '__main__': 120 | 121 | parser = argparse.ArgumentParser(description='Test MiP Sounds.') 122 | mippy.add_arguments(parser) 123 | parser.add_argument( 124 | '-s', 125 | '--sound', 126 | default=0x4d, 127 | help='Specify sound number (1-106). 105 is no sound', type=int) 128 | args = parser.parse_args() 129 | 130 | gt = mippy.GattTool(args.adaptor, args.device) 131 | 132 | mip = mippy.Mip(gt) 133 | # roam whistle 134 | mip.playSound(args.sound) 135 | -------------------------------------------------------------------------------- /src/examples/mip_test_continuous_drive_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Setup a movement GUI. 5 | Move Mip based on the GUI 6 | mip_test_continuous_drive_gui.py -i hci0 -b D0:39:72:C4:7A:01 7 | """ 8 | 9 | #from Tkinter import * 10 | import Tkinter 11 | from movement_canvas import MovementCanvas 12 | import logging 13 | import mippy 14 | import argparse 15 | 16 | def updateMovement(): 17 | # movementCanvas.positionX,movementCanvas.positionY, (-50 - 50) 18 | # movementCanvas.positionAngle,movementCanvas.positionMagnitude 19 | if movementCanvas.positionMagnitude < 20: 20 | logging.debug('updateMovement : stopping') 21 | mip.continuousDriveForward(0x0) 22 | elif movementCanvas.positionY < 0 and movementCanvas.positionAngle > 45 and movementCanvas.positionAngle < 135: 23 | speed = int(movementCanvas.positionMagnitude / 3) 24 | logging.debug('updateMovement : forward : speed %d ' % speed) 25 | mip.continuousDriveForward(int(speed)) 26 | elif movementCanvas.positionY > 0 and movementCanvas.positionAngle > 225 and movementCanvas.positionAngle < 315: 27 | speed = int(movementCanvas.positionMagnitude / 3) 28 | logging.debug('updateMovement : backward : speed %d ' % speed) 29 | mip.continuousDriveBackward(speed) 30 | # forwards turns 31 | elif movementCanvas.positionAngle >= 0 and movementCanvas.positionAngle <= 45: 32 | forwardSpeed = int(movementCanvas.positionY * (0x20/50)) 33 | turnSpeed = int(movementCanvas.positionX * (0x20/50)) 34 | logging.debug('updateMovement : forward right : forward speed %d : turn speed %d' % (forwardSpeed,turnSpeed)) 35 | mip.continuousTurnForwardRight(forwardSpeed,turnSpeed) 36 | elif movementCanvas.positionAngle >= 135 and movementCanvas.positionAngle <= 180: 37 | forwardSpeed = int(movementCanvas.positionY * (0x20/50)) 38 | turnSpeed = int(movementCanvas.positionX * (0x20/50)) 39 | turnSpeed = -turnSpeed 40 | logging.debug('updateMovement : forward left : forward speed %d : turn speed %d' % (forwardSpeed,turnSpeed)) 41 | mip.continuousTurnForwardLeft(forwardSpeed,turnSpeed) 42 | # backwards turns 43 | elif movementCanvas.positionAngle >= 315 and movementCanvas.positionAngle <= 360: 44 | forwardSpeed = int(movementCanvas.positionY * (0x20/50)) 45 | forwardSpeed = -forwardSpeed 46 | turnSpeed = int(movementCanvas.positionX * (0x20/50)) 47 | logging.debug('updateMovement : backward right : forward speed %d : turn speed %d' % (forwardSpeed,turnSpeed)) 48 | mip.continuousTurnBackwardRight(forwardSpeed,turnSpeed) 49 | elif movementCanvas.positionAngle >= 180 and movementCanvas.positionAngle <= 225: 50 | forwardSpeed = int(movementCanvas.positionY * (0x20/50)) 51 | forwardSpeed = -forwardSpeed 52 | turnSpeed = int(movementCanvas.positionX * (0x20/50)) 53 | turnSpeed = -turnSpeed 54 | logging.debug('updateMovement : backward left : forward speed %d : turn speed %d' % (forwardSpeed,turnSpeed)) 55 | mip.continuousTurnBackwardLeft(forwardSpeed,turnSpeed) 56 | 57 | top.after(50,updateMovement) 58 | 59 | 60 | if __name__ == '__main__': 61 | parser = argparse.ArgumentParser(description='Continuous Drive GUI.') 62 | mippy.add_arguments(parser) 63 | args = parser.parse_args() 64 | 65 | logging.basicConfig(level=logging.DEBUG) 66 | gt = mippy.GattTool(args.adaptor, args.device) 67 | mip = mippy.Mip(gt) 68 | # start gui 69 | top = Tkinter.Tk() 70 | movementCanvas = MovementCanvas(top,300,300) 71 | movementCanvas.setBindings() 72 | #movementCanvas.addUpdateCallback(movement) 73 | movementCanvas.pack() 74 | top.after(50,updateMovement) 75 | top.mainloop() 76 | -------------------------------------------------------------------------------- /src/examples/movement_canvas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | MovementCanvas. 4 | Presents a small canvas with a couple of circles in it. By pressing down the left hand button/clicking in the canvas 5 | a representation of a direction/speed to move in can be returned. 6 | Based on: 7 | http://www.tutorialspoint.com/python/tk_canvas.htm 8 | """ 9 | import math 10 | import Tkinter 11 | 12 | 13 | 14 | class MovementCanvas: 15 | """ 16 | MovementCanvas. 17 | Presents a small canvas with a couple of circles in it. By pressing down the left hand button/clicking 18 | in the canvas a representation of a direction/speed to move in can be returned. 19 | """ 20 | buttonPressed = 0 21 | width = 0 22 | height = 0 23 | innerRadius = 10 24 | centreX = 0 25 | centreY = 0 26 | positionX = 0 27 | positionY = 0 28 | positionAngle = 0 29 | positionMagnitude = 0 30 | callbackList = [] 31 | 32 | def __init__(self, parent,width=300,height=300): 33 | """ 34 | Constructs canvas attached inside parent of specified width and height 35 | """ 36 | self.canvas = Tkinter.Canvas(parent, bg="grey", height=height, width=width) 37 | self.width = width 38 | self.height = height 39 | self.innerRadius = 10 40 | self.centreX = (width/2) 41 | self.centreY = (height/2) 42 | self.outerOval = self.canvas.create_oval(10,10,width-10,height-10,fill="white") 43 | self.centreOval = self.canvas.create_oval(self.centreX-self.innerRadius,self.centreY-self.innerRadius, 44 | self.centreX+self.innerRadius,self.centreY+self.innerRadius, 45 | fill="black") 46 | self.innerOval = self.canvas.create_oval(self.centreX-self.innerRadius,self.centreY-self.innerRadius, 47 | self.centreX+self.innerRadius,self.centreY+self.innerRadius, 48 | fill="red") 49 | self.buttonPressed = 0 50 | 51 | def setBindings(self): 52 | """ 53 | Add internal movement bindings to the canvas. 54 | """ 55 | self.canvas.bind('',self.buttonPress) 56 | self.canvas.bind('',self.buttonRelease) 57 | self.canvas.bind('',self.motion) 58 | 59 | def pack(self): 60 | self.canvas.pack() 61 | 62 | def addUpdateCallback(self,callback): 63 | """ 64 | Add a callback to the lsit of callbacks to be called on a movement 65 | Callbacks of the form: 66 | callback(positionX,positionY,positionAngle,positionMagnitude) 67 | Where: 68 | positionX and positionY are -50-50 based around the centre position. 69 | positionMagnitude is 0-50 based from the centre 70 | positionAngle is the angle in degrees, from the horizontal X axies increasing anti-clockwise. 71 | """ 72 | self.callbackList.append(callback) 73 | 74 | def motion(self,event): 75 | #print("Mouse position: (%s %s) : buttonPressed : %d " % (event.x, event.y,self.buttonPressed)) 76 | if self.buttonPressed == 1: 77 | #print("Mouse position: (%s %s) Button is pressed" % (event.x, event.y)) 78 | self.canvas.coords(self.innerOval,event.x-self.innerRadius,event.y-self.innerRadius, 79 | event.x+self.innerRadius,event.y+self.innerRadius) 80 | self.updatePosition(event.x,event.y) 81 | return 82 | 83 | def updatePosition(self,eventX,eventY): 84 | """ 85 | Update positionX/positionY/positionAngle/positionMagnitude variables based on event position 86 | (eventX,eventY) 87 | positionX and positionY are -50-50 based around the centre position. 88 | positionMagnitude is 0-50 based from the centre 89 | positionAngle is the angle in degrees, from the horizontal X axies increasing anti-clockwise. 90 | """ 91 | # calculate position offset from centre, scaled to -50..50 based on with/height of widget 92 | self.positionX = ((eventX-self.centreX)*100)/self.width 93 | self.positionY = ((eventY-self.centreY)*100)/self.height 94 | # Distance from centre point (0-50) 95 | self.positionMagnitude = math.sqrt((self.positionX*self.positionX)+((self.positionY*self.positionY))) 96 | # Only calculate angle if we are away from the centre, otherwise angle is zero (division by zero) 97 | if self.positionMagnitude > 0.0: 98 | self.positionAngle = (math.acos(self.positionX/self.positionMagnitude)*180.0)/math.pi 99 | # If y position is positive(below centre in Y) make angle consistent 100 | if self.positionY > 0: 101 | self.positionAngle = 360.0 - self.positionAngle 102 | else: 103 | self.positionAngle = 0.0 104 | # call callbacks 105 | for fn in self.callbackList: 106 | fn(self.positionX,self.positionY,self.positionAngle,self.positionMagnitude) 107 | return 108 | 109 | def buttonPress(self,event): 110 | self.buttonPressed = 1 111 | print("Button Press at position: (%s %s) : buttonPressed : %d " % (event.x, event.y,self.buttonPressed)) 112 | self.canvas.coords(self.innerOval,event.x-self.innerRadius,event.y-self.innerRadius, 113 | event.x+self.innerRadius,event.y+self.innerRadius) 114 | self.updatePosition(event.x,event.y) 115 | return 116 | 117 | def buttonRelease(self,event): 118 | self.buttonPressed = 0 119 | print("Button Release : buttonPressed : %d " % (self.buttonPressed)) 120 | return 121 | 122 | 123 | def movement(x=0,y=0,angle=0.0,magnitude=0.0): 124 | print("Movement Callback: x = %d, y = %d, angle = %f, magnitude = %f " % (x,y,angle,magnitude)) 125 | 126 | def getLatest(): 127 | print("After callback: x = %d, y = %d, angle = %f, magnitude = %f " % (movementCanvas.positionX,movementCanvas.positionY,movementCanvas.positionAngle,movementCanvas.positionMagnitude)) 128 | top.after(50,getLatest) 129 | 130 | if __name__ == '__main__': 131 | top = Tkinter.Tk() 132 | movementCanvas = MovementCanvas(top,300,300) 133 | movementCanvas.setBindings() 134 | movementCanvas.addUpdateCallback(movement) 135 | movementCanvas.pack() 136 | top.after(50,getLatest) 137 | top.mainloop() 138 | 139 | -------------------------------------------------------------------------------- /src/mippy/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import pexpect 4 | import sys 5 | import time 6 | 7 | 8 | class GattTool: 9 | PROMPT = '.*\[LE\]>' 10 | 11 | def __init__(self, interface, address): 12 | cmd = 'gatttool -i %s -b %s -I' % (interface, address) 13 | self.child = pexpect.spawn(cmd) 14 | self.child.logfile = sys.stdout 15 | self.child.expect(self.PROMPT, timeout=1) 16 | 17 | def connect(self): 18 | self.child.sendline('connect') 19 | self.child.expect(self.PROMPT, timeout=1) 20 | 21 | def disconnect(self): 22 | self.child.sendline('disconnect') 23 | self.child.expect(self.PROMPT, timeout=1) 24 | 25 | def charWriteCmd(self, handle, byte_vals=[]): 26 | 27 | val = '' 28 | for byte_val in byte_vals: 29 | val += '%2.2x' % (byte_val) 30 | 31 | cmd = 'char-write-cmd 0x%4.4x ' % (handle) + val 32 | logging.debug('gatttool cmd: %s' % (cmd)) 33 | self.child.sendline(cmd) 34 | 35 | def charReadReply(self, handle ,command, timeout=1): 36 | """ 37 | parse reply of the form: 38 | Notification handle = 0x000e value: 38 33 30 30 46 46 30 30 30 30 30 30 39 | The return string is a series of hex numbers representing ascii chars e.g. 40 | = 56 51 48 70 70 48 48 48 48 48 48 41 | = '8' '3' '0' '0' 'f' 'f' '0' '0' '0' '0' '0' '0' 42 | The actual numbers you want back are hex numbers based on tuples of the letters e.g.: 43 | = 0x83 0x00 0xff 0x00 0x00 0x00 44 | Which is a reply from an 0x83 (read chest led) command telling you it is green 45 | (0x00,0xff,0x00) 46 | This routine cheks the reply is from the correct command, unless command 47 | is set to -1 in which case the first notification received is returned. 48 | """ 49 | self.child.expect(self.PROMPT, timeout) 50 | done = 0 51 | notificationCount = 0 52 | while(done == 0): 53 | logging.debug('charReadReply: Awaiting Notification handle') 54 | self.child.expect('Notification handle = 0x000e value:', timeout) 55 | logging.debug('charReadReply: Got Notification handle') 56 | returnString = self.child.readline() 57 | logging.debug('charReadReply: return string was: %s' % (returnString)) 58 | # create list of decimal integers 59 | intList = [int(s,16) for s in returnString.split() if s.isdigit()] 60 | # create 2 char strings of ASCII characters represented by dec numbers 61 | hexStringList = [] 62 | # logging.debug('charReadReply: looping over : %d' % (len(returnIntList)/2)) 63 | for i in range(len(intList)/2): 64 | # logging.debug('charReadReply: i is: %d' % (i)) 65 | ili = i * 2 66 | s = [ ] 67 | # logging.debug('charReadReply: ili is: %d' % (ili)) 68 | # logging.debug('charReadReply: char 0 is: %c' % (chr(intList[ili]))) 69 | s.append(chr(intList[ili])) 70 | # logging.debug('charReadReply: char 1 is: %c' % (chr(intList[ili+1]))) 71 | s.append(chr(intList[ili+1])) 72 | hexStringList.append("".join(s)) 73 | #for s in hexStringList: 74 | # logging.debug('charReadReply: Hex String was: %s' % (s)) 75 | # Convert 2 char string representation of hex numbers to a list 76 | # of decimal integers 0..255 77 | returnIntList = [int(s,16) for s in hexStringList] 78 | #for i in returnIntList: 79 | # logging.debug('charReadReply: Hex: %x' % (i)) 80 | # logging.debug('charReadReply: Decimal: %d' % (i)) 81 | for i in range(len(returnIntList)): 82 | logging.debug('charReadReply: Byte %d Hex: %x' % (i,returnIntList[i])) 83 | done = (command == returnIntList[0]) or (command == -1) 84 | if (done == 0): 85 | logging.debug('charReadReply: Expecting command %x, received command %x' % (command,returnIntList[0])) 86 | notificationCount += 1 87 | return returnIntList 88 | 89 | class Mip: 90 | def __init__(self, gt): 91 | self.gt = gt 92 | self.gt.connect() 93 | time.sleep(2) 94 | 95 | def playSound(self, id): 96 | self.gt.charWriteCmd(0x13, [0x06, id, 0]) 97 | 98 | def setMipPosition(self, position): 99 | self.gt.charWriteCmd(0x13, [0x08, position]) 100 | 101 | def distanceDrive(self, distance, angle=0): 102 | """ 103 | distance (+/-m) 104 | angle (+/-deg) 105 | """ 106 | 107 | if distance > 0: 108 | direction = 0 109 | else: 110 | direction = 1 111 | distance = abs(distance) 112 | 113 | if angle > 0: 114 | rotation = 0 115 | else: 116 | rotation = 1 117 | angle = abs(angle) 118 | self.gt.charWriteCmd( 119 | 0x13, [ 120 | 0x70, 121 | direction, 122 | int(round(distance * 100)), 123 | rotation, angle >> 8, 124 | angle & 0xff]) 125 | 126 | t = distance * 5 127 | time.sleep(t) 128 | 129 | def driveWithTime(self, speed, time_): 130 | """ 131 | speed (-30...+30) 132 | time (s) 133 | """ 134 | 135 | t = int(round(time_/0.05)) 136 | 137 | speed_mag = abs(speed) 138 | speed_sign = speed/speed_mag 139 | 140 | if speed_sign > 0: 141 | # Forward 142 | self.gt.charWriteCmd(0x13, [0x71, speed_mag, t/0.07]) 143 | else: 144 | # Reverse 145 | self.gt.charWriteCmd(0x13, [0x72, speed_mag, t/0.07]) 146 | 147 | def turnByAngle(self, angle, speed=15): 148 | """ 149 | angle (deg) 150 | speed (0-24) 151 | """ 152 | 153 | angle_mag = abs(angle) 154 | angle_sign = angle / angle_mag 155 | 156 | if angle_sign > 0: 157 | self.gt.charWriteCmd( 158 | 0x13, [ 159 | 0x74, 160 | int(round(angle_mag / 5.0)), 161 | speed]) 162 | else: 163 | self.gt.charWriteCmd( 164 | 0x13, [ 165 | 0x73, 166 | int(round(angle_mag / 5.0)), 167 | speed]) 168 | 169 | t = angle_mag / 360.0 * 0.9 170 | time.sleep(t) 171 | 172 | def stopDrive(self): 173 | """ 174 | Stop continuous drive? 175 | """ 176 | self.gt.charWriteCmd(0x13, [0x77]) 177 | 178 | def continuousDrive(self, speed, turnSpeed=0x0): 179 | """ 180 | Continuous Drive - this method needs to be called every 50ms to maintain drive 181 | speed (-0x20 - 0x20) - speed forwards or backwards 182 | turnSpeed (-0x20 - 0x20) - turn speed, negative for left, positive for right 183 | """ 184 | if speed < 0: 185 | direction = 0x20 186 | speed = -speed 187 | else: 188 | direction = 0x0 189 | if speed < 1: 190 | speed = 1 191 | if speed > 0x20: 192 | speed = 0x20 193 | if turnSpeed < 0: 194 | turnDirection = 0x60 195 | turnSpeed = -turnSpeed 196 | elif turnSpeed > 0: 197 | turnDirection = 0x40 198 | else: 199 | turnDirection = 0x0 200 | if turnSpeed < 0: 201 | turnSpeed = 0 202 | if turnSpeed > 0x20: 203 | turnSpeed = 0x20 204 | self.gt.charWriteCmd(0x13, [0x78, direction+speed, turnDirection+turnSpeed]) 205 | 206 | def continuousCarzyDrive(self, speed, turnSpeed=0x0): 207 | """ 208 | Some other sort of Continuous Drive - what does it do? 209 | this method needs to be called every 50ms to maintain drive 210 | speed (-0x20 - 0x20) - speed forwards or backwards 211 | turnSpeed (-0x20 - 0x20) - turn speed, negative for left, positive for right 212 | """ 213 | if speed < 0: 214 | direction = 0xA0 215 | speed = -speed 216 | else: 217 | direction = 0x80 218 | if speed < 1: 219 | speed = 1 220 | if speed > 0x20: 221 | speed = 0x20 222 | if turnSpeed < 0: 223 | turnDirection = 0xE0 224 | turnSpeed = -turnSpeed 225 | elif turnSpeed > 0: 226 | turnDirection = 0xC0 227 | else: 228 | turnDirection = 0x0 229 | if turnSpeed < 0: 230 | turnSpeed = 0 231 | if turnSpeed > 0x20: 232 | turnSpeed = 0x20 233 | self.gt.charWriteCmd(0x13, [0x78, direction+speed, turnDirection+turnSpeed]) 234 | 235 | def continuousDriveForward(self, speed): 236 | """ 237 | Start driving forwards at a certain speed 238 | This method needs to be called about once every 50ms to maintain speed 239 | speed (0..32) (0x0..0x20) 240 | """ 241 | if speed < 0: 242 | speed = 0 243 | if speed > 0x20: 244 | speed = 0x20 245 | self.gt.charWriteCmd(0x13, [0x78, speed]) 246 | 247 | def continuousDriveBackward(self, speed): 248 | """ 249 | Start driving backwards at a certain speed 250 | This method needs to be called about once every 50ms to maintain speed 251 | speed (0..32) (0x0..0x20) 252 | """ 253 | if speed < 0: 254 | speed = 0 255 | if speed > 0x20: 256 | speed = 0x20 257 | # backwards is actually 0x21-0x40, so add 0x20 258 | self.gt.charWriteCmd(0x13, [0x78, speed+0x20]) 259 | 260 | def continuousTurnForwardRight(self, speed, turnSpeed): 261 | """ 262 | Start turning right at a certain speed 263 | This method needs to be called about once every 50ms to maintain speed 264 | speed (forwards) (0..32) (0x0..0x20) 265 | turnSpeed (0..32) (0x0..0x20) 266 | """ 267 | if speed < 0: 268 | speed = 0 269 | if speed > 0x20: 270 | speed = 0x20 271 | if turnSpeed < 1: 272 | turnSpeed = 1 273 | if turnSpeed > 0x20: 274 | turnSpeed = 0x20 275 | # right is actually 0x41-0x60 so add 0x40 276 | self.gt.charWriteCmd(0x13, [0x78, speed, turnSpeed+0x40]) 277 | 278 | def continuousTurnForwardLeft(self, speed, turnSpeed): 279 | """ 280 | Start turning left at a certain speed 281 | This method needs to be called about once every 50ms to maintain speed 282 | speed (forwards) (0..32) (0x0..0x20) 283 | turnSpeed (0..32) (0x0..0x20) 284 | """ 285 | if speed < 0: 286 | speed = 0 287 | if speed > 0x20: 288 | speed = 0x20 289 | if turnSpeed < 1: 290 | turnSpeed = 1 291 | if turnSpeed > 0x20: 292 | turnSpeed = 0x20 293 | # left is actually 0x61-0x80 so add 0x60 294 | self.gt.charWriteCmd(0x13, [0x78, speed, turnSpeed+0x60]) 295 | 296 | def continuousTurnBackwardRight(self, speed, turnSpeed): 297 | """ 298 | Start turning backwards right at a certain speed 299 | This method needs to be called about once every 50ms to maintain speed 300 | speed (backwards) (0..32) (0x0..0x20) 301 | turnSpeed (0..32) (0x0..0x20) 302 | """ 303 | if speed < 0: 304 | speed = 0 305 | if speed > 0x20: 306 | speed = 0x20 307 | if turnSpeed < 1: 308 | turnSpeed = 1 309 | if turnSpeed > 0x20: 310 | turnSpeed = 0x20 311 | # backwards is actually 0x21-0x40, so add 0x20 312 | # right is actually 0x41-0x60 so add 0x40 313 | self.gt.charWriteCmd(0x13, [0x78, speed+0x20, turnSpeed+0x40]) 314 | 315 | def continuousTurnBackwardLeft(self, speed, turnSpeed): 316 | """ 317 | Start turning backward left at a certain speed 318 | This method needs to be called about once every 50ms to maintain speed 319 | speed (backwards) (0..32) (0x0..0x20) 320 | turnSpeed (0..32) (0x0..0x20) 321 | """ 322 | if speed < 0: 323 | speed = 0 324 | if speed > 0x20: 325 | speed = 0x20 326 | if turnSpeed < 1: 327 | turnSpeed = 1 328 | if turnSpeed > 0x20: 329 | turnSpeed = 0x20 330 | # backwards is actually 0x21-0x40, so add 0x20 331 | # left is actually 0x61-0x80 so add 0x60 332 | self.gt.charWriteCmd(0x13, [0x78, speed+0x20, turnSpeed+0x60]) 333 | 334 | def setGameMode(self, mode): 335 | """ 336 | Set game mode, mode is one of: 337 | 0x01 - App 338 | 0x02 - Cage Play back 339 | 0x03 - Tracking 340 | 0x04 - Dance Play back 341 | 0x05 - Default Mip Mode 342 | 0x06 - Stack Play back 343 | 0x07 - Trick programming and playback 344 | 0x08 - Roam Mode Play back 345 | """ 346 | self.gt.charWriteCmd(0x13, [0x76, mode]) 347 | 348 | def setChestLed(self, r, g, b): 349 | 350 | r = int(r * 255) 351 | g = int(g * 255) 352 | b = int(b * 255) 353 | 354 | self.gt.charWriteCmd(0x13, [0x84, r, g, b]) 355 | 356 | def getChestLed(self): 357 | self.gt.charWriteCmd(0x13, [0x83]) 358 | returnVals = self.gt.charReadReply(0x13, 0x83) 359 | colourVals = [] 360 | # first return val is 0x83 - the request code 361 | colourVals.append(returnVals[1]) 362 | colourVals.append(returnVals[2]) 363 | colourVals.append(returnVals[3]) 364 | return colourVals 365 | 366 | def setHeadLed(self, light1, light2, light3, light4): 367 | """ 368 | Set Head LEDs (eyes). Each light control one half of one of MiPS eyes, 369 | light1 left hand side left eye 370 | light2 right hand side left eye 371 | light3 left hand side right eye 372 | light4 right hand side right eye 373 | 374 | legal values for each light are: 375 | 0 = off 376 | 1 = on 377 | 2 = blink slow 378 | 3 = blink fast 379 | """ 380 | self.gt.charWriteCmd(0x13, [0x8A, light1, light2, light3, light4 ]) 381 | 382 | def getBatteryLevel(self): 383 | """ 384 | Returns battery level in volts. 385 | Should be between 4.0v and 6.4v. 386 | """ 387 | returnVals = [] 388 | returnVals = self.getMiPStatus() 389 | levelByte = returnVals[1] 390 | logging.debug('getBatteryLevel: level byte (0x4d = 4.0v,0x7c = 6.4v): %x' % (levelByte)) 391 | # battery level 0x4d = 4.0v 0x7c = 6.4v 392 | # 2.4v in 0x2f 393 | voltage = ((levelByte-0x4d)*(2.4/0x2f)) + 4.0 394 | logging.debug('getBatteryLevel: voltage : %f' % (voltage)) 395 | return voltage 396 | 397 | def getMiPOrientationStatus(self): 398 | """ 399 | Return the current orientation of MiP 400 | 0 - on back 401 | 1 - face down 402 | 2 - upright 403 | 3 - picked up 404 | 4 - hand stand 405 | 5 - face down on tray 406 | 6 - on back with kickstand 407 | """ 408 | returnVals = [] 409 | returnVals = self.getMiPStatus() 410 | orientation = returnVals[2] 411 | logging.debug('getMiPOrientationStatus: status : %d' % (orientation)) 412 | orientationString = ['on back' , 'face down' , 'upright' , 'picked up', 413 | 'hand stand' , 'face down on tray' , 414 | 'on back with kickstand' ] 415 | logging.debug('getMiPOrientationStatus: %s' % (orientationString[orientation])) 416 | return orientation 417 | 418 | def getMiPStatus(self): 419 | retryCount = 0 420 | done = 0 421 | while((retryCount < 10) and (done == 0)): 422 | try: 423 | logging.debug('getMiPStatus: writing MiP Status request 0x79, attempt %d.' % (retryCount)) 424 | self.gt.charWriteCmd(0x13, [0x79]) 425 | returnVals = self.gt.charReadReply(0x13, 0x79) 426 | done = 1 427 | except pexpect.TIMEOUT: 428 | retryCount += 1 429 | return returnVals 430 | 431 | def getUp(self,mode): 432 | """ 433 | Attempt to right MiP after he has had a fall. This only works if he hasn't fallen too far. 434 | mode = 435 | 0x0 - get up when MiP has fallen on his front 436 | 0x1 - get up when MiP has fallen on his back 437 | 0x2 - get up when MiP has fallen on his front or back 438 | """ 439 | self.gt.charWriteCmd(0x13, [0x23, mode]) 440 | 441 | def getWeight(self): 442 | """ 443 | Get how much weight MiP is holding. 444 | This is actually returmed as a float angle between -45 and 45 445 | As MiP is a balancing robot the angle is proportional to the weight 446 | somehow. 447 | A negative angle means the weight is on the front 448 | A positive angle means the weight is on the back 449 | """ 450 | logging.debug('getWeight: writing MiP Weight Update 0x81.') 451 | self.gt.charWriteCmd(0x13, [0x81]) 452 | returnVals = self.gt.charReadReply(0x13, 0x81) 453 | weightByte = returnVals[1] 454 | logging.debug('getWeight: weight byte : %x' % (weightByte)) 455 | # 0xD3(-45 degree) - 0x2D(+45 degree) 456 | # 0xD3 (211) (max)~0xFF(min) (255) is holding the weight on the front 457 | # 0x00(min)~0x2D(max) is holding the weight on the back 458 | if weightByte < 0x2d: 459 | weightAngle = weightByte * 45.0 / 0x2d 460 | elif weightByte > 0xd3: 461 | weightAngle = ( weightByte - 0xff ) * 45.0 / ( 0xff - 0x2d ) 462 | else: 463 | weightAngle = 0.0 464 | logging.debug('getWeight: weight angle (+ve = weight on back) : %f' % (weightAngle)) 465 | return weightAngle 466 | 467 | def resetOdomemeter(self): 468 | logging.debug('resetOdomemeter: writing MiP Reset Odometer 0x86.') 469 | self.gt.charWriteCmd(0x13, [0x86]) 470 | 471 | def getOdometer(self): 472 | """ 473 | Return the odometer since the last reset in cm 474 | """ 475 | distance = 0.0 476 | retryCount = 0 477 | done = 0 478 | while((retryCount < 10) and (done == 0)): 479 | try: 480 | logging.debug('getOdometer: writing MiP Get Odometer 0x85 : attempt %d.' % (retryCount)) 481 | self.gt.charWriteCmd(0x13, [0x85]) 482 | returnVals = self.gt.charReadReply(0x13, 0x85) 483 | # 4 byte return value, first byte is MSB 484 | odometerValue = (returnVals[1] << 24) + (returnVals[2] << 16) + (returnVals[3] << 8) + returnVals[4] 485 | logging.debug('getOdometer: Odometer value (48.5 per cm) : %d' % (odometerValue)) 486 | # 48.5 units per cm 487 | distance = odometerValue/48.5 488 | done = 1 489 | except pexpect.TIMEOUT: 490 | retryCount += 1 491 | return distance 492 | 493 | def setGestureRadarMode(self, mode): 494 | """ 495 | Set whether to turn gesture or radar mode on. 496 | mode = 0 gesture off, radar off 497 | mode = 2 Gesture on, radar off 498 | mode = 4 Radar on, gesture off 499 | Note the radar updates only appear until the next command is sent? 500 | """ 501 | logging.debug('setGestureRadarMode: writing MiP Set Gesture Radar Mode 0x0c %x .' % mode) 502 | self.gt.charWriteCmd(0x13, [0x0c, mode]) 503 | 504 | def getRadarResponse(self): 505 | """ 506 | Return current status of radar. 507 | Send a setGestureRadarMode(mode = 0x04) to trigger a reply 508 | return value 1 - no object 509 | return value 2 - object between 10cm-30cm 510 | return value 3 - object less than 10cm 511 | """ 512 | retryCount = 0 513 | done = 0 514 | while((retryCount < 10) and (done == 0)): 515 | try: 516 | logging.debug('getRadarResponse: sending getRadarResponse : attempt %d.' % (retryCount)) 517 | # setGestureRadarMode(0x04) = turn Radar mode on 518 | self.gt.charWriteCmd(0x13, [0x0c, 0x04]) 519 | returnVals = self.gt.charReadReply(0x13, 0x0c) 520 | thisResponse = returnVals[1] 521 | done = 1 522 | except pexpect.TIMEOUT: 523 | retryCount += 1 524 | logging.debug('getRadarResponse: response is %x.' % (thisResponse)) 525 | # setGestureRadarMode(0x0) = turn Radar mode off 526 | self.gt.charWriteCmd(0x13, [0x0c, 0x0]) 527 | return thisResponse 528 | 529 | def getGesture(self): 530 | """ 531 | Attempt to recognise a gesture. 532 | Sends a setGestureRadarMode(0x02), and then hopes a gesture is returned before 533 | a timeout occurs. If a timeout occurs then 'no gesture' is returned. 534 | Return values: 535 | 0x00 - no gesture 536 | 0x0a - left 537 | 0x0b - right 538 | 0x0c - centre sweep left 539 | 0x0d - centre sweep right 540 | 0x0e - centre hold 541 | 0x0f - forward 542 | 0x10 - backward 543 | """ 544 | try: 545 | logging.debug('getGesture: sending setGestureRadarMode.') 546 | # setGestureRadarMode(0x02) = turn Gesture mode on 547 | self.gt.charWriteCmd(0x13, [0x0c, 0x02]) 548 | logging.debug('getGesture: Waiting for reply.') 549 | returnVals = self.gt.charReadReply(0x13, 0x0a) 550 | thisResponse = returnVals[1] 551 | logging.debug('getGesture: Reply was: %x.' % (thisResponse)) 552 | except pexpect.TIMEOUT: 553 | thisResponse = 0x0 554 | logging.debug('getGesture: No Reply detected.') 555 | return thisResponse 556 | 557 | def continuousDriveForwardUntilRadar(self,speed): 558 | """ 559 | Start driving at a certain speed 560 | speed (0..32) (0x0..0x20) 561 | Until a radar response is received that indicates we are near an object 562 | Or we are no longer vertical. 563 | """ 564 | # turn radar mode on 565 | logging.debug('continuousDriveForwardUntilRadar: writing MiP Set Gesture Radar Mode ON: 0x0c 0x04 .') 566 | self.gt.charWriteCmd(0x13, [0x0c, 0x04]) 567 | radarResponse = 0 568 | orientation = 2 # upright 569 | done = 0 570 | while(done == 0): 571 | # continuous drive forwards 572 | logging.debug('continuousDriveForwardUntilRadar: Driving forwards at speed: %x .' % speed) 573 | self.gt.charWriteCmd(0x13, [0x78, speed]) 574 | try: 575 | # Try and read a radar response, timeout after 0.02secs 576 | logging.debug('continuousDriveForwardUntilRadar: Trying to read any response.') 577 | returnVals = self.gt.charReadReply(0x13, -1 ,timeout=0.02) 578 | if returnVals[0] == 0x0c: # radar response 579 | radarResponse = returnVals[1] 580 | logging.debug('continuousDriveForwardUntilRadar: Radar response was %d.' % radarResponse) 581 | elif returnVals[0] == 0x79: # MiPStatus response 582 | # returnVals[1] is battery level 583 | orientation = returnVals[2] 584 | orientationString = ['on back' , 'face down' , 585 | 'upright' , 'picked up', 586 | 'hand stand' , 'face down on tray' , 587 | 'on back with kickstand' ] 588 | logging.debug('continuousDriveForwardUntilRadar: MiP Status orientation %s' % (orientationString[orientation])) 589 | except pexpect.TIMEOUT: 590 | radarResponse = 0 591 | orientation = 2 592 | logging.debug('continuousDriveForwardUntilRadar: NO Radar response.') 593 | # radarResponse value 0 - no radar response 594 | # radarResponse value 1 - no object 595 | # radarResponse value 2 - object between 10cm-30cm 596 | # radarResponse value 3 - object less than 10cm 597 | # orientation 0 on back 598 | # orientation 2 upright 599 | # Stop if we detect an object or we are not upright 600 | done = (radarResponse > 1) or (orientation != 2) 601 | # turn radar mode off 602 | logging.debug('continuousDriveForwardUntilRadar: writing MiP Set Gesture Radar Mode Off 0x0c 0x0 .') 603 | self.gt.charWriteCmd(0x13, [0x0c, 0x0]) 604 | 605 | 606 | def setVolume(self, volume): 607 | """ 608 | Set MiP sound volume: 609 | volume : 0..7 610 | """ 611 | self.gt.charWriteCmd(0x13, [0x15, volume]) 612 | 613 | def getVolume(self): 614 | """ 615 | Return current MiP sound volume: 0..7 616 | """ 617 | retryCount = 0 618 | done = 0 619 | while((retryCount < 10) and (done == 0)): 620 | try: 621 | logging.debug('getVolume: sending Command 0x16 : attempt %d.' % (retryCount)) 622 | self.gt.charWriteCmd(0x13, [0x16]) 623 | returnVals = self.gt.charReadReply(0x13, 0x16) 624 | volume = returnVals[1] 625 | done = 1 626 | except pexpect.TIMEOUT: 627 | retryCount += 1 628 | logging.debug('getVolume: volume is %x.' % (volume)) 629 | return volume 630 | 631 | def getClapTimes(self): 632 | """ 633 | Return clap times (whether claps have been detected) 634 | Use clapEnable to turn on clap recognition first 635 | """ 636 | done = 0 637 | try: 638 | logging.debug('getClapTimes: sending Command 0x1D.') 639 | self.gt.charWriteCmd(0x13, [0x1D]) 640 | returnVals = self.gt.charReadReply(0x13, 0x1D) 641 | clapTimes = returnVals[1] 642 | done = 1 643 | except pexpect.TIMEOUT: 644 | clapTimes = 0 645 | logging.debug('getClapTimes: TIMEOUT.') 646 | logging.debug('getClapTimes: returning %x.' % (clapTimes)) 647 | return clapTimes 648 | 649 | def clapEnable(self, onoff=0x01): 650 | """ 651 | Enable clap recognition 652 | """ 653 | logging.debug('clapEnable: Sending Command 0x1E.') 654 | self.gt.charWriteCmd(0x13, [0x1E, onoff]) 655 | 656 | def requestClapStatus(self): 657 | """ 658 | Get the current clap config 659 | """ 660 | retryCount = 0 661 | done = 0 662 | while((retryCount < 10) and (done == 0)): 663 | try: 664 | logging.debug('requestClapStatus: sending Command 0x1F : attempt %d.' % (retryCount)) 665 | self.gt.charWriteCmd(0x13, [0x1F]) 666 | returnVals = self.gt.charReadReply(0x13, 0x1F) 667 | onoff = returnVals[1] 668 | delayTime1 = returnVals[2] 669 | delayTime2 = returnVals[3] 670 | done = 1 671 | except pexpect.TIMEOUT: 672 | retryCount += 1 673 | onoff = 0 674 | delayTime1 = 0 675 | delayTime2 = 0 676 | logging.debug('requestClapStatus: returning onoff=%x, clap delay times (%x,%x).' % (onoff,delayTime1,delayTime2)) 677 | return onoff 678 | 679 | class Turtle: 680 | def __init__(self, mip): 681 | self.mip = mip 682 | 683 | def left(self, angle): 684 | self.mip.turnByAngle(-angle) 685 | 686 | def right(self, angle): 687 | self.mip.turnByAngle(angle) 688 | 689 | def forward(self, distance): 690 | self.mip.distanceDrive(distance) 691 | 692 | def reverse(self, distance): 693 | self.mip.distanceDrive(-distance) 694 | 695 | 696 | def add_arguments(parser): 697 | 698 | """Add gatttool-style arguments to an optparse parser.""" 699 | 700 | parser.add_argument( 701 | '-i', 702 | '--adaptor', 703 | default='hci0', 704 | help='Specify local adaptor interface') 705 | 706 | parser.add_argument( 707 | '-b', 708 | '--device', 709 | default='D0:39:72:B8:C5:84', 710 | help='Specify remote bluetooth address') 711 | 712 | 713 | if __name__ == '__main__': 714 | 715 | parser = argparse.ArgumentParser(description='Quick test program.') 716 | 717 | parser.add_argument( 718 | '-i', 719 | '--adaptor', 720 | default='hci0', 721 | help='Specify local adaptor interface') 722 | 723 | parser.add_argument( 724 | '-b', 725 | '--device', 726 | default='D0:39:72:B8:C5:84', 727 | help='Specify remote bluetooth address') 728 | 729 | args = parser.parse_args() 730 | 731 | gt = GattTool(args.adaptor, args.device) 732 | 733 | mip = Mip(gt) 734 | 735 | mip.playSound(0x4d) 736 | -------------------------------------------------------------------------------- /src/examples/mip_explorer_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | This GUI is designed for general exploration using MiP. 5 | ./mip_explorer_gui.py -i hci0 -b D0:39:72:C4:7A:01 6 | """ 7 | from Tkinter import * 8 | from movement_canvas import MovementCanvas 9 | import logging 10 | import mippy 11 | import argparse 12 | import time 13 | 14 | class SoundWindow(Toplevel): 15 | """ 16 | Class to manage and control the sound window. 17 | """ 18 | 19 | def __init__(self, master=None): 20 | Toplevel.__init__(self, master) 21 | #self.pack() 22 | self.startSoundWindow() 23 | 24 | def getVolume(self): 25 | volumeString = self.volumeEntry.get() 26 | volume = int(volumeString) 27 | return volume 28 | 29 | def setVolume(self): 30 | volume = self.getVolume() 31 | mip.setVolume(volume) 32 | 33 | def soundCallback(self,soundNumber): 34 | mip.playSound(soundNumber) 35 | 36 | def startSoundWindow(self): 37 | """ 38 | Manage a sound control window 39 | """ 40 | self.title("Sound Window") 41 | self.geometry("600x350") 42 | soundMenubar = Menu(self) 43 | soundFileMenu = Menu(soundMenubar, tearoff=0) 44 | soundFileMenu.add_command(label="Exit", command=self.destroy) 45 | soundMenubar.add_cascade(label="File", menu=soundFileMenu) 46 | self.config(menu=soundMenubar) 47 | soundFrame = Frame(self) 48 | soundFrame.pack() 49 | # Volume control 50 | volumeFrame = Frame(soundFrame) 51 | volumeFrame.pack() 52 | volumeButton = Button(volumeFrame, text = "Volume(0..7):", command = self.setVolume) 53 | volumeButton.grid(column=0,row=0) 54 | volumeStringVar = StringVar() 55 | self.volumeEntry = Entry(volumeFrame, textvariable = volumeStringVar) 56 | self.volumeEntry.insert(0,"1") 57 | self.volumeEntry.grid(column=1,row=0) 58 | # Add sounds 59 | soundsFrame1 = Frame(soundFrame) 60 | soundsFrame1.pack() 61 | button1 = Button(soundsFrame1, text ="Beep", command = lambda: self.soundCallback(1)) 62 | button1.pack(side = LEFT) 63 | button2 = Button(soundsFrame1, text ="Burp", command = lambda: self.soundCallback(2)) 64 | button2.pack(side = LEFT) 65 | button5 = Button(soundsFrame1, text ="Raspberry", command = lambda: self.soundCallback(5)) 66 | button5.pack(side = LEFT) 67 | button14 = Button(soundsFrame1, text ="Ah! (interested)", command = lambda: self.soundCallback(14)) 68 | button14.pack(side = LEFT) 69 | button15 = Button(soundsFrame1, text ="Ah! (disappointed)", command = lambda: self.soundCallback(15)) 70 | button15.pack(side = LEFT) 71 | button16 = Button(soundsFrame1, text ="Oh Yeah!", command = lambda: self.soundCallback(16)) 72 | button16.pack(side = LEFT) 73 | button17 = Button(soundsFrame1, text ="Meh", command = lambda: self.soundCallback(17)) 74 | button17.pack(side = LEFT) 75 | soundsFrame2 = Frame(soundFrame) 76 | soundsFrame2.pack() 77 | button19 = Button(soundsFrame2, text ="See yah", command = lambda: self.soundCallback(19)) 78 | button19.pack(side = LEFT) 79 | button20 = Button(soundsFrame2, text ="MiP chatter", command = lambda: self.soundCallback(20)) 80 | button20.pack(side = LEFT) 81 | button22 = Button(soundsFrame2, text ="Stop", command = lambda: self.soundCallback(22)) 82 | button22.pack(side = LEFT) 83 | button23 = Button(soundsFrame2, text ="Goodnight", command = lambda: self.soundCallback(23)) 84 | button23.pack(side = LEFT) 85 | button26 = Button(soundsFrame2, text ="Hi Yah!", command = lambda: self.soundCallback(26)) 86 | button26.pack(side = LEFT) 87 | button29 = Button(soundsFrame2, text ="Lets Go!", command = lambda: self.soundCallback(29)) 88 | button29.pack(side = LEFT) 89 | soundsFrame3 = Frame(soundFrame) 90 | soundsFrame3.pack() 91 | button32 = Button(soundsFrame3, text ="Eigh (something horrible)", command = lambda: self.soundCallback(32)) 92 | button32.pack(side = LEFT) 93 | button35 = Button(soundsFrame3, text ="Hellllooo", command = lambda: self.soundCallback(35)) 94 | button35.pack(side = LEFT) 95 | button36 = Button(soundsFrame3, text ="Bah?", command = lambda: self.soundCallback(36)) 96 | button36.pack(side = LEFT) 97 | button37 = Button(soundsFrame3, text ="Ohaye", command = lambda: self.soundCallback(37)) 98 | button37.pack(side = LEFT) 99 | button38 = Button(soundsFrame3, text ="Huh?", command = lambda: self.soundCallback(38)) 100 | button38.pack(side = LEFT) 101 | soundsFrame4 = Frame(soundFrame) 102 | soundsFrame4.pack() 103 | button39 = Button(soundsFrame4, text ="Mip Humming", command = lambda: self.soundCallback(39)) 104 | button39.pack(side = LEFT) 105 | button40 = Button(soundsFrame4, text ="Mip Humming", command = lambda: self.soundCallback(40)) 106 | button40.pack(side = LEFT) 107 | button41 = Button(soundsFrame4, text ="Mip Laughing", command = lambda: self.soundCallback(41)) 108 | button41.pack(side = LEFT) 109 | button42 = Button(soundsFrame4, text ="Heaaahhh", command = lambda: self.soundCallback(42)) 110 | button42.pack(side = LEFT) 111 | button43 = Button(soundsFrame4, text ="Harp sound", command = lambda: self.soundCallback(43)) 112 | button43.pack(side = LEFT) 113 | button44 = Button(soundsFrame4, text ="Lets MiP", command = lambda: self.soundCallback(44)) 114 | button44.pack(side = LEFT) 115 | soundsFrame5 = Frame(soundFrame) 116 | soundsFrame5.pack() 117 | button45 = Button(soundsFrame5, text ="MiP chatter", command = lambda: self.soundCallback(45)) 118 | button45.pack(side = LEFT) 119 | button46 = Button(soundsFrame5, text ="'kay", command = lambda: self.soundCallback(46)) 120 | button46.pack(side = LEFT) 121 | button47 = Button(soundsFrame5, text ="Music (verse 1)", command = lambda: self.soundCallback(47)) 122 | button47.pack(side = LEFT) 123 | button48 = Button(soundsFrame5, text ="Music (verse 2)", command = lambda: self.soundCallback(48)) 124 | button48.pack(side = LEFT) 125 | button49 = Button(soundsFrame5, text ="Out of power", command = lambda: self.soundCallback(49)) 126 | button49.pack(side = LEFT) 127 | button50 = Button(soundsFrame5, text ="Happy", command = lambda: self.soundCallback(50)) 128 | button50.pack(side = LEFT) 129 | soundsFrame6 = Frame(soundFrame) 130 | soundsFrame6.pack() 131 | button51 = Button(soundsFrame6, text ="Yeuh", command = lambda: self.soundCallback(51)) 132 | button51.pack(side = LEFT) 133 | button53 = Button(soundsFrame6, text ="Music", command = lambda: self.soundCallback(53)) 134 | button53.pack(side = LEFT) 135 | button54 = Button(soundsFrame6, text ="Oh ah", command = lambda: self.soundCallback(54)) 136 | button54.pack(side = LEFT) 137 | button55 = Button(soundsFrame6, text ="Oh oh (bad)", command = lambda: self.soundCallback(55)) 138 | button55.pack(side = LEFT) 139 | button56 = Button(soundsFrame6, text ="Oh yeah!", command = lambda: self.soundCallback(56)) 140 | button56.pack(side = LEFT) 141 | button58 = Button(soundsFrame6, text ="Howell", command = lambda: self.soundCallback(58)) 142 | button58.pack(side = LEFT) 143 | button60 = Button(soundsFrame6, text ="Play", command = lambda: self.soundCallback(60)) 144 | button60.pack(side = LEFT) 145 | soundsFrame7 = Frame(soundFrame) 146 | soundsFrame7.pack() 147 | button61 = Button(soundsFrame7, text ="Lets fish", command = lambda: self.soundCallback(61)) 148 | button61.pack(side = LEFT) 149 | button62 = Button(soundsFrame7, text ="Fire", command = lambda: self.soundCallback(62)) 150 | button62.pack(side = LEFT) 151 | button64 = Button(soundsFrame7, text ="Rar", command = lambda: self.soundCallback(64)) 152 | button64.pack(side = LEFT) 153 | button65 = Button(soundsFrame7, text ="La la la la la (derogatory)", command = lambda: self.soundCallback(65)) 154 | button65.pack(side = LEFT) 155 | button66 = Button(soundsFrame7, text ="Ah-choo", command = lambda: self.soundCallback(66)) 156 | button66.pack(side = LEFT) 157 | button67 = Button(soundsFrame7, text ="Snoring", command = lambda: self.soundCallback(67)) 158 | button67.pack(side = LEFT) 159 | button68 = Button(soundsFrame7, text ="Feck", command = lambda: self.soundCallback(68)) 160 | button68.pack(side = LEFT) 161 | soundsFrame8 = Frame(soundFrame) 162 | soundsFrame8.pack() 163 | button72 = Button(soundsFrame8, text ="Feck", command = lambda: self.soundCallback(72)) 164 | button72.pack(side = LEFT) 165 | button73 = Button(soundsFrame8, text ="duh duh duh (cage escape sound)", command = lambda: self.soundCallback(73)) 166 | button73.pack(side = LEFT) 167 | button74 = Button(soundsFrame8, text ="Waaah", command = lambda: self.soundCallback(74)) 168 | button74.pack(side = LEFT) 169 | button75 = Button(soundsFrame8, text ="Wakey Wakey", command = lambda: self.soundCallback(75)) 170 | button75.pack(side = LEFT) 171 | button76 = Button(soundsFrame8, text ="Yay", command = lambda: self.soundCallback(76)) 172 | button76.pack(side = LEFT) 173 | button77 = Button(soundsFrame8, text ="Roam whistle", command = lambda: self.soundCallback(77)) 174 | button77.pack(side = LEFT) 175 | soundsFrame9 = Frame(soundFrame) 176 | soundsFrame9.pack() 177 | button82 = Button(soundsFrame9, text ="You", command = lambda: self.soundCallback(82)) 178 | button82.pack(side = LEFT) 179 | button86 = Button(soundsFrame9, text ="Ribit", command = lambda: self.soundCallback(86)) 180 | button86.pack(side = LEFT) 181 | button87 = Button(soundsFrame9, text ="Boring", command = lambda: self.soundCallback(87)) 182 | button87.pack(side = LEFT) 183 | button89 = Button(soundsFrame9, text ="Lets Go", command = lambda: self.soundCallback(89)) 184 | button89.pack(side = LEFT) 185 | soundsFrame10 = Frame(soundFrame) 186 | soundsFrame10.pack() 187 | button90 = Button(soundsFrame10, text ="Yipppee!", command = lambda: self.soundCallback(90)) 188 | button90.pack(side = LEFT) 189 | button91 = Button(soundsFrame10, text ="ho ho ho ho ho", command = lambda: self.soundCallback(91)) 190 | button91.pack(side = LEFT) 191 | button93 = Button(soundsFrame10, text ="Crafty", command = lambda: self.soundCallback(93)) 192 | button93.pack(side = LEFT) 193 | button94 = Button(soundsFrame10, text ="Ha ha", command = lambda: self.soundCallback(94)) 194 | button94.pack(side = LEFT) 195 | button95 = Button(soundsFrame10, text ="This is MiP", command = lambda: self.soundCallback(95)) 196 | button95.pack(side = LEFT) 197 | button97 = Button(soundsFrame10, text ="Crying", command = lambda: self.soundCallback(97)) 198 | button97.pack(side = LEFT) 199 | soundsFrame11 = Frame(soundFrame) 200 | soundsFrame11.pack() 201 | button101 = Button(soundsFrame11, text ="Beeping", command = lambda: self.soundCallback(101)) 202 | button101.pack(side = LEFT) 203 | button103 = Button(soundsFrame11, text ="Laser Beam", command = lambda: self.soundCallback(103)) 204 | button103.pack(side = LEFT) 205 | button104 = Button(soundsFrame11, text ="Swanny whistle", command = lambda: self.soundCallback(104)) 206 | button104.pack(side = LEFT) 207 | button106 = Button(soundsFrame11, text ="MiP", command = lambda: self.soundCallback(106)) 208 | button106.pack(side = LEFT) 209 | 210 | class ModeWindow(Toplevel): 211 | """ 212 | Class to manage and control the mode dialog. Used for chagning MiPs mode in and out of App. 213 | """ 214 | 215 | def __init__(self, master=None): 216 | Toplevel.__init__(self, master) 217 | self.startModeWindow() 218 | 219 | def startModeWindow(self): 220 | self.title("Mode Window") 221 | self.geometry("200x100") 222 | modeMenubar = Menu(self) 223 | modeFileMenu = Menu(modeMenubar, tearoff=0) 224 | modeFileMenu.add_command(label="Exit", command=self.destroy) 225 | modeMenubar.add_cascade(label="File", menu=modeFileMenu) 226 | self.config(menu=modeMenubar) 227 | modeFrame = Frame(self) 228 | modeFrame.pack() 229 | self.modeVar = IntVar() 230 | self.modeVar.set(0x1) # We are by default in App mode 231 | appRadio = Radiobutton(modeFrame, text="App", variable=self.modeVar, value=0x1, 232 | command=self.modeSelected) 233 | appRadio.pack(side=TOP) 234 | danceRadio = Radiobutton(modeFrame, text="Dance", variable=self.modeVar, value=0x4, 235 | command=self.modeSelected) 236 | danceRadio.pack(side=TOP) 237 | defaultRadio = Radiobutton(modeFrame, text="Default", variable=self.modeVar, value=0x5, 238 | command=self.modeSelected) 239 | defaultRadio.pack(side=TOP) 240 | roamRadio = Radiobutton(modeFrame, text="Roam", variable=self.modeVar, value=0x8, 241 | command=self.modeSelected) 242 | roamRadio.pack(side=TOP) 243 | 244 | def modeSelected(self): 245 | mip.setGameMode(self.modeVar.get()) 246 | 247 | 248 | class EyesWindow(Toplevel): 249 | """ 250 | Class to manage and control the eyes. 251 | """ 252 | 253 | def __init__(self, master=None): 254 | Toplevel.__init__(self, master) 255 | self.startEyesWindow() 256 | 257 | def startEyesWindow(self): 258 | self.title("Eyes Window") 259 | self.geometry("400x100") 260 | eyesMenubar = Menu(self) 261 | eyesFileMenu = Menu(eyesMenubar, tearoff=0) 262 | eyesFileMenu.add_command(label="Exit", command=self.destroy) 263 | eyesMenubar.add_cascade(label="File", menu=eyesFileMenu) 264 | self.config(menu=eyesMenubar) 265 | eyesFrame = Frame(self) 266 | eyesFrame.pack() 267 | self.leftEyeLeftVar = StringVar() 268 | self.leftEyeLeftVar.set("on") 269 | self.leftEyeRightVar = StringVar() 270 | self.leftEyeRightVar.set("on") 271 | self.rightEyeLeftVar = StringVar() 272 | self.rightEyeLeftVar.set("on") 273 | self.rightEyeRightVar = StringVar() 274 | self.rightEyeRightVar.set("on") 275 | self.leftEyeLeftOption = OptionMenu(eyesFrame, self.leftEyeLeftVar, 276 | "off", "on", "slow blink", "fast blink", 277 | command=self.updateEyes) 278 | self.leftEyeLeftOption.grid(column=0,row=0) 279 | self.leftEyeRightOption = OptionMenu(eyesFrame, self.leftEyeRightVar, 280 | "off", "on", "slow blink", "fast blink", 281 | command=self.updateEyes) 282 | self.leftEyeRightOption.grid(column=1,row=0) 283 | self.rightEyeLeftOption = OptionMenu(eyesFrame,self.rightEyeLeftVar, 284 | "off", "on", "slow blink", "fast blink", 285 | command=self.updateEyes) 286 | self.rightEyeLeftOption.grid(column=2,row=0) 287 | self.rightEyeRightOption = OptionMenu(eyesFrame,self.rightEyeRightVar, 288 | "off","on", "slow blink", "fast blink", 289 | command=self.updateEyes) 290 | self.rightEyeRightOption.grid(column=3,row=0) 291 | leftWinkButton = Button(eyesFrame, text = "Wink", command = lambda: self.winkEyes(0x0)) 292 | leftWinkButton.grid(column=0,row=1) 293 | rightWinkButton = Button(eyesFrame, text = "Wink", command = lambda: self.winkEyes(0x1)) 294 | rightWinkButton.grid(column=3,row=1) 295 | 296 | def updateEyes(self,value): 297 | """ 298 | Based on the eyes window per-eye option menus, update MiPs eyes 299 | """ 300 | #leftEyeLeftString = self.leftEyeLeftVar.get() 301 | #logging.debug('updateEyes : leftEyeLeftString = %s' % (leftEyeLeftString)) 302 | #leftEyeLeftInt = self.optionToInt(leftEyeLeftString) 303 | #logging.debug('updateEyes : leftEyeLeftInt = %d' % (leftEyeLeftInt)) 304 | leftEyeLeft = self.optionToInt(self.leftEyeLeftVar.get()) 305 | leftEyeRight = self.optionToInt(self.leftEyeRightVar.get()) 306 | rightEyeLeft = self.optionToInt(self.rightEyeLeftVar.get()) 307 | rightEyeRight = self.optionToInt(self.rightEyeRightVar.get()) 308 | mip.setHeadLed(leftEyeLeft,leftEyeRight,rightEyeLeft,rightEyeRight) 309 | 310 | def optionToInt(self,s): 311 | """ 312 | Convert an option menu string to an integer value suitable for setHeadLed i.e. 313 | "off" returns 0 314 | "on" returns 1 315 | "slow blink" returns 2 316 | "fast blink" returns 3 317 | """ 318 | if s == "off": 319 | return 0 320 | elif s == "on": 321 | return 1 322 | elif s == "slow blink": 323 | return 2 324 | elif s == "fast blink": 325 | return 3 326 | else: 327 | return 0 328 | 329 | def winkEyes(self,eye=0x1): 330 | """ 331 | Wink an eye. 332 | eye=0x0 left eye 333 | eye=0x1 right eye 334 | """ 335 | # all on 336 | mip.setHeadLed(0x1,0x1,0x1,0x1) 337 | time.sleep(1.0) 338 | # first half 339 | if eye == 1: 340 | mip.setHeadLed(0x1,0x1,0x1,0x0) 341 | else: 342 | mip.setHeadLed(0x0,0x1,0x1,0x1) 343 | time.sleep(0.2) 344 | # wink 345 | if eye == 1: 346 | mip.setHeadLed(0x1,0x1,0x0,0x0) 347 | else: 348 | mip.setHeadLed(0x0,0x0,0x1,0x1) 349 | time.sleep(1.0) 350 | # second half 351 | if eye == 1: 352 | mip.setHeadLed(0x1,0x1,0x1,0x0) 353 | else: 354 | mip.setHeadLed(0x0,0x1,0x1,0x1) 355 | time.sleep(0.2) 356 | # all on 357 | mip.setHeadLed(0x1,0x1,0x1,0x1) 358 | 359 | class TelemetryWindow(Toplevel): 360 | """ 361 | Class to manage and control the mode dialog. Used for chagning MiPs mode in and out of App. 362 | """ 363 | global updateTelemetry 364 | global distanceValueLabel 365 | global batteryValueLabel 366 | updateTelemetry = 0 367 | 368 | def __init__(self, master=None): 369 | Toplevel.__init__(self, master) 370 | self.startTelemetryWindow() 371 | 372 | def startTelemetryWindow(self): 373 | global updateTelemetry 374 | global distanceValueLabel 375 | global batteryValueLabel 376 | global orientationValueLabel 377 | self.title("Telemetry Window") 378 | self.geometry("300x300") 379 | telemetryMenubar = Menu(self) 380 | telemetryFileMenu = Menu(telemetryMenubar, tearoff=0) 381 | telemetryFileMenu.add_command(label="Exit", command=self.stopTelemetryWindow) 382 | telemetryMenubar.add_cascade(label="File", menu=telemetryFileMenu) 383 | self.config(menu=telemetryMenubar) 384 | telemetryFrame = Frame(self) 385 | telemetryFrame.pack() 386 | updateTelemetry = 1 387 | # orientation 388 | orientationLabel = Label(telemetryFrame, text = "Orientation:") 389 | orientationLabel.grid(column=0,row=0) 390 | orientationValueLabel = Label(telemetryFrame, text = "Upright") 391 | orientationValueLabel.grid(column=1,row=0) 392 | # battery level 393 | batteryLabel = Label(telemetryFrame, text = "Battery(v):") 394 | batteryLabel.grid(column=0,row=1) 395 | batteryValueLabel = Label(telemetryFrame, text = "0.0") 396 | batteryValueLabel.grid(column=1,row=1) 397 | # distance 398 | resetOdometerButton = Button(telemetryFrame, text = "Reset", command = self.resetOdometer) 399 | resetOdometerButton.grid(column=0,row=2) 400 | distanceLabel = Label(telemetryFrame, text = "Distance(cm):") 401 | distanceLabel.grid(column=1,row=2) 402 | distanceValueLabel = Label(telemetryFrame, text = "0.0") 403 | distanceValueLabel.grid(column=2,row=2) 404 | 405 | def stopTelemetryWindow(self): 406 | global updateTelemetry 407 | updateTelemetry = 0 408 | self.destroy() 409 | 410 | def resetOdometer(self): 411 | mip.resetOdomemeter() 412 | 413 | def getDistance(): 414 | distanceString = distanceEntry.get() 415 | distance = float(distanceString) 416 | # distance = 3.0 causes MiP to spin instead - number overflow? 417 | if distance > 2.5: 418 | distance = 2.5 419 | return distance 420 | 421 | def getAngle(): 422 | angleString = angleEntry.get() 423 | angle = int(angleString) 424 | return angle 425 | 426 | def getSpeed(): 427 | speedString = speedEntry.get() 428 | speed = int(speedString) 429 | return speed 430 | 431 | def forwardCallBack(): 432 | distance = getDistance() 433 | mip.distanceDrive(distance=distance) 434 | 435 | def backwardCallBack(): 436 | distance = getDistance() 437 | mip.distanceDrive(distance=-distance) 438 | 439 | def forwardRadarCallBack(): 440 | speed = getSpeed() 441 | mip.continuousDriveForwardUntilRadar(speed=speed) 442 | 443 | def leftCallBack(): 444 | angle = getAngle() 445 | mip.turnByAngle(angle=-angle) 446 | 447 | def rightCallBack(): 448 | angle = getAngle() 449 | mip.turnByAngle(angle=angle) 450 | 451 | def startSoundWindow(): 452 | soundWindow = SoundWindow() 453 | 454 | def startTelemetryWindow(): 455 | """ 456 | Manage a telemetry window 457 | """ 458 | telemetryWindow = TelemetryWindow() 459 | 460 | def startModeWindow(): 461 | """ 462 | Manage a mode changing window 463 | """ 464 | # move MiP in and out of computer control, allow selection of roam and music modes 465 | modeWindow = ModeWindow() 466 | 467 | def startEyesWindow(): 468 | """ 469 | Manage an eyes control window 470 | """ 471 | eyesWindow = EyesWindow() 472 | 473 | def updateLoop(): 474 | """ 475 | Top-level update loop. 476 | Called by Tk every 50ms 477 | Currenly just calls updateMovement to drive MiP in continuous drive mode, 478 | if applicable 479 | """ 480 | global updateTelemetry 481 | global lastOrientationUpdateTime 482 | global lastDistanceUpdateTime 483 | global lastBatteryUpdateTime 484 | global distanceValueLabel 485 | global batteryValueLabel 486 | global orientationValueLabel 487 | 488 | updateMovement() 489 | thisTime = time.time() 490 | if(updateTelemetry == 1): 491 | #logging.debug('updateLoop : updateTelemetry = 1') 492 | if((thisTime - lastOrientationUpdateTime) > 1.0): 493 | #logging.debug('updateLoop : Attempting orientation update') 494 | orientation = mip.getMiPOrientationStatus() 495 | orientationString = ['on back' , 'face down' , 'upright' , 'picked up', 496 | 'hand stand' , 'face down on tray' , 497 | 'on back with kickstand' ] 498 | #logging.debug('updateLoop : Orientation = %s ' % (orientationString[orientation])) 499 | orientationValueLabel.config(text=orientationString[orientation]) 500 | if(orientation == 2): 501 | orientationValueLabel.config(background='green') 502 | else: 503 | orientationValueLabel.config(background='red') 504 | lastOrientationUpdateTime = thisTime 505 | if((thisTime - lastDistanceUpdateTime) > 10.0): 506 | #logging.debug('updateLoop : Attempting distance update') 507 | distance = mip.getOdometer() 508 | #logging.debug('updateLoop : Distance = %f cm' % (distance)) 509 | # distanceValueLabel.config(text=str(distance)) 510 | distanceValueLabel.config(text=("%.2f" % distance).strip()) 511 | lastDistanceUpdateTime = thisTime 512 | if((thisTime - lastBatteryUpdateTime) > 60.0): 513 | #logging.debug('updateLoop : Attempting battery level update') 514 | batteryLevel = mip.getBatteryLevel() 515 | #logging.debug('updateLoop : Battery Level = %f v' % (batteryLevel)) 516 | # batteryValueLabel.config(text=str(batteryLevel)) 517 | batteryValueLabel.config(text=("%.2f" % batteryLevel).strip()) 518 | lastBatteryUpdateTime = thisTime 519 | root.after(50,updateLoop) 520 | 521 | def updateMovement(): 522 | """ 523 | Called to read current pointer position from the movement canvas 524 | and drive MiP in continuous drive mode based on it's potision. 525 | If the magnitude of movement is small, leave MiP and do not drive him 526 | """ 527 | # movementCanvas.positionX,movementCanvas.positionY, (-50 - 50) 528 | # movementCanvas.positionAngle,movementCanvas.positionMagnitude 529 | if movementCanvas.positionMagnitude > 10: 530 | forwardSpeed = int((-movementCanvas.positionY * 0x20)/50) 531 | turnSpeed = int((movementCanvas.positionX * 0x20)/50) 532 | logging.debug('updateMovement : forwardSpeed %d : turnSpeed %d' % (forwardSpeed,turnSpeed)) 533 | mip.continuousDrive(forwardSpeed,turnSpeed) 534 | 535 | if __name__ == '__main__': 536 | parser = argparse.ArgumentParser(description='MiP Exploration GUI.') 537 | mippy.add_arguments(parser) 538 | args = parser.parse_args() 539 | logging.basicConfig(level=logging.DEBUG) 540 | gt = mippy.GattTool(args.adaptor, args.device) 541 | mip = mippy.Mip(gt) 542 | # initialise some global variables 543 | lastBatteryUpdateTime = 0.0 544 | lastDistanceUpdateTime = 0.0 545 | lastOrientationUpdateTime = 0.0 546 | # start gui 547 | root = Tk() 548 | root.title("MiP Explorer GUI") 549 | root.geometry("600x300") 550 | #logging.debug('main:1') 551 | 552 | menubar = Menu(root) 553 | fileMenu = Menu(menubar, tearoff=0) 554 | fileMenu.add_command(label="Exit", command=root.quit) 555 | menubar.add_cascade(label="File", menu=fileMenu) 556 | windowMenu = Menu(menubar, tearoff=0) 557 | windowMenu.add_command(label="Sound", command=startSoundWindow) 558 | windowMenu.add_command(label="Telemetry", command=startTelemetryWindow) 559 | windowMenu.add_command(label="Mode", command=startModeWindow) 560 | windowMenu.add_command(label="Eyes", command=startEyesWindow) 561 | menubar.add_cascade(label="Window", menu=windowMenu) 562 | root.config(menu=menubar) 563 | 564 | #logging.debug('main:2') 565 | rootFrame = Frame(root) 566 | rootFrame.grid(column=0,row=1) 567 | 568 | #logging.debug('main:3') 569 | # Fixed drive GUI 570 | fixedDriveFrame = Frame(rootFrame) 571 | fixedDriveFrame.grid(column=0,row=0) 572 | configFrame = Frame(fixedDriveFrame) 573 | configFrame.grid(column=0,row=0) 574 | 575 | #logging.debug('main:4') 576 | labeld = Label(configFrame, text = "Distance(m):") 577 | labeld.grid(column=0,row=0) 578 | 579 | distanceStringVar = StringVar() 580 | distanceEntry = Entry(configFrame, textvariable = distanceStringVar) 581 | distanceEntry.insert(0,"0.1") 582 | distanceEntry.grid(column=1,row=0) 583 | 584 | labela = Label(configFrame, text = "Angle(deg):") 585 | labela.grid(column=0,row=1) 586 | 587 | angleStringVar = StringVar() 588 | angleEntry = Entry(configFrame, textvariable = angleStringVar ) 589 | angleEntry.insert(0,"90") 590 | angleEntry.grid(column=1,row=1) 591 | 592 | labels = Label(configFrame, text = "Speed(1-32):") 593 | labels.grid(column=0,row=2) 594 | 595 | speedStringVar = StringVar() 596 | speedEntry = Entry(configFrame, textvariable = speedStringVar ) 597 | speedEntry.insert(0,"10") 598 | speedEntry.grid(column=1,row=2) 599 | 600 | controlFrame = Frame(fixedDriveFrame) 601 | controlFrame.grid(column=0,row=1) 602 | 603 | buttonf = Button(controlFrame, text="F", command=forwardCallBack) 604 | buttonf.grid(column=1,row=0) 605 | 606 | buttonf = Button(controlFrame, text="FR", command=forwardRadarCallBack) 607 | buttonf.grid(column=1,row=1) 608 | 609 | buttonb = Button(controlFrame, text="B", command=backwardCallBack) 610 | buttonb.grid(column=1,row=2) 611 | 612 | buttonl = Button(controlFrame, text="L", command=leftCallBack) 613 | buttonl.grid(column=0,row=1) 614 | 615 | buttonr = Button(controlFrame, text="R", command=rightCallBack) 616 | buttonr.grid(column=2,row=1) 617 | 618 | quitButton = Button(controlFrame, text='Quit', command=root.destroy) 619 | quitButton.grid(column=1,row=3) 620 | 621 | #logging.debug('main:5') 622 | # movement canvas (continuous drive mode) 623 | movementCanvas = MovementCanvas(rootFrame,300,300) 624 | movementCanvas.canvas.grid(column=1,row=0) 625 | movementCanvas.setBindings() 626 | # Initialise odometer 627 | mip.resetOdomemeter() 628 | 629 | #logging.debug('main:6') 630 | root.after(50,updateLoop) 631 | #logging.debug('main:7') 632 | root.mainloop() 633 | --------------------------------------------------------------------------------