diff --git a/lib/connection.js b/lib/connection.js index abc2082..cd50a34 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -10,9 +10,12 @@ var EventEmitter = require('events').EventEmitter; -var Logger = require('./logger'), +var Registry = require('./registry'), + Logger = require('./logger'), Utils = require('./utils'); +var testMode = process.env.NODE_ENV === 'test' && !CYLON_TEST; + // Public: Creates a new Connection // // opts - hash of acceptable params: @@ -87,8 +90,30 @@ Connection.prototype.disconnect = function(callback) { // // Returns the set-up adaptor Connection.prototype.initAdaptor = function(opts) { - Logger.debug("Loading adaptor '" + opts.adaptor + "'."); - return this.robot.initAdaptor(opts.adaptor, this, opts); + var module = Registry.findByAdaptor(opts.adaptor); + + opts.connection = this; + + if (!module) { + Registry.register('cylon-' + opts.adaptor); + module = Registry.findByAdaptor(opts.adaptor); + } + + var adaptor = module.adaptor(opts); + + if (testMode) { + var testAdaptor = Registry.findByAdaptor('test').adaptor(opts); + + for (var prop in adaptor) { + if (typeof adaptor[prop] === 'function' && !testAdaptor[prop]) { + testAdaptor[prop] = function() { return true; }; + } + } + + return testAdaptor; + } + + return adaptor; }; Connection.prototype._logstring = function _logstring(action) { diff --git a/lib/device.js b/lib/device.js index 6abce55..ec621f1 100644 --- a/lib/device.js +++ b/lib/device.js @@ -10,9 +10,12 @@ var EventEmitter = require('events').EventEmitter; -var Logger = require('./logger'), +var Registry = require('./registry'), + Logger = require('./logger'), Utils = require('./utils'); +var testMode = process.env.NODE_ENV === 'test' && !CYLON_TEST; + // Public: Creates a new Device // // opts - object containing Device params @@ -34,7 +37,7 @@ var Device = module.exports = function Device(opts) { this.robot = opts.robot; this.name = opts.name; this.pin = opts.pin; - this.connection = this.determineConnection(opts.connection) || this.defaultConnection(); + this.connection = opts.connection; this.driver = this.initDriver(opts); this.details = {}; @@ -93,29 +96,6 @@ Device.prototype.toJSON = function() { }; }; -// Public: Retrieves the connections from the parent Robot instances -// -// conn - name of the connection to fetch -// -// Returns a Connection instance -Device.prototype.determineConnection = function(conn) { - return this.robot.connections[conn]; -}; - -// Public: Returns a default Connection to use -// -// Returns a Connection instance -Device.prototype.defaultConnection = function() { - var first = 0; - - for (var c in this.robot.connections) { - var connection = this.robot.connections[c]; - first = first || connection; - } - - return first; -}; - // Public: sets up driver with @robot // // opts - object containing options when initializing driver @@ -123,10 +103,28 @@ Device.prototype.defaultConnection = function() { // // Returns the set-up driver Device.prototype.initDriver = function(opts) { - if (opts == null) { - opts = {}; + var module = Registry.findByDriver(opts.driver); + + opts.device = this; + + if (!module) { + Registry.register('cylon-' + opts.driver); + module = Registry.findByDriver(opts.driver); } - Logger.debug("Loading driver '" + opts.driver + "'."); - return this.robot.initDriver(opts.driver, this, opts); + var driver = module.driver(opts); + + if (testMode) { + var testDriver = Registry.findByDriver('test').driver(opts); + + for (var prop in driver) { + if (typeof driver[prop] === 'function' && !testDriver[prop]) { + testDriver[prop] = function() { return true; }; + } + } + + return testDriver; + } + + return driver; }; diff --git a/lib/driver.js b/lib/driver.js index 798217e..7867efa 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -21,13 +21,14 @@ var Basestar = require('./basestar'), // Returns a new Driver var Driver = module.exports = function Driver(opts) { opts = opts || {}; - var extraParams = opts.extraParams || {} this.name = opts.name; + this.device = opts.device; - this.connection = this.device.connection; + this.connection = opts.connection; this.adaptor = this.connection.adaptor; - this.interval = extraParams.interval || 10; + + this.interval = opts.interval || 10; this.commands = {}; }; diff --git a/lib/registry.js b/lib/registry.js new file mode 100644 index 0000000..d1c4807 --- /dev/null +++ b/lib/registry.js @@ -0,0 +1,81 @@ +/* + * Registry + * + * The Registry contains references to all Drivers and Adaptors Cylon is aware + * of, along with which module they live in (e.g. cylon-firmata). + * + * cylonjs.com + * + * Copyright (c) 2013-2014 The Hybrid Group + * Licensed under the Apache 2.0 license. +*/ + +"use strict"; + +var missingModuleError = function(module) { + var string = "Cannot find the '" + module + "' module.\n"; + string += "This problem might be fixed by installing it with 'npm install " + module + "' and trying again."; + + console.log(string); + + process.emit('SIGINT'); +}; + +var Registry = module.exports = { + data: {}, + + register: function(module) { + if (this.data[module]) { + return; + } + + var pkg; + + try { + pkg = require(module); + } catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + missingModuleError(module); + } + + throw e; + } + + this.data[module] = { + module: pkg, + adaptors: pkg.adaptors || [], + drivers: pkg.drivers || [] + }; + + if (pkg.dependencies && Array.isArray(pkg.dependencies)) { + pkg.dependencies.forEach(function(dep) { + Registry.register(dep); + }); + } + }, + + findByAdaptor: function(adaptor) { + return this.search("adaptors", adaptor); + }, + + findByDriver: function(driver) { + return this.search("drivers", driver); + }, + + search: function(entry, value) { + for (var name in this.data) { + var repo = this.data[name]; + + if (~repo[entry].indexOf(value)) { + return repo.module; + } + } + + return false; + } +}; + +// Default drivers/adaptors: +['loopback', 'ping', 'test-adaptor', 'test-driver'].forEach(function(module) { + Registry.register('./test/' + module); +}); diff --git a/lib/robot.js b/lib/robot.js index 122d8bc..b11026a 100644 --- a/lib/robot.js +++ b/lib/robot.js @@ -60,10 +60,6 @@ var Robot = module.exports = function Robot(opts) { var methods = [ "toString", - "registerDriver", - "requireDriver", - "registerAdaptor", - "requireAdaptor", "halt", "startDevices", "startConnections", @@ -89,18 +85,9 @@ var Robot = module.exports = function Robot(opts) { this.work = function() { Logger.debug("No work yet."); }; } - this.registerDefaults(); - this.initConnections(opts.connection || opts.connections); this.initDevices(opts.device || opts.devices); - var hasDevices = !!Object.keys(this.devices).length, - hasConnections = !!Object.keys(this.connections).length; - - if (hasDevices && !hasConnections) { - throw new Error("No connections specified"); - } - for (var n in opts) { var opt = opts[n]; @@ -143,17 +130,6 @@ var Robot = module.exports = function Robot(opts) { Utils.subclass(Robot, EventEmitter); -// Public: Registers the default Drivers and Adaptors with Cylon. -// -// Returns nothing. -Robot.prototype.registerDefaults = function registerDefaults() { - this.registerAdaptor("./test/loopback", "loopback"); - this.registerAdaptor("./test/test-adaptor", "test"); - - this.registerDriver("./test/ping", "ping"); - this.registerDriver("./test/test-driver", "test"); -}; - // Public: Generates a random name for a Robot. // // Returns a string name @@ -200,6 +176,8 @@ Robot.prototype.initConnections = function(connections) { connections = [].concat(connections); connections.forEach(function(conn) { + conn.robot = this; + if (this.connections[conn.name]) { var original = conn.name; conn.name = Utils.makeUnique(original, Object.keys(this.connections)); @@ -207,7 +185,7 @@ Robot.prototype.initConnections = function(connections) { } Logger.info("Initializing connection '" + conn.name + "'."); - conn['robot'] = this; + this.connections[conn.name] = new Connection(conn); }.bind(this)); @@ -226,17 +204,40 @@ Robot.prototype.initDevices = function(devices) { return; } + // check that there are connections to use + if (!Object.keys(this.connections).length) { + throw new Error("No connections specified") + } + devices = [].concat(devices); devices.forEach(function(device) { + device.robot = this; + if (this.devices[device.name]) { var original = device.name; device.name = Utils.makeUnique(original, Object.keys(this.devices)); Logger.warn("Device names must be unique. Renaming '" + original + "' to '" + device.name + "'"); } + if (typeof device.connection === 'string') { + if (this.connections[device.connection] == null) { + var str = "No connection found with the name " + device.connection + ".\n"; + Logger.fatal(str); + process.emit('SIGINT'); + } + + device.connection = this.connections[device.connection]; + } else { + for (var conn in this.connections) { + device.connection = this.connections[conn]; + break; + } + } + + device.adaptor = device.connection.adaptor; + Logger.info("Initializing device '" + device.name + "'."); - device['robot'] = this; this.devices[device.name] = new Device(device); }.bind(this)); @@ -365,142 +366,6 @@ Robot.prototype.halt = function(callback) { this.running = false; }; -// Public: Initialize an adaptor and adds it to @robot.adaptors -// -// adaptorName - module name of adaptor to require -// connection - the Connection that requested the adaptor be required -// -// Returns the adaptor -Robot.prototype.initAdaptor = function(adaptorName, connection, opts) { - if (opts == null) { - opts = {}; - } - - var adaptor = this.requireAdaptor(adaptorName, opts).adaptor({ - name: adaptorName, - connection: connection, - extraParams: opts - }); - - if (process.env.NODE_ENV === 'test' && !CYLON_TEST) { - var testAdaptor = this.requireAdaptor('test').adaptor({ - name: adaptorName, - connection: connection, - extraParams: opts - }); - - Utils.proxyTestStubs(adaptor.commands, testAdaptor); - - for (var prop in adaptor) { - if (typeof adaptor[prop] === 'function' && !testAdaptor[prop]) { - testAdaptor[prop] = function() { return true; }; - } - } - - return testAdaptor; - } else { - return adaptor; - } -}; - -// Public: Requires a hardware adaptor and adds it to @robot.adaptors -// -// adaptorName - module name of adaptor to require -// -// Returns the module for the adaptor -Robot.prototype.requireAdaptor = function(adaptorName, opts) { - if (this.adaptors[adaptorName] == null) { - var moduleName = opts.module || adaptorName; - this.registerAdaptor("cylon-" + moduleName, adaptorName); - this.adaptors[adaptorName].register(this); - } - return this.adaptors[adaptorName]; -}; - -// Public: Registers an Adaptor with the Robot -// -// moduleName - name of the Node module to require -// adaptorName - name of the adaptor to register the moduleName under -// -// Returns the registered module name -Robot.prototype.registerAdaptor = function(moduleName, adaptorName) { - if (this.adaptors[adaptorName] == null) { - try { - return this.adaptors[adaptorName] = require(moduleName); - } catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - missingModuleError(moduleName); - } else { - throw e; - } - } - } -}; - -// Public: Init a hardware driver -// -// driverName - driver name -// device - the Device that requested the driver be initialized -// opts - object containing options when initializing driver -// -// Returns the new driver -Robot.prototype.initDriver = function(driverName, device, opts) { - if (opts == null) { - opts = {}; - } - - var driver = this.requireDriver(driverName).driver({ - name: driverName, - device: device, - extraParams: opts - }); - - if (process.env.NODE_ENV === 'test' && !CYLON_TEST) { - var testDriver = this.requireDriver('test').driver({ - name: driverName, - device: device, - extraParams: opts - }); - - return Utils.proxyTestStubs(driver.commands, testDriver); - } else { - return driver; - } -}; - -// Public: Requires module for a driver and adds it to @robot.drivers -// -// driverName - module name of driver to require -// -// Returns the module for driver -Robot.prototype.requireDriver = function(driverName) { - if (this.drivers[driverName] == null) { - this.registerDriver("cylon-" + driverName, driverName); - this.drivers[driverName].register(this); - } - return this.drivers[driverName]; -}; - -// Public: Registers an Driver with the Robot -// -// moduleName - name of the Node module to require -// driverName - name of the driver to register the moduleName under -// -// Returns the registered module name -Robot.prototype.registerDriver = function(moduleName, driverName) { - if (this.drivers[driverName] == null) { - try { - return this.drivers[driverName] = require(moduleName); - } catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - missingModuleError(moduleName); - } else { - throw e; - } - } - } -}; - // Public: Returns basic info about the robot as a String // // Returns a String diff --git a/lib/test/loopback.js b/lib/test/loopback.js index ee651fd..9a39d0d 100644 --- a/lib/test/loopback.js +++ b/lib/test/loopback.js @@ -27,4 +27,5 @@ Loopback.prototype.disconnect = function(callback) { callback(); }; +Loopback.adaptors = ['loopback']; Loopback.adaptor = function(opts) { return new Loopback(opts); }; diff --git a/lib/test/ping.js b/lib/test/ping.js index 3be176a..47564e3 100644 --- a/lib/test/ping.js +++ b/lib/test/ping.js @@ -34,6 +34,5 @@ Ping.prototype.halt = function(callback) { callback(); }; -Ping.driver = function(opts) { - return new Ping(opts); -}; +Ping.drivers = ['ping']; +Ping.driver = function(opts) { return new Ping(opts); }; diff --git a/lib/test/test-adaptor.js b/lib/test/test-adaptor.js index a00520e..3918a6d 100644 --- a/lib/test/test-adaptor.js +++ b/lib/test/test-adaptor.js @@ -19,4 +19,5 @@ module.exports = TestAdaptor = function TestAdaptor() { Utils.subclass(TestAdaptor, Adaptor); +TestAdaptor.adaptors = ['test']; TestAdaptor.adaptor = function(opts) { return new TestAdaptor(opts); }; diff --git a/lib/test/test-driver.js b/lib/test/test-driver.js index b2c2253..640c5ae 100644 --- a/lib/test/test-driver.js +++ b/lib/test/test-driver.js @@ -19,4 +19,5 @@ module.exports = TestDriver = function TestDriver() { Utils.subclass(TestDriver, Driver); +TestDriver.drivers = ['test']; TestDriver.driver = function(opts) { return new TestDriver(opts); }; diff --git a/spec/lib/device.spec.js b/spec/lib/device.spec.js index 30b34f4..3f1b2c5 100644 --- a/spec/lib/device.spec.js +++ b/spec/lib/device.spec.js @@ -19,23 +19,28 @@ describe("Device", function() { driver = new Ping({ name: 'driver', - device: { connection: connection, port: 13 } + device: { connection: connection, pin: 13 }, + connection: connection }); driver.cmd = spy(); driver.string = ""; driver.robot = spy(); - initDriver = stub(robot, 'initDriver').returns(driver); + initDriver = stub(Device.prototype, 'initDriver').returns(driver); device = new Device({ robot: robot, name: "ping", pin: 13, - connection: 'loopback' + connection: connection }); }); + afterEach(function() { + initDriver.restore(); + }); + describe("constructor", function() { it("sets @robot to the passed robot", function() { expect(device.robot).to.be.eql(robot); @@ -56,7 +61,6 @@ describe("Device", function() { it("asks the Robot to init a driver", function() { expect(device.driver).to.be.eql(driver); expect(initDriver).to.be.calledOnce - initDriver.restore(); }); it("does not override existing functions", function() { @@ -158,16 +162,4 @@ describe("Device", function() { expect(json.commands).to.be.eql(Object.keys(driver.commands)); }); }); - - describe("#determineConnection", function() { - it("returns the connection with the given name from the Robot", function() { - expect(device.determineConnection("loopback")).to.be.eql(connection); - }); - }); - - describe("#defaultConnection", function() { - it("returns the first connection found on the robot", function() { - expect(device.defaultConnection()).to.be.eql(connection); - }); - }); }); diff --git a/spec/lib/driver.spec.js b/spec/lib/driver.spec.js index 6a5ca37..8df2c4e 100644 --- a/spec/lib/driver.spec.js +++ b/spec/lib/driver.spec.js @@ -7,17 +7,22 @@ var Driver = source("driver"), Utils = source('utils'); describe("Driver", function() { - var device, driver; + var connection, device, driver; beforeEach(function() { + connection = { + adaptor: 'adaptor' + }; + device = { - connection: {}, + connection: connection, emit: spy() }; driver = new Driver({ name: 'driver', - device: device + device: device, + connection: connection }); }); @@ -44,13 +49,14 @@ describe("Driver", function() { it("sets @interval to 10ms by default, or the provided value", function() { expect(driver.interval).to.be.eql(10); + driver = new Driver({ name: 'driver', device: device, - extraParams: { - interval: 2000 - } + interval: 2000, + connection: { } }); + expect(driver.interval).to.be.eql(2000); }); }); diff --git a/spec/lib/registry.spec.js b/spec/lib/registry.spec.js new file mode 100644 index 0000000..cffd331 --- /dev/null +++ b/spec/lib/registry.spec.js @@ -0,0 +1,56 @@ +"use strict"; + +var Registry = source('registry'); + +var path = './../spec/support/mock_module.js'; + +var module = require('./../support/mock_module.js') + +describe("Registry", function() { + var original; + + beforeEach(function() { + original = Registry.data; + Registry.data = {}; + }); + + afterEach(function() { + Registry.data = original; + }) + + describe("#register", function() { + it("adds the supplied module to the Registry", function() { + expect(Registry.data).to.be.eql({}); + + Registry.register(path); + + expect(Registry.data).to.be.eql({ + "./../spec/support/mock_module.js": { + module: module, + drivers: ['test-driver'], + adaptors: ['test-adaptor'] + } + }); + }); + }); + + describe("#findByAdaptor", function() { + beforeEach(function() { + Registry.register(path) + }); + + it("finds the appropriate module containing the adaptor", function() { + expect(Registry.findByAdaptor('test-adaptor')).to.be.eql(module); + }); + }); + + describe("#findByDriver", function() { + beforeEach(function() { + Registry.register(path) + }); + + it("finds the appropriate module containing the driver", function() { + expect(Registry.findByDriver('test-driver')).to.be.eql(module); + }); + }); +}); diff --git a/spec/lib/robot.spec.js b/spec/lib/robot.spec.js index be93fd5..6057812 100644 --- a/spec/lib/robot.spec.js +++ b/spec/lib/robot.spec.js @@ -59,14 +59,6 @@ describe("Robot", function() { expect(robot.devices).to.be.eql({}); }); - it("sets @adaptors to an object containing all adaptors the Robot knows of", function() { - expect(robot.adaptors).to.have.keys(["loopback", "test"]); - }); - - it("sets @drivers to an object containing all drivers the Robot knows of", function() { - expect(robot.drivers).to.have.keys(["ping", "test"]); - }); - it("sets @work to the passed work function", function() { expect(robot.work).to.be.eql(work); }); @@ -277,7 +269,9 @@ describe("Robot", function() { var bot; beforeEach(function() { - bot = new Robot(); + bot = new Robot({ + connection: { name: 'loopback', adaptor: 'loopback' } + }); }); context("when not passed anything", function() { diff --git a/spec/support/mock_module.js b/spec/support/mock_module.js new file mode 100644 index 0000000..7c1f82c --- /dev/null +++ b/spec/support/mock_module.js @@ -0,0 +1,7 @@ +// A mock Cylon module for use in internal testing. +"use strict"; + +module.exports = { + adaptors: [ 'test-adaptor' ], + drivers: [ 'test-driver' ] +};