├── .gitignore ├── .npmignore ├── .travis.yml ├── spec ├── lib │ ├── driver.spec.js │ ├── adaptor.spec.js │ └── cylon-robot-disco.spec.js ├── helper.js └── testbot.js ├── .jshintrc ├── History.md ├── Makefile ├── lib ├── cylon-robot-disco.js ├── driver.js ├── proxy │ ├── device.js │ ├── command.js │ ├── event.js │ └── robot.js └── adaptor.js ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_install: 3 | - npm update -g npm 4 | node_js: 5 | - "0.10" 6 | -------------------------------------------------------------------------------- /spec/lib/driver.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RobotDisco = source("driver"); 4 | 5 | describe("Cylon.Drivers.RobotDisco", function() { 6 | var driver = new RobotDisco({ 7 | connection: {} 8 | }); 9 | 10 | it("has no drivers", function(){ 11 | expect(true).to.equal(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "predef": [ 15 | "Cylon", 16 | "Logger", 17 | 18 | "after", 19 | "bind", 20 | "constantly", 21 | "every", 22 | "hasProp", 23 | "proxyFunctionsToObject", 24 | "proxyTestStubs", 25 | "sleep", 26 | "slice", 27 | "subclass" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 1.0.0 / 2015-04-24 2 | ================== 3 | 4 | * Made cylon-robot-disco compatible w/cylon 1.0 5 | * Verified compatibility using spec/testbot.js 6 | * Bumped up the compatibility version to 1.0 7 | 8 | 0.1.4 / 2015-02-20 9 | ================== 10 | 11 | * Even better arrived/left events 12 | 13 | 0.1.3 / 2015-02-20 14 | ================== 15 | 16 | * Better arrived/left events 17 | 18 | 0.1.2 / 2015-02-19 19 | ================== 20 | 21 | * Made compatible with Cylon 0.22.x 22 | 23 | 0.1.1 / 2015-01-26 24 | ================== 25 | 26 | * Initial publish 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN := ./node_modules/.bin 2 | TEST_FILES := spec/helper.js $(shell find spec/lib -type f -name "*.js") 3 | 4 | VERSION := $(shell node -e "console.log(require('./package.json').version)") 5 | 6 | .PHONY: test bdd release 7 | 8 | test: 9 | @$(BIN)/mocha -r cylon --colors -R dot $(TEST_FILES) 10 | 11 | bdd: 12 | @$(BIN)/mocha -r cylon --colors -R spec $(TEST_FILES) 13 | 14 | release: 15 | @git push origin master 16 | @git checkout release ; git merge master ; git push ; git checkout master 17 | @git tag -m "$(VERSION)" v$(VERSION) 18 | @git push --tags 19 | @npm publish ./ 20 | -------------------------------------------------------------------------------- /spec/lib/adaptor.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Cylon = require('cylon'); 4 | var StubAdaptor = source("adaptor"); 5 | 6 | describe("Cylon.Adaptors.RobotDisco", function() { 7 | var adaptor = new StubAdaptor(); 8 | 9 | it("has an adaptor", function() { 10 | expect(adaptor).to.be.instanceOf(Cylon.Adaptor); 11 | }); 12 | 13 | it("Calls the connect callback", function(done) { 14 | adaptor.robot = {events:[]}; 15 | adaptor.connect(done); 16 | }); 17 | 18 | it("Calls the disconnect callback", function(done) { 19 | adaptor.disconnect(done); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /lib/cylon-robot-disco.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cylon-robot-disco 3 | * 4 | * Copyright (c) 2015 Loren West 5 | * Licensed under the MIT license. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var Adaptor = require('./adaptor'), 11 | Driver = require('./driver'); 12 | 13 | module.exports = { 14 | // Adaptors your module provides, e.g. ['spark'] 15 | adaptors: ['robot-disco'], 16 | 17 | // Drivers your module provides, e.g. ['led', 'button'] 18 | drivers: [], 19 | 20 | // Modules intended to be used with yours, e.g. ['cylon-gpio'] 21 | dependencies: [], 22 | 23 | adaptor: function(opts) { 24 | return new Adaptor(opts); 25 | }, 26 | 27 | driver: function(opts) { 28 | return new Driver(opts); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /spec/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'test'; 4 | 5 | var path = require('path'); 6 | 7 | var chai = require('chai'), 8 | sinon = require('sinon'), 9 | sinonChai = require('sinon-chai'); 10 | 11 | chai.use(sinonChai); 12 | 13 | global.chai = chai; 14 | global.sinon = sinon; 15 | 16 | global.should = chai.should(); 17 | global.expect = chai.expect; 18 | global.assert = chai.assert; 19 | global.AssertionError = chai.AssertionError; 20 | 21 | global.spy = sinon.spy; 22 | global.stub = sinon.stub; 23 | 24 | // convenience function to require modules in lib directory 25 | global.source = function(module) { 26 | return require(path.normalize('./../lib/' + module)); 27 | }; 28 | 29 | var Cylon = require('cylon'); 30 | 31 | Cylon.config({ 32 | logging: { logger: false } 33 | }); 34 | 35 | Cylon.Logger.setup(false); 36 | -------------------------------------------------------------------------------- /lib/driver.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cylon-robot-disco driver 3 | * http://cylonjs.com 4 | * 5 | * Copyright (c) 2015 Loren West 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | "use strict"; 10 | 11 | var Cylon = require('cylon'); 12 | 13 | var Driver = module.exports = function Driver(opts) { 14 | Driver.__super__.constructor.apply(this, arguments); 15 | 16 | opts = opts || {}; 17 | 18 | // Include a list of commands that will be made available to the API. 19 | this.commands = { 20 | // This is how you register a command function for the API; 21 | // the command should be added to the prototype, see below. 22 | hello: this.hello 23 | }; 24 | }; 25 | 26 | Cylon.Utils.subclass(Driver, Cylon.Driver); 27 | 28 | Driver.prototype.start = function(callback) { 29 | callback(); 30 | }; 31 | 32 | Driver.prototype.halt = function(callback) { 33 | callback(); 34 | }; 35 | 36 | Driver.prototype.hello = function() { 37 | Cylon.Logger.info('Hello World!'); 38 | } 39 | -------------------------------------------------------------------------------- /spec/testbot.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Minimum robot - for testing 4 | // usage: testbot {cylon-api-port} [secret] 5 | 6 | // Run 2 or 3 of these at the same time - they run on different ports. 7 | // Connect your browser to any one of them and you should see all the 8 | // robots from any robot. You should be able to interact with the local 9 | // robot and any of the remote robots from the same UI. 10 | 11 | var apiPort = +process.argv[2] || 3000 + Math.round((Math.random() * 1000)); 12 | var secret = process.argv[3] || null; 13 | 14 | var Cylon = require('cylon'); 15 | Cylon.config({ 16 | logging: {level:'debug'}, 17 | }) 18 | 19 | // The API must be running in order to be discoverable 20 | Cylon.api({ 21 | port:apiPort, 22 | host:'0.0.0.0', 23 | ssl: false 24 | }); 25 | 26 | // Define a robot with this adaptor 27 | var robot = Cylon.robot({ 28 | name: 'testbot-' + apiPort, 29 | connections: { 30 | disco: {adaptor: 'robot-disco', secret:secret} 31 | }, 32 | devices: { 33 | pingback: {driver: 'ping'} 34 | } 35 | }).start(); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015, Loren West and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /spec/lib/cylon-robot-disco.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var module = source("cylon-robot-disco"); 4 | 5 | var Adaptor = source('adaptor'), 6 | Driver = source('driver'); 7 | 8 | describe("Cylon.RobotDisco", function() { 9 | describe("#adaptors", function() { 10 | it('is an array of supplied adaptors', function() { 11 | expect(module.adaptors).to.be.eql(['robot-disco']); 12 | }); 13 | }); 14 | 15 | describe("#drivers", function() { 16 | it('is an array of supplied drivers', function() { 17 | expect(module.drivers).to.be.eql([]); 18 | }); 19 | }); 20 | 21 | describe("#dependencies", function() { 22 | it('is an array of supplied dependencies', function() { 23 | expect(module.dependencies).to.be.eql([]); 24 | }); 25 | }); 26 | 27 | describe("#driver", function() { 28 | it("returns an instance of the Driver", function() { 29 | expect(module.driver()).to.be.instanceOf(Driver); 30 | }); 31 | }); 32 | 33 | describe("#adaptor", function() { 34 | it("returns an instance of the Adaptor", function() { 35 | expect(module.adaptor()).to.be.instanceOf(Adaptor); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cylon-robot-disco", 3 | "version": "1.0.0", 4 | "main": "lib/cylon-robot-disco.js", 5 | "description": "Cylon Robot Discovery", 6 | "author": "Loren West ", 7 | "homepage": "https://github.com/lorenwest/cylon-robot-disco", 8 | "keywords": ["cylon", "cylonjs", "robot", "discovery", "network", "robotdisco", "robodisco", "cppp-io", "artoo", "gobot", "arduino", "beaglebone", "edison", "raspi", "raspberrypi"], 9 | "repository": { 10 | "type" : "git", 11 | "url" : "https://github.com/lorenwest/cylon-robot-disco.git" 12 | }, 13 | "licenses": [ 14 | { 15 | "type": "MIT", 16 | "url": "https://github.com/lorenwest/cylon-robot-disco/blob/master/LICENSE" 17 | } 18 | ], 19 | 20 | "devDependencies": { 21 | "sinon-chai": "2.6.0", 22 | "chai": "1.9.2", 23 | "mocha": "1.21.5", 24 | "sinon": "1.10.3" 25 | }, 26 | 27 | "peerDependencies": { 28 | "cylon": ">=1.0.0 < 2", 29 | "cylon-api-http": ">=0.3.0 < 1" 30 | }, 31 | 32 | "dependencies": { 33 | "bluebird": ">=2.3.11 <3.0.0", 34 | "clone": ">=1.0.0 <2.0.0", 35 | "deep-extend": ">=0.3.2 <1.0.0", 36 | "eventsource": ">=0.1.4 <1.0.0", 37 | "node-discover": ">=0.0.14 <1.0.0", 38 | "node-uuid": ">=1.4.2 <2.0.0", 39 | "request": ">=2.48.0 <3.0.0" 40 | }, 41 | 42 | "engines": { "node": ">= 0.10.0" }, 43 | "scripts": { 44 | "test": "make bdd" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/proxy/device.js: -------------------------------------------------------------------------------- 1 | /* 2 | * proxy/device.js - Local proxy for a remote robot device 3 | * 4 | * Copyright (c) 2015 Loren West 5 | * Licensed under the MIT license. 6 | */ 7 | var Command = require('./command'); 8 | var Event = require('./event'); 9 | var extend = require('deep-extend'); 10 | var util = require('util'); 11 | var EventEmitter = require('events').EventEmitter; 12 | 13 | var Device = module.exports = function Device(discoNode, robot, remote) { 14 | var my = this; 15 | my.robotName = robot.name; 16 | var i; 17 | extend(my, remote); 18 | 19 | // Convert command names to remote proxies 20 | my.commands = Command.proxyCommands(discoNode, robot.name, my.name, remote.commands) 21 | 22 | // Capture event listeners to set up remote listening and local emitting 23 | my.on = function(eventName, listener) { 24 | if (my.listeners(eventName).length === 0) { 25 | Event.proxyEvent(my, discoNode, my.robotName, my.name, eventName); 26 | } 27 | EventEmitter.prototype.on.apply(my, arguments); 28 | } 29 | my.removeListener = function(eventName, listener) { 30 | var my = this; 31 | EventEmitter.prototype.removeListener.apply(my, arguments); 32 | if (my.listeners(eventName).length === 0) { 33 | Event.removeProxiedEvent(my.robotName, my.name, eventName); 34 | } 35 | } 36 | 37 | }; 38 | util.inherits(Device, EventEmitter); 39 | var proto = Device.prototype; 40 | 41 | proto.toJSON = function() { 42 | var my = this; 43 | var json = { 44 | name: my.name, 45 | driver: my.driver, 46 | connection: my.connection, 47 | commands: Object.keys(my.commands), 48 | details: my.details 49 | } 50 | return json; 51 | }; 52 | 53 | -------------------------------------------------------------------------------- /lib/proxy/command.js: -------------------------------------------------------------------------------- 1 | /* 2 | * proxy/command.js - Local proxy for a remote command 3 | * 4 | * Copyright (c) 2015 Loren West 5 | * Licensed under the MIT license. 6 | */ 7 | var request = require('request'); 8 | var Promise = require('bluebird'); 9 | 10 | // Maybe one day this will be a real class 11 | var Command = module.exports = function Command() {}; 12 | 13 | // Make a proxy to the actual robot for each command 14 | // This returns a hash of command names to implementations 15 | Command.proxyCommands = function(discoNode, robotName, deviceName, commands) { 16 | var my = this; 17 | var proxiedCommands = {}; 18 | commands.forEach(function(commandName) { 19 | proxiedCommands[commandName] = function() { 20 | var args = Array.prototype.slice.call(arguments); 21 | return new Promise(function(resolve, reject) { 22 | 23 | // Build the url to the command 24 | var mcp = discoNode.advertisement; 25 | url = mcp.protocol + '://' + discoNode.address + ':' + mcp.port + '/api'; 26 | url += robotName ? '/robots/' + encodeURIComponent(robotName) : ''; 27 | url += deviceName ? '/devices/' + encodeURIComponent(deviceName) : ''; 28 | url += '/commands/' + encodeURIComponent(commandName); 29 | for (var i = 0; i < args.length; i++) { 30 | url += (i === 0 ? '?' : '&'); 31 | url += '' + i + '=' + encodeURIComponent(args[i]); 32 | } 33 | 34 | request.get(url, function(error, response, body) { 35 | if (error) {return reject(error);} 36 | try { 37 | body = JSON.parse(body); 38 | } 39 | catch (e) { 40 | return reject('JSON parse error: ' + body); 41 | } 42 | resolve(body.result); 43 | }); 44 | }); 45 | } 46 | }); 47 | return proxiedCommands; 48 | }; 49 | -------------------------------------------------------------------------------- /lib/proxy/event.js: -------------------------------------------------------------------------------- 1 | /* 2 | * proxy/event.js - Local proxy for a remote event 3 | * 4 | * Copyright (c) 2015 Loren West 5 | * Licensed under the MIT license. 6 | */ 7 | var Cylon = require('cylon'); 8 | var EventSource = require('eventsource'); 9 | var sources = {}; // robotName:deviceName:eventName -> eventSource 10 | 11 | // Maybe one day this will be a real class. For now, all methods are static. 12 | var Event = module.exports = function Event() {}; 13 | 14 | // Called on first listener to an event 15 | Event.proxyEvent = function(my, discoNode, robotName, deviceName, eventName) { 16 | 17 | // Build the url to the event source 18 | var mcp = discoNode.advertisement; 19 | url = mcp.protocol + '://' + discoNode.address + ':' + mcp.port + '/api'; 20 | url += robotName ? '/robots/' + encodeURIComponent(robotName) : ''; 21 | url += deviceName ? '/devices/' + encodeURIComponent(deviceName) : ''; 22 | url += '/events/' + encodeURIComponent(eventName); 23 | 24 | // Build and save the EventSource, proxying the event to the 'my' object passed in 25 | var sourceKey = robotName + ':' + deviceName + ':' + eventName; 26 | eventSource = new EventSource(url); 27 | eventSource.onmessage = function(e) { 28 | var data = e.data; 29 | try { 30 | data = JSON.parse(e.data); 31 | } 32 | catch (e) { 33 | Cylon.Logger.error("Cannot parse remote event data: '" + e.data + "'"); 34 | } 35 | my.emit(eventName, data); 36 | }; 37 | eventSource.onerror = function(e) { 38 | Cylon.Logger.error('Event ' + sourceKey + ': ', e); 39 | }; 40 | sources[sourceKey] = eventSource; 41 | console.log("ADDED EVENT LISTENING FOR " + sourceKey); 42 | } 43 | 44 | // Called when last listener is removed from an event 45 | Event.removeProxiedEvent = function(robotName, deviceName, eventName) { 46 | var sourceKey = robotName + ':' + deviceName + ':' + eventName; 47 | sources[sourceKey].close(); 48 | delete sources[sourceKey]; 49 | console.log("REMOVED EVENT LISTENING FOR " + sourceKey); 50 | } 51 | 52 | -------------------------------------------------------------------------------- /lib/proxy/robot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * proxy/robot.js - Local proxy for a remote robot 3 | * 4 | * Copyright (c) 2015 Loren West 5 | * Licensed under the MIT license. 6 | */ 7 | var Cylon = require('cylon'); 8 | var DeviceProxy = require('./device'); 9 | var Command = require('./command'); 10 | var extend = require('deep-extend'); 11 | var util = require('util'); 12 | var EventEmitter = require('events').EventEmitter; 13 | var NOP = function(){}; 14 | 15 | var Robot = module.exports = function Robot(discoNode, remote) { 16 | var my = this; 17 | var i; 18 | extend(my, remote); 19 | my.isRemote = true; 20 | 21 | // Build local command proxies to remote calls 22 | my.commands = Command.proxyCommands(discoNode, my.name, null, remote.commands) 23 | my.connections = {}; 24 | remote.connections.forEach(function(connection) { 25 | my.connections[connection.name] = connection; 26 | }); 27 | my.devices = {}; 28 | remote.devices.forEach(function(device) { 29 | my.devices[device.name] = new DeviceProxy(discoNode, my, device); 30 | }); 31 | 32 | // Capture event listeners to set up remote listening and local emitting 33 | my.on = function(eventName, listener) { 34 | if (my.listeners(eventName).length === 0) { 35 | Event.proxyEvent(my, discoNode, my.name, '', eventName); 36 | } 37 | EventEmitter.prototype.on.apply(my, arguments); 38 | } 39 | my.removeListener = function(eventName, listener) { 40 | var my = this; 41 | EventEmitter.prototype.removeListener.apply(my, arguments); 42 | if (my.listeners(eventName).length === 0) { 43 | Event.removeProxiedEvent(my.name, '', eventName); 44 | } 45 | } 46 | 47 | }; 48 | util.inherits(Robot, EventEmitter); 49 | var proto = Robot.prototype; 50 | 51 | // Emulate the real robot.toJSON 52 | proto.toJSON = function() { 53 | var my = this; 54 | var connections = []; 55 | var devices = []; 56 | var i; 57 | 58 | for (i in my.connections) { 59 | connections.push(my.connections[i]); 60 | } 61 | 62 | for (i in my.devices) { 63 | devices.push(my.devices[i].toJSON()); 64 | } 65 | 66 | return { 67 | name: my.name, 68 | connections: connections, 69 | devices: devices, 70 | commands: Object.keys(my.commands) 71 | }; 72 | }; 73 | 74 | proto.halt = NOP; 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cylon Robot Discovery 2 | 3 | [![NPM](https://nodei.co/npm/cylon-robot-disco.svg?downloads=true&downloadRank=true)](https://nodei.co/npm/cylon-robot-disco/)   4 | [![Build Status](https://secure.travis-ci.org/lorenwest/cylon-robot-disco.svg?branch=master)](https://travis-ci.org/lorenwest/cylon-robot-disco)   5 | [release notes](https://github.com/lorenwest/cylon-robot-disco/blob/master/History.md) 6 | 7 | # Cylon Robot Discovery 8 | 9 | This package discovers other robots in a local network running RobotDisco. 10 | 11 | Once discovered, it attaches the remote robot as a proxy robot into the `Cylon.robots` in this process. This lets you easily interact with all robots in the disco. 12 | 13 | To invoke a command on a remote robot, just invoke their command directly. It will return a [promise](https://www.promisejs.org/): 14 | 15 | ``` 16 | var Jane = Cylon.robots.Jane; 17 | Jane.getMood() 18 | .then(function(mood) { 19 | Cylon.logger("Jane's mood is: " + mood); 20 | }); 21 | ``` 22 | 23 | To listen for events on a remote robot, just start listening. Robot Disco takes care of connecting with and forwarding events to you: 24 | 25 | ``` 26 | var Jane = Cylon.robots.Jane; 27 | Jane.on('mood_change', function(mood) { 28 | Cylon.logger("Jane's mood has changed to: " + mood); 29 | }); 30 | 31 | var Florist = Cylon.robots.Florist; 32 | Florist.sendRoseTo('Jane'); 33 | ``` 34 | 35 | When running the [Cylon API](http://cylonjs.com/documentation/core/api/), all robots in the network are represented in your process. 36 | 37 | This allows API clients such as the [Robeaux UI](https://github.com/hybridgroup/robeaux) to interact with all robots by connecting with any robot running Robot Disco. 38 | 39 | For more information about Cylon, check out the repo at 40 | https://github.com/hybridgroup/cylon 41 | 42 | ## Getting Started 43 | 44 | Install the module with: `npm install cylon-robot-disco` 45 | 46 | ## Getting Discovered 47 | 48 | To join the club, just add robot-disco to your connections: 49 | 50 | ```javascript 51 | 52 | Cylon.robot({ 53 | connections: { 54 | ... 55 | disco: {adaptor: 'robot-disco' } 56 | }, 57 | ... 58 | }).start(); 59 | ``` 60 | 61 | ## License 62 | 63 | Copyright (c) 2015 Loren West and other contributors. 64 | See `LICENSE` for more details 65 | -------------------------------------------------------------------------------- /lib/adaptor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * cylon-robot-disco adapor 3 | * 4 | * Copyright (c) 2015 Loren West 5 | * Licensed under the MIT license. 6 | */ 7 | 8 | "use strict"; 9 | 10 | var Cylon = require('cylon'); 11 | var logger = Cylon.Logger; 12 | var NOP = function(){}; 13 | var Discover = require('node-discover'); 14 | var RobotProxy = require('./proxy/robot'); 15 | 16 | /** 17 | * Connect with the underlying Robot Disco 18 | * 19 | * @constructor {Function} 20 | * @param opts - {Object} Constructor options 21 | * @param opts.port {Integer} Discovery port - default 12345 22 | * @param opts.secret {String} Shared secret among all disco members - default: null 23 | */ 24 | var RobotDisco = module.exports = function RobotDisco(opts) { 25 | var my = this; 26 | opts = opts || {}; 27 | RobotDisco.__super__.constructor.apply(my, arguments); 28 | opts.port = opts.port || 12345; 29 | opts.secret = opts.secret || null; 30 | my.opts = opts; 31 | if (my.details && my.details.secret) { 32 | my.details.secret = '*****'; 33 | } 34 | }; 35 | Cylon.Utils.subclass(RobotDisco, Cylon.Adaptor); 36 | var proto = RobotDisco.prototype; 37 | 38 | // This is run once per robot.start() 39 | proto.connect = function(callback) { 40 | var my = this; 41 | logger.info("Joining the robot-disco."); 42 | 43 | // Start up the discovery mechanism 44 | var discoOpts = { 45 | port: my.opts.port, 46 | key: my.opts.secret 47 | }; 48 | my.robot.events = my.robot.events || []; 49 | my.robot.events.push('disco_robot_arrived', 'disco_robot_left'); 50 | my.disco = new Discover(discoOpts, function(err) { 51 | if (err) { 52 | logger.error('Error connecting with robot discovery'); 53 | return callback(err); 54 | } 55 | 56 | // Advertise my capabilities 57 | my.advertise(); 58 | my.intervalTimer = setInterval(function(){my.advertise()}, 1000); 59 | 60 | // Re-connect whenever someone leaves or joins 61 | my.disco.on('added', function(newNode) { 62 | var robots = newNode.advertisement && newNode.advertisement.robots; 63 | if (robots) { 64 | for (var robotName in robots) { 65 | logger.info('Remote robot ' + robotName + ' joined robot-disco'); 66 | my.robot.emit('disco_robot_arrived', robots[robotName]); 67 | } 68 | my.connectTheBots(); 69 | } 70 | }); 71 | my.disco.on('removed', function(oldNode) { 72 | var robots = oldNode.advertisement && oldNode.advertisement.robots; 73 | if (robots) { 74 | for (var robotName in robots) { 75 | logger.info('Remote robot ' + robotName + ' left robot-disco'); 76 | my.robot.emit('disco_robot_left', robots[robotName]); 77 | } 78 | my.connectTheBots(); 79 | } 80 | }); 81 | 82 | // Done connecting 83 | callback(null); 84 | 85 | }); 86 | 87 | }; 88 | 89 | proto.disconnect = function(callback) { 90 | var my = this; 91 | if (my.disco) { 92 | my.disco.removeAllListeners(); 93 | my.disconnectTheBots(); 94 | my.disco = null; 95 | } 96 | if (my.intervalTimer) { 97 | clearInterval(my.intervalTimer); 98 | my.intervalTimer = null; 99 | } 100 | callback(); 101 | }; 102 | 103 | // Advertise my capabilities 104 | proto.advertise = function() { 105 | var my = this; 106 | var api = Cylon.api_instance; 107 | if (!api && Cylon.apiInstances && Cylon.apiInstances.length) { 108 | api = Cylon.apiInstances[0]; 109 | } 110 | if (!api) { 111 | return; 112 | } 113 | 114 | // Take out a full page ad 115 | var fullPageAd = { 116 | port: api.port, 117 | protocol: api.ssl ? 'https' : 'http', 118 | robots: {} 119 | }; 120 | 121 | // Only advertise local robots 122 | for (var robotName in Cylon.robots) { 123 | var robot = Cylon.robots[robotName]; 124 | if (!robot.isRemote) { 125 | fullPageAd.robots[robot.name] = robot.toJSON(); 126 | } 127 | }; 128 | 129 | // Place the ad. This gets sent every second in the 'hello' packet. 130 | my.disco.advertise(fullPageAd); 131 | }; 132 | 133 | // Connect remote robots to interact as local robots 134 | proto.connectTheBots = function() { 135 | var my = this; 136 | my.disconnectTheBots(); 137 | my.disco.eachNode(function(node) { 138 | if (node.advertisement && node.advertisement.robots) { 139 | var robots = node.advertisement.robots; 140 | for (var name in robots) { 141 | var robot = robots[name]; 142 | Cylon.robots[robot.name] = new RobotProxy(node, robot); 143 | } 144 | } 145 | }); 146 | }; 147 | 148 | // Disconnect all remote robots 149 | proto.disconnectTheBots = function() { 150 | var my = this; 151 | for (var robotName in Cylon.robots) { 152 | if (Cylon.robots[robotName].isRemote) { 153 | delete Cylon.robots[robotName]; 154 | } 155 | } 156 | }; 157 | 158 | --------------------------------------------------------------------------------