diff --git a/index.js b/index.js new file mode 100644 index 0000000..3f23808 --- /dev/null +++ b/index.js @@ -0,0 +1,43 @@ +"use strict"; + +function lib(path) { return require("./lib/" + path); } + +var Config = lib("config"), + MCP = lib("mcp"), + API = lib("api"); + +var exports = module.exports = {}; + +exports.MCP = lib("mcp"); +exports.Robot = lib("robot"); +exports.Driver = lib("driver"); +exports.Adaptor = lib("adaptor"); + +exports.Utils = lib("utils"); +exports.Logger = lib("logger"); + +exports.IO = { + DigitalPin: lib("io/digital-pin"), + Utils: lib("io/utils") +}; + +exports.robot = MCP.create; +exports.start = MCP.start; +exports.halt = MCP.halt; + +exports.api = API.create; + +exports.config = Config.update; + +process.on("SIGINT", function() { + exports.halt(function() { + process.kill(process.pid); + }); +}); + +if (process.platform === "win32") { + var io = { input: process.stdin, output: process.stdout }, + quit = process.emit.bind(process, "SIGINT"); + + require("readline").createInterface(io).on("SIGINT", quit); +} diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..4de7927 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,52 @@ +"use strict"; + +var MCP = require("./mcp"), + Logger = require("./logger"), + _ = require("./utils/helpers"); + +var api = module.exports = {}; + +api.instances = []; + +/** + * Creates a new API instance + * + * @param {String} [Server] which API plugin to use (e.g. "http" loads + * cylon-api-http) + * @param {Object} opts options for the new API instance + * @return {void} + */ +api.create = function create(Server, opts) { + // if only passed options (or nothing), assume HTTP server + if (Server == null || _.isObject(Server) && !_.isFunction(Server)) { + opts = Server; + Server = "http"; + } + + opts = opts || {}; + + if (_.isString(Server)) { + var req = "cylon-api-" + Server; + + try { + Server = require(req); + } catch (e) { + if (e.code !== "MODULE_NOT_FOUND") { + throw e; + } + + [ + "Cannot find the " + req + " API module.", + "You may be able to install it: `npm install " + req + "`" + ].forEach(_.arity(Logger.fatal, 1)); + + throw new Error("Missing API plugin - cannot proceed"); + } + } + + opts.mcp = MCP; + + var instance = new Server(opts); + api.instances.push(instance); + instance.start(); +}; diff --git a/lib/config.js b/lib/config.js index 98387bb..befd71e 100644 --- a/lib/config.js +++ b/lib/config.js @@ -7,13 +7,14 @@ var config = module.exports = {}, // default data config.logging = {}; +config.haltTimeout = 3000; config.testMode = false; /** * Updates the Config, and triggers handler callbacks * * @param {Object} data new configuration information to set - * @return {void} + * @return {Object} the updated configuration */ config.update = function update(data) { var forbidden = ["update", "subscribe", "unsubscribe"]; @@ -23,12 +24,14 @@ config.update = function update(data) { }); if (!Object.keys(data).length) { - return; + return config; } _.extend(config, data); callbacks.forEach(function(callback) { callback(data); }); + + return config; }; /** diff --git a/lib/cylon.js b/lib/cylon.js deleted file mode 100644 index 1b2db34..0000000 --- a/lib/cylon.js +++ /dev/null @@ -1,164 +0,0 @@ -"use strict"; - -var Logger = require("./logger"), - Robot = require("./robot"), - Config = require("./config"), - Utils = require("./utils"), - _ = require("./utils/helpers"); - -var EventEmitter = require("events").EventEmitter; - -var Cylon = module.exports = new EventEmitter(); - -Cylon.Logger = Logger; -Cylon.Driver = require("./driver"); -Cylon.Adaptor = require("./adaptor"); -Cylon.Utils = Utils; - -Cylon.IO = { - DigitalPin: require("./io/digital-pin"), - Utils: require("./io/utils") -}; - -Cylon.apiInstances = []; - -Cylon.robots = {}; -Cylon.commands = {}; - -Cylon.events = [ "robot_added", "robot_removed" ]; - -// Public: Creates a new Robot -// -// opts - hash of Robot attributes -// -// Returns a shiny new Robot -// Examples: -// Cylon.robot -// connection: { name: "arduino", adaptor: "firmata" } -// device: { name: "led", driver: "led", pin: 13 } -// -// work: (me) -> -// me.led.toggle() -Cylon.robot = function robot(opts) { - opts = opts || {}; - - // check if a robot with the same name exists already - if (opts.name && this.robots[opts.name]) { - var original = opts.name; - opts.name = Utils.makeUnique(original, Object.keys(this.robots)); - - var str = "Robot names must be unique. Renaming '"; - str += original + "' to '" + opts.name + "'"; - - Logger.warn(str); - } - - var bot = new Robot(opts); - this.robots[bot.name] = bot; - this.emit("robot_added", bot.name); - - return bot; -}; - -// Public: Initializes an API instance based on provided options. -// -// Returns nothing -Cylon.api = function api(Server, opts) { - // if only passed options (or nothing), assume HTTP server - if (Server == null || _.isObject(Server) && !_.isFunction(Server)) { - opts = Server; - Server = "http"; - } - - opts = opts || {}; - - if (_.isString(Server)) { - var req = "cylon-api-" + Server; - - try { - Server = require(req); - } catch (e) { - if (e.code !== "MODULE_NOT_FOUND") { - throw e; - } - - [ - "Cannot find the " + req + " API module.", - "You may be able to install it: `npm install " + req + "`" - ].forEach(_.arity(Logger.fatal, 1)); - - throw new Error("Missing API plugin - cannot proceed"); - } - } - - opts.mcp = this; - var instance = new Server(opts); - this.apiInstances.push(instance); - instance.start(); -}; - -// Public: Starts up the API and the robots -// -// Returns nothing -Cylon.start = function start() { - var starters = _.pluck(this.robots, "start"); - - _.parallel(starters, function() { - var mode = Utils.fetch(Config, "workMode", "async"); - - if (mode === "sync") { - _.invoke(this.robots, "startWork"); - } - }.bind(this)); -}; - -// Public: Sets the internal configuration, based on passed options -// -// opts - object containing configuration key/value pairs -// -// Returns the current config -Cylon.config = function(opts) { - if (_.isObject(opts)) { Config.update(opts); } - return Config; -}; - -// Public: Halts the API and the robots -// -// callback - callback to be triggered when Cylon is ready to shutdown -// -// Returns nothing -Cylon.halt = function halt(callback) { - callback = callback || function() {}; - - var fns = _.pluck(this.robots, "halt"); - - // if robots can"t shut down quickly enough, forcefully self-terminate - var timeout = Config.haltTimeout || 3000; - Utils.after(timeout, callback); - - _.parallel(fns, callback); -}; - -Cylon.toJSON = function() { - return { - robots: _.invoke(this.robots, "toJSON"), - commands: Object.keys(this.commands), - events: this.events - }; -}; - -if (process.platform === "win32") { - var readline = require("readline"); - - var io = { input: process.stdin, output: process.stdout }; - - readline.createInterface(io).on("SIGINT", function() { - process.emit("SIGINT"); - }); -} - -process.on("SIGINT", function() { - Cylon.halt(function() { - process.kill(process.pid); - }); -}); diff --git a/lib/mcp.js b/lib/mcp.js new file mode 100644 index 0000000..112e23b --- /dev/null +++ b/lib/mcp.js @@ -0,0 +1,82 @@ +"use strict"; + +var EventEmitter = require("events").EventEmitter; + +var Config = require("./config"), + Logger = require("./logger"), + Utils = require("./utils"), + Robot = require("./robot"), + _ = require("./utils/helpers"); + +var mcp = module.exports = new EventEmitter(); + +mcp.robots = {}; +mcp.commands = {}; +mcp.events = [ "robot_added", "robot_removed" ]; + +/** + * Creates a new Robot with the provided options. + * + * @param {Object} opts robot options + * @return {Robot} the new robot + */ +mcp.create = function create(opts) { + opts = opts || {}; + + // check if a robot with the same name exists already + if (opts.name && mcp.robots[opts.name]) { + var original = opts.name; + opts.name = Utils.makeUnique(original, Object.keys(mcp.robots)); + + var str = "Robot names must be unique. Renaming '"; + str += original + "' to '" + opts.name + "'"; + + Logger.warn(str); + } + + var bot = new Robot(opts); + mcp.robots[bot.name] = bot; + mcp.emit("robot_added", bot.name); + + return bot; +}; + +mcp.start = function start(callback) { + var fns = _.pluck(mcp.robots, "start"); + + _.parallel(fns, function() { + var mode = Utils.fetch(Config, "workMode", "async"); + if (mode === "sync") { _.invoke(mcp.robots, "startWork"); } + callback(); + }); +}; + +/** + * Halts all MCP robots. + * + * @param {Function} callback function to call when done halting robots + * @return {void} + */ +mcp.halt = function halt(callback) { + callback = callback || function() {}; + + var timeout = setTimeout(callback, Config.haltTimeout || 3000); + + _.parallel(_.pluck(mcp.robots, "halt"), function() { + clearTimeout(timeout); + callback(); + }); +}; + +/** + * Serializes MCP robots, commands, and events into a JSON-serializable Object. + * + * @return {Object} a serializable representation of the MCP + */ +mcp.toJSON = function() { + return { + robots: _.invoke(mcp.robots, "toJSON"), + commands: Object.keys(mcp.commands), + events: mcp.events + }; +}; diff --git a/lib/utils/helpers.js b/lib/utils/helpers.js index 5cfab44..9d5af0d 100644 --- a/lib/utils/helpers.js +++ b/lib/utils/helpers.js @@ -211,6 +211,8 @@ function parallel(functions, done) { results = [], error; + if (typeof done !== "function") { done = function() {}; } + function callback(err, result) { if (error) { return; @@ -243,6 +245,8 @@ function series(functions, done) { var results = [], error; + if (typeof done !== "function") { done = function() {}; } + function callback(err, result) { if (err || error) { error = err; diff --git a/package.json b/package.json index 04bf646..e9ebd8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "cylon", "version": "1.0.0", - "main": "lib/cylon.js", "description": "A JavaScript robotics framework for Node.js", "homepage": "http://cylonjs.com", "bugs": "https://github.com/hybridgroup/cylon/issues", @@ -37,7 +36,8 @@ "hardware": { "*": false, "./": false, - "./lib": true + "./lib": true, + "index.js": true }, "engines" : { diff --git a/spec/helper.js b/spec/helper.js index b4e0389..519a4f0 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -26,7 +26,7 @@ global.lib = function(module) { return require(path.normalize("./../lib/" + module)); }; -var Cylon = lib("cylon"); +var Cylon = require("./../"); Cylon.config({ mode: "manual", diff --git a/spec/lib/api.spec.js b/spec/lib/api.spec.js new file mode 100644 index 0000000..2afd68c --- /dev/null +++ b/spec/lib/api.spec.js @@ -0,0 +1,41 @@ +"use strict"; + +var API = lib("api"), + MCP = lib("mcp"); + +describe("API", function() { + describe("#create", function() { + afterEach(function() { + API.instances = []; + }); + + context("with a provided API server and opts", function() { + var Server, opts, instance; + + beforeEach(function() { + instance = { start: spy() }; + opts = { https: false }; + Server = stub().returns(instance); + + API.create(Server, opts); + }); + + it("creates an API instance", function() { + expect(Server).to.be.calledWithNew; + expect(Server).to.be.calledWith(opts); + }); + + it("passes MCP through to the instance as opts.mcp", function() { + expect(opts.mcp).to.be.eql(MCP); + }); + + it("stores the API instance in @instances", function() { + expect(API.instances).to.be.eql([instance]); + }); + + it("tells the API instance to start", function() { + expect(instance.start).to.be.called; + }); + }); + }); +}); diff --git a/spec/lib/cylon.spec.js b/spec/lib/cylon.spec.js index a8b1aa3..e51b182 100644 --- a/spec/lib/cylon.spec.js +++ b/spec/lib/cylon.spec.js @@ -1,186 +1,78 @@ "use strict"; -var Cylon = lib("cylon"), - Robot = lib("robot"); +var Cylon = lib("../index"); -var Logger = lib("logger"), - Adaptor = lib("adaptor"), +var MCP = lib("mcp"), + API = lib("api"), + Robot = lib("robot"), Driver = lib("driver"), - Config = lib("config"); + Adaptor = lib("adaptor"), + Utils = lib("utils"), + Config = lib("config"), + Logger = lib("logger"); + +var IO = { + DigitalPin: lib("io/digital-pin"), + Utils: lib("io/utils") +}; describe("Cylon", function() { - describe("exports", function() { - it("sets Logger to the Logger module", function() { - expect(Cylon.Logger).to.be.eql(Logger); - }); + it("exports the MCP as Cylon.MCP", function() { + expect(Cylon.MCP).to.be.eql(MCP); + }); - it("sets Adaptor to the Adaptor module", function() { - expect(Cylon.Adaptor).to.be.eql(Adaptor); - }); + it("exports the Robot as Cylon.Robot", function() { + expect(Cylon.Robot).to.be.eql(Robot); + }); - it("sets Driver to the Driver module", function() { - expect(Cylon.Driver).to.be.eql(Driver); - }); + it("exports the Driver as Cylon.Driver", function() { + expect(Cylon.Driver).to.be.eql(Driver); + }); - it("sets @apiInstances to an empty array by default", function() { - expect(Cylon.apiInstances).to.be.eql([]); - }); + it("exports the Adaptor as Cylon.Adaptor", function() { + expect(Cylon.Adaptor).to.be.eql(Adaptor); + }); - it("sets @robots to an empty object by default", function() { - expect(Cylon.robots).to.be.eql({}); - }); + it("exports the Utils as Cylon.Utils", function() { + expect(Cylon.Utils).to.be.eql(Utils); + }); - it("sets @robots to an empty object by default", function() { - expect(Cylon.commands).to.be.eql({}); - }); + + it("exports the Logger as Cylon.Logger", function() { + expect(Cylon.Logger).to.be.eql(Logger); + }); + + it("exports the IO DigitalPin and Utils as Cylon.IO", function() { + expect(Cylon.IO).to.be.eql(IO); }); describe("#robot", function() { - afterEach(function() { - Cylon.robots = {}; - }); - - it("uses passed options to create a new Robot", function() { - var opts = { name: "Ultron" }; - var robot = Cylon.robot(opts); - - expect(robot.toString()).to.be.eql("[Robot name='Ultron']"); - expect(Cylon.robots.Ultron).to.be.eql(robot); - }); - - it("avoids duplicating names", function() { - Cylon.robot({ name: "Ultron" }); - Cylon.robot({ name: "Ultron" }); - - var bots = Object.keys(Cylon.robots); - expect(bots).to.be.eql(["Ultron", "Ultron-1"]); - }); - }); - - describe("#api", function() { - afterEach(function() { - Cylon.apiInstances = []; - }); - - context("with a provided API server and opts", function() { - var API, opts, instance; - - beforeEach(function() { - instance = { start: spy() }; - opts = { https: false }; - API = stub().returns(instance); - - Cylon.api(API, opts); - }); - - it("creates an API instance", function() { - expect(API).to.be.calledWithNew; - expect(API).to.be.calledWith(opts); - }); - - it("passes Cylon through to the instance as opts.mcp", function() { - expect(opts.mcp).to.be.eql(Cylon); - }); - - it("stores the API instance in @apiInstances", function() { - expect(Cylon.apiInstances).to.be.eql([instance]); - }); - - it("tells the API instance to start", function() { - expect(instance.start).to.be.called; - }); + it("proxies to MCP.create", function() { + expect(Cylon.robot).to.be.eql(MCP.create); }); }); describe("#start", function() { - it("calls #start() on all robots", function() { - var bot1 = { start: spy() }, - bot2 = { start: spy() }; - - Cylon.robots = { - bot1: bot1, - bot2: bot2 - }; - - Cylon.start(); - - expect(bot1.start).to.be.called; - expect(bot2.start).to.be.called; - }); - }); - - describe("#config", function() { - beforeEach(function() { - delete Config.a; - delete Config.b; - }); - - it("sets config variables", function() { - Cylon.config({ a: 1, b: 2 }); - expect(Config.a).to.be.eql(1); - expect(Config.b).to.be.eql(2); - }); - - it("updates existing config", function() { - Cylon.config({ a: 1, b: 2 }); - Cylon.config({ a: 3 }); - expect(Config.a).to.be.eql(3); - expect(Config.b).to.be.eql(2); - }); - - it("returns updated config", function() { - var config = Cylon.config({ a: 1, b: 2 }); - expect(Config).to.be.eql(config); - }); - - it("doesn't ignores non-object arguments", function() { - var config = Cylon.config({ a: 1, b: 2 }); - Cylon.config(["a", 1, "b", 2]); - Cylon.config("hello world"); - expect(Config).to.be.eql(config); + it("proxies to MCP.start", function() { + expect(Cylon.start).to.be.eql(MCP.start); }); }); describe("#halt", function() { - it("calls #halt() on all robots", function() { - var bot1 = { halt: spy() }, - bot2 = { halt: spy() }; - - Cylon.robots = { - bot1: bot1, - bot2: bot2 - }; - - Cylon.halt(); - - expect(bot1.halt).to.be.called; - expect(bot2.halt).to.be.called; + it("proxies to MCP.halt", function() { + expect(Cylon.halt).to.be.eql(MCP.halt); }); }); - describe("#toJSON", function() { - var json, bot1, bot2; - - beforeEach(function() { - bot1 = new Robot(); - bot2 = new Robot(); - - Cylon.robots = { bot1: bot1, bot2: bot2 }; - Cylon.commands.echo = function(arg) { return arg; }; - - json = Cylon.toJSON(); + describe("#api", function() { + it("proxies to API.create", function() { + expect(Cylon.api).to.be.eql(API.create); }); + }); - it("contains all robots the MCP knows about", function() { - expect(json.robots).to.be.eql([bot1.toJSON(), bot2.toJSON()]); - }); - - it("contains an array of MCP commands", function() { - expect(json.commands).to.be.eql(["echo"]); - }); - - it("contains an array of MCP events", function() { - expect(json.events).to.be.eql(["robot_added", "robot_removed"]); + describe("#config", function() { + it("proxies to Config.update", function() { + expect(Cylon.config).to.be.eql(Config.update); }); }); }); diff --git a/spec/lib/mcp.spec.js b/spec/lib/mcp.spec.js new file mode 100644 index 0000000..4647260 --- /dev/null +++ b/spec/lib/mcp.spec.js @@ -0,0 +1,100 @@ +"use strict"; + +var MCP = lib("mcp"), + Robot = lib("robot"); + +describe("MCP", function() { + it("contains a collection of robots", function() { + expect(MCP.robots).to.be.eql({}); + }); + + it("contains a collection of commands", function() { + expect(MCP.commands).to.be.eql({}); + }); + + it("contains a collection of events", function() { + expect(MCP.events).to.be.eql(["robot_added", "robot_removed"]); + }); + + describe("#create", function() { + afterEach(function() { + MCP.robots = {}; + }); + + it("uses passed options to create a new Robot", function() { + var opts = { name: "Ultron" }; + var robot = MCP.create(opts); + + expect(robot.toString()).to.be.eql("[Robot name='Ultron']"); + expect(MCP.robots.Ultron).to.be.eql(robot); + }); + + it("avoids duplicating names", function() { + MCP.create({ name: "Ultron" }); + MCP.create({ name: "Ultron" }); + + var bots = Object.keys(MCP.robots); + expect(bots).to.be.eql(["Ultron", "Ultron-1"]); + }); + }); + + describe("#start", function() { + it("calls #start() on all robots", function() { + var bot1 = { start: spy() }, + bot2 = { start: spy() }; + + MCP.robots = { + bot1: bot1, + bot2: bot2 + }; + + MCP.start(); + + expect(bot1.start).to.be.called; + expect(bot2.start).to.be.called; + }); + }); + + describe("#halt", function() { + it("calls #halt() on all robots", function() { + var bot1 = { halt: spy() }, + bot2 = { halt: spy() }; + + MCP.robots = { + bot1: bot1, + bot2: bot2 + }; + + MCP.halt(); + + expect(bot1.halt).to.be.called; + expect(bot2.halt).to.be.called; + }); + }); + + describe("#toJSON", function() { + var json, bot1, bot2; + + beforeEach(function() { + bot1 = new Robot(); + bot2 = new Robot(); + + MCP.robots = { bot1: bot1, bot2: bot2 }; + MCP.commands.echo = function(arg) { return arg; }; + + json = MCP.toJSON(); + }); + + it("contains all robots the MCP knows about", function() { + expect(json.robots).to.be.eql([bot1.toJSON(), bot2.toJSON()]); + }); + + it("contains an array of MCP commands", function() { + expect(json.commands).to.be.eql(["echo"]); + }); + + it("contains an array of MCP events", function() { + expect(json.events).to.be.eql(["robot_added", "robot_removed"]); + }); + }); +});