diff --git a/examples/connect_spheros.js b/examples/connect_spheros.js new file mode 100755 index 0000000..a9f3789 --- /dev/null +++ b/examples/connect_spheros.js @@ -0,0 +1,115 @@ +#!/usr/bin/env node +/** + * Creates virtual serial ports in /tmp for each Sphero port and holds the + * bluetooth ones open. This is helpful for avoiding connection errors and + * interference from iOS devices when connecting multiple Spheros. + * + * Requires the 'socat' command-line tool to create the virtual serial ports. + */ +var spawn = require('child_process').spawn; +var extname = require('path').extname; + +// where to store the virtual ports while running +var virtual_port_dir = '/tmp/'; + +// socat displays this when successfully connected +var success_str = 'starting data transfer loop'; + +// socat binary +//var socat = './testsocat.sh'; +var socat = 'socat'; + +// terminal colors +red   = '\033[31m'; +blue  = '\033[34m'; +green  = '\033[32m'; +reset = '\033[0m'; + +var paths = process.argv.slice(2); +var numPaths = paths.length; +if(paths.length === 0) { + console.log('usage connectSpheros.js [sphero serial port paths]'); + process.exit(1); +} +console.log('Connecting ' + numPaths + ' paired Spheros'); + +var connectPort = function(index, path, callback) { + var self; + var virtual_port = virtual_port_dir + extname(path).slice(1); + console.log('Starting connection to ' + path + ' at ' + virtual_port); + + // Run socat to connect virtual serial port to real one + child = spawn(socat, [ + '-d','-d','-d', // Extra debugging + 'pty,link=' + virtual_port + ',raw,echo=0', // Create virtual serial port + 'file:' + path // Connect to Sphero serial port + ]); + + child.on('exit', function() { + if(index in activeStreams) { + // We lost an active connection + console.log(red + 'Connection lost for ' + path + reset); + } else { + console.log('Failed to connect to ' + path); + } + + delete activeStreams[index]; + + // Restart self + children[self.index] = connectPort(index, path, callback); + }); + + var handleOutput = function(buffer) { + var data = buffer.toString(); + //console.log(index + ' stdout: ' + data); + + if(data.search('starting data transfer loop') !== -1) { + self.ready = true; + + console.log(self.path, ': stream is now active at ' + self.port); + callback(); + + activeStreams[index] = true; + if(Object.keys(activeStreams).length === numPaths) { + console.log('\n' + green + 'READY TO GO!!!' + reset + '\n'); + } + } + }; + child.stdout.on('data', handleOutput); + child.stderr.on('data', handleOutput); + + self = { + index: index, + path: path, + port: virtual_port, + child: child, + ready: false + }; + return self; +}; + +var children = Object.create(null); +var activeStreams = Object.create(null); + +// connect one at a time, waiting for callback +var i = 0; +var connectNext = function() { + if(!paths.length) { return; } + + i = i + 1; + var path = paths.shift(); + + children[i] = connectPort(i, path, connectNext); +}; +connectNext(); + +process.on('SIGINT', function() { + process.exit(); +}); +process.on('exit', function() { + for(var i=0; i '); +var stdin = process.openStdin(); +stdin.once('data', function() { + spheros.forEach(function(sphero) { + reset(sphero); + }); +}); + diff --git a/examples/setup_sphero.js b/examples/setup_sphero.js new file mode 100755 index 0000000..a95a0e8 --- /dev/null +++ b/examples/setup_sphero.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node +var Sphero = require("../lib/sphero.js").Sphero; + +var run = function(sphero) { + + sphero.getBluetoothInfo(function(name, id) { + console.log("Device name is " + name + ' / ' + id); + }) + .setAutoReconnect(false, 5, function() { + console.log("Turned off auto reconnect"); + }) + .send({ + device: sphero.devices.core, + command: 0x25, + data: new Buffer([0xFF, 0xFF]), + success: function(packet) { + console.log("Set timeout to 65535 seconds"); + sphero.disconnect(); + process.exit(0); + } + }); +}; + + +var sphero; +if(process.argv.length === 2) { + sphero = new Sphero(); +} +else if(process.argv.length === 3) { + sphero = new Sphero(process.argv[2]); +} else { + console.log('usage: setup_sphero.js [path to sphero port]'); + process.exit(1); +} + +run(sphero); diff --git a/lib/core.js b/lib/core.js new file mode 100644 index 0000000..086a270 --- /dev/null +++ b/lib/core.js @@ -0,0 +1,196 @@ +// NodeJS Sphero SDK + +module.exports = function (Sphero) { + + Sphero.prototype.ping = function(cb) { + var options = { + device: this.devices.core, + command: 0x01 + }; + if (cb) { + options.success = function() { + cb(); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.getVersioning = function(cb) { + var options = { + device: this.devices.core, + command: 0x02 + }; + if (cb) { + var self = this; + options.success = function(packet) { + cb(self.parseData(packet)); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.setDeviceName = function(name, cb) { + if (typeof(name) === "string") { + name = name.split(""); + } + var options = { + device: this.devices.core, + command: 0x10, + data: name + }; + if (cb) { + var self = this; + options.success = function(packet) { + cb(self.parseData(packet)); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.getBluetoothInfo = function(cb) { + var options = { + device: this.devices.core, + command: 0x11 + }; + if (cb) { + var self = this; + options.success = function(packet) { + var data = self.parseData(packet); + var name = data.slice(0, 15).toString(); + var id = data.slice(16).toString(); + cb(name, id); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.setAutoReconnect = function(enable, time, cb) { + // Allow user to pass callback without time for disabling + if (typeof time === "function") { + cb = time; + time = 0; + } + if (enable && typeof time !== "number") { + throw new Error("Reconnect time is required"); + } + var options = { + device: this.devices.core, + command: 0x12, + // Default time to 30 to be safe + data: new Buffer([enable ? 0x01 : 0x00, time]) + }; + if (cb) { + var self = this; + options.success = function() { + cb(); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.getAutoReconnect = function(cb) { + var options = { + device: this.devices.core, + command: 0x13 + }; + if (cb) { + var self = this; + options.success = function(packet) { + cb(self.parseData(packet)); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.getPowerState = function(cb) { + var options = { + device: this.device.core, + command: 0x20 + }; + if (cb) { + var self = this; + options.success = function(packet) { + cb(self.parseData(packet)); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.setPowerNotification = function(flag, cb) { + var options = { + device: this.device.core, + command: 0x21, + success: cb, + data: [flag] + }; + this.send(options); + return this; + }; + + Sphero.prototype.sleep = function(time, macro, orbBasic, cb) { + var options = { + device: this.device.core, + command: 0x22, + success: cb, + data: [time/256, time%256, macro, orbBasic] + }; + this.send(options); + return this; + }; + + Sphero.prototype.getVoltageTripPoints = function(cb) { + var options = { + device: this.device.core, + command: 0x23 + }; + if (cb) { + var self = this; + options.success = function(packet) { + cb(packet[5]*256 + packet[6], packet[7]*256 + packet[8]); + }; + } + this.send(options); + return this; + }; + + Sphero.prototype.setVoltageTripPoints = function(vLow, vCrit, cb) { + var options = { + device: this.device.core, + command: 0x24, + success: cb, + data: [vLow/256, vLow%256, vCrit/256, vCrit%256] + }; + this.send(options); + return this; + }; + + Sphero.prototype.setInactivityTimeout = function(time, cb) { + var options = { + device: this.device.core, + command: 0x25, + success: cb, + data: [time/256, time%256] + }; + this.send(options); + return this; + }; + + Sphero.prototype.jumpToBootloader = function(cb) { + var options = { + device: this.device.core, + command: 0x30, + success: cb + }; + this.send(options); + return this; + }; + + return Sphero; +}; diff --git a/lib/sphero.js b/lib/sphero.js index 67c51a8..999a1e4 100644 --- a/lib/sphero.js +++ b/lib/sphero.js @@ -1,59 +1,68 @@ // NodeJS Sphero SDK -var SerialPort = require("serialport").SerialPort; +var SerialPort = require("serialport").SerialPort, + glob = require('glob'); // Constant values // Buffer positions for packet fields -var COMMAND_SOP1 = 0 - , COMMAND_SOP2 = 1 - , COMMAND_DID = 2 - , COMMAND_CID = 3 - , COMMAND_SEQ = 4 - , COMMAND_DLEN = 5 - // First data position - , COMMAND_DATA = 6 - // CHK will be variable position if data is set - , COMMAND_CHK = 6 - - // Response packet fields - , RESPONSE_SOP1 = 0 - , RESPONSE_SOP2 = 1 - , RESPONSE_MRSP = 2 - , RESPONSE_SEQ = 3 - , RESPONSE_DLEN = 4 - // First data position - , RESPONSE_DATA = 5 - // CHK will be variable position if data is set - , RESPONSE_CHK = 5 - - // Async - , RESPONSE_ID = 2 - , RESPONSE_DLEN_MSB = 3 - , RESPONSE_DLEN_LSB = 4 - - // Message response codes - , RSP_CODE_OK = 0x00 - , RSP_CODE_EGEN = 0x01 - , RSP_CODE_ECHKSUM = 0x02 - , RSP_CODE_EFRAG = 0x03 - , RSP_CODE_EBAD_CMD = 0x04 - , RSP_CODE_EUNSUPP = 0x05 - , RSP_CODE_EBAD_MSG = 0x06 - , RSP_CODE_EPARAM = 0x07 - , RSP_CODE_EEXEC = 0x08 - , RSP_CODE_EBAD_DID = 0x09 - , RSP_CODE_POWER_NOGOOD = 0x31 - , RSP_CODE_PAGE_ILLEGAL = 0x32 - , RSP_CODE_FLASH_FAIL = 0x33 - , RSP_CODE_MA_CORRUPT = 0x34 - , RSP_CODE_MSG_TIMEOUT = 0x35; +var COMMAND_SOP1 = 0, + COMMAND_SOP2 = 1, + COMMAND_DID = 2, + COMMAND_CID = 3, + COMMAND_SEQ = 4, + COMMAND_DLEN = 5, + // First data position + COMMAND_DATA = 6, + // CHK will be variable position if data is set + COMMAND_CHK = 6, + + // Response packet fields + RESPONSE_SOP1 = 0, + RESPONSE_SOP2 = 1, + RESPONSE_MRSP = 2, + RESPONSE_SEQ = 3, + RESPONSE_DLEN = 4, + // First data position + RESPONSE_DATA = 5, + // CHK will be variable position if data is set + RESPONSE_CHK = 5, + + // Async + RESPONSE_ID = 2, + RESPONSE_DLEN_MSB = 3, + RESPONSE_DLEN_LSB = 4, + + // Message response codes + RSP_CODE_OK = 0x00, + RSP_CODE_EGEN = 0x01, + RSP_CODE_ECHKSUM = 0x02, + RSP_CODE_EFRAG = 0x03, + RSP_CODE_EBAD_CMD = 0x04, + RSP_CODE_EUNSUPP = 0x05, + RSP_CODE_EBAD_MSG = 0x06, + RSP_CODE_EPARAM = 0x07, + RSP_CODE_EEXEC = 0x08, + RSP_CODE_EBAD_DID = 0x09, + RSP_CODE_POWER_NOGOOD = 0x31, + RSP_CODE_PAGE_ILLEGAL = 0x32, + RSP_CODE_FLASH_FAIL = 0x33, + RSP_CODE_MA_CORRUPT = 0x34, + RSP_CODE_MSG_TIMEOUT = 0x35, + + // Glob expression for Sphero serial ports + DEFAULT_SPHERO_PORT = '/dev/tty.Sphero*'; var Sphero = function(path) { var self = this; - // Path to port Sphero is on - path = path || "/dev/tty.Sphero"; + // Path to port Sphero is on - use glob to find default port + if(typeof path === 'undefined') { + path = glob.sync(DEFAULT_SPHERO_PORT)[0]; + if(typeof path === 'undefined') { + throw new Error('No Sphero found matching ' + DEFAULT_SPHERO_PORT); + } + } // Command sequence number this.seq = 0x00; @@ -115,18 +124,39 @@ var Sphero = function(path) { var packet = self.buffer.slice(0, end); self.buffer = self.buffer.slice(end); - // Do the callback - if (packet[RESPONSE_MRSP] == RSP_CODE_OK) { - // Success! - if (self.calls[packet[RESPONSE_SEQ]].success) { - self.calls[packet[RESPONSE_SEQ]].success(packet); + if(typeof self.calls[packet[RESPONSE_SEQ]] !== 'undefined') { + // Do the callback + if (packet[RESPONSE_MRSP] == RSP_CODE_OK) { + // Success! + if (self.calls[packet[RESPONSE_SEQ]].success) { + self.calls[packet[RESPONSE_SEQ]].success(packet); + } } - } else { - // Error :( - if (self.calls[packet[RESPONSE_SEQ]].error) { - self.calls[packet[RESPONSE_SEQ]].error(packet); + else if (self.onHit && packet[RESPONSE_MRSP] == 0x07) { + // Collision Detection Async Response + packet = packet.slice(5, packet.length - 1); + var object = { + x: packet[0]*256 + packet[1], + y: packet[2]*256 + packet[3], + z: packet[4]*256 + packet[5], + axis: ((packet[6]-1) && 'Y') || 'X', + xMagnitude: packet[7]*256 + packet[8], + yMagnitude: packet[9]*256 + packet[10], + speed: packet[11], + timeStamp: packet[12]*256*256*256 + packet[13]*256*256 + packet[14]*256 + packet[15] + }; + + self.onHit(object); + } else { + console.log('Error packet: '); + console.log(packet); + // Error :( + if (self.calls[packet[RESPONSE_SEQ]].error) { + self.calls[packet[RESPONSE_SEQ]].error(packet); + } } } + // Remove the saved options, no longer needed delete self.calls[packet[RESPONSE_SEQ]]; @@ -136,7 +166,7 @@ var Sphero = function(path) { } } } - } + }; this.port.on("data", processData); }; @@ -159,6 +189,9 @@ Sphero.prototype.send = function(options) { } // Determine length of data. Length is always data length + 1 (for checksum) + if(typeof options.data === 'number') { + options.data = [options.data]; + } var dataLength = options.data.length > 254 ? 0xFF : options.data.length + 0x01; // Construct the packet (minus checksum) @@ -182,7 +215,8 @@ Sphero.prototype.send = function(options) { this.calls[this.seq] = options; this.port.write(packet); - this.seq++; + + this.seq = (this.seq + 1) % 256; }; // Returns new Buffer of result data (if any) out of a response packet buffer @@ -197,101 +231,10 @@ Sphero.prototype.parseData = function(packet) { return result; }; -/** - * These are the helper functions that make sending raw calls easy. - */ - -Sphero.prototype.ping = function(cb) { - var options = { - device: this.devices.core, - command: 0x01 - }; - if (cb) { - options.success = function() { - cb(); - } - } - this.send(options); - return this; +Sphero.prototype.disconnect = function() { + this.port.close(); }; -Sphero.prototype.getVersioning = function(cb) { - var options = { - device: this.devices.core, - command: 0x02 - }; - if (cb) { - var self = this; - options.success = function(packet) { - cb(self.parseData(packet)); - } - } - this.send(options); - return this; -}; - -// setDeviceName here - -Sphero.prototype.getBluetoothInfo = function(cb) { - var options = { - device: this.devices.core, - command: 0x11 - }; - if (cb) { - var self = this; - options.success = function(packet) { - var data = self.parseData(packet); - var name = data.slice(0, 15).toString(); - var id = data.slice(16).toString(); - cb(name, id); - }; - } - this.send(options); - return this; -}; - -Sphero.prototype.setAutoReconnect = function(enable, time, cb) { - // Allow user to pass callback without time for disabling - if (typeof time === "function") { - cb = time; - time = 0; - } - if (enable && typeof time !== "number") { - throw new Error("Reconnect time is required"); - } - var options = { - device: this.devices.core, - command: 0x12, - // Default time to 30 to be safe - data: new Buffer([enable ? 0x01 : 0x00, time]) - }; - if (cb) { - var self = this; - options.success = function() { - cb(); - } - } - this.send(options); - return this; -}; - -Sphero.prototype.getAutoReconnect = function(cb) { - var options = { - device: this.devices.core, - command: 0x13 - }; - if (cb) { - var self = this; - options.success = function(packet) { - cb(self.parseData(packet)); - }; - } - this.send(options); - return this; -}; - -// Bunch of missing features go here - /** * Sphero commands */ @@ -307,22 +250,22 @@ Sphero.prototype.setHeading = function(heading, cb) { if (cb) { options.success = function() { cb(); - } + }; } this.send(options); return this; }; -Sphero.prototype.setStabilization = function(enable, cb) { +Sphero.prototype.setStabilization = function(data, cb) { var options = { device: this.devices.sphero, command: 0x02, - data: enable ? 0x01 : 0x00 + data: data }; if (cb) { options.success = function() { cb(); - } + }; } this.send(options); return this; @@ -337,14 +280,12 @@ Sphero.prototype.setRotationRate = function(rate, cb) { if (cb) { options.success = function() { cb(); - } + }; } this.send(options); return this; }; -// More missing functions here - Sphero.prototype.roll = function(speed, heading, timeout, cb) { var self = this; @@ -376,7 +317,7 @@ Sphero.prototype.roll = function(speed, heading, timeout, cb) { } else if (cb) { options.success = function(packet) { cb(packet); - } + }; } this.send(options); return this; @@ -391,10 +332,47 @@ Sphero.prototype.stop = function(cb) { if (cb) { options.success = function() { cb(); - } + }; } this.send(options); return this; }; +Sphero.prototype.setCollisionDetection = function (options, onHit) { + options.device = this.devices.sphero; + options.command = 0x12; + options.success = options.success || function() { return; }, + options.error = options.error || function() { return; }, + options.data = options.data || [0x01, 120, 120, 120, 120, 120]; + this.onHit = onHit || function() { return; }; + this.send(options); + return this; +}; + +Sphero.prototype.setColor = function(red, green, blue, cb) { + var options = { + device: this.devices.sphero, + command: 0x20, + success: cb, + data: [red, green, blue] + }; + this.send(options); + return this; +}; + +Sphero.prototype.setBackLED = function(val, cb) { + var options = { + device: this.devices.sphero, + command: 0x21, + data: [val], + success: function(packet) { + cb(packet); + } + }; + this.send(options); + return this; +}; + +require('./core')(Sphero); + exports.Sphero = Sphero; diff --git a/package.json b/package.json index e9a3588..a7662e5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "node": "~0.6.2" }, "dependencies": { - "serialport": "0.6.3" + "serialport": "0.6.3", + "glob": "3.1.x" }, "devDependencies": {} }