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');