Cylon.JS - Adding New Hardware Support

Made by Humans, for Humans.

Adding Cylon.JS support for new hardware is straightforward, thanks to the simple patterns and tools included with Cylon itself.

Generating

The easiest way to get started is with the cylon-cli toolkit. Install it globally with NPM:

$ sudo npm install -g cylon-cli

With that done, you can use it to generate a new template Cylon module, by calling cylon generate module with the name of the platform you're adding support for. For example, to generate a module for the Leap Motion:

$ cylon leapmotion
  create : leapmotion
  create : leapmotion/.gitignore
  create : leapmotion/.jshintrc
  create : leapmotion/Makefile
  create : leapmotion/package.json
  create : leapmotion/lib
  create : leapmotion/lib/index.js
  create : leapmotion/lib/adaptor.js
  create : leapmotion/lib/driver.js
  create : leapmotion/spec
  create : leapmotion/spec/helper.js
  create : leapmotion/spec/lib
  create : leapmotion/spec/lib/index.spec.js
  create : leapmotion/spec/lib/adaptor.spec.js
  create : leapmotion/spec/lib/driver.spec.js

After running this, cd into the folder and look around:

$ cd leapmotion ; ls
Makefile  lib/  package.json  spec/

The generater makes a basic module, with Cylon as a dependency and a starting point for writing your own tests.

Writing Adaptors

Your module's Adaptor classes should subclass the Cylon.Adaptor core class, and are responsible for connecting to the platform you're integrating with. They should also handle any necessary device set-up, and proxy events the user should know about.

Some things to note about Adaptor subclasses:

They must implement #connect - the 'connect' method dictates what setup an Adaptor needs to do on startup. This might involve connecting to the hardware, or listening for events. This method must be implemented, and trigger the provided callback, or Cylon cannot start properly.

They must implement #disconnect - the 'disconnect' method dictates what teardown a Adaptor needs to do on shutdown. This can involve safely disconnecting from hardware, restoring device state, etc. This method must be implemented, and trigger the provided callback, or Cylon cannot shut down properly.

For an example, here's the Adaptor from the cylon-ardrone module, with some explanatory comments added below:

"use strict";

var LibARDrone = require('ar-drone'),
    Cylon = require('cylon');

// an Array of commands the ARDrone library responds to
// e.g. "takeoff", "hover", "land"
var Commands = require('./commands');

/**
 * An ARDrone adaptor
 *
 * @constructor ardrone
 *
 * @param {Object} opts
 * @param {String=} opts.host IP address of the ARDrone
 */
var ARDrone = module.exports = function ARDrone(opts) {
  ARDrone.__super__.constructor.apply(this, arguments);
  opts = opts || {};

  this.ip = opts.host || opts.port || "192.168.1.1";

  this.connector = this.ardrone = null;

  this.events = events;
};

Cylon.Utils.subclass(ARDrone, Cylon.Adaptor);

/**
 * Connects to the ARDrone
 *
 * @param {Function} callback to be triggered when connected
 * @return {null}
 */
ARDrone.prototype.connect = function(callback) {
  this.connector = this.ardrone = new LibARDrone.createClient({
    ip: this.ip
  });

  this.proxyMethods(Commands, this.ardrone, this);

  this.events.forEach(function(name) {
    this.defineAdaptorEvent(name);
  }.bind(this));

  callback();
};

/**
 * Disconnects from the ARDrone
 *
 * @param {Function} callback to be triggered when disconnected
 * @return {null}
 */
ARDrone.prototype.disconnect = function(callback) {
  callback();
}

Writing Drivers

Drivers subclass Cylon.Driver, and are the parts of your system that provide a higher-level abstraction over the Adaptor. They should provide wrapping of commands where helpful, and process data from events if needed.

A few things to note about Driver subclasses:

They must implement #start - the 'start' method dictates what setup a Driver needs to do on startup. This might involve setting initial state, or listening for events. This method must be implemented, and trigger the provided callback, or Cylon cannot start properly.

They must implement #halt - the 'halt' method dictates what teardown a Driver needs to do on shutdown. This can involve restoring state, clearing up references, etc. This method must be implemented, and trigger the provided callback, or Cylon cannot shut down properly.

The Adaptor instance associated with your Device instance is accessible as this.connection.

The #setupCommands method is your friend - pass it an array of methods, and it'll automatically proxy them to the connection with no user intervention. They will also be snake_cased and added as API commands.

Here's the example driver from the cylon-sphero module.

'use strict';

var Cylon = require('cylon');

// an Array of commands the Sphero library/Adaptor responds to
// e.g. "roll", "setRGB", "stop"
var Commands = require('./commands');

var Driver = module.exports = function Driver() {
  Driver.__super__.constructor.apply(this, arguments);
  this.setupCommands(Commands);
};

Cylon.Utils.subclass(Driver, Cylon.Driver);

// Public: Starts the driver.
//
// callback - params
//
// Returns null.
Driver.prototype.start = function(callback) {
  var events = ['message', 'update', 'notification', 'collision', 'data'];

  events.forEach(function(e) {
    this.defineDriverEvent(e);
  }.bind(this));

  this.connection.setTemporaryOptionFlags(0x01);

  callback();
};

Driver.prototype.halt = function(callback) {
  callback();
};

// Public: This commands Sphero to roll along the provided vector. Both a speed and a
// heading are required; the latter is considered relative to the last calibrated direction.
//
// speed - params
// heading - params
// state - params
//
// Returns null.
Driver.prototype.roll = function(speed, heading, state) {
  if (state == null) {
    state = 1;
  }

  this.connection.roll(speed, heading, state);
};

// Public: Sets the sphero to detect collisions and report them.
//
// Returns null.
Driver.prototype.detectCollisions = function() {
  this.connection.detectCollisions();
};

// Public: Sets the sphero to detect collisions and report them.
//
// Returns null.
Driver.prototype.detectLocator = function() {
  this.connection.detectLocator();
};

// Public: Stop the driver.
//
// Returns null.
Driver.prototype.stop = function() {
  this.connection.stop();
};

// Public: This allows you to set the RGB LED color, just pass an array containing
// RGB hex or a string with one of the color names of the list.
//
// color - params
// persist - params
//
// Returns null.
Driver.prototype.setRGB = function(color, persist) {
  if (persist == null) {
    persist = true;
  }

  this.connection.setRGB(color, persist);
};

// Public: Starts the calibration of the driver.
//
// Returns null.
Driver.prototype.startCalibration = function() {
  this.connection.setBackLED(127);
  this.connection.setStabilization(0);
};

// Public: Finish the calibration of the driver.
//
// Returns null.
Driver.prototype.finishCalibration = function() {
  this.connection.setHeading(0);
  this.connection.setBackLED(0);
  this.connection.setStabilization(1);
};

Event Propagation

Inside of Drivers and Adaptors, you can manually emit events (this.emit). This will work for most drivers, and lets you tweak data from events you're listening to to make it more user-friendly.

However, if you just want to pipe events through from your source (this.connector in Adaptor, this.connection in Driver), Cylon provides a shortcut for you, in the form of the Adaptor#defineAdaptorEvent and Driver#defineDriverEvent functions.

The functions can be used in a number of ways, which we'll go through now. For purposes of simplification, we'll be talking about Adaptor#defineAdaptorEvent. But for Drivers, you simply change the method name to #defineDriverEvent, the arguments follow the same pattern.

#defineDriverEvent also proxies from this.connection, instead of this.connector as in #defineAdaptorEvent.

With a source and target event name. This is the most flexible approach, and lets you rename events that will actually be received by the robot. Additionally, if you pass the 'update' param, an update event will also be emitted:

this.defineAdaptorEvent({
  eventName: 'greeting',
  targetEventName: 'hello',
  update: true
});

With this code, whenever the Adaptor's connector receives the 'greeting' event, it will emit the 'hello' and 'update' events, with the data that the 'greeting' event received.

With just an eventName. This invocation assumes you want the event to be proxied 1-to-1.

this.defineAdaptorEvent({ eventName: 'greeting' });

This invocation means that when the connector receives the 'greeting' event, an identical event will be emitted from the Adaptor.

With just an eventName (string version). Similar to the above example, but a bit shorter.

this.defineAdaptorEvent('greeting');