├── python_app ├── __init__.py ├── requirements.txt ├── py_overdrive_sdk │ ├── __init__.py │ └── py_overdrive.py ├── track_piece_list.txt ├── track_image.jpg ├── track_images │ ├── 17.png │ ├── 18.png │ ├── 20.png │ ├── 23.png │ ├── 34.png │ ├── 36.png │ ├── 39.png │ └── 57.png ├── constant_speed_example.py ├── lap_time_example.py ├── custom_policy_example.py ├── track_discovery_example.py └── create_track_image.py ├── node_app └── node_socket_app │ ├── package.json │ ├── node_server.js │ └── package-lock.json ├── anki_python_overview_github_readme.jpeg ├── .gitignore └── README.md /python_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_app/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_app/py_overdrive_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_app/track_piece_list.txt: -------------------------------------------------------------------------------- 1 | 34 2 | 39 3 | 17 4 | 18 5 | 57 6 | 36 7 | 20 8 | 23 9 | -------------------------------------------------------------------------------- /node_app/node_socket_app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "noble": "^1.9.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /python_app/track_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_image.jpg -------------------------------------------------------------------------------- /python_app/track_images/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/17.png -------------------------------------------------------------------------------- /python_app/track_images/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/18.png -------------------------------------------------------------------------------- /python_app/track_images/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/20.png -------------------------------------------------------------------------------- /python_app/track_images/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/23.png -------------------------------------------------------------------------------- /python_app/track_images/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/34.png -------------------------------------------------------------------------------- /python_app/track_images/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/36.png -------------------------------------------------------------------------------- /python_app/track_images/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/39.png -------------------------------------------------------------------------------- /python_app/track_images/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/python_app/track_images/57.png -------------------------------------------------------------------------------- /anki_python_overview_github_readme.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacopotagliabue/anki-drive-python-sdk/HEAD/anki_python_overview_github_readme.jpeg -------------------------------------------------------------------------------- /python_app/constant_speed_example.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from py_overdrive_sdk.py_overdrive import Overdrive 3 | 4 | # parse input 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument("--car", help="id of the bluetooth car") 7 | parser.add_argument("--host", help="host of the node gateway for bluetooth communication", default='127.0.0.1') 8 | parser.add_argument("--port", help="port of the node gateway for bluetooth communication", type=int, default=8005) 9 | args = parser.parse_args() 10 | 11 | # let's drive! 12 | car = Overdrive(args.host, args.port, args.car) # init overdrive object 13 | car.change_speed(400, 2000) # set car speed with speed = 400, acceleration = 2000 14 | input() # hold the program so it won't end abruptly 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/dictionaries 8 | .idea/**/shelf 9 | 10 | # Sensitive or high-churn files 11 | .idea/**/dataSources/ 12 | .idea/**/dataSources.ids 13 | .idea/**/dataSources.local.xml 14 | .idea/**/sqlDataSources.xml 15 | .idea/**/dynamic.xml 16 | .idea/**/uiDesigner.xml 17 | .idea/**/dbnavigator.xml 18 | 19 | # Gradle 20 | .idea/**/gradle.xml 21 | .idea/**/libraries 22 | 23 | # CMake 24 | cmake-build-debug/ 25 | cmake-build-release/ 26 | 27 | # Mongo Explorer plugin 28 | .idea/**/mongoSettings.xml 29 | 30 | # File-based project format 31 | *.iws 32 | 33 | # IntelliJ 34 | out/ 35 | 36 | # mpeltonen/sbt-idea plugin 37 | .idea_modules/ 38 | 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Cursive Clojure plugin 43 | .idea/replstate.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | fabric.properties 50 | 51 | # Editor-based Rest Client 52 | .idea/httpRequests 53 | 54 | venv/ 55 | vendored/ 56 | settings.ini 57 | 58 | node_modules/ -------------------------------------------------------------------------------- /python_app/lap_time_example.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from py_overdrive_sdk.py_overdrive import Overdrive 3 | 4 | # parse input 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument("--car", help="id of the bluetooth car") 7 | parser.add_argument("--host", help="host of the node gateway for bluetooth communication", default='127.0.0.1') 8 | parser.add_argument("--port", help="port of the node gateway for bluetooth communication", type=int, default=8005) 9 | args = parser.parse_args() 10 | 11 | 12 | # define custom driving policy 13 | def my_lap_driving_policy(self, **kwargs): 14 | """ 15 | Just an example of driving policy for the oval truck: approx measure how long it takes to complete a lap 16 | 17 | :param kwargs will be a location event as dict produced by the 'build_location_event' function 18 | :return: 19 | """ 20 | current_piece = kwargs['piece'] 21 | if current_piece == 33: 22 | current_time = kwargs['notification_time'] 23 | # time the lap and print it: if there is no last time, it means it's the first time 24 | if hasattr(self, 'last_starting_line_event'): 25 | time_lap = (current_time - self.last_starting_line_event).total_seconds() 26 | print('time lap in seconds was {}'.format(time_lap)) 27 | # update lap time 28 | self.last_starting_line_event = current_time 29 | 30 | return 31 | 32 | 33 | # let's drive! 34 | car = Overdrive(args.host, args.port, args.car, my_lap_driving_policy) # init overdrive object with custom policy 35 | car.change_speed(800, 2000) # set car speed with speed = 400, acceleration = 2000 36 | input() # hold the program so it won't end abruptly 37 | -------------------------------------------------------------------------------- /python_app/custom_policy_example.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from py_overdrive_sdk.py_overdrive import Overdrive 3 | 4 | # parse input 5 | parser = argparse.ArgumentParser() 6 | parser.add_argument("--car", help="id of the bluetooth car") 7 | parser.add_argument("--host", help="host of the node gateway for bluetooth communication", default='127.0.0.1') 8 | parser.add_argument("--port", help="port of the node gateway for bluetooth communication", type=int, default=8005) 9 | args = parser.parse_args() 10 | 11 | 12 | TOP_SPEED = 2000 13 | BRAKE_SPEED = 1000 14 | 15 | 16 | # define custom driving policy 17 | def my_driving_policy(self, **kwargs): 18 | """ 19 | Just an example of driving policy for the oval truck: if some condition is met, adjust speed 20 | 21 | :param kwargs will be a location event as dict produced by the 'build_location_event' function 22 | :return: 23 | """ 24 | print(kwargs) 25 | # if the car is in the starting straight segments of the oval, set speed to 1000 if not already there 26 | if kwargs['piece'] in [34, 57] and kwargs['self_speed'] != TOP_SPEED: 27 | print('Ride baby!') 28 | self.change_speed(TOP_SPEED, 2000) 29 | # if the car is in the curving segments of the oval, set speed to 400 if not already there 30 | elif kwargs['piece'] in [39, 36] and kwargs['self_speed'] != BRAKE_SPEED: 31 | print('Slowing down!') 32 | self.change_speed(BRAKE_SPEED, 2000) 33 | else: 34 | return 35 | 36 | 37 | # let's drive! 38 | car = Overdrive(args.host, args.port, args.car, my_driving_policy) # init overdrive object with custom policy 39 | car.change_speed(400, 2000) # set car speed with speed = 400, acceleration = 2000 40 | # the car will change speed when traversing the starting straight segment 41 | input() # hold the program so it won't end abruptly 42 | -------------------------------------------------------------------------------- /python_app/track_discovery_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic and NON-general example of a track discovery policy: the car will collect all piece IDs from the track 3 | and store them in a text file. 4 | 5 | If you then run 'create_track_image.py' a png image of the map will be produced. 6 | """ 7 | 8 | import argparse 9 | from py_overdrive_sdk.py_overdrive import Overdrive 10 | 11 | # parse input 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument("--car", help="id of the bluetooth car") 14 | parser.add_argument("--host", help="host of the node gateway for bluetooth communication", default='127.0.0.1') 15 | parser.add_argument("--port", help="port of the node gateway for bluetooth communication", type=int, default=8005) 16 | args = parser.parse_args() 17 | 18 | # name of the output file 19 | TRACK_FILE = 'track_piece_list.txt' 20 | 21 | 22 | # define custom driving policy for track discovery 23 | def discovery_driving_policy(self, **kwargs): 24 | """ 25 | Just an example of policy keeping track of all the piece IDs in the track 26 | 27 | :param kwargs will be a location event as dict produced by the 'build_location_event' function 28 | :return: 29 | """ 30 | current_piece = kwargs['piece'] 31 | print('piece id: {}'.format(current_piece)) 32 | # create a variable to store pieces if is not there 33 | if not hasattr(self, 'track_pieces'): 34 | self.track_pieces = [] 35 | # first, ignore the start piece which has no image attached 36 | if current_piece == 33: 37 | return 38 | # then, always start from the finish line 39 | if current_piece != 34 and not self.track_pieces: 40 | return 41 | # if it's a new id, add it 42 | if current_piece not in self.track_pieces: 43 | self.track_pieces.append(kwargs['piece']) 44 | print('Added piece {}'.format(current_piece)) 45 | # if id is there AND it is not the last one added (as we may have more consecutive location events 46 | # for the same id), we already map everything once - so quit 47 | else: 48 | if current_piece != self.track_pieces[-1]: 49 | with open(TRACK_FILE, 'w') as track_f: 50 | for p in self.track_pieces: 51 | track_f.write('{}\n'.format(p)) 52 | print('Stopping now!') 53 | car.change_speed(0, 1000) 54 | 55 | return 56 | 57 | 58 | # let's drive! 59 | car = Overdrive(args.host, args.port, args.car, discovery_driving_policy) # init overdrive object with custom policy 60 | car.change_speed(400, 2000) # set car speed with speed = 400, acceleration = 2000 61 | # the car will change speed when traversing the starting straight segment 62 | input() # hold the program so it won't end abruptly 63 | -------------------------------------------------------------------------------- /python_app/create_track_image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple helper script that takes the txt file produced by 'track_discovery_example' combine the thumbs 3 | of the pieces together in a single image. It's just a quick 10 minutes script and it 4 | won't work generally, but it should get you started if you'd like to explore track mapping more. 5 | Images are stored in the 'track_images' folder and come from here 6 | https://github.com/tiker/AnkiNodeDrive/tree/master/images. 7 | 8 | Thanks to https://gist.github.com/glombard/7cd166e311992a828675 for getting us started. 9 | 10 | 11 | ATTENTION: please note that this script has different dependencies than the general sdk. 12 | 13 | # Combine multiple images into one. 14 | # 15 | # To install the Pillow module on Mac OS X: 16 | # 17 | # $ xcode-select --install 18 | # $ brew install libtiff libjpeg webp little-cms2 19 | # $ pip install Pillow 20 | """ 21 | from PIL import Image 22 | 23 | 24 | THUMB_SIZE = 256 # image of the thumb-sized pieces 25 | FINAL_IMAGE_SIZE = 1200 # final size image - should be probably be cropped at the end... 26 | TRACK_FILE = 'track_piece_list.txt' # input file 27 | PIECE_TYPE = { 28 | '34': ('straight', 'EAST'), # mapping track id with type and "direction" 29 | '36': ('straight', 'WEST'), # direction could vary with track orientation but this 30 | '39': ('straight', 'EAST'), # will work for our sample oval track 31 | '57': ('straight', 'WEST'), 32 | '17': ('turn', 'SOUTH'), 33 | '18': ('turn', 'WEST'), 34 | '20': ('turn', 'NORTH'), 35 | '23': ('turn', 'EAST'), 36 | } 37 | 38 | 39 | def get_next_coors(last_piece_type, last_x, last_y, size): 40 | if last_piece_type[1] == "EAST": 41 | return last_x + size, last_y 42 | elif last_piece_type[1] == "WEST": 43 | return last_x - size, last_y 44 | elif last_piece_type[1] == "SOUTH": 45 | return last_x, last_y + size 46 | elif last_piece_type[1] == "NORTH": 47 | return last_x, last_y - size 48 | else: 49 | raise Exception('Type not allowed {}'.format(last_piece_type)) 50 | 51 | 52 | def main(): 53 | with open(TRACK_FILE, 'r') as track_file: 54 | pieces = [l.strip() for l in track_file] 55 | print('{} pieces found in the track!'.format(len(pieces))) 56 | 57 | # start calculating x/y for the thumbs 58 | thumbs_coors = [] 59 | # the first one is always the start piece n. 34 and it starts in the middle of w/h 60 | assert(pieces[0] == '34') 61 | current_x = int(FINAL_IMAGE_SIZE / 2) 62 | current_y = int(FINAL_IMAGE_SIZE / 2) 63 | thumbs_coors.append({ 64 | 'id': pieces[0], 65 | 'coors': (current_x, current_y) 66 | }) 67 | next_x, next_y = get_next_coors(PIECE_TYPE[thumbs_coors[-1]['id']], current_x, current_y, THUMB_SIZE) 68 | # loop over other pieces and calculate position with some hacks 69 | for idx, piece_id in enumerate(pieces[1:]): 70 | current_x = next_x 71 | current_y = next_y 72 | thumbs_coors.append({ 73 | 'id': piece_id, 74 | 'coors': (current_x, current_y) 75 | }) 76 | next_x, next_y = get_next_coors(PIECE_TYPE[thumbs_coors[-1]['id']], current_x, current_y, THUMB_SIZE) 77 | 78 | # finally compose the image 79 | track_image = Image.new("RGB", (FINAL_IMAGE_SIZE, FINAL_IMAGE_SIZE)) 80 | for t in thumbs_coors: 81 | img = Image.open('track_images/{}.png'.format(t['id'])) 82 | img.thumbnail((THUMB_SIZE, THUMB_SIZE), Image.ANTIALIAS) 83 | track_image.paste(img, (t['coors'][0], t['coors'][1])) 84 | 85 | track_image.save('track_image.jpg') 86 | 87 | print('All done, see you, space cowboys!') 88 | 89 | return 90 | 91 | 92 | if __name__ == '__main__': 93 | main() 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anki-drive-python-sdk 2 | This is a Python wrapper to read/send message from/to [Anki Overdrive](https://www.anki.com/en-us/overdrive) 3 | bluetooth vehicles. 4 | 5 | The project was designed for the A.I. blog series _Self-driving (very small) cars_: 6 | please refer to the Medium [post](https://towardsdatascience.com/self-driving-very-small-cars-part-i-398cca26f930) for a full explanation on the code structure and the philosophy behind it. 7 | 8 | ## TL;DR 9 | We share a cross-platform Python+node setup that allows for quick experimentation and prototyping of interesting ideas 10 | in the toy universe of bluetooth cars. 11 | 12 | 13 | ![overview](anki_python_overview_github_readme.jpeg) 14 | 15 | 16 | In particular: 17 | 18 | * a node server leverages [noble](https://www.npmjs.com/package/noble) to establish communication with 19 | Anki cars; 20 | * a Python app leverages sockets to reliably read/send messages from/to the node gateway, which abstracts 21 | away all the complexity of the bluetooth channel. 22 | 23 | ## Setup 24 | To use _py-overdrive-sdk_ you'll need: 25 | 26 | * node 27 | * Python 3 (the project was originally coded in 3.6) 28 | * [Anki overdrive set](https://www.anki.com/en-us/overdrive) 29 | 30 | We run our code from a 2017 MacBook Pro and 31 | a [Rasp Pi 3B+](https://www.raspberrypi.org/products/raspberry-pi-3-model-b-plus/). 32 | We did not test with other hardware 33 | setups but _in theory_ the wrapper should be pretty flexible. 34 | 35 | ## Deployment 36 | After cloning the repo, proceed to install node and python dependencies (we suggest using `virtualenv` but 37 | it's not necessary of course) as usual: 38 | 39 | ``` 40 | npm install 41 | ``` 42 | 43 | ``` 44 | pip install -r requirements.txt 45 | ``` 46 | 47 | Prepare the bluetooth id for the car you want to drive (if you don't know it, you can just start-up the node server with 48 | a fake id and write down the id that gets printed to the console in the format `SCAN|{BLUETOOTH-CAR-ID}|{BLUETOOTH-CAR-ADDRESS}`). 49 | 50 | Start-up the node gateway: 51 | 52 | ```node node_server.js {YOUR-SERVER-PORT} {YOUR-BLUETOOTH-CAR-ID}``` 53 | 54 | and wait for the console to notify that SCAN has been completed (you should see the id of the Anki 55 | cars as they are discovered by noble). 56 | 57 | Finally run your python script. To start with a simple example, run: 58 | 59 | ```python constant_speed_example.py --car={YOUR-BLUETOOTH-CAR-ID} --port={GATEWAY-PORT}``` 60 | 61 | and see your car moving around (make sure to specify the same `port` for both node and Python). [This](https://drive.google.com/file/d/1h1tjzRUQm2BZqDkZn6zhacXShGYioxgU/view) is 62 | a one-minute video going from git to a running car: please refer to the Medium post for more details. 63 | 64 | 65 | ## Current release and next steps 66 | Please note that the current master (as of Sep. 2018) is released as _alpha_ as it just contains the bare minimum 67 | the get things going: no unit tests, no fancy stuff, almost no protections from errors, etc. 68 | We'll hope to make several improvements to the code base as we progress with our experiments: 69 | feedback and contributions are most welcomed! Even without considering A.I. stuff (i.e. making the car 70 | learning how to optimally drive), there are several engineering improvements to be considered, such as for example 71 | (non-exhaustive list in no particular order): 72 | 73 | * set up unit and integration tests; 74 | * re-use node gateway for multiple cars at the same time; 75 | * a better abstraction for "driving policies", so that it becomes easier to instantiate custom policies while 76 | keeping the rest of the code (i.e. communication layer) pretty much intact and re-usable; 77 | * a "car-level" abstraction on top of the current Overdrive class, so that we could easily simulate different hardware 78 | abilities and constrain car learning in dynamic ways; 79 | * data ingestion/persisting mechanism, so that we can log in a reliable and consistent way everything that happens 80 | within a run 81 | 82 | Some experimental code will be published in the _develop_ branch and hopefully merged later on into a _beta_ release. 83 | 84 | 85 | ## Acknowledgments 86 | It would have been a _month_ project, not a weekend one, without Internet and the fantastic people on it sharing their 87 | code and ideas: as far as copy+paste-ing goes, this project is indeed second to none. In particular: 88 | 89 | * Python wrapper was inspired by [overdrive-python](https://github.com/xerodotc/overdrive-python) 90 | * Node gateway was inspired by [anki-drive-java](https://github.com/adessoAG/anki-drive-java) 91 | * Protocol and communication was reverse engineered from the official [Anki SDK](https://github.com/anki/drive-sdk) 92 | and [node-mqtt-for-anki-overdrive](https://github.com/IBM-Cloud/node-mqtt-for-anki-overdrive) 93 | * Track images are from [AnkiNodeDrive](https://github.com/tiker/AnkiNodeDrive/tree/master/images) 94 | 95 | ## License 96 | All the code in this repo is provided "AS IS" and it is freely available under the 97 | [Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). 98 | -------------------------------------------------------------------------------- /node_app/node_socket_app/node_server.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ORIGINAL IDEA FROM https://github.com/adessoAG/anki-drive-java/blob/master/src/main/nodejs/server.js 4 | CODE RE-ADAPTED AND MODIFIED FOR THE Self-driving (very small) cars BLOG SERIES 5 | LAST UPDATE: AUG. 2018 6 | FOR FULL LICENSE, SEE README 7 | 8 | */ 9 | 10 | var net = require('net'); 11 | var noble = require('noble'); 12 | var util = require('util'); 13 | 14 | // const from ANKI docs 15 | const ANKI_DRIVE_SERVICE_UUIDS = ["be15beef6186407e83810bd89c4d8df4"]; 16 | const ANKI_DRIVE_CHARACTERISTIC_UUIDS = ["be15bee06186407e83810bd89c4d8df4", "be15bee16186407e83810bd89c4d8df4"]; 17 | // get port as the first arg from the command line 18 | var CURRENT_PORT = parseInt(process.argv[2]); 19 | // get current car UUID as the second arg from the command line 20 | var CURRENT_CAR_UUID = process.argv[3]; 21 | 22 | 23 | noble.on('stateChange', function(state) { 24 | if (state === 'poweredOn') 25 | { 26 | console.log("powered on!"); 27 | noble.on('discover', function(device) { 28 | console.log(util.format("SCAN|%s|%s", device.id, device.address)); 29 | }); 30 | noble.startScanning(ANKI_DRIVE_SERVICE_UUIDS); 31 | setTimeout(function() { 32 | noble.stopScanning(); 33 | console.log("SCAN|COMPLETED"); 34 | }, 2500); 35 | } 36 | }); 37 | 38 | var server = net.createServer(function(client) { 39 | client.vehicles = []; 40 | 41 | // bind event for data received from Python app 42 | client.on("data", function(data) { 43 | data.toString().split("\r\n").forEach(function(line) { 44 | // parse commmand from Python 45 | var normalizedData = line.trim(); 46 | var token = normalizedData.split('|'); 47 | var command = token[0]; 48 | var args = token.slice(1); 49 | console.log(util.format("command received %s", normalizedData)); 50 | console.log(util.format("args received %s", args)); 51 | // based on the command, execute business logic 52 | // default option is just gateway for bluetooth messages 53 | switch (command) { 54 | // connect to a car based on uuid, e.g. CONNECT|83d9630daa534025ab4a29a4c398d552 55 | case "CONNECT": 56 | console.log("connection begins..."); 57 | var vehicle = noble._peripherals[args[0]]; 58 | vehicle.connect(function(error) { 59 | vehicle.discoverSomeServicesAndCharacteristics( 60 | ANKI_DRIVE_SERVICE_UUIDS, 61 | ANKI_DRIVE_CHARACTERISTIC_UUIDS, 62 | function(error, services, characteristics) { 63 | vehicle.reader = characteristics.find(x => !x.properties.includes("write")); 64 | vehicle.writer = characteristics.find(x => x.properties.includes("write")); 65 | 66 | vehicle.reader.notify(true); 67 | vehicle.reader.on('read', function(data, isNotification) { 68 | client.write(util.format("%s", data.toString("hex"))); 69 | }); 70 | client.vehicles.push(vehicle); 71 | console.log("connection success!"); 72 | } 73 | ); 74 | }); 75 | 76 | break; 77 | // disconnect from a car based on uuid, e.g. DISCONNECT|83d9630daa534025ab4a29a4c398d552 78 | case "DISCONNECT": 79 | var vehicle = noble._peripherals[args[0]]; 80 | vehicle.disconnect(); 81 | console.log("disconnect success"); 82 | break; 83 | // disconnect from all cars discovered, e.g. QUIT 84 | case "QUIT": 85 | client.vehicles.forEach(function(v) {v.disconnect()}); 86 | console.log("all disconnected successfully"); 87 | break; 88 | // default option: just send the message to the car unfiltered 89 | default: 90 | console.log('run command ' + command); 91 | var vehicle = noble._peripherals[CURRENT_CAR_UUID]; 92 | vehicle.writer.write(new Buffer(command, 'hex')); 93 | } 94 | }); 95 | }); // on data received 96 | 97 | // bind event on error 98 | client.on("error", function(err) { 99 | console.log("unexpected error: disconnecting all vehicles"); 100 | // disconnect from all vehicles 101 | client.vehicles.forEach(function(v) {v.disconnect()}); 102 | }); // on error 103 | 104 | process.on('SIGINT', function () { 105 | console.log('bye bye: disconnecting all vehichles now...'); 106 | // disconnect from all vehicles 107 | client.vehicles.forEach(function(v) {v.disconnect()}); 108 | process.exit(0); 109 | }); 110 | 111 | }); 112 | 113 | server.listen(CURRENT_PORT); 114 | console.log(util.format("Node gateway started, port %s, car id %s", CURRENT_PORT, CURRENT_CAR_UUID)); 115 | 116 | -------------------------------------------------------------------------------- /python_app/py_overdrive_sdk/py_overdrive.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python wrapper for Anki Overdrive vehicle communication through a node gateway. 3 | This code has been developed with educational and R&D purposes for the 'Self-driving (very small) cars' 4 | blog series, 5 | 6 | The code is released under the Apache License 2.0: FOR FULL LICENSE AND MORE INFO, PLEASE SEE README 7 | """ 8 | 9 | import struct 10 | import threading 11 | import socket 12 | import time 13 | from datetime import datetime 14 | from queue import Queue, Empty 15 | 16 | 17 | class Overdrive: 18 | 19 | def __init__(self, host, port, uuid, driving_policy=None, verbose=False): 20 | """ 21 | 22 | :param host: socket host 23 | :param port: socket port 24 | :param uuid: uuid of the target blueetooth vehicle 25 | :param driving_policy: optional function supplied by the user to take action based on location events 26 | """ 27 | # init node socket 28 | self.node_socket = socket.socket() 29 | self.node_socket.connect((host, port)) 30 | # set some class variables 31 | self.uuid = uuid 32 | self._connected = False 33 | # store queues 34 | self._queues = { 35 | 'commands': Queue(), # queue to write commands to the gateway 36 | 'locations': Queue() # queue to write location events received from the gateway 37 | } 38 | self._threads = [] 39 | # driving policy supplied by the user overwrite the standard one in _standard_driving_policy 40 | self._driving_policy = driving_policy 41 | # init current speed at 0 42 | self._speed = 0 43 | # set verbose to know how much to print (debug friendly=True) 44 | self._verbose = verbose 45 | # finally try to connect to the car through node socket 46 | self._connect(self.uuid) 47 | return 48 | 49 | def __del__(self): 50 | self._connected = False 51 | self._disconnect(self.uuid) 52 | 53 | """ 54 | CONNECTION FUNCTIONS 55 | """ 56 | 57 | def _disconnect(self, uuid): 58 | self.node_socket.send("DISCONNECT|{}\n".format(uuid).encode()) 59 | 60 | def _connect(self, uuid): 61 | self._send_connect_message_to_socket(uuid) 62 | self._connected = True 63 | # fork thread to read and write to the bluetooth socket 64 | self._start_thread(self._send_thread) 65 | self._start_thread(self._read_thread) 66 | self._start_thread(self._location_changed_thread) 67 | self.turn_on_sdk_mode() 68 | # give it some time - this is fairly horrible but it's the startup of the process 69 | # so waiting a bit doesn't hurt anybody ;-) 70 | time.sleep(1) 71 | 72 | def _send_connect_message_to_socket(self, uuid): 73 | self.node_socket.send("CONNECT|{}\n".format(uuid).encode()) 74 | # give it some time - this is fairly horrible but it's the startup of the process 75 | # so waiting a bit doesn't hurt anybody ;-) 76 | time.sleep(1) 77 | return 78 | 79 | """ 80 | THREADING FUNCTIONS 81 | """ 82 | 83 | def _start_thread(self, target_function): 84 | new_thread = threading.Thread(target=target_function) 85 | self._threads.append(new_thread) 86 | new_thread.start() 87 | 88 | def _read_thread(self): 89 | while self._connected: 90 | data = self.node_socket.recv(1024).decode() 91 | if data: 92 | b_data = bytes.fromhex(data) 93 | command_id = hex(struct.unpack_from("B", b_data, 1)[0]) 94 | self._handle_notification(command_id, b_data) 95 | 96 | def _send_thread(self): 97 | while self._connected: 98 | try: 99 | data = self._queues['commands'].get_nowait() 100 | self.node_socket.send('{}\n'.format(data.hex()).encode()) 101 | except Empty as empty_ex: 102 | continue 103 | except Exception as ex: 104 | raise ex 105 | 106 | def _location_changed_thread(self): 107 | while self._connected: 108 | try: 109 | (location, piece, offset, speed, clockwise, notification_time) = self._queues['locations'].get_nowait() 110 | location_event = self.build_location_event(location, piece, offset, speed, clockwise, notification_time) 111 | if self._driving_policy: 112 | self._driving_policy(self, **location_event) 113 | else: 114 | self._standard_driving_policy(**location_event) 115 | except Empty as empty_ex: 116 | continue 117 | except Exception as ex: 118 | raise ex 119 | 120 | def _handle_notification(self, command_id, data): 121 | # location notification 122 | if command_id == '0x27': 123 | # parse location 124 | location, piece, offset, speed, clockwise = struct.unpack_from("