orbotix/sphero.js

View on GitHub
lib/adaptors/ble.js

Summary

Maintainability
A
2 hrs
Test Coverage
"use strict";

var util = require("util"),
    EventEmitter = require("events").EventEmitter;

var ble;

function initBLE() {
  var isChrome = typeof chrome !== "undefined";

  // thanks to https://github.com/jgautier/firmata/blob/master/lib/firmata.js
  try {
    if (isChrome) {
      // browser BLE interface here...
      console.error("Browser-based BLE interface is not yet supported.");
    } else {
      ble = require("noble");
    }
  } catch (error) {
    ble = null;
  }

  if (ble == null) {
    var err = [
      "It looks like you want to connect to a Sphero BB-8 or Sphero Ollie,",
      "but did not install the 'noble' module.", "",
      "To install it run this command:",
      "npm install noble", "",
      "For more information go to https://github.com/sandeepmistry/noble"
    ].join("\n");

    console.error(err);
    throw new Error("Missing noble dependency");
  }
}

/**
 * An adaptor to communicate with a Bluetooth LE (aka 4.x) Interface
 *
 * @constructor
 * @param {String} peripheralId the BLE address to connect to
 * @param {Object} options optional parameters
 */
var Adaptor = module.exports = function Adaptor(peripheralId, options) {
  var uuid = peripheralId.split(":").join("").toLowerCase();
  this.uuid = uuid;
  var opts = options || {};
  this.noblePeripheral = opts.peripheral;
  if (this.noblePeripheral) {
    ble = this.noblePeripheral._noble;
  } else {
    initBLE();
  }
  this.peripheral = null;
  this.connectedPeripherals = {};
  this.isConnected = false;
  this.readHandler = function() {
    return;
  };
};

util.inherits(Adaptor, EventEmitter);

Adaptor.BLEService = "22bb746f2bb075542d6f726568705327";
Adaptor.WakeCharacteristic = "22bb746f2bbf75542d6f726568705327";
Adaptor.TXPowerCharacteristic = "22bb746f2bb275542d6f726568705327";
Adaptor.AntiDosCharacteristic = "22bb746f2bbd75542d6f726568705327";
Adaptor.RobotControlService = "22bb746f2ba075542d6f726568705327";
Adaptor.CommandsCharacteristic = "22bb746f2ba175542d6f726568705327";
Adaptor.ResponseCharacteristic = "22bb746f2ba675542d6f726568705327";

/**
 * Opens a connection to the BLE device.
 * Triggers the provided callback when ready.
 *
 * @param {Function} callback (err)
 * @return {void}
 */
Adaptor.prototype.open = function open(callback) {
  var self = this;
  if (this.noblePeripheral != null) {
    // an existing noble peripheral was passed into adaptor
    self._connectPeripheral(this.noblePeripheral, callback);
  } else {
    // connect to peripheral using noble
    ble.on("discover", function(peripheral) {
      if (peripheral.id === self.uuid) {
        ble.stopScanning();
        self._connectPeripheral(peripheral, callback);
      }
    });

    if (ble.state === "poweredOn") {
      ble.startScanning();
    } else {
      ble.on("stateChange", function(state) {
        if (state === "poweredOn") {
          ble.startScanning();
        } else {
          ble.stopScanning();
        }
      });
    }
  }
};

/**
 * Writes data to the BLE device on the
 * RobotControlService/CommandsCharacteristic.
 * Triggers the provided callback when done.
 *
 * @param {Any} data info to be written to the device. turned into a buffer.
 * @param {Function} [callback] triggered when write is complete
 * @return {void}
 */
Adaptor.prototype.write = function write(data, callback) {
  this.writeServiceCharacteristic(
    Adaptor.RobotControlService,
    Adaptor.CommandsCharacteristic,
    new Buffer(data),
    callback
  );
};

/**
 * The provided callback will be triggered whenever the BLE device receives data
 * from the RobotControlService/ResponseCharacteristic "notify" event
 *
 * @param {Function} callback function to be invoked when data is read
 * @return {void}
 */
Adaptor.prototype.onRead = function onRead(callback) {
  this.readHandler = callback;
};

/**
 * Disconnects from the BLE device
 * The provided callback will be triggered after disconnecting
 *
 * @param {Function} callback function to be invoked when disconnected
 * @return {void}
 */
Adaptor.prototype.close = function close(callback) {
  callback();
};

/**
 * Enables developer mode on the Ollie/BB-8.
 *
 * This is accomplished via sending a special string to the Anti-DoS service,
 * setting TX power to 7, and telling the robot to wake up.
 *
 * @param {Function} callback function to call when done
 * @return {void}
 * @publish
 */
Adaptor.prototype.devModeOn = function(callback) {
  var self = this;

  self.setAntiDos(function() {
    self.setTXPower(7, function() {
      self.wake(function() {
        self.getCharacteristic(Adaptor.RobotControlService,
                               Adaptor.ResponseCharacteristic,
                               function(e, c) {
          if (e) { return callback(e); }
          c.on("read", function(data) {
            if (data && data.length > 5) {
              self.readHandler(data);
            }
          });
          c.notify(true, function() {
            callback();
          });
        });
      });
    });
  });
};

/**
 * Tells the Ollie/BB-8 to wake up
 *
 * @param {Function} callback function to be triggered when the robot is awake
 * @return {void}
 * @publish
 */
Adaptor.prototype.wake = function(callback) {
  this.writeServiceCharacteristic(
    Adaptor.BLEService,
    Adaptor.WakeCharacteristic,
    1,
    callback
  );
};

/**
 * Sets the BLE transmit power for the Ollie/BB-8.
 *
 * Uses more battery, but gives longer range
 *
 * @param {Number} level power to set
 * @param {Function} callback function to call when done
 * @return {void}
 * @publish
 */
Adaptor.prototype.setTXPower = function(level, callback) {
  this.writeServiceCharacteristic(
    Adaptor.BLEService,
    Adaptor.TXPowerCharacteristic,
    level,
    callback
  );
};

/**
 * Sends a special Anti-DoS string to the Ollie/BB-8.
 *
 * Used when enabling developer mode
 *
 * @param {Function} callback function to call when done
 * @return {void}
 */
Adaptor.prototype.setAntiDos = function(callback) {
  var str = "011i3";
  var bytes = [];

  for (var i = 0; i < str.length; ++i) {
    bytes.push(str.charCodeAt(i));
  }

  this.writeServiceCharacteristic(
    Adaptor.BLEService,
    Adaptor.AntiDosCharacteristic,
    bytes,
    callback
  );
};

/**
 * Reads a service characteristic from the BLE peripheral.
 *
 * Triggers the provided callback when data is retrieved.
 *
 * @param {Number} serviceId ID of service to get details for
 * @param {Number} characteristicId ID of characteristic to get details for
 * @param {Function} callback function to be invoked with value
 * @return {void}
 * @publish
 */
Adaptor.prototype.readServiceCharacteristic = function(serviceId,
                                                       characteristicId,
                                                       callback) {
  this.getCharacteristic(serviceId, characteristicId, function(error, c) {
    if (error) {
      return callback(error);
    }

    c.read(callback);
  });
};

/**
 * Writes a service characteristic value to the BLE peripheral.
 *
 * Triggers the provided callback when data is written.
 *
 * @param {Number} serviceId ID of service to get details for
 * @param {Number} characteristicId ID of characteristic to get details for
 * @param {Number} value value to write to the characteristic
 * @param {Function} callback function to be invoked when data is written
 * @return {void}
 * @publish
 */
Adaptor.prototype.writeServiceCharacteristic = function(serviceId,
                                                        characteristicId,
                                                        value,
                                                        callback) {
  this.getCharacteristic(serviceId, characteristicId, function(error, c) {
    if (error) { return callback(error); }

    c.write(new Buffer(value), true, function() {
      if (callback != null) { callback(null); }
    });
  });
};

/**
 * Finds a BLE service characteristic
 *
 * Triggers the provided callback when the characteristic is found
 *
 * @param {Number} serviceId ID of service to look for
 * @param {Number} characteristicId ID of characteristic to look for
 * @param {Function} callback function to be invoked with requested
 * characteristic
 * @return {void}
 * @publish
 */
Adaptor.prototype.getCharacteristic = function(serviceId,
                                               characteristicId,
                                               callback) {
  var self = this;
  this._connectBLE(function() {
    self._connectService(serviceId, function(error) {
      if (error) { callback(error, null); }
      self._connectCharacteristic(serviceId, characteristicId, function(e, c) {
        callback(e, c);
      });
    });
  });
};

Adaptor.prototype._connectPeripheral = function(np, callback) {
  this.peripheral = np;

  var p = { peripheral: np, services: {} };
  this.connectedPeripherals[this.uuid] = p;

  this.devModeOn(function() {
    callback();
  });
};

Adaptor.prototype._connectBLE = function(callback) {
  var p = this._connectedPeripheral();

  if (p.state === "connected") {
    callback();
  } else {
    var self = this;
    p.connect(function() {
      self.isConnected = true;
      callback();
    });
  }
};

Adaptor.prototype._connectService = function(serviceId, callback) {
  var self = this;
  var p = this._connectedPeripheral();
  if (this._connectedServices()) {
    callback(null, self._connectedService(serviceId));
  } else {
    p.discoverServices(null, function(serErr, services) {
      if (serErr) { return callback(serErr); }

      if (services.length > 0) {
        var service = self._connectedService(serviceId);
        callback(null, service);
      } else {
        callback("No services found", null);
      }
    });
  }
};

Adaptor.prototype._connectCharacteristic = function(serviceId,
                                                    characteristicId,
                                                    callback) {
  var self = this;
  if (self._connectedCharacteristics(serviceId)) {
    callback(null, self._connectedCharacteristic(serviceId, characteristicId));
  } else {
    var s = self._connectedService(serviceId);

    s.discoverCharacteristics(null, function(charErr, characteristics) {
      if (charErr) { return callback(charErr); }

      if (characteristics.length > 0) {
        var characteristic = self._connectedCharacteristic(serviceId,
                                                           characteristicId);
        callback(null, characteristic);
      } else {
        callback("No characteristics found", null);
      }
    });
  }
};

// peripherals helpers
Adaptor.prototype._connectedPeripheral = function() {
  return this.connectedPeripherals[this.uuid].peripheral;
};

// services helpers
Adaptor.prototype._connectedServices = function() {
  var p = this._connectedPeripheral();

  if (p.state !== "connected") {
    return null;
  }

  return p.services;
};

Adaptor.prototype._connectedService = function(serviceId) {
  var services = this._connectedServices();
  for (var s in services) {
    if (services[s].uuid === serviceId) {
      return services[s];
    }
  }
  return null;
};

// characteristics helpers
Adaptor.prototype._connectedCharacteristics = function(serviceId) {
  return this._connectedService(serviceId).characteristics;
};


Adaptor.prototype._connectedCharacteristic = function(serviceId, characteristicId) {
  var characteristics = this._connectedCharacteristics(serviceId);
  for (var c in characteristics) {
    if (characteristics[c].uuid === characteristicId) {
      return characteristics[c];
    }
  }
  return null;
};