├── scripts └── chromify ├── index.js ├── .eslintrc.json ├── .travis.yml ├── AUTHORS.md ├── CONTRIBUTING.md ├── eg ├── repl.js ├── discover.js ├── reconnect.js ├── index.js ├── gamepad.js ├── old.js ├── swarm.js └── keyboard.js ├── .editorconfig ├── LICENSE.md ├── test └── drone.js ├── package.json ├── .gitignore ├── CHANGELOG.md ├── lib ├── swarm.js └── drone.js └── README.md /scripts/chromify: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | browserify lib/drone.js -o drone.browserify.js -i noble -i debug 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/drone'); 2 | module.exports.Swarm = require('./lib/swarm'); 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eta", 3 | "rules": { 4 | "camelcase": 1, 5 | "comma-spacing": 1, 6 | "no-unused-vars": 1 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | language: node_js 5 | 6 | node_js: 7 | - '4' 8 | - '6' 9 | 10 | before_install: 11 | sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev 12 | 13 | before_script: 14 | - npm run lint 15 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # The main collaborators of this library 2 | 3 | * Jack Watson-Hamblin 4 | * Chris Williams 5 | 6 | # Contributors ordered by first contribution. 7 | 8 | * DroneWorks 9 | * Chris Taylor 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Running the test suite 4 | 5 | ```js 6 | $ git clone 7 | $ cd 8 | $ npm install 9 | $ npm test 10 | ``` 11 | 12 | Funny story, as of right now we don't have many tests, they mainly live in the 13 | `eg` directory for use with a real drone. Please, please, please if 14 | you rock at test, help out! 15 | 16 | ### Fixing bugs 17 | 18 | Bug fixes are always welcome. Please add a test! 19 | -------------------------------------------------------------------------------- /eg/repl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Drone = require('../'); 4 | var repl = require('repl'); 5 | 6 | if (process.env.UUID) { 7 | var d = new Drone(process.env.UUID); 8 | d.connect(function() { 9 | d.setup(function() { 10 | d.startPing(); 11 | 12 | var replServer = repl.start({ 13 | prompt: 'Drone (' + d.uuid + ') > ' 14 | }); 15 | 16 | replServer.context.drone = d; 17 | 18 | replServer.on('exit', function() { 19 | d.land(); 20 | process.exit(); 21 | }); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Matches multiple files with brace expansion notation 13 | # Set default charset 14 | [*.{js,py}] 15 | charset = utf-8 16 | 17 | # Two space indentation (no size specified) 18 | [*.js] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | 23 | # Indentation override for all JS under lib directory 24 | [lib/**.js] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | # Matches the exact files either package.json or .travis.yml 29 | [{package.json,.travis.yml}] 30 | -------------------------------------------------------------------------------- /eg/discover.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Drone = require('rolling-spider'); 4 | var noble = require('noble'); 5 | var knownDevices = []; 6 | 7 | if (noble.state === 'poweredOn') { 8 | start(); 9 | } else { 10 | noble.on('stateChange', start); 11 | } 12 | 13 | function start () { 14 | noble.startScanning(); 15 | 16 | noble.on('discover', function(peripheral) { 17 | if (!Drone.isDronePeripheral(peripheral)) { 18 | return; // not a rolling spider 19 | } 20 | 21 | var details = { 22 | name: peripheral.advertisement.localName, 23 | uuid: peripheral.uuid, 24 | rssi: peripheral.rssi 25 | }; 26 | 27 | knownDevices.push(details); 28 | console.log(knownDevices.length + ': ' + details.name + ' (' + details.uuid + '), RSSI ' + details.rssi); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /eg/reconnect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var keypress = require('keypress'); 4 | var Drone = require('../'); 5 | 6 | 7 | 8 | 9 | var d = new Drone(process.env.UUID); 10 | 11 | 12 | 13 | 14 | function launch() { 15 | d.connect(function () { 16 | console.log('Prepare for take off! ', d.name); 17 | d.flatTrim(); 18 | setTimeout(function () { 19 | console.log('take off'); 20 | d.takeOff(); 21 | d.startPing(); 22 | }, 1000); 23 | 24 | setTimeout(function () { 25 | console.log('land'); 26 | d.land(); 27 | }, 6000); 28 | 29 | 30 | setTimeout(function () { 31 | console.log('disconnect'); 32 | d.disconnect(); 33 | }, 10000); 34 | }); 35 | } 36 | 37 | 38 | 39 | 40 | launch(); 41 | 42 | 43 | setTimeout(launch, 20000); 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Jack Watson-Hamblin 2 | 2015 Chris Taylor 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /test/drone.js: -------------------------------------------------------------------------------- 1 | var Drone = require('../'); 2 | var should = require('should'); 3 | 4 | describe('Drone.isDronePeripheral', function() { 5 | 6 | it('returns false if no peripheral record', function(done) { 7 | should.equal(Drone.isDronePeripheral(), false); 8 | done(); 9 | }); 10 | 11 | it('returns true if peripheral.advertisement.localName begins with "RS_"', function(done) { 12 | var peripheral = { 13 | advertisement: { 14 | localName: 'RS_whatever' 15 | } 16 | }; 17 | should.equal(Drone.isDronePeripheral(peripheral), true); 18 | done(); 19 | }); 20 | 21 | it('returns true if peripheral.advertisement.localName begins with "Mambo_"', function(done) { 22 | var peripheral = { 23 | advertisement: { 24 | localName: 'Mambo_whatever' 25 | } 26 | }; 27 | should.equal(Drone.isDronePeripheral(peripheral), true); 28 | done(); 29 | }); 30 | 31 | it('returns true if peripheral.advertisement.manufacturerData is correct', function(done) { 32 | var peripheral = { 33 | advertisement: { 34 | manufacturerData: new Buffer([0x43, 0x00, 0xcf, 0x19, 0x00, 0x09, 0x01, 0x00]) 35 | } 36 | }; 37 | should.equal(Drone.isDronePeripheral(peripheral), true); 38 | done(); 39 | }); 40 | 41 | it('returns true if custom name, but peripheral.advertisement.manufacturerData is correct', function(done) { 42 | var peripheral = { 43 | advertisement: { 44 | localName: 'ArachnaBot', 45 | manufacturerData: new Buffer([0x43, 0x00, 0xcf, 0x19, 0x00, 0x09, 0x01, 0x00]) 46 | } 47 | }; 48 | should.equal(Drone.isDronePeripheral(peripheral), true); 49 | done(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rolling-spider", 3 | "version": "1.7.0", 4 | "description": "A library for sending BLE commands to a Parrot Rolling Spider drone", 5 | "main": "index.js", 6 | "keywords": [ 7 | "bluetooth", 8 | "ble", 9 | "parrot", 10 | "rolling-spider", 11 | "drone", 12 | "quadcopter", 13 | "multicopter", 14 | "flight" 15 | ], 16 | "author": { 17 | "name": "Jack Watson-Hamblin", 18 | "email": "info@fluffyjack.com", 19 | "web": "https://motioninmotion.tv/" 20 | }, 21 | "maintainers": [ 22 | { 23 | "name": "Jack Watson-Hamblin", 24 | "email": "info@fluffyjack.com" 25 | }, 26 | { 27 | "name": "Chris Williams", 28 | "email": "voodootikigod@gmail.com" 29 | }, 30 | { 31 | "name": "Chris Taylor", 32 | "email": "christhebaron@gmail.com", 33 | "web": "http://christhebaron.co.uk" 34 | }, 35 | { 36 | "name": "Linda Nichols", 37 | "email": "lynnaloo@gmail.com" 38 | } 39 | ], 40 | "bugs": { 41 | "url": "https://github.com/FluffyJack/node-rolling-spider/issues" 42 | }, 43 | "scripts": { 44 | "test": "mocha", 45 | "lint": "eslint . --ext .js" 46 | }, 47 | "dependencies": { 48 | "debug": "^2.1.1", 49 | "keypress": "^0.2.1", 50 | "lodash": "3.9.3", 51 | "noble": "voodootikigod/noble" 52 | }, 53 | "devDependencies": { 54 | "mocha": "2.2.5", 55 | "should": "6.0.3", 56 | "sinon": "1.14.1", 57 | "temporal": "^0.4.0", 58 | "eslint-config-eta": "^0.0.9", 59 | "eslint": "^3.19.0" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "git@github.com:voodootikigod/node-rolling-spider.git" 64 | }, 65 | "license": "MIT", 66 | "os": [ 67 | "darwin", 68 | "linux", 69 | "win32", 70 | "android" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Linux ### 4 | *~ 5 | 6 | # KDE directory preferences 7 | .directory 8 | 9 | # Linux trash folder which might appear on any partition or disk 10 | .Trash-* 11 | drone.browserify.js 12 | 13 | ### Node ### 14 | # Logs 15 | logs 16 | *.log 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directory 39 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 40 | node_modules 41 | 42 | 43 | ### WebStorm ### 44 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 45 | 46 | *.iml 47 | 48 | ## Directory-based project format: 49 | .idea/ 50 | # if you remove the above rule, at least ignore the following: 51 | 52 | # User-specific stuff: 53 | # .idea/workspace.xml 54 | # .idea/tasks.xml 55 | # .idea/dictionaries 56 | 57 | # Sensitive or high-churn files: 58 | # .idea/dataSources.ids 59 | # .idea/dataSources.xml 60 | # .idea/sqlDataSources.xml 61 | # .idea/dynamic.xml 62 | # .idea/uiDesigner.xml 63 | 64 | # Gradle: 65 | # .idea/gradle.xml 66 | # .idea/libraries 67 | 68 | # Mongo Explorer plugin: 69 | # .idea/mongoSettings.xml 70 | 71 | ## File-based project format: 72 | *.ipr 73 | *.iws 74 | 75 | ## Plugin-specific files: 76 | 77 | # IntelliJ 78 | out/ 79 | 80 | # mpeltonen/sbt-idea plugin 81 | .idea_modules/ 82 | 83 | # JIRA plugin 84 | atlassian-ide-plugin.xml 85 | 86 | # Crashlytics plugin (for Android Studio and IntelliJ) 87 | com_crashlytics_export_strings.xml 88 | crashlytics.properties 89 | crashlytics-build.properties 90 | -------------------------------------------------------------------------------- /eg/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Drone = require('../'); 4 | var temporal = require('temporal'); 5 | var d = new Drone(process.env.UUID); 6 | 7 | d.connect(function () { 8 | d.setup(function () { 9 | d.flatTrim(); 10 | d.startPing(); 11 | d.flatTrim(); 12 | console.log('Connected to drone', d.name); 13 | 14 | temporal.queue([ 15 | { 16 | delay: 5000, 17 | task: function () { 18 | console.log('Getting ready for takeOff!'); 19 | d.takeOff(); 20 | d.flatTrim(); 21 | } 22 | }, 23 | { 24 | delay: 4500, 25 | task: function () { 26 | console.log('Going forward'); 27 | d.forward({steps: 12}); 28 | } 29 | }, 30 | { 31 | delay: 4500, 32 | task: function () { 33 | console.log('Going up'); 34 | d.up({steps: 20}); 35 | } 36 | }, 37 | { 38 | delay: 4500, 39 | task: function () { 40 | console.log('Going down'); 41 | d.down({steps: 20}); 42 | } 43 | }, 44 | { 45 | delay: 4500, 46 | task: function () { 47 | console.log('Going left'); 48 | d.tiltLeft({steps: 12, speed: 100}); 49 | } 50 | }, 51 | { 52 | delay: 4500, 53 | task: function () { 54 | console.log('Going right'); 55 | d.tiltRight({steps: 12, speed: 100}); 56 | } 57 | }, 58 | { 59 | delay: 5000, 60 | task: function () { 61 | console.log('OMG Flip!'); 62 | d.frontFlip(); 63 | } 64 | }, 65 | { 66 | delay: 5000, 67 | task: function () { 68 | console.log('Time to land'); 69 | d.land(); 70 | } 71 | }, 72 | { 73 | delay: 5000, 74 | task: function () { 75 | temporal.clear(); 76 | process.exit(0); 77 | } 78 | } 79 | ]); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /eg/gamepad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /* 5 | For use with the Logitech Dual Action Controller F310 6 | Button Mapping 7 | 8 | X => 1 9 | A => 2 10 | B => 3 11 | Y => 4 12 | 13 | */ 14 | 15 | 16 | 17 | // Create a new one 18 | var Controller = require('logitech-dual-action-controller'); 19 | 20 | var controller = new Controller(); 21 | 22 | 23 | 24 | var Drone = require('../'); 25 | 26 | var ACTIVE = true; 27 | var STEPS = 2; 28 | 29 | 30 | function cooldown() { 31 | ACTIVE = false; 32 | setTimeout(function () { 33 | ACTIVE = true; 34 | }, STEPS * 12); 35 | } 36 | 37 | 38 | if (process.env.UUID) { 39 | console.log('Searching for ', process.env.UUID); 40 | } 41 | 42 | var d = new Drone(process.env.UUID); 43 | 44 | 45 | d.connect(function () { 46 | d.setup(function () { 47 | console.log('Configured for Rolling Spider! ', d.name); 48 | d.flatTrim(); 49 | d.startPing(); 50 | d.flatTrim(); 51 | 52 | // d.on('battery', function () { 53 | // console.log('Battery: ' + d.status.battery + '%'); 54 | // d.signalStrength(function (err, val) { 55 | // console.log('Signal: ' + val + 'dBm'); 56 | // }); 57 | 58 | // }); 59 | 60 | // d.on('stateChange', function () { 61 | // console.log(d.status.flying ? "-- flying" : "-- down"); 62 | // }) 63 | setTimeout(function () { 64 | console.log('Ready for Flight'); 65 | ACTIVE = true; 66 | }, 1000); 67 | }); 68 | }); 69 | 70 | 71 | var state = { 72 | tilt: 0, 73 | forward: 0, 74 | turn: 0, 75 | up: 0 76 | }; 77 | 78 | function variance(val) { 79 | return ((val >= -5 && val <= 5) ? 0 : val); 80 | } 81 | 82 | controller.on('1:release', function () { 83 | d.leftFlip(); 84 | }); 85 | 86 | controller.on('2:release', function () { 87 | d.backFlip(); 88 | }); 89 | 90 | controller.on('3:release', function () { 91 | d.rightFlip(); 92 | }); 93 | 94 | 95 | controller.on('4:release', function () { 96 | d.frontFlip(); 97 | }); 98 | 99 | 100 | controller.on('9:release', function () { 101 | d.flatTrim(); 102 | }); 103 | 104 | controller.on('10:release', function () { 105 | d.toggle(); // take off or land 106 | }); 107 | 108 | controller.on('left:move', function (data) { 109 | console.log('left'); 110 | state.up = variance(data.y); 111 | state.turn = variance(data.x); 112 | d.drive(state, -1); 113 | }); 114 | 115 | controller.on('right:move', function (data) { 116 | state.forward = variance(data.y); 117 | state.tilt = variance(data.x); 118 | d.drive(state, -1); 119 | }); 120 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.4.0 2 | * Unleash the swarm.... Swarm capability now added to the rolling spider library. (@voodootikigod) 3 | 4 | # 1.3.1 5 | * Fixes callback issue when callback not present. (@kevinold) 6 | 7 | # 1.3.0 8 | * Even more callback flushing for all events and protective execution. (Fix for #43) 9 | * Gamepad support for logitech dual action controller as an example. 10 | 11 | # 1.2.0 12 | * Callback style for all functional components (@garetht) 13 | * Readme fix 14 | * Verified functional for io 2.x+ and node 0.10.x, 0.12.x 15 | 16 | # 1.0.11 17 | * `stateChange` is now reliable. 18 | * Better instrumentation of disconnect handling. 19 | * Better use of state management. 20 | 21 | # 1.0.10 22 | * `stateChange` notification in the RollingSpider. 23 | 24 | # 1.0.10 25 | * Remove disconnect from emergency to allow re-takeoff after emergency 26 | 27 | # 1.0.4 - 1.0.8 28 | * Minor fixes while using in production. 29 | * Increased logging for bug trace down (@voodootikigod) 30 | * All callbacks protected (@voodootikigod) 31 | * Flush on emergency (@voodootikigod) 32 | * Multiple UUID selection capability (@voodootikigod) 33 | * Reconnect now works. (@voodootikigod) 34 | 35 | # 1.0.3 36 | * Allow waiting for settle (default) and forceConnect for assumed bluetooth settling (@voodootikigod) 37 | 38 | # 1.0.2 39 | * Proper display of battery and signal on the keyboard example. (@lynnaloo) 40 | 41 | # 1.0.1 42 | * Waits for adapter ready notification before attempting to scan for the drone. (@voodootikigod) 43 | 44 | # 1.0.0 45 | * Code Comments thanks to @christhebaron 46 | * Merger of @christhebaron fork. (@voodootikigod) 47 | * JSHint passes. 48 | 49 | # 0.3.1 50 | * Improvements to discover. (@sandeepmistry) 51 | * Added battery and signal strength to `eg/keyboard.js`. (@voodootikigod) 52 | * Move to use `eg` directory instead of `SamplesAndTools`. (@voodootikigod) 53 | 54 | # 0.3.0 55 | * Merged in code changes from DroneWorks team (@droneworks) 56 | * Refactored code base to use an inline and responsive ping with yaw, pitch, altitude, and roll commands. (@voodootikigod) 57 | * Refactored drive to use ping (@droneworks/@voodootikigod) 58 | 59 | # 0.1.2 60 | 61 | * Parity with the client API of the [node-ar-drone](https://github.com/felixge/node-ar-drone#client-api) where appropriate. @voodootikigod 62 | * Included `debug` library to output information about the system when needed. Requires further instrumentation, but its a start. @voodootikigod 63 | * Removed unneeded dependency on temporal for just the module (not used yet, will eventually). @voodootikigod 64 | 65 | # 0.1.1 66 | 67 | * Removed the need for utilizing discover prior to use by simply choosing the first peripheral with 'RS_' in the localname. @voodootikigod 68 | * Cleaned up directory structure and moved drone code into a lib directory. @voodootikigod 69 | * Converted sample code to use temporal over chained setTimeout. @voodootikigod 70 | * Added a keyboard control sample code. @voodootikigod 71 | -------------------------------------------------------------------------------- /eg/old.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var noble = require('noble'); 4 | var util = require('util'); 5 | 6 | var connectedDrone; 7 | var pingValue = 0; 8 | 9 | var Drone = function(peripheral, services, characteristics) { 10 | this.peripheral = peripheral; 11 | this.services = services; 12 | this.characteristics = characteristics; 13 | }; 14 | 15 | Drone.prototype.connect = function(cb) { 16 | console.log('connecting'); 17 | 18 | this.findCharacteristic('fb0f').notify(true); 19 | this.findCharacteristic('fb0e').notify(true); 20 | this.findCharacteristic('fb1b').notify(true); 21 | this.findCharacteristic('fb1c').notify(true); 22 | this.findCharacteristic('fd23').notify(true); 23 | this.findCharacteristic('fd53').notify(true); 24 | 25 | var drone = this; 26 | setTimeout(function() { 27 | drone.findCharacteristic('fa0b').write(new Buffer([0x04,0x01,0x00,0x04,0x01,0x00,0x32,0x30,0x31,0x34,0x2D,0x31,0x30,0x2D,0x32,0x38,0x00]), true, function(error) { 28 | console.log('connected'); 29 | if (error) { console.log('error connecting'); } 30 | 31 | // setInterval(function() { 32 | // console.log("Ping: " + pingValue); 33 | // drone.findCharacteristic("fa0a").write(new Buffer([0x02,pingValue,0x02,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]), true); 34 | // pingValue++; 35 | // }, 500); 36 | 37 | setTimeout(function() { 38 | cb(); 39 | }, 100); 40 | }); 41 | }, 100); 42 | }; 43 | 44 | Drone.prototype.takeOff = function() { 45 | console.log('Taking off... prepare for pain'); 46 | 47 | this.findCharacteristic('fa0b').write(new Buffer([0x02,0x05,0x02,0x00,0x01,0x00]), true); 48 | var self = this; 49 | setTimeout(function() { 50 | self.findCharacteristic('fa0a').write(new Buffer([0x02,0x01,0x02,0x00,0x02,0x00,0x01,0x00,0x00,0x32,0x00,0x00]), true); 51 | }, 2000); 52 | }; 53 | 54 | Drone.prototype.findCharacteristic = function(unique_uuid_segment) { 55 | var theChars = this.characteristics.filter(function(characteristic) { 56 | return characteristic.uuid.search(new RegExp(unique_uuid_segment)) != -1; 57 | }); 58 | 59 | return theChars[0]; 60 | }; 61 | 62 | if (process.env.UUID) { 63 | console.log('Looking for device with UUID: ' + process.env.UUID); 64 | 65 | noble.startScanning(); 66 | 67 | noble.on('discover', function(peripheral) { 68 | if (peripheral.uuid === process.env.UUID) { 69 | peripheral.connect(); 70 | peripheral.on('connect', function(error) { 71 | if (error) { return; } 72 | 73 | peripheral.discoverAllServicesAndCharacteristics(function(error, services, characteristics) { 74 | if (error) { return; } 75 | 76 | connectedDrone = new Drone(peripheral, services, characteristics); 77 | connectedDrone.connect(function(error) { 78 | if (error) { console.log('Problem connecting'); } 79 | 80 | connectedDrone.takeOff(); 81 | }); 82 | }); 83 | }); 84 | } 85 | }); 86 | } else { 87 | console.log('No UUID specified'); 88 | } 89 | -------------------------------------------------------------------------------- /eg/swarm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var keypress = require('keypress'); 4 | var Swarm = require('../').Swarm; 5 | 6 | var ACTIVE = true; 7 | var STEPS = 2; 8 | 9 | 10 | function cooldown() { 11 | ACTIVE = false; 12 | setTimeout(function () { 13 | ACTIVE = true; 14 | }, STEPS * 12); 15 | } 16 | 17 | // make `process.stdin` begin emitting 'keypress' events 18 | keypress(process.stdin); 19 | 20 | // listen for the 'keypress' event 21 | 22 | 23 | process.stdin.setRawMode(true); 24 | process.stdin.resume(); 25 | 26 | 27 | 28 | 29 | var swarm = new Swarm({timeout: 10}); 30 | 31 | swarm.assemble(); 32 | 33 | swarm.on('assembled', function () { 34 | ACTIVE = true; 35 | }); 36 | 37 | 38 | 39 | 40 | process.stdin.on('keypress', function (ch, key) { 41 | if (ACTIVE && key) { 42 | if (key.name === 'm') { 43 | swarm.emergency(); 44 | setTimeout(function () { 45 | process.exit(); 46 | }, 3000); 47 | } else if (key.name === 't') { 48 | console.log('swarm#takeoff'); 49 | swarm.takeOff(); 50 | } else if (key.name === 'w') { 51 | console.log('swarm#forward'); 52 | swarm.forward({ steps: STEPS }); 53 | cooldown(); 54 | } else if (key.name === 's') { 55 | console.log('swarm#backward'); 56 | swarm.backward({ steps: STEPS }); 57 | cooldown(); 58 | } else if (key.name === 'left') { 59 | console.log('swarm#turnleft'); 60 | swarm.turnLeft({ steps: STEPS }); 61 | cooldown(); 62 | } else if (key.name === 'a') { 63 | console.log('swarm#tiltleft'); 64 | swarm.tiltLeft({ steps: STEPS }); 65 | cooldown(); 66 | } else if (key.name === 'd') { 67 | console.log('swarm#tiltright'); 68 | swarm.tiltRight({ steps: STEPS }); 69 | cooldown(); 70 | } else if (key.name === 'right') { 71 | console.log('swarm#turnright'); 72 | swarm.turnRight({ steps: STEPS }); 73 | cooldown(); 74 | } else if (key.name === 'up') { 75 | console.log('swarm#up'); 76 | swarm.up({ steps: STEPS * 2.5 }); 77 | cooldown(); 78 | } else if (key.name === 'down') { 79 | console.log('swarm#down'); 80 | swarm.down({ steps: STEPS * 2.5 }); 81 | cooldown(); 82 | } else if (key.name === 'i' || key.name === 'f') { 83 | swarm.frontFlip({ steps: STEPS }); 84 | cooldown(); 85 | } else if (key.name === 'j') { 86 | swarm.leftFlip({ steps: STEPS }); 87 | cooldown(); 88 | } else if (key.name === 'l') { 89 | swarm.rightFlip({ steps: STEPS }); 90 | cooldown(); 91 | } else if (key.name === 'k') { 92 | swarm.backFlip({ steps: STEPS }); 93 | cooldown(); 94 | } else if (key.name === 'q') { 95 | console.log('Initiated Landing Sequence...'); 96 | swarm.land(function () { 97 | console.log('land'); 98 | swarm.release( function () { 99 | console.log('release'); 100 | }); 101 | }); 102 | 103 | // setTimeout(function () { 104 | // process.exit(); 105 | // }, 3000); 106 | } 107 | } 108 | if (key && key.ctrl && key.name === 'c') { 109 | process.stdin.pause(); 110 | process.exit(); 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /eg/keyboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var keypress = require('keypress'); 4 | var Drone = require('../'); 5 | 6 | var ACTIVE = true; 7 | var STEPS = 2; 8 | 9 | 10 | function cooldown() { 11 | ACTIVE = false; 12 | setTimeout(function () { 13 | ACTIVE = true; 14 | }, STEPS * 12); 15 | } 16 | 17 | // make `process.stdin` begin emitting 'keypress' events 18 | keypress(process.stdin); 19 | 20 | // listen for the 'keypress' event 21 | 22 | 23 | process.stdin.setRawMode(true); 24 | process.stdin.resume(); 25 | 26 | if (process.env.UUID) { 27 | console.log('Searching for ', process.env.UUID); 28 | } 29 | 30 | var d = new Drone(); 31 | 32 | d.connect(function () { 33 | d.setup(function () { 34 | console.log('Configured for Rolling Spider! ', d.name); 35 | d.flatTrim(); 36 | d.startPing(); 37 | d.flatTrim(); 38 | 39 | // d.on('battery', function () { 40 | // console.log('Battery: ' + d.status.battery + '%'); 41 | // d.signalStrength(function (err, val) { 42 | // console.log('Signal: ' + val + 'dBm'); 43 | // }); 44 | 45 | // }); 46 | 47 | // d.on('stateChange', function () { 48 | // console.log(d.status.flying ? "-- flying" : "-- down"); 49 | // }) 50 | setTimeout(function () { 51 | console.log('ready for flight'); 52 | ACTIVE = true; 53 | }, 1000); 54 | 55 | }); 56 | }); 57 | 58 | process.stdin.on('keypress', function (ch, key) { 59 | if (ACTIVE && key) { 60 | if (key.name === 'm') { 61 | d.emergency(); 62 | setTimeout(function () { 63 | process.exit(); 64 | }, 3000); 65 | } else if (key.name === 't') { 66 | console.log('takeoff'); 67 | d.takeOff(); 68 | 69 | } else if (key.name === 'w') { 70 | d.forward({ steps: STEPS }); 71 | cooldown(); 72 | } else if (key.name === 's') { 73 | d.backward({ steps: STEPS }); 74 | cooldown(); 75 | } else if (key.name === 'left') { 76 | d.turnLeft({ steps: STEPS }); 77 | cooldown(); 78 | } else if (key.name === 'a') { 79 | d.tiltLeft({ steps: STEPS }); 80 | cooldown(); 81 | } else if (key.name === 'd') { 82 | d.tiltRight({ steps: STEPS }); 83 | cooldown(); 84 | } else if (key.name === 'right') { 85 | d.turnRight({ steps: STEPS }); 86 | cooldown(); 87 | } else if (key.name === 'up') { 88 | d.up({ steps: STEPS * 2.5 }); 89 | cooldown(); 90 | } else if (key.name === 'down') { 91 | d.down({ steps: STEPS * 2.5 }); 92 | cooldown(); 93 | } else if (key.name === 'i' || key.name === 'f') { 94 | d.frontFlip({ steps: STEPS }); 95 | cooldown(); 96 | } else if (key.name === 'j') { 97 | d.leftFlip({ steps: STEPS }); 98 | cooldown(); 99 | } else if (key.name === 'l') { 100 | d.rightFlip({ steps: STEPS }); 101 | cooldown(); 102 | } else if (key.name === 'k') { 103 | d.backFlip({ steps: STEPS }); 104 | cooldown(); 105 | } else if (key.name === 'q') { 106 | console.log('Initiated Landing Sequence...'); 107 | d.land(); 108 | // setTimeout(function () { 109 | // process.exit(); 110 | // }, 3000); 111 | } 112 | } 113 | if (key && key.ctrl && key.name === 'c') { 114 | process.stdin.pause(); 115 | process.exit(); 116 | } 117 | }); 118 | 119 | 120 | 121 | 122 | //launch(); 123 | -------------------------------------------------------------------------------- /lib/swarm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var noble = require('noble'); 3 | var Drone = require('./drone'); 4 | var debug = require('debug')('rollingspider'); 5 | var EventEmitter = require('events').EventEmitter; 6 | var util = require('util'); 7 | var _ = require('lodash'); 8 | 9 | 10 | /** 11 | * Constructs a new RollingSpider Swarm 12 | * 13 | * @param {Object} options to construct the drone with: 14 | * - {String} A comma seperated list (as a string) of UUIDs or names to connect to. This could also be an array of the same items. If this is omitted then it will add any device with the manufacturer data value for Parrot.. 15 | * - logger function to call if/when errors occur. If omitted then uses console#log 16 | * @constructor 17 | */ 18 | var Swarm = function(options) { 19 | this.ble = noble; 20 | 21 | var membership = (typeof options === 'string' ? options : undefined); 22 | options = options || {}; 23 | 24 | this.targets = membership || options.membership; 25 | 26 | this.peripherals = []; 27 | this.members = []; 28 | this.timeout = (options.timeout || 30) * 1000; // in seconds 29 | 30 | //define membership 31 | if (this.targets && !util.isArray(this.targets)) { 32 | this.targets = this.targets.split(','); 33 | } else { 34 | this.targets = []; 35 | } 36 | 37 | this.logger = options.logger || debug; //use debug instead of console.log 38 | this.discovering = false; 39 | 40 | this.active = false; 41 | 42 | // handle disconnect gracefully 43 | this.ble.on('warning', function(message) { 44 | this.onDisconnect(); 45 | }.bind(this)); 46 | 47 | return this; 48 | }; 49 | 50 | util.inherits(Swarm, EventEmitter); 51 | 52 | 53 | Swarm.prototype.at = function (id, callback) { 54 | this.logger('RollingSpider.Swarm#at'); 55 | var found = null; 56 | this.members.forEach(function (member) { 57 | if (member.name === id) { 58 | found = member; 59 | } 60 | }); 61 | if (typeof callback === 'function') { 62 | callback(found); 63 | } else { 64 | return found; 65 | } 66 | }; 67 | 68 | Swarm.prototype.isMember = function(peripheral) { 69 | this.logger('RollingSpider.Swarm#isMember'); 70 | if (!peripheral) { 71 | return false; 72 | } 73 | 74 | var localName = peripheral.advertisement.localName; 75 | var manufacturer = peripheral.advertisement.manufacturerData; 76 | if (this.targets.length === 0) { 77 | // handle "any" case 78 | var localNameMatch = localName 79 | && (localName.indexOf('RS_') === 0 || localName.indexOf('Mars_') === 0 || localName.indexOf('Travis_') === 0 || localName.indexOf('Maclan_')=== 0 || localName.indexOf('NewZ_')=== 0); 80 | var manufacturerMatch = manufacturer 81 | && (['4300cf1900090100', '4300cf1909090100', '4300cf1907090100', '4300cf190a090100'].indexOf(manufacturer) >= 0); 82 | 83 | // Is true for EITHER an "RS_" name OR manufacturer code. 84 | return localNameMatch || manufacturerMatch; 85 | } else { 86 | // console.log(this.targets, localName); 87 | // console.log(this.targets, peripheral.uuid); 88 | // in target list 89 | return (this.targets.indexOf(localName) >= 0 || this.targets.indexOf(peripheral.uuid) >= 0); 90 | } 91 | 92 | }; 93 | 94 | Swarm.prototype.closeMembership = function(callback) { 95 | this.logger('RollingSpider.Swarm#closeMembership'); 96 | this.ble.stopScanning(); 97 | this.discovering = false; 98 | this.active = true; 99 | if (callback) { 100 | callback(); 101 | } 102 | }; 103 | 104 | Swarm.prototype.assemble = function(callback) { 105 | this.logger('RollingSpider.Swarm#assemble'); 106 | 107 | this.once('assembled', function () { 108 | //when assembled clean up 109 | if (this.TIMEOUT_HANDLER) { 110 | clearTimeout(this.TIMEOUT_HANDLER); 111 | } 112 | this.closeMembership(); 113 | }); 114 | 115 | 116 | if (this.targets) { 117 | this.logger('RollingSpider Swarm Assemble: ' + this.targets.join(', ')); 118 | } 119 | 120 | var incr = 0; 121 | var onSetup = function () { 122 | incr++; 123 | this.logger(incr+'/'+ this.targets.length); 124 | if (this.targets.length > 0 && incr === this.targets.length) { 125 | this.emit('assembled'); 126 | } 127 | }.bind(this); 128 | 129 | this.ble.on('discover', function(peripheral) { 130 | this.logger('RollingSpider.Swarm#assemble.on(discover)'); 131 | 132 | 133 | // Is this peripheral a Parrot Rolling Spider? 134 | var isSwarmMember = this.isMember(peripheral); 135 | 136 | 137 | this.logger(peripheral.advertisement.localName + (isSwarmMember ? ' is a member' : ' is not a member')); 138 | if (isSwarmMember) { 139 | 140 | 141 | var swarmMember = new Drone(); 142 | swarmMember.ble = this.ble; // share the same noble instance 143 | 144 | swarmMember.connectPeripheral(peripheral, function() { 145 | this.logger(peripheral.advertisement.localName + ' is connected'); 146 | swarmMember.setup(function() { 147 | this.logger(peripheral.advertisement.localName + ' is setup'); 148 | this.members.push(swarmMember); 149 | swarmMember.flatTrim(); 150 | swarmMember.startPing(); 151 | onSetup(); 152 | }.bind(this)); 153 | }.bind(this)); 154 | } 155 | }.bind(this)); 156 | 157 | this.TIMEOUT_HANDLER = setTimeout(function () { 158 | this.logger('Swarm#assemble.timeout'); 159 | this.emit('assembled'); 160 | }.bind(this), this.timeout); // timeout after 30s 161 | 162 | if (this.forceConnect || this.ble.state === 'poweredOn') { 163 | this.logger('RollingSpider.Swarm.forceConnect'); 164 | this.discovering = true; 165 | this.ble.startScanning(); 166 | } else { 167 | this.logger('RollingSpider.on(stateChange)'); 168 | this.ble.on('stateChange', function(state) { 169 | if (state === 'poweredOn') { 170 | this.logger('RollingSpider#poweredOn'); 171 | this.discovering = true; 172 | this.ble.startScanning(); 173 | if (typeof callback === 'function') { 174 | callback(); 175 | } 176 | } else { 177 | this.logger('stateChange == ' + state); 178 | this.ble.stopScanning(); 179 | if (typeof callback === 'function') { 180 | callback(new Error('Error with Bluetooth Adapter, please retry')); 181 | } 182 | } 183 | }.bind(this)); 184 | } 185 | 186 | }; 187 | 188 | function broadcast (fn) { 189 | 190 | return function (opts, callback) { 191 | this.logger('RollingSpider.Swarm#broadcast-'+fn); 192 | if (typeof opts === 'function') { 193 | callback = opts; 194 | opts = {}; 195 | } 196 | var max = this.members.length, count = 0; 197 | _.forEach(this.members, function (drone) { 198 | try { 199 | drone[fn](opts || {}, function () { 200 | count++; 201 | this.logger(fn+': '+count+'/'+max); 202 | if (count === max && callback) { 203 | callback(); 204 | } 205 | }.bind(this)); 206 | } catch (e) { 207 | // handle quietly 208 | } 209 | }.bind(this)); 210 | }; 211 | } 212 | 213 | var takeOff = broadcast('takeOff'); 214 | var land = broadcast('land'); 215 | 216 | Swarm.prototype.release = function (callback) { 217 | var max = this.members.length, count = 0; 218 | _.forEach(this.members, function (drone) { 219 | drone.disconnect(function () { 220 | count++; 221 | if (count === max && callback) { 222 | this.members = []; 223 | callback(); 224 | } 225 | }); 226 | }); 227 | 228 | if (max === 0 && callback) { 229 | callback(); 230 | } 231 | }; 232 | 233 | var cutOff = broadcast('emergency'); 234 | var flatTrim = broadcast('flatTrim'); 235 | 236 | // provide options for use case 237 | Swarm.prototype.takeoff = takeOff; 238 | Swarm.prototype.takeOff = takeOff; 239 | Swarm.prototype.wheelOff = broadcast('wheelOff'); 240 | Swarm.prototype.wheelOn = broadcast('wheelOn'); 241 | Swarm.prototype.land = land; 242 | Swarm.prototype.toggle = broadcast('toggle'); 243 | Swarm.prototype.emergency = cutOff; 244 | Swarm.prototype.emergancy = cutOff; 245 | Swarm.prototype.flatTrim = flatTrim; 246 | Swarm.prototype.calibrate = flatTrim; 247 | Swarm.prototype.up = broadcast('up'); 248 | Swarm.prototype.down = broadcast('down'); 249 | // animation 250 | Swarm.prototype.frontFlip = broadcast('frontFlip'); 251 | Swarm.prototype.backFlip = broadcast('backFlip'); 252 | Swarm.prototype.rightFlip = broadcast('rightFlip'); 253 | Swarm.prototype.leftFlip = broadcast('leftFlip'); 254 | 255 | // rotational 256 | Swarm.prototype.turnRight = broadcast('turnRight'); 257 | Swarm.prototype.clockwise = broadcast('turnRight'); 258 | Swarm.prototype.turnLeft = broadcast('turnLeft'); 259 | Swarm.prototype.counterClockwise = broadcast('turnLeft'); 260 | 261 | // directional 262 | Swarm.prototype.forward = broadcast('forward'); 263 | Swarm.prototype.backward = broadcast('backward'); 264 | Swarm.prototype.tiltRight = broadcast('tiltRight'); 265 | Swarm.prototype.tiltLeft = broadcast('tiltLeft'); 266 | Swarm.prototype.right = broadcast('tiltRight'); 267 | Swarm.prototype.left = broadcast('tiltLeft'); 268 | 269 | Swarm.prototype.hover = broadcast('hover'); 270 | 271 | 272 | 273 | 274 | 275 | Swarm.prototype.onDisconnect = function() { 276 | // end of swarm 277 | }; 278 | 279 | 280 | module.exports = Swarm; 281 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rolling Spider for Node.js 2 | 3 | An implementation of the networking protocols (Bluetooth LE) used by the 4 | Parrot MiniDrone - [Rolling Spider](http://www.parrot.com/usa/products/rolling-spider/) and [Airborne Night Drone - MACLANE](http://www.parrot.com/usa/products/airborne-night-drone/). This offers an off-the-shelf $99 USD drone that can be controlled by JS -- yay! 5 | 6 | Prerequisites: 7 | 8 | * See [noble prerequisites](https://github.com/sandeepmistry/noble#prerequisites) for your platform 9 | 10 | To install: 11 | 12 | ```bash 13 | npm install rolling-spider 14 | ``` 15 | 16 | ## Status 17 | 18 | Stable! 19 | 20 | 21 | ## Getting Started 22 | 23 | There are a few steps you should take when getting started with this. We're going to learn how to get there by building out a simple script that will take off, move forward a little, then land. 24 | 25 | 26 | ### Connecting 27 | 28 | To connect you need to create a new `Drone` instance. 29 | 30 | ```javascript 31 | var RollingSpider = require("rolling-spider"); 32 | var rollingSpider = new RollingSpider(); 33 | ``` 34 | 35 | After you've created an instance you now have access to all the functionality of the drone, but there is some stuff you need to do first, namely connecting, running the setup, and starting the ping to keep it connected. 36 | 37 | ```javascript 38 | var RollingSpider = require("rolling-spider"); 39 | var rollingSpider = new RollingSpider(); 40 | 41 | // NEW CODE BELOW HERE 42 | 43 | rollingSpider.connect(function() { 44 | rollingSpider.setup(function() { 45 | rollingSpider.flatTrim(); 46 | rollingSpider.startPing(); 47 | rollingSpider.flatTrim(); 48 | console.log('Connected to drone', rollingSpider.name); 49 | }); 50 | }); 51 | ``` 52 | ### Taking off, moving, and landing 53 | 54 | We're now going to create a function that takes a drone and then by using a sequence of `temporal` tasks creates a timed sequence of calls to actions on the drone. 55 | 56 | We recommend using `temporal` over a series of `setTimeout` chained calls for your sanity. Please abide by this when playing with the drone and ESPECIALLY if filing a ticket. 57 | 58 | ```javascript 59 | 'use strict'; 60 | 61 | var RollingSpider = require('rolling-spider'); 62 | var temporal = require('temporal'); 63 | var rollingSpider = new RollingSpider(); 64 | 65 | rollingSpider.connect(function() { 66 | rollingSpider.setup(function() { 67 | rollingSpider.flatTrim(); 68 | rollingSpider.startPing(); 69 | rollingSpider.flatTrim(); 70 | 71 | temporal.queue([ 72 | { 73 | delay: 5000, 74 | task: function () { 75 | rollingSpider.takeOff(); 76 | rollingSpider.flatTrim(); 77 | } 78 | }, 79 | { 80 | delay: 3000, 81 | task: function () { 82 | rollingSpider.forward({steps: 12}); 83 | } 84 | }, 85 | { 86 | delay: 5000, 87 | task: function () { 88 | rollingSpider.land(); 89 | } 90 | }, 91 | { 92 | delay: 5000, 93 | task: function () { 94 | temporal.clear(); 95 | process.exit(0); 96 | } 97 | } 98 | ]); 99 | }); 100 | }); 101 | 102 | ``` 103 | 104 | ### Done! 105 | 106 | And there you have it, you can now control your drone. 107 | 108 | ### Flying Multiple MiniDrones 109 | 110 | [![Spider Swarm](http://img.youtube.com/vi/PLWJMR61Qs0/0.jpg)](http://www.youtube.com/watch?v=PLWJMR61Qs0) 111 | 112 | Previous versions of the `rolling-spider` library required you to specify the UUID for your drone through a discover process. This has been removed in favor of just using the first BLE device that broadcasts with "RS_" as its localname. ***If you are flying multiple minidrones or in a very populated BLE area***, you will want to use the discovery process in order to identify specifically the drone(s) you want to control. Use the [Discovery Tool](https://github.com/voodootikigod/node-rolling-spider/blob/master/eg/discover.js) to get the UUID of all nearby BLE devices. 113 | 114 | If you want to fly multiple drones at once, please use the Swarm API for that. An example of the swarm, as well as other examples, is available in the `eg/` directory. [Source Code Sample](https://github.com/voodootikigod/node-rolling-spider/blob/master/eg/swarm.js) 115 | 116 | ### Client API 117 | 118 | #### RollingSpider.createClient([options]) __or__ new RollingSpider([options]) 119 | 120 | Options 121 | 122 | > * `uuid`: The uuid (Bluetooth UUID) or the Published Name (something like RS_XXXXXX) of the drone. Defaults to finding first announced. Can be a list of uuids that are separated by a comma (in the case of a string) or as an array of strings. 123 | > * `logger`: The logging engine to utilize. Defaults to `debug`, but you could provide `console.log` or other similar logging system that can accept strings and output them. 124 | > * `forceConnect`: When set to true, this will not wait for the bluetooth module to settle. This is necessary for some known use cases. 125 | 126 | #### client.connect([callback]) 127 | 128 | Connects to the drone over BLE. `callback` is invoked when it is connected or receives an `error` if there is a problem establishing the connection. 129 | 130 | #### client.setup([callback]) 131 | 132 | Sets up the connection to the drone and enumerate all of the services and characteristics. `callback` is invoked when setup completes or receives an `error` if there is a problem setting up the connection. 133 | 134 | #### client.on('battery', callback) 135 | 136 | Event that is emitted on battery change activity. Caution, battery drains pretty fast on this so this may create a high velocity of events. 137 | 138 | #### client.takeoff(callback) __or__ client.takeOff(callback) 139 | 140 | Sets the internal `fly` state to `true`, `callback` is invoked after the drone 141 | reports that it is hovering. 142 | 143 | #### client.land(callback) 144 | 145 | Sets the internal `fly` state to `false`, `callback` is invoked after the drone 146 | reports it has landed. 147 | 148 | #### client.up([options], [callback]) / client.down([options], [callback]) 149 | 150 | Options 151 | 152 | > * `speed` at which the drive should occur, a number from 0 to 100 inclusive. 153 | > * `steps` the length of steps (time) the drive should happen, a number from 0 to 100 inclusive. 154 | 155 | Makes the drone gain or reduce altitude. `callback` is invoked after all the steps are completed. 156 | 157 | #### client.clockwise([options], [callback]) / client.counterClockwise([options], [callback]) __or__ client.turnRight([options], [callback]) / client.turnLeft([options], [callback]) 158 | 159 | Options 160 | 161 | > * `speed` at which the rotation should occur 162 | > * `steps` the length of steps (time) the turning should happen, a number from 0 to 100 inclusive. 163 | 164 | Causes the drone to spin. `callback` is invoked after all the steps are completed. 165 | 166 | #### client.forward([options], [callback]) / client.backward([options], [callback]) 167 | 168 | > * `speed` at which the drive should occur, a number from 0 to 100 inclusive. 169 | > * `steps` the length of steps (time) the drive should happen, a number from 0 to 100 inclusive. 170 | 171 | Controls the pitch. `callback` is invoked after all the steps are completed. 172 | 173 | #### client.left([options], [callback]) / client.right([options], [callback]) __or__ client.tiltLeft([options], [callback]) / client.tiltRight([options], [callback]) 174 | 175 | > * `speed` at which the drive should occur, a number from 0 to 100 inclusive. 176 | > * `steps` the length of steps (time) the drive should happen, a number from 0 to 100 inclusive. 177 | 178 | Controls the roll, which is a horizontal movement. `callback` is invoked after all the steps are completed. 179 | 180 | #### client.frontFlip([callback]) 181 | 182 | Causes the drone to do an amazing front flip. 183 | 184 | #### client.backFlip([callback]) 185 | 186 | Causes the drone to do an amazing back flip. 187 | 188 | #### client.leftFlip([callback]) 189 | 190 | Causes the drone to do an amazing left flip. **DO NOT USE WITH WHEELS ON!!!** 191 | 192 | #### client.rightFlip([callback]) 193 | 194 | Causes the drone to do an amazing right flip. **DO NOT USE WITH WHEELS ON!!!** 195 | 196 | #### client.takePicture([callback]) 197 | 198 | Causes the drone to take a picture with bottom camera. 199 | 200 | #### client.calibrate([callback]) __or__ client.flatTrim([callback]) 201 | 202 | Resets the trim so that your drone's flight is stable. It should always be 203 | called before taking off. 204 | 205 | #### client.signalStrength(callback) 206 | 207 | Obtains the signal strength as an RSSI value returned as the second parameter of the callback. 208 | 209 | #### client.disconnect([callback]) 210 | 211 | Disconnects from the drone if it is connected. 212 | 213 | #### client.emergancy([callback]) __or__ client.emergency([callback]) 214 | 215 | Causes the drone to shut off the motors "instantly" (sometimes has to wait for other commands ahead of it to complete... not fully safe yet) 216 | 217 | ### Swarm API 218 | 219 | If you have more than one (or ten) Rolling Spiders, you will eventually want to control them all as a single, somewhat unified swarm. This became such a common request, we made it part of the API for the RollingSpider. This will allow you to initialize a set of members, defined or otherwise, and broadcast commands to them all at once. 220 | 221 | Common implementation boilerplate 222 | 223 | ```javascript 224 | var Swarm = require('rolling-spider').Swarm; 225 | var swarm = new Swarm({timeout: 10}); 226 | 227 | swarm.assemble(); 228 | 229 | swarm.on('assembled', function () { 230 | // For The Swarm!!!!! 231 | }); 232 | ``` 233 | 234 | #### new Swarm(options) 235 | 236 | 237 | Options (anything additional is passed on to individual members upon initialization) 238 | 239 | > * `membership`: The uuid(s) or the Published Name(s) of the drone that are members of the swarm. If left empty/undefined, it will find any and all Rolling Spiders it can possibly find. 240 | > * `timeout`: The number of seconds before closing the membership forcibly. Use this to ensure that membership enrollment doesn't last forever. 241 | > * `forceConnect`: When set to true, this will not wait for the bluetooth module to settle. This is necessary for some known use cases. 242 | 243 | #### swarm.assemble([callback]) 244 | 245 | Initiates the swarm collection process. This will attempt to seek out bluetooth RollingSpiders that have been identified in the membership or isMember validation components and enrolls them into the swarm. 246 | 247 | #### swarm.closeMembership([callback]) 248 | 249 | Stops the open membership process and sets the swarm to active. 250 | 251 | #### swarm.at(id, [callback]) 252 | 253 | Returns (or executes provided callback) with the swarm member that has the provided `id` value for localName or UUID. Use this to issue commands to specific drones in the swarm. 254 | 255 | #### swarm.isMember(device) 256 | 257 | Returns true if the provide device should be admitted as a member of the swarm or false if it should be ignored. Can be overridden for more complex membership definition. 258 | 259 | 260 | #### swarm.release([callback]) 261 | 262 | Releases all of the drones from the swarm. 263 | 264 | 265 | #### Broadcasted Commands e.g. swarm.takeOff([options], [callback]) 266 | 267 | All other commands for the swarm follow the command structure of an individual RollingSpider and it is broadcast to all roughly at the same time (bluetooth isn't always exact.) The signature is the same for all of the commands and passes options and a callback to the function. 268 | -------------------------------------------------------------------------------- /lib/drone.js: -------------------------------------------------------------------------------- 1 | /* global Buffer */ 2 | 3 | 'use strict'; 4 | 5 | 6 | var noble = require('noble'); 7 | var debug = require('debug')('rollingspider'); 8 | var EventEmitter = require('events').EventEmitter; 9 | var util = require('util'); 10 | var _ = require('lodash'); 11 | 12 | 13 | 14 | /** 15 | * Constructs a new RollingSpider 16 | * 17 | * @param {Object} options to construct the drone with: 18 | * - {String} uuid to connect to. If this is omitted then it will connect to the first device starting with 'RS_' as the local name. 19 | * - logger function to call if/when errors occur. If omitted then uses console#log 20 | * @constructor 21 | */ 22 | var Drone = function(options) { 23 | EventEmitter.call(this); 24 | 25 | var uuid = (typeof options === 'string' ? options : undefined); 26 | options = options || {}; 27 | 28 | this.uuid = null; 29 | this.targets = uuid || options.uuid; 30 | 31 | if (this.targets && !util.isArray(this.targets)) { 32 | this.targets = this.targets.split(','); 33 | } 34 | 35 | this.logger = options.logger || debug; //use debug instead of console.log 36 | this.forceConnect = options.forceConnect || false; 37 | this.connected = false; 38 | this.discovered = false; 39 | this.ble = noble; 40 | this.peripheral = null; 41 | this.takenOff = false; 42 | 43 | this.driveStepsRemaining = 0; 44 | this.speeds = { 45 | yaw: 0, // turn 46 | pitch: 0, // forward/backward 47 | roll: 0, // left/right 48 | altitude: 0 // up/down 49 | }; 50 | 51 | /** 52 | * Used to store the 'counter' that's sent to each characteristic 53 | */ 54 | this.steps = { 55 | 'fa0a': 0, 56 | 'fa0b': 0, 57 | 'fa0c': 0 58 | }; 59 | 60 | this.status = { 61 | stateValue: 0, 62 | flying: false, 63 | battery: 100 64 | }; 65 | 66 | 67 | // handle disconnect gracefully 68 | this.ble.on('warning', function(message) { 69 | this.onDisconnect(); 70 | }.bind(this)); 71 | }; 72 | 73 | 74 | util.inherits(Drone, EventEmitter); 75 | 76 | /** 77 | * Drone.isDronePeripheral 78 | * 79 | * Accepts a BLE peripheral object record and returns true|false 80 | * if that record represents a Rolling Spider Drone or not. 81 | * 82 | * @param {Object} peripheral A BLE peripheral record 83 | * @return {Boolean} 84 | */ 85 | Drone.isDronePeripheral = function(peripheral) { 86 | if (!peripheral) { 87 | return false; 88 | } 89 | 90 | var localName = peripheral.advertisement.localName; 91 | var manufacturer = peripheral.advertisement.manufacturerData; 92 | 93 | var acceptedNames = [ 94 | 'RS_', 95 | 'Mars_', 96 | 'Travis_', 97 | 'Maclan_', 98 | 'Mambo_', 99 | 'Blaze_', 100 | 'Swat_', 101 | 'NewZ_' 102 | ]; 103 | var acceptedManufacturers = [ 104 | '4300cf1900090100', 105 | '4300cf1909090100', 106 | '4300cf1907090100', 107 | '4300cf190a090100' 108 | ]; 109 | 110 | var localNameMatch = localName 111 | && (acceptedNames.findIndex(function(name) { return localName.startsWith(name); }) >= 0); 112 | 113 | var manufacturerMatch = manufacturer 114 | && (acceptedManufacturers.indexOf(manufacturer.toString('hex')) >= 0); 115 | 116 | // Is true for EITHER a valid name prefix OR manufacturer code. 117 | return localNameMatch || manufacturerMatch; 118 | }; 119 | 120 | 121 | // create client helper function to match ar-drone 122 | Drone.createClient = function(options) { 123 | return new Drone(options); 124 | }; 125 | 126 | /** 127 | * Connects to the drone over BLE 128 | * 129 | * @param callback to be called once connected 130 | * @todo Make the callback be called with an error if encountered 131 | */ 132 | Drone.prototype.connect = function(callback) { 133 | this.logger('RollingSpider#connect'); 134 | if (this.targets) { 135 | this.logger('RollingSpider finding: ' + this.targets.join(', ')); 136 | } 137 | 138 | this.ble.on('discover', function(peripheral) { 139 | this.logger('RollingSpider.on(discover)'); 140 | this.logger(peripheral); 141 | 142 | var isFound = false; 143 | var connectedRun = false; 144 | var matchType = 'Fuzzy'; 145 | 146 | // Peripheral specific 147 | var localName = peripheral.advertisement.localName; 148 | var uuid = peripheral.uuid; 149 | 150 | // Is this peripheral a Parrot Rolling Spider? 151 | var isDrone = Drone.isDronePeripheral(peripheral); 152 | 153 | var onConnected = function(error) { 154 | if (connectedRun) { 155 | return; 156 | } else { 157 | connectedRun = true; 158 | } 159 | if (error) { 160 | if (typeof callback === 'function') { 161 | callback(error); 162 | } 163 | } else { 164 | this.logger('Connected to: ' + localName); 165 | this.ble.stopScanning(); 166 | this.connected = true; 167 | this.setup(callback); 168 | } 169 | }.bind(this); 170 | 171 | this.logger(localName); 172 | 173 | if (this.targets) { 174 | this.logger(this.targets.indexOf(uuid)); 175 | this.logger(this.targets.indexOf(localName)); 176 | } 177 | 178 | if (!this.discovered) { 179 | 180 | if (this.targets && 181 | (this.targets.indexOf(uuid) >= 0 || this.targets.indexOf(localName) >= 0)) { 182 | matchType = 'Exact'; 183 | isFound = true; 184 | } else if ((typeof this.targets === 'undefined' || this.targets.length === 0) && isDrone) { 185 | isFound = true; 186 | } 187 | 188 | if (isFound) { 189 | this.logger(matchType + ' match found: ' + localName + ' <' + uuid + '>'); 190 | this.connectPeripheral(peripheral, onConnected); 191 | } 192 | } 193 | }.bind(this)); 194 | 195 | if (this.forceConnect || this.ble.state === 'poweredOn') { 196 | this.logger('RollingSpider.forceConnect'); 197 | this.ble.startScanning(); 198 | } else { 199 | this.logger('RollingSpider.on(stateChange)'); 200 | this.ble.on('stateChange', function(state) { 201 | if (state === 'poweredOn') { 202 | this.logger('RollingSpider#poweredOn'); 203 | this.ble.startScanning(); 204 | } else { 205 | this.logger('stateChange == ' + state); 206 | this.ble.stopScanning(); 207 | if (typeof callback === 'function') { 208 | callback(new Error('Error with Bluetooth Adapter, please retry')); 209 | } 210 | } 211 | }.bind(this)); 212 | } 213 | }; 214 | 215 | 216 | Drone.prototype.connectPeripheral = function(peripheral, onConnected) { 217 | this.discovered = true; 218 | this.uuid = peripheral.uuid; 219 | this.name = peripheral.advertisement.localName; 220 | this.peripheral = peripheral; 221 | this.ble.stopScanning(); 222 | this.peripheral.connect(onConnected); 223 | this.peripheral.on('disconnect', function() { 224 | this.onDisconnect(); 225 | }.bind(this)); 226 | }; 227 | 228 | /** 229 | * Sets up the connection to the drone and enumerate all of the services and characteristics. 230 | * 231 | * 232 | * @param callback to be called once set up 233 | * @private 234 | */ 235 | Drone.prototype.setup = function(callback) { 236 | this.logger('RollingSpider#setup'); 237 | this.peripheral.discoverAllServicesAndCharacteristics(function(error, services, characteristics) { 238 | if (error) { 239 | if (typeof callback === 'function') { 240 | callback(error); 241 | } 242 | } else { 243 | this.services = services; 244 | this.characteristics = characteristics; 245 | 246 | this.handshake(callback); 247 | } 248 | }.bind(this)); 249 | }; 250 | 251 | /** 252 | * Performs necessary handshake to initiate communication with the device. Also configures all notification handlers. 253 | * 254 | * 255 | * @param callback to be called once set up 256 | * @private 257 | */ 258 | Drone.prototype.handshake = function(callback) { 259 | this.logger('RollingSpider#handshake'); 260 | ['fb0f', 'fb0e', 'fb1b', 'fb1c', 'fd22', 'fd23', 'fd24', 'fd52', 'fd53', 'fd54'].forEach(function(key) { 261 | var characteristic = this.getCharacteristic(key); 262 | characteristic.notify(true); 263 | }.bind(this)); 264 | 265 | // Register listener for battery notifications. 266 | this.getCharacteristic('fb0f').on('data', function(data, isNotification) { 267 | if (!isNotification) { 268 | return; 269 | } 270 | this.status.battery = data[data.length - 1]; 271 | this.emit('battery'); 272 | this.logger('Battery level: ' + this.status.battery + '%'); 273 | }.bind(this)); 274 | 275 | /** 276 | * Flying statuses: 277 | * 278 | * 0: Landed 279 | * 1: Taking off 280 | * 2: Hovering 281 | * 3: ?? 282 | * 4: Landing 283 | * 5: Emergency / Cut out 284 | */ 285 | this.getCharacteristic('fb0e').on('data', function(data, isNotification) { 286 | if (!isNotification) { 287 | return; 288 | } 289 | if (data[2] !== 2) { 290 | return; 291 | } 292 | 293 | 294 | var prevState = this.status.flying, 295 | prevFlyingStatus = this.status.stateValue; 296 | 297 | this.logger('Flying status: ' + data[6]); 298 | if ([1, 2, 3, 4].indexOf(data[6]) >= 0) { 299 | this.status.flying = true; 300 | } 301 | 302 | this.status.stateValue = data[6]; 303 | 304 | if (prevState !== this.status.flying) { 305 | this.emit('stateChange'); 306 | } 307 | 308 | if (prevFlyingStatus !== this.status.stateValue) { 309 | this.emit('flyingStatusChange', this.status.stateValue); 310 | } 311 | 312 | }.bind(this)); 313 | 314 | 315 | setTimeout(function() { 316 | 317 | this.writeTo( 318 | 'fa0b', 319 | new Buffer([0x04, ++this.steps.fa0b, 0x00, 0x04, 0x01, 0x00, 0x32, 0x30, 0x31, 0x34, 0x2D, 0x31, 0x30, 0x2D, 0x32, 0x38, 0x00]), 320 | function(error) { 321 | setTimeout(function() { 322 | if (typeof callback === 'function') { 323 | callback(error); 324 | } 325 | }, 100); 326 | } 327 | ); 328 | }.bind(this), 100); 329 | }; 330 | 331 | 332 | 333 | /** 334 | * Gets a Characteristic by it's unique_uuid_segment 335 | * 336 | * @param {String} unique_uuid_segment 337 | * @returns Characteristic 338 | */ 339 | Drone.prototype.getCharacteristic = function(unique_uuid_segment) { 340 | var filtered = this.characteristics.filter(function(c) { 341 | return c.uuid.search(new RegExp(unique_uuid_segment)) !== -1; 342 | }); 343 | 344 | return filtered[0]; 345 | 346 | }; 347 | 348 | /** 349 | * Writes a Buffer to a Characteristic by it's unique_uuid_segment 350 | * 351 | * @param {String} unique_uuid_segment 352 | * @param {Buffer} buffer 353 | */ 354 | Drone.prototype.writeTo = function(unique_uuid_segment, buffer, callback) { 355 | if (!this.characteristics) { 356 | var e = new Error('You must have bluetooth enabled and be connected to a drone before executing a command. Please ensure Bluetooth is enabled on your machine and you are connected.'); 357 | if (callback) { 358 | callback(e); 359 | } else { 360 | throw e; 361 | } 362 | } else { 363 | if (typeof callback === 'function') { 364 | this.getCharacteristic(unique_uuid_segment).write(buffer, true, callback); 365 | } else { 366 | this.getCharacteristic(unique_uuid_segment).write(buffer, true); 367 | } 368 | } 369 | }; 370 | 371 | Drone.prototype.onDisconnect = function() { 372 | if (this.connected) { 373 | this.logger('Disconnected from drone: ' + this.name); 374 | if (this.ping) { 375 | clearInterval(this.ping); 376 | } 377 | this.ble.removeAllListeners(); 378 | this.connected = false; 379 | this.discovered = false; 380 | // 381 | // CSW - Removed because we do not know if the device is flying or not, so leave state as is. 382 | // var prevState = this.status.flying; 383 | // this.status.flying = false; 384 | // if (prevState !== this.status.flying) { 385 | // this.emit('stateChange'); 386 | // } 387 | // this.status.stateValue = 0; 388 | // 389 | this.emit('disconnected'); 390 | } 391 | }; 392 | 393 | /** 394 | * 'Disconnects' from the drone 395 | * 396 | * @param callback to be called once disconnected 397 | */ 398 | Drone.prototype.disconnect = function(callback) { 399 | this.logger('RollingSpider#disconnect'); 400 | 401 | if (this.connected) { 402 | this.peripheral.disconnect(function(error) { 403 | this.onDisconnect(); 404 | if (typeof callback === 'function') { 405 | callback(error); 406 | } 407 | }.bind(this)); 408 | } else { 409 | if (typeof callback === 'function') { 410 | callback(); 411 | } 412 | } 413 | }; 414 | 415 | 416 | /** 417 | * Starts sending the current speed values to the drone every 50 milliseconds 418 | * 419 | * This is only sent when the drone is in the air 420 | * 421 | * @param callback to be called once the ping is started 422 | */ 423 | Drone.prototype.startPing = function() { 424 | this.logger('RollingSpider#startPing'); 425 | 426 | this.ping = setInterval(function() { 427 | var buffer = new Buffer(19); 428 | buffer.fill(0); 429 | buffer.writeInt16LE(2, 0); 430 | buffer.writeInt16LE(++this.steps.fa0a, 1); 431 | buffer.writeInt16LE(2, 2); 432 | buffer.writeInt16LE(0, 3); 433 | buffer.writeInt16LE(2, 4); 434 | buffer.writeInt16LE(0, 5); 435 | buffer.writeInt16LE((this.driveStepsRemaining ? 1 : 0), 6); 436 | 437 | buffer.writeInt16LE(this.speeds.roll, 7); 438 | buffer.writeInt16LE(this.speeds.pitch, 8); 439 | buffer.writeInt16LE(this.speeds.yaw, 9); 440 | buffer.writeInt16LE(this.speeds.altitude, 10); 441 | buffer.writeFloatLE(0, 11); 442 | 443 | this.writeTo('fa0a', buffer); 444 | if (this.driveStepsRemaining < 0) { 445 | // go on the last command blindly 446 | 447 | } else if (this.driveStepsRemaining > 1) { 448 | // decrement the drive chain 449 | this.driveStepsRemaining--; 450 | } else { 451 | // reset to hover states 452 | this.emit('driveComplete', this.speeds); 453 | this.driveStepsRemaining = 0; 454 | this.hover(); 455 | } 456 | 457 | }.bind(this), 50); 458 | }; 459 | 460 | 461 | 462 | 463 | 464 | /** 465 | * Obtains the signal strength of the connected drone as a dBm metric. 466 | * 467 | * @param callback to be called once the signal strength has been identified 468 | */ 469 | Drone.prototype.signalStrength = function(callback) { 470 | this.logger('RollingSpider#signalStrength'); 471 | if (this.connected) { 472 | this.peripheral.updateRssi(callback); 473 | } else { 474 | if (typeof callback === 'function') { 475 | callback(new Error('Not connected to device')); 476 | } 477 | } 478 | }; 479 | 480 | Drone.prototype.drive = function(parameters, steps) { 481 | this.logger('RollingSpider#drive'); 482 | this.logger('driveStepsRemaining', this.driveStepsRemaining); 483 | var params = parameters || {}; 484 | if (!this.driveStepsRemaining || steps < 0) { 485 | this.logger('setting state'); 486 | // only apply when not driving currently, this causes you to exactly move -- prevents fluid 487 | this.driveStepsRemaining = steps || 1; 488 | this.speeds.roll = params.tilt || 0; 489 | this.speeds.pitch = params.forward || 0; 490 | this.speeds.yaw = params.turn || 0; 491 | this.speeds.altitude = params.up || 0; 492 | 493 | this.logger(this.speeds); 494 | // inject into ping flow. 495 | } 496 | }; 497 | 498 | // Operational Functions 499 | // Multiple use cases provided to support initial build API as well as 500 | // NodeCopter API and parity with the ar-drone library. 501 | 502 | 503 | 504 | /** 505 | * Instructs the drone to take off if it isn't already in the air 506 | */ 507 | function takeOff(options, callback) { 508 | if (typeof options === 'function') { 509 | callback = options; 510 | options = {}; 511 | } 512 | this.logger('RollingSpider#takeOff'); 513 | 514 | if (this.status.battery < 10) { 515 | this.logger('!!! BATTERY LEVEL TOO LOW !!!'); 516 | } 517 | if (!this.status.flying) { 518 | this.writeTo( 519 | 'fa0b', 520 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x00, 0x01, 0x00]) 521 | ); 522 | this.status.flying = true; 523 | } 524 | 525 | this.on('flyingStatusChange', function(newStatus) { 526 | if (newStatus === 2) { 527 | if (typeof callback === 'function') { 528 | callback(); 529 | } 530 | } 531 | }); 532 | 533 | } 534 | 535 | 536 | 537 | /** 538 | * Configures the drone to fly in 'wheel on' or protected mode. 539 | * 540 | */ 541 | 542 | function wheelOn(options, callback) { 543 | if (typeof options === 'function') { 544 | callback = options; 545 | options = {}; 546 | } 547 | this.logger('RollingSpider#wheelOn'); 548 | this.writeTo( 549 | 'fa0b', 550 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x01, 0x02, 0x00, 0x01]) 551 | ); 552 | 553 | if (callback) { 554 | callback(); 555 | } 556 | } 557 | 558 | /** 559 | * Configures the drone to fly in 'wheel off' or unprotected mode. 560 | * 561 | */ 562 | function wheelOff(options, callback) { 563 | if (typeof options === 'function') { 564 | callback = options; 565 | options = {}; 566 | } 567 | this.logger('RollingSpider#wheelOff'); 568 | this.writeTo( 569 | 'fa0b', 570 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x01, 0x02, 0x00, 0x00]) 571 | ); 572 | if (callback) { 573 | callback(); 574 | } 575 | } 576 | 577 | /** 578 | * Instructs the drone to land if it's in the air. 579 | */ 580 | 581 | function land(options, callback) { 582 | if (typeof options === 'function') { 583 | callback = options; 584 | options = {}; 585 | } 586 | this.logger('RollingSpider#land'); 587 | if (this.status.flying) { 588 | this.writeTo( 589 | 'fa0b', 590 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x00, 0x03, 0x00]) 591 | ); 592 | 593 | this.on('flyingStatusChange', function(newStatus) { 594 | if (newStatus === 0) { 595 | this.status.flying = false; 596 | if (typeof callback === 'function') { 597 | callback(); 598 | } 599 | } 600 | }); 601 | 602 | } else { 603 | this.logger('Calling RollingSpider#land when it\'s not in the air isn\'t going to do anything'); 604 | if (callback) { 605 | callback(); 606 | } 607 | } 608 | } 609 | 610 | 611 | function toggle(options, callback) { 612 | if (typeof options === 'function') { 613 | callback = options; 614 | options = {}; 615 | } 616 | this.logger('RollingSpider#toggle'); 617 | if (this.status.flying) { 618 | this.land(options, callback); 619 | } else { 620 | this.takeOff(options, callback); 621 | } 622 | } 623 | 624 | /** 625 | * Instructs the drone to do an emergency landing. 626 | */ 627 | function cutOff(options, callback) { 628 | if (typeof options === 'function') { 629 | callback = options; 630 | options = {}; 631 | } 632 | this.logger('RollingSpider#cutOff'); 633 | this.status.flying = false; 634 | this.writeTo( 635 | 'fa0c', 636 | new Buffer([0x02, ++this.steps.fa0c & 0xFF, 0x02, 0x00, 0x04, 0x00]) 637 | , callback); 638 | } 639 | 640 | /** 641 | * Instructs the drone to trim. Make sure to call this before taking off. 642 | */ 643 | function flatTrim(options, callback) { 644 | if (typeof options === 'function') { 645 | callback = options; 646 | options = {}; 647 | } 648 | this.logger('RollingSpider#flatTrim'); 649 | this.writeTo( 650 | 'fa0b', 651 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x00, 0x00, 0x00]), 652 | callback 653 | ); 654 | } 655 | 656 | 657 | 658 | /** 659 | * Instructs the drone to do a front flip. 660 | * 661 | * It will only do this if it's in the air 662 | * 663 | */ 664 | 665 | function frontFlip(options, callback) { 666 | if (typeof options === 'function') { 667 | callback = options; 668 | options = {}; 669 | } 670 | this.logger('RollingSpider#frontFlip'); 671 | if (this.status.flying) { 672 | this.writeTo( 673 | 'fa0b', 674 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 675 | callback 676 | ); 677 | } else { 678 | this.logger('Calling RollingSpider#frontFlip when it\'s not in the air isn\'t going to do anything'); 679 | if (typeof callback === 'function') { 680 | callback(); 681 | } 682 | } 683 | if (callback) { 684 | callback(); 685 | } 686 | } 687 | 688 | /** 689 | * Instructs the drone to do a back flip. 690 | * 691 | * It will only do this if it's in the air 692 | * 693 | */ 694 | 695 | function backFlip(options, callback) { 696 | if (typeof options === 'function') { 697 | callback = options; 698 | options = {}; 699 | } 700 | this.logger('RollingSpider#backFlip'); 701 | if (this.status.flying) { 702 | this.writeTo( 703 | 'fa0b', 704 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00]), 705 | callback 706 | ); 707 | } else { 708 | this.logger('Calling RollingSpider#backFlip when it\'s not in the air isn\'t going to do anything'); 709 | if (typeof callback === 'function') { 710 | callback(); 711 | } 712 | } 713 | if (callback) { 714 | callback(); 715 | } 716 | } 717 | 718 | /** 719 | * Instructs the drone to do a right flip. 720 | * 721 | * It will only do this if it's in the air 722 | * 723 | */ 724 | function rightFlip(options, callback) { 725 | if (typeof options === 'function') { 726 | callback = options; 727 | options = {}; 728 | } 729 | this.logger('RollingSpider#rightFlip'); 730 | if (this.status.flying) { 731 | this.writeTo( 732 | 'fa0b', 733 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]), 734 | callback 735 | ); 736 | } else { 737 | this.logger('Calling RollingSpider#rightFlip when it\'s not in the air isn\'t going to do anything'); 738 | if (typeof callback === 'function') { 739 | callback(); 740 | } 741 | } 742 | 743 | if (callback) { 744 | callback(); 745 | } 746 | } 747 | 748 | /** 749 | * Instructs the drone to do a left flip. 750 | * 751 | * It will only do this if it's in the air 752 | * 753 | */ 754 | 755 | function leftFlip(options, callback) { 756 | if (typeof options === 'function') { 757 | callback = options; 758 | options = {}; 759 | } 760 | this.logger('RollingSpider#leftFlip'); 761 | if (this.status.flying) { 762 | this.writeTo( 763 | 'fa0b', 764 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x04, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00]), 765 | callback 766 | ); 767 | } else { 768 | this.logger('Calling RollingSpider#leftFlip when it\'s not in the air isn\'t going to do anything'); 769 | if (typeof callback === 'function') { 770 | callback(); 771 | } 772 | } 773 | if (callback) { 774 | callback(); 775 | } 776 | } 777 | 778 | /** 779 | * Instructs the drone to take a picture. 780 | */ 781 | 782 | function takePicture(options, callback) { 783 | if (typeof options === 'function') { 784 | callback = options; 785 | options = {}; 786 | } 787 | this.logger('RollingSpider#takePicture'); 788 | this.writeTo( 789 | 'fa0b', 790 | new Buffer([0x02, ++this.steps.fa0b & 0xFF, 0x02, 0x06, 0x00, 0x00, 0x00]) 791 | ); 792 | if (callback) { 793 | callback(); 794 | } 795 | } 796 | 797 | function driveBuilder(parameters) { 798 | var name = parameters.name, 799 | parameterToChange = parameters.parameterToChange, 800 | scaleFactor = parameters.scaleFactor; 801 | 802 | scaleFactor = scaleFactor || 1; 803 | 804 | return function(possibleOptions, possibleCallback) { 805 | var options, callback; 806 | if (_.isPlainObject(possibleOptions)) { 807 | options = possibleOptions; 808 | callback = _.isFunction(possibleCallback) ? possibleCallback : _.noop; 809 | } else if (_.isFunction(possibleOptions)) { 810 | callback = possibleOptions; 811 | } else { 812 | callback = _.noop; 813 | } 814 | 815 | this.logger('RollingSpider#' + name); 816 | if (this.status.flying) { 817 | options = options || {}; 818 | var speed = options.speed || 50; 819 | var steps = options.steps || 50; 820 | if (!validSpeed(speed)) { 821 | this.logger('RollingSpider#' + name + 'was called with an invalid speed: ' + speed); 822 | callback(); 823 | } else { 824 | var driveParams = {}; 825 | driveParams[parameterToChange] = speed * scaleFactor; 826 | this.drive(driveParams, steps); 827 | this.once('driveComplete', callback); 828 | } 829 | } else { 830 | this.logger('RollingSpider#' + name + ' when it\'s not in the air isn\'t going to do anything'); 831 | callback(); 832 | } 833 | }; 834 | 835 | } 836 | 837 | /** 838 | * Instructs the drone to start moving upward at speed 839 | * 840 | * @param {float} speed at which the drive should occur 841 | * @param {float} steps the length of steps (time) the drive should happen 842 | */ 843 | var up = driveBuilder({ 844 | name: 'up', 845 | parameterToChange: 'up' 846 | }); 847 | 848 | /** 849 | * Instructs the drone to start moving downward at speed 850 | * 851 | * @param {float} speed at which the drive should occur 852 | * @param {float} steps the length of steps (time) the drive should happen 853 | */ 854 | var down = driveBuilder({ 855 | name: 'down', 856 | parameterToChange: 'up', 857 | scaleFactor: -1 858 | }); 859 | 860 | /** 861 | * Instructs the drone to start moving forward at speed 862 | * 863 | * @param {float} speed at which the drive should occur. 0-100 values. 864 | * @param {float} steps the length of steps (time) the drive should happen 865 | */ 866 | var forward = driveBuilder({ 867 | name: 'forward', 868 | parameterToChange: 'forward' 869 | }); 870 | 871 | 872 | /** 873 | * Instructs the drone to start moving backward at speed 874 | * 875 | * @param {float} speed at which the drive should occur 876 | * @param {float} steps the length of steps (time) the drive should happen 877 | */ 878 | var backward = driveBuilder({ 879 | name: 'backward', 880 | parameterToChange: 'forward', 881 | scaleFactor: -1 882 | }); 883 | 884 | 885 | /** 886 | * Instructs the drone to start spinning clockwise at speed 887 | * 888 | * @param {float} speed at which the rotation should occur 889 | * @param {float} steps the length of steps (time) the turning should happen 890 | */ 891 | var turnRight = driveBuilder({ 892 | name: 'turnRight', 893 | parameterToChange: 'turn' 894 | }); 895 | 896 | /** 897 | * Instructs the drone to start spinning counter-clockwise at speed 898 | * 899 | * @param {float} speed at which the rotation should occur 900 | * @param {float} steps the length of steps (time) the turning should happen 901 | */ 902 | var turnLeft = driveBuilder({ 903 | name: 'turnLeft', 904 | parameterToChange: 'turn', 905 | scaleFactor: -1 906 | }); 907 | 908 | 909 | /** 910 | * Instructs the drone to start moving right at speed 911 | * 912 | * @param {float} speed at which the rotation should occur 913 | * @param {float} steps the length of steps (time) the turning should happen 914 | */ 915 | var tiltRight = driveBuilder({ 916 | name: 'tiltRight', 917 | parameterToChange: 'tilt' 918 | }); 919 | 920 | 921 | /** 922 | * Instructs the drone to start moving left at speed 923 | * 924 | * @param {float} speed at which the rotation should occur 925 | * @param {float} steps the length of steps (time) the turning should happen 926 | */ 927 | var tiltLeft = driveBuilder({ 928 | name: 'tiltLeft', 929 | parameterToChange: 'tilt', 930 | scaleFactor: -1 931 | }); 932 | 933 | function hover(options, callback) { 934 | if (typeof options === 'function') { 935 | callback = options; 936 | options = {}; 937 | } 938 | //this.logger('RollingSpider#hover'); 939 | this.driveStepsRemaining = 0; 940 | this.speeds.roll = 0; 941 | this.speeds.pitch = 0; 942 | this.speeds.yaw = 0; 943 | this.speeds.altitude = 0; 944 | if (callback) { 945 | callback(); 946 | } 947 | } 948 | 949 | 950 | /** 951 | * Checks whether a speed is valid or not 952 | * 953 | * @private 954 | * @param {float} speed 955 | * @returns {boolean} 956 | */ 957 | function validSpeed(speed) { 958 | return (0 <= speed && speed <= 100); 959 | } 960 | 961 | /** 962 | * Gets the Bluetooth name of the drone 963 | * @returns {string} 964 | */ 965 | function getDroneName() { 966 | return this.peripheral.advertisement.localName; 967 | } 968 | 969 | 970 | // provide options for use case 971 | Drone.prototype.takeoff = takeOff; 972 | Drone.prototype.takeOff = takeOff; 973 | Drone.prototype.wheelOff = wheelOff; 974 | Drone.prototype.wheelOn = wheelOn; 975 | Drone.prototype.land = land; 976 | Drone.prototype.toggle = toggle; 977 | Drone.prototype.emergency = cutOff; 978 | Drone.prototype.emergancy = cutOff; 979 | Drone.prototype.flatTrim = flatTrim; 980 | Drone.prototype.calibrate = flatTrim; 981 | Drone.prototype.up = up; 982 | Drone.prototype.down = down; 983 | // animation 984 | Drone.prototype.frontFlip = frontFlip; 985 | Drone.prototype.backFlip = backFlip; 986 | Drone.prototype.rightFlip = rightFlip; 987 | Drone.prototype.leftFlip = leftFlip; 988 | 989 | // rotational 990 | Drone.prototype.turnRight = turnRight; 991 | Drone.prototype.clockwise = turnRight; 992 | Drone.prototype.turnLeft = turnLeft; 993 | Drone.prototype.counterClockwise = turnLeft; 994 | 995 | // directional 996 | Drone.prototype.forward = forward; 997 | Drone.prototype.backward = backward; 998 | Drone.prototype.tiltRight = tiltRight; 999 | Drone.prototype.tiltLeft = tiltLeft; 1000 | Drone.prototype.right = tiltRight; 1001 | Drone.prototype.left = tiltLeft; 1002 | 1003 | // camera 1004 | Drone.prototype.takePicture = takePicture; 1005 | 1006 | Drone.prototype.hover = hover; 1007 | Drone.prototype.getDroneName = getDroneName; 1008 | 1009 | module.exports = Drone; 1010 | --------------------------------------------------------------------------------